Using Material Web Components within the neo.mjs application worker | by Tobias Uhlig | Aug, 2021


One way is to add script tags into the index.html file of your app, since web components have to live within a main thread.

This is not super nice though, since we want to lazy load the dependencies when they are actually needed and we want different version for each environment.

So, we do want to create a new main thread addon:
src/main/addon/Mwc.mjs

We can include the main thread addon inside our neo-config.json file:

You might have noticed the constructor inside our button class:

The first ctor call will load our Google MWC dependencies async and we are set.

Now this is the tricky part where I spent most of the time dealing with.

Google has implemented their Web Components based on ES2017, which is nice.

!!! BUT !!!, they are using bare module specifiers, which is a real bummer.

Meaning: The import statements are written inside a format, which browsers can not understand.

import {TextAreaCharCounter} from './mwc-textfield-base';

It is mostly just the missing file name extension and sometimes no real paths.

If you look back at the index file: I added the module from a CDN in which case all “wrong” import paths get replaced with URLs.

This approach works perfectly fine inside a browser without any builds / transpilations, meaning: except from the broken import statements, the code is good to go.

I will create a feature request soon.

Inside neo.mjs, we have 3 different environments:

  1. development
    Runs directly inside the browser, without any builds or transpilations
  2. dist/development
    Webpack based build using source maps (this is what you would call the dev mode in Angular or React)
  3. dist/production
    Minified webpack based build without using source maps

Obviously we do want to support all 3 envs in the best possible way. So let us take a look at the main thread addon now:

For our dev env, we have to stick to using the CDN. It would be way nicer in case there was a JS module driven output available. Loading the lib(s) via a CDN can take several seconds for a first page load.

We do need to tell webpack to ignore this import.

For the dist environments, we can simply install the node module(s) and then use bare module specifiers on our own. Webpack will create the split chunks accordingly.

Our 4 methods inside our main thread addon are exposed to the app worker using the remote config. This way, we can call them directly as Promises from within our app worker scope. E.g.:

Neo.main.addon.Mwc.loadButtonModule();

We are mapping component configs to vdom top level attributes again. I will skip this part to focus on the important ones.

You can find the full source code here:
src/component/mwc/TextField.mjs

The textfield provides methods inside its API, like checkValidity() and reportValidity() .

checkValidity() {
return Neo.main.addon.Mwc.checkValidity(this.id);
}

We just defined those 2 methods inside our main thread addon and exposed them to the app worker via the remote API:

checkValidity(id) {
return document.getElementById(id).checkValidity();
}

Since the workers communication is async, we need to trigger the method as a Promise. Inside our component based method, we just return this Promise.

Inside the textfield demo app, I am using:

exampleComponent.checkValidity().then(value => console.log(value))

You could also use async & await.

We are adding an input domListener which gets bound to:

onInputValueChange(data) {
let me = this,
value = data.value,
oldValue = me.value;

if (value !== oldValue) {
me.value = value;
}
}

In case there is a change, we do want to update our value config to keep the state in sync.

afterSetValue(value, oldValue) {
let me = this;

me.changeVdomRootKey('value', value);

me.fire('change', {
component: me,
oldValue : oldValue,
value : value
});
}

We also want to fire an app worker based event, which other components or controllers can subscribe to.

Inside one of the demos, we can configure the value of the web component field using a neo textfield. We want to update this one, in case we type into our web component:

So this is fairly easy to do. It would be even simpler, in case we add view models ( model.Component ) and bindings into the mix.

Here is a quick walkthrough of the 4 different demo apps. You can change neo component based configs directly inside the console, when logging your instances.

Inside the tabbed button demo, focus on the console logs: we are removing empty cards (tabs) from the DOM, but the neo component ids stay the same when navigating back (same JS instances).

You can find the 2 components here:
src/component/mwc

Main thread addon:
src/main/addon/Mwc.mjs

Code of the 4 demo apps:
examples/component/mwc

You can remove the dist/production from the URLs to run the demos inside the dev mode. While dist/prod runs inside all major browsers, the dev mode is limited to Chromium and Safari Tech Preview.

It is definitely a big trade-off.

In case you are using different component libs, each of them will create the basic logic on their own and this logic won’t get shared → resulting in a bigger file size.

In case you already have a big collection of Web Components, the approach of this article can make sense to get your app running inside the neo.mjs workers setup, which is a big performance boost.

However, Web Components live within main threads. The performance boost is bigger, the more neo based components you are using (keeping main threads as idle as possible).

Neo components have the ability to mount() and unmount() their DOM. Inside the TabContainer based examples (video), you noticed that we click on a button and get a log like id:1. Switching to the second tab removes the DOM. Navigating back and clicking the button logs id:1 again → it is the same neo component JS instance.

This is however not the case for the Web Component counterparts: every time we re-mount the DOM, new JS instances will get created. This is not a big deal for simple components like buttons or textfields, but imagine a buffered grid or calendar.

layout.Card (which is used inside tab.Container ) has a config called: removeInactiveCards . The default value is true:
src/layout/Card.mjs#L52

You can change this one to false for use cases where you are using Web Components. However, then you have a bigger DOM markup. This won’t affect the layout performance, since nodes with display: 'none' are excluded from browser based layout / reflow calculations, but it does affect the memory usage.

By now, there are a lot of widgets already added into the framework. E.g. for buttons & textfields you could just adjust the styling to make them look “material”.

You are very welcome to create feature request tickets for new widgets as well as for new features for existing ones:
neomjs/neo/issues

So far, I created wrappers for the button and textfield components. Obviously there are a lot more inside the MWC lib.

I would love to get your feedback if you like to see more wrapper components inside the neo.mjs framework or if this topic is not interesting for you.

This is especially important to know, since I will most likely keep pushing to work on internal widgets (like the calendar) unless there is a big demand for creating more Web Component wrappers.

After reading this article, you definitely know how to do this on your own: You are welcome to help creating more wrapper components for Google’s MWC or suggest / work on different Web Component library wrappers.

You can find a lot more stunning performance demos inside the neo repo → online examples:

I can strongly recommend to take a deep dive into them, since the performance boost of using multiple CPUs in parallel is intense.

For questions and feedback you are very welcome to join the Slack Channel:

Best regards & happy coding,
Tobias



Source link

Latest articles

Related articles

Leave a reply

Please enter your comment!
Please enter your name here