Lazy load global custom elements

We are building an application where we allow our customers to build “apps” and install them into our application on runtime. We don’t want our customers to remember adding <require> statements in their views (this also wouldn’t make any sense, as they don’t have the source code of our application available), so we would like to make custom elements (built by us) available globally.

Right now we use FrameworkConfiguration.globalResources() on startup, but it is giving us a slow startup time, so we are exploring how to lazy load custom elements whenever a view is using it.

In this issue, @jods4 mentions one way that this could be implemented in the framework: https://github.com/aurelia/framework/issues/145#issuecomment-381528080

Currently we have experimented with a solution where we can parse the views from customers before loading them, and find any (unloaded) custom elements, and load them using something like this:

  1. In boot.ts, initialize an array like this:
const customElements = {
    custom1: PLATFORM.moduleName('element/custom1', 'elm-1'), 
    custom2: PLATFORM.moduleName('element/custom2', 'elm-2)
}
  1. Parse a view and find any custom elements (not already loaded)
  2. E.g. <custom1> will then be loaded like this:
await new FrameworkConfiguration.globalResource(customElements.custom1).apply()

This works, but if one of the custom elements contains any not already loaded custom elements, then we’re stuck, as we haven’t found a way to find custom elements in other custom elements.

Do you have any ideas on a better approach, preferably using some of Aurelias components (ViewCompiler, VIewResources, etc.)?

From what you described, the “lazy global resources” could be very much local to those elements. It feels so to me because you still need to teach those how to load anyway ? Or they are supposed to trigger the load with very simple configuration name like "load GlobalA if you load me, and I only know the name of the thing I want, its GlobalA" ?

Edit: also if you allow arbitrary modules to be loaded, then i guess you are not using webpack. Which i think made the task simpler ? You can tell the loader how to map a certain name to a certain url

The customElements-object is set by us, and our customers know which custom elements are available. Basically they just know, that they can use the tags in their views, they are not concerned about loading at all.

The custom elements can be used in apps (from customers) and/or in modules built by us, so they should be used both in compile-time modules and runtime-apps. The lazy loader should work, so whenever a view is using a custom element, it should be loaded. When other views are using the same custom element in the same session, they obviously don’t need to load the custom element as it has already been loaded.

We are using Webpack, combined with DLL, that our customers can use in their building process (using a tool). Thus they can only use the specific modules we have made available in the DLL, including the custom elements we specify in the customElements object.

Hope this clarifies some stuff :slight_smile:

I have solved the requirement of automatically load the necessary module once, without <require>, but it still required some configuration. I have some ideas how to auto trigger the loading, but will need some experiment first. Ill see what i can come up with and get back to you.

Great, we really appreciate that.

I’m sure it’s possible, I’m just worried that our current solution is too custom (and also still not solves the problem completely), i.e. that we should use some of Aurelias templating-modules instead.

Hi @bigopon,
I was wondering if you have had the time to experiment more with this?

Im still making it. Probably it will be released as plugin instead an official API. It will support lazy loading for standard resources types: elements, attributes, binding behavior and value converter. I got some progress but delayed a bit. Should be ready around this weekend

@boysensci I got an easy monkey patch for you :slight_smile:

Add this to top of your main.js. You might need few cleanup if you use TypeScript.

import {TemplateRegistryEntry} from 'aurelia-loader';

function traverse(element, cb) {
  const {children} = element;
  for (let i = 0, len = children.length; i < len; i++) {
    const e = children[i];
    cb(e);
    traverse(e, cb);
  }
}

// The elements you want to be auto required
const AUTO_REQUIRES = ['test-element'];

const templateDescriptor = Object.getOwnPropertyDescriptor(TemplateRegistryEntry.prototype, 'template');

const oldGet = templateDescriptor.get;
const oldSet = templateDescriptor.set;

Object.defineProperty(TemplateRegistryEntry.prototype, 'template', {
  get: oldGet,
  set: function(value) {
    oldSet.call(this, value);

    traverse(value.content, element => {
      const tagName = element.tagName.toLowerCase();
      if (AUTO_REQUIRES.indexOf(tagName) === -1) return;

      // assume that's your global elements location
      const requireId = 'resources/elements/' + tagName;
      if (this.dependencies.findIndex(d => d.src === requireId) === -1) {
        console.log('auto-require ' + requireId);
        this.addDependency(requireId);
      }
    });
  }
});
2 Likes

I like the patch, it’s simple and managable inside your application @boysensci . Also it requires only low level patching. If that fits your need probably I won’t be releasing the patch I intended for it, since it requires heavier patching.

1 Like

@huochunpeng, this works perfectly! Thank you so much :smiley:

@bigopon, I also like this simple approach, so I’ll use this - at least until this is supported out-of-the-box by Aurelia.

Thanks again, guys.

1 Like