Load Ember-addons on demand from main application

I’m building an application where ember components are needed on-demand when certain conditions are met. For this purpose I’m building ember add-ons which will have independent components. But I need to import add on files only on-demand and render the components in addon.

Is there any way to load addon separately rather than bundling it with the main application?

Unfortunately it requires some hacking. Making it trivially easy is a key goal for Embroider, but Embroider is not stable yet.

If I had to make this work today without embroider, I would

  1. Make a separate NPM package to hold the components that’s not an ember addon. (The reason for this is that ember-auto-import can dynamically load code from non-ember packages but not ember packages, because ember packages need too much preprocessing.)
  2. Make a custom build pipeline for that package that compiles the components the same way ember-cli would have (so they have AMD define statements, and the templates are compiled, etc). The goal is to have a plain NPM package that is prebuilt into the form the ember app can consume.
  3. Use ember-auto-import to dynamic import() the code from the package on demand.

Some caveats to be aware of:

  1. Making the build pipeline isn’t super hard but it’s annoying, and you’re going to need to read ember-cli code to figure out what to do. It mostly comes down to getting the same babel config that applies in ember-cli.
  2. Template compilation only applies to one ember version at a time, so your component library’s precompiled templates will only work with one version of ember. That’s fine for an internal library that will only be used in one app, but not ok for a shared library.
  3. If you want to use other addons from inside your special component library, it’s going to be painful because at that point you’re basically rewriting embroider.

Is lazy loading of components already possible using Embroider today?

Yes, embroider can lazily load components (with their whole subgraph of dependencies).

The caveat is that Ember itself doesn’t yet offer a convenient way to invoke a component that you just imported. Like, this doesn’t work:

import Component from '@glimmer/component';
import SomeComponent from './some-component';
export default class extends Component {
  constructor() {
    super();
    this.SomeComponent = SomeComponent;
  }
}
<this.SomeComponent/>

So even though embroider lets you do this and all the code will load correctly:

import Component from '@glimmer/component';
export default class extends Component {
  @action
  loadSomeComponent() {
    this.SomeComponent = await import('./some-component');
  }
}
{{#if this.SomeComponent}}
  <this.SomeComponent/>
{{/if}}

Ember won’t be able to invoke the component, because you’ve imported the component class which is not the same thing as the component definition.

This is likely to get fixed in Ember itself, because lots of people want this kind of pattern to work and it’s basically a requirement for things like strict mode rfc and sfc and template imports rfc.

But until then, it can be worked around something like:

@action
loadComponent() {
  let component = await import('./the-lazy-component');
  this.componentName = 'whatever-name-you-want';
  define(`my-app/components/${this.componentName}`, await import('./the-lazy-component'));
  // if you aren't using template colocation, you would also need to
  // load and define the template separately. So probably 
  // just use template  colocation because that's simpler
}
{{#if this.componentName}}
  {{component this.componentName}}
{{/if}}
5 Likes