Dynamic import build time template only components

Hello, I’m building a new hbs strategy for ember-svg-jar and wonder if it would be possible to dynamic import template only components?

Hbs strategy

The idea is to create a component which dynamic import template only components created at build time

export default class SvgComponent extends GlimmerComponent {
  constructor() {
    super(...arguments);
    this.loadSvg(this.args.name);
  }

  loadSvg(name){
    if(!CACHE.has(name)) {
        return import(name).then(comp => CACHE.set(name))
    }
  }
}
{{#unless this.isLoading}}
 {{component this.args.name}}
{{/unless}}

Would something like is viable with ember-auto-import?

As long as we’re talking about octane-style co-located templates, being template-only doesn’t really affect this question. Template-only components still preprocess into javascript modules that, from the outside, look indistinguishable from components that have separate JS and HBS.

Today ember-auto-import can only import things from NPM packages that are not ember addons. The reason is that ember-addons have their own idiosyncratic way of getting modules into the build, and trying to auto-import them would be redundant with that. In contrast, embroider does do the thing you want here, which is why it’s designed around the v2 addon format that is designed to be analyzable.

Under embroider, the thing you show would basically work in terms of loading components the first time they’re needed. There’s no need for your own CACHE, because ES modules guarantee only-once evaluation – calling import()a second time doesn’t load or run the code twice. After loading a component, to make it invocable in a template you’d still need to call define (see Load Ember-addons on demand from main application - #4 by ef4), at least until Ember lets us directly invoke component classes (which I expect is coming soon).

But I don’t think you should always use dynamic import() for every SVG. That adds asynchrony and potentially more network requests when it isn’t truly wanted. If you just compile all SVGs to template-only components, there’s really no step two. People could invoke the components directly, and Embroider would include only the ones that are used, and they will split correctly into multiple bundles if people are splitting up their apps, etc.

A lot of the work svg-jar needs to do today is only needed because of the lack of such a general-purpose, pull-based build system. Once we have that, svg-jar can be much simpler. I think ideally it would only need to be a preprocessor that rewrites SVG files to HBS files, adding ...attributes like in your linked issue.

2 Likes

Thanks for the detailed answer @ef4 and your continuos work, looking forward for the upcoming features…

People could invoke the components directly, and Embroider would include only the ones that are used, and they will split correctly into multiple bundles if people are splitting up their apps, etc.

I totally agree and I’m glad that embroider with splitting would practically solve most of the scenarios… as long as the usage is statically analyzable, i.e. <Svg::MyCar /> or using AST at build time <Svg @name="my-car" />

But what if we need to support a dynamic scenario?

AFAIK, unused components will be “trimmed” or tree shacked, so in that case I think a dynamic import is the only way? (unless of course you bundle everything) i.e.

<Svg @name={{this.model.picture}} />

{{component this.args.name}}

But I don’t think you should always use dynamic import() for every SVG

I agree, static usage would be encouraged for splitting, but for this use case I want to kind of “mimic” the DX around the default <img src="my-car.svg" /> behaviour

Thanks again

@ef4 coming back around to this thread for some extra clarification.

Let’s say someone is working on an icon component as part of a UI addon for their whole company to use. I don’t think I’d expect anyone to design a template-only component that just puts ...attributes on the svg in the hbs; they’d likely want to deliver something that gives better guidance/guard rails. Especially if a lot of folks will be using the UI addon.

In the end, the icon component will probably need to provide a few things that are customizable- if an SVG is used, then things like fill, height and width (and the dimensions would feed viewBox values) are things you might want to allow a developer to indicate when they use the component. Also, they would probably want to generate a unique id for each (something that should be dynamically generated).

Is there a template-only answer to something like this? Thanks in advance for clarifying!

The original post was asking how to dynamically load template-only components. My reply was that there’s nothing special about template-only components when it comes to how you’d load them. Under embroider, you can dynamically load any component. And since the above was written, we released ember-auto-import 2.0 which lets you dynamically import components out of v2 addons even without switching your app to embroider.

Your question is about how much can be done with template-only components.

Firstly, there’s no pressure to use template-only components. They’re appropriate sometimes but not all times, and JS-backed Glimmer components are a first-class part of Octane-edition Ember.

Second, I think we should be precise about what we mean by template-only. One meaning is “a component with no javascript” and another is “a component with no backing class”. Traditionally in ember those collapsed into one concept, because if you want to have any Javascript you need to create a backing class. But that is not true for proposed formats like GJS, where you can make locally-scoped helpers, modifiers, and components and then invoke them from the template, all without ever making a class extends Component {}.

As an illustrative example, imagine you want to both offer a default fill and assert that any developer-provided fill satisfies a11y contrast requirements within your design system. I can see doing that with a template-only component like this:

import { fillFor } from '@bigco/theme-system';

<template>
  <svg fill={{fillFor "icon" @fill}}>Your Icon Here</svg>
</template>

I’m using the proposed GJS format as implemented in GitHub - ember-template-imports/ember-template-imports: Template import support in Ember!.

The use of the ES import makes it clearer where your theme rules are coming from. fillFor would be implemented as a helper. And even though this is a “template-only component”, if you needed a little Javascript to glue things together or adjust them, you could have some, even though your component is still just a template with no JS class:

import Theme from '@bigco/theme-system';

function fillFor(userFill) {
  return Theme.find("new-mobile").fillFor("icon", userFill);
}

<template>
  <svg fill={{ (fillFor @fill) }}>Your Icon Here</svg>
</template>

In this second example, I’m assuming Default Helper Manager by NullVoxPopuli · Pull Request #756 · emberjs/rfcs · GitHub which eliminates the need to wrap our funciton fillFor in Ember’s helper() utility.

2 Likes