ES Modules for custom components

Hi @patrick, @george,

As you probably know a lot of libraries are moving to ESM and deprecating UMD and plain JS on the browser.

Is there a best practice when using ESM and registering a custom component?

I’ve created a custom component based on ESM but I’m wondering if there is a better way.

Here is how it works:

I’m adding the main JS file as a module:

      linkFiles: [
        {
        src: 'js/jonl_avatars_dicebear.js',
        type: 'js',
        module: true,
        defer: true
        }
      ],

Results in:

<script src="/js/jonl_avatars_dicebear.js" type="module" defer="true"></script>

I believe all modules are deferred by default so the defer attribute is probably redundant.

As this library has 20+ styles each contained in it’s own ESM file I need to pass somehow this information to jonl_avatars_dicebear.js so it can be imported dynamically.

I do this with a droplist control and the scriptInclude and includeParam options

{
              name: 'style',
              title: 'Style',
              type: 'droplist',
              required: true,
              scriptInclude: 'js/jonl_avatars_dicebear.js',
              includeParam: 'style',
              values: [
                {
                  title: 'Adventurer',
                  value: 'adventurer'                  
                },
                {
                  title: 'Adventurer - Neutral',
                  value: 'adventurer-neutral',
                },
                {
                  title: 'Avataaars',
                  value: 'avataaars',
                },
                ...

So when a style is selected the script tag will be updated and the parameter appended.

<script src="/js/jonl_avatars_dicebear.js?style=adventurer" type="module" defer="true"></script>

The js file contains the logic for the component and the imports but to make it work they need to be called in a specific order:

First a static import from the main library which is common for all the styles, the register the component and finally perform a dynamic import using the style parameter in the script tag

import { createAvatar } from 'https://cdn.jsdelivr.net/npm/@dicebear/core@5.3/+esm';

let style;

dmx.Component("jonl_avatars_dicebear", {
    methods: {
        generate: function (seed) {
            if (style) {
                const avatar = createAvatar(style, {
                    seed: seed,
                    dataUri: true
                })
                return avatar.toDataUriSync();
            }

        }
    }
});

// Has to be added after registering the component or it will delay the register for when the DOM renders the component throwing 'component not found' error.
style = await import(`https://cdn.jsdelivr.net/npm/@dicebear/${new URL(import.meta.url).searchParams.get("style")}@5.3/+esm`)

I’m having certain timing issues that I’m working around in a new version of the JS file but I’m wondering if there is a better way to handle this ESM library in particular and any ESM in general in App Connect.

1 Like

Hi Jon,

Yes you can do ESM with just adding the module="true" parameter

It is better to include the selected style directly and not injecting it in your component. So just specify indeed a dropdown with the style choice and add a scriptInclude directly to the right style.

Like we do this in the calendar component:

        { name: 'calendarViews', attribute: 'views', title: 'Views', type: 'enum',
          defaultValue: '',
          values: [
            {title: 'Month', value: 'dayGridMonth', scriptInclude: 'https://unpkg.com/@fullcalendar/daygrid@4.4.2/main.min.js', styleInclude: 'https://unpkg.com/@fullcalendar/daygrid@4.4.2/main.min.css'},
            {title: 'Week', value: 'dayGridWeek', scriptInclude: 'https://unpkg.com/@fullcalendar/daygrid@4.4.2/main.min.js', styleInclude: 'https://unpkg.com/@fullcalendar/daygrid@4.4.2/main.min.css'},
            {title: 'Day', value: 'dayGridDay', scriptInclude: 'https://unpkg.com/@fullcalendar/daygrid@4.4.2/main.min.js', styleInclude: 'https://unpkg.com/@fullcalendar/daygrid@4.4.2/main.min.css'},
            {title: 'Time Week', value: 'timeGridWeek', scriptInclude: 'https://unpkg.com/@fullcalendar/timegrid@4.4.2/main.min.js', styleInclude: 'https://unpkg.com/@fullcalendar/timegrid@4.4.2/main.min.css'},
            {title: 'Time Day', value: 'timeGridDay', scriptInclude: 'https://unpkg.com/@fullcalendar/timegrid@4.4.2/main.min.js', styleInclude: 'https://unpkg.com/@fullcalendar/timegrid@4.4.2/main.min.css'},
            {title: 'List Year', value: 'listYear', scriptInclude: 'https://unpkg.com/@fullcalendar/list@4.4.2/main.min.js', styleInclude: 'https://unpkg.com/@fullcalendar/list@4.4.2/main.min.css'},
            {title: 'List Month', value: 'listMonth', scriptInclude: 'https://unpkg.com/@fullcalendar/list@4.4.2/main.min.js', styleInclude: 'https://unpkg.com/@fullcalendar/list@4.4.2/main.min.css'},
            {title: 'List Week', value: 'listWeek', scriptInclude: 'https://unpkg.com/@fullcalendar/list@4.4.2/main.min.js', styleInclude: 'https://unpkg.com/@fullcalendar/list@4.4.2/main.min.css'},
            {title: 'List Day', value: 'listDay', scriptInclude: 'https://unpkg.com/@fullcalendar/list@4.4.2/main.min.js', styleInclude: 'https://unpkg.com/@fullcalendar/list@4.4.2/main.min.css'}
          ]
        },

Here we used UI type enum for multiple choice, but you can use droplist for single choice

And to specify the include order you can also specify jsOrder, like:

  jsOrder: [
    'https://unpkg.com/@fullcalendar/core@4.4.2/main.min.js',
    'https://unpkg.com/@fullcalendar/core@4.4.2/locales-all.min.js',
    'dmxCalendar.js'
  ]

So that the includes come in this order, you can enter any matching path part.

If I add the script in the head as modules how can I access an exported variable by that module in my js file?

So if I were to add the core and the library style to the head as:

<script src="https://cdn.jsdelivr.net/npm/@dicebear/core@5.3/+esm" type="module" defer="true"></script>
<script src="https://cdn.jsdelivr.net/npm/@dicebear/adventurer@5.3/+esm" type="module" defer="true"></script>

How would I access the exported variables createAvatar(core) and libStyle(adventurer) from the component? I could probably attach them to the window object in the script tag but I can’t add that behaviour in the hjson.

This was the reason I had to import them in the JS file. To have access to the exported variables that are part of the API.

dmx.Component("jonl_avatars_dicebear", {
      methods: {
        generate: function (seed) {
            if (libStyle) {
                const avatar = createAvatar(libStyle, {
                    seed: seed,
                    dataUri: true
                })
                return avatar.toDataUriSync();
            }

        }
    }
});

Well mixing ESM and UMD modules can be pain indeed. I would suggest sticking to UMD with regular includes for App Connect components as this is what they currently use.

Otherwise if using ESM you can try to export the functions to the global window. But racing conditions might occur.

Yeah. I’m feeling the pain.

But as I mentioned in the first post:

I am experiencing this more and more. I live in fear that the next release of whatever library I’m using states that they are dropping UMD support in favour of ESM.

Another reason for using tooling. We still need to open that conversation in the forum as you mentioned in another post.

For App Connect it doesn’t matter if you use ESM, UMD or just plain JS. Important for registering a component is that you do this before the DOM ready event, that is the moment App Connect will start parsing the DOM to render it all. If you have a module or a script with defer, they will execute just before the DOM ready event, when using async functions you have to watch out that the component doesn’t register after the ready event, so make sure there is not an await before the register.

If you don’t mind an extra build step it is probably a good idea to use webpack or another bundler, it will then include all the dependencies in a single JavaScript file and using tree shaking it can minify the code making the download a lot smaller.

1 Like

Thanks for the additional insights on App Connect. It helps and it clears out that AC is agnostic on how the JS is managed.

I would certainly use tooling, but that would only help my projects(which I am ok with of course).

But for publishing extensions for other users to take advantage of, I see that ESM doesn’t play well with Wappler extension framework.

Wappler can inject script tags in html to import a JS library that will be accesible for the component defined in another file. But this won’t work with ESM as it’s not possible to import exported functions loaded in a script tag in html.

So I think @George that some adjustments will be needed in order to provide support for ESM on the browser for extensions.

  1. tooling for bundling and/or tree-shaking
    or
  2. adding code programmatically to JS files same as you do with pages.

First option has the advantage of using other features that come with tooling to improve the building and deployment but it looks like this will take some time to implement.

For the the second option. Same as we define in the hjson the files that should imported into the html via script tag I think you could easily do the same by adding programmatically import statements to the JS file where the component will be registered.

Something similar to scriptInclude but for imports in the main JS file.

For the current App Connect it will be difficult (at least a lot work) to convert it to ESM modules, we want to do that with version 2 which will be completely different. It is however not a problem to develop your own components using ESM modules.

If you want to make functions available from an ESM module, add them to the window.dmx namespace, it should be accessible from the other modules and on the page. Scripts with defer and module are run in order they appear in the DOM, so it is still possible to have dependencies this way.

The way you had it in the initial post should work, having the required modules loaded using script tags will indeed not work since you don’t have a way to access the exported methods.

Yes. Static imports are not the problem. The problem comes with dynamic import() as timing issues come into play as the component is registered before some functions/variables are available to the component.

I use dynamic import because I need to load on demand ESM modules so I don’t have to import all of them which are 20+. So something similar to what you guys do with the calendar component where you only include the JS files that are needed.

  { name: 'calendarViews', attribute: 'views', title: 'Views', type: 'enum',
          defaultValue: '',
          values: [
            {title: 'Month', value: 'dayGridMonth', scriptInclude: 'https://unpkg.com/@fullcalendar/daygrid@4.4.2/main.min.js', styleInclude: 'https://unpkg.com/@fullcalendar/daygrid@4.4.2/main.min.css'},
            {title: 'Week', value: 'dayGridWeek', scriptInclude: 'https://unpkg.com/@fullcalendar/daygrid@4.4.2/main.min.js', styleInclude: 'https://unpkg.com/@fullcalendar/daygrid@4.4.2/main.min.css'},
            {title: 'Day', value: 'dayGridDay', scriptInclude: 'https://unpkg.com/@fullcalendar/daygrid@4.4.2/main.min.js', styleInclude: 'https://unpkg.com/@fullcalendar/daygrid@4.4.2/main.min.css'},
            {title: 'Time Week', value: 'timeGridWeek', scriptInclude: 'https://unpkg.com/@fullcalendar/timegrid@4.4.2/main.min.js', styleInclude: 'https://unpkg.com/@fullcalendar/timegrid@4.4.2/main.min.css'},
            {title: 'Time Day', value: 'timeGridDay', scriptInclude: 'https://unpkg.com/@fullcalendar/timegrid@4.4.2/main.min.js', styleInclude: 'https://unpkg.com/@fullcalendar/timegrid@4.4.2/main.min.css'},
            {title: 'List Year', value: 'listYear', scriptInclude: 'https://unpkg.com/@fullcalendar/list@4.4.2/main.min.js', styleInclude: 'https://unpkg.com/@fullcalendar/list@4.4.2/main.min.css'},
            {title: 'List Month', value: 'listMonth', scriptInclude: 'https://unpkg.com/@fullcalendar/list@4.4.2/main.min.js', styleInclude: 'https://unpkg.com/@fullcalendar/list@4.4.2/main.min.css'},
            {title: 'List Week', value: 'listWeek', scriptInclude: 'https://unpkg.com/@fullcalendar/list@4.4.2/main.min.js', styleInclude: 'https://unpkg.com/@fullcalendar/list@4.4.2/main.min.css'},
            {title: 'List Day', value: 'listDay', scriptInclude: 'https://unpkg.com/@fullcalendar/list@4.4.2/main.min.js', styleInclude: 'https://unpkg.com/@fullcalendar/list@4.4.2/main.min.css'}
          ]
        },

That’s why I’m suggesting to be able to do something similar to scriptInclude but for static imports in file/files defined in:

linkFiles: [
        {
        src: 'js/jonl_avatars_dicebear.js',
        type: 'js',
        module: true,
        defer: true
        }
      ],

So users can add ‘dinamically’ from the UI any of the 20 libraries through static imports to the JS file(js/jonl_avatars_dicebear.js) instead of me having to use a dynamic import which causes timing issues with the component.

1 Like

Dynamic imports are only useful for scripts that are not needed directly, they return a promise and you can do for example the rendering of the component when the promise resolves.

As a workaround you can create separate js files for the different files where you do the import yourself.

// jonl_avatars_dicebear_core.js
import { createAvatar } from 'https://cdn.jsdelivr.net/npm/@dicebear/core@5.3/+esm';

dmx.Component("jonl_avatars_dicebear", {
      methods: {
        generate: function (seed) {
            if (dmx.dicebearStyle) {
                const avatar = createAvatar(dmx.dicebearStyle, {
                    seed: seed,
                    dataUri: true
                })
                return avatar.toDataUriSync();
            }

        }
    }
});
// jonl_avatars_dicebear_adventurer.js
import * as style from 'https://cdn.jsdelivr.net/npm/@dicebear/adventurer@5.3/+esm';

dmx.dicebearStyle =  style;
<script src="/js/jonl_avatars_dicebear_core.js" type="module">
<script src="/js/jonl_avatars_dicebear_adventurer.js" type="module">

Now the style is directly available on DOM ready.

Looks “promising”. Pun intended!

This was the first thing I tried but registering the component instead of rendering it. I wasn’t aware at the time of:

I will implement the workaround until App Connect 2.0 comes out :slight_smile:

Thanks Patrick for all the advice and insights!

I was so proud of myself by trying the workaround of using scriptInclude and adding the style as a parameter of the source of the script tag and reading it inside to import dynamically the library :slight_smile:

<script src="/js/jonl_avatars_dicebear.js?style=adventurer"...

I was so close :slight_smile:

@patrick @George Unless there is an undocumented option in the hjson I can’t use this with scriptInclude as it won’t add the type=module to the script tag.

@george can probably add it to scriptInclude when it isn’t currently supported.

1 Like

We will add this in the next update. Probably just implement linkFiles as option there next to scriptInclude and styleInclude as linkFiles is much more generic and powerful.

1 Like