Yes, we need to do an RFC on how to close the “dynamic component hole”.
But that doesn’t mean all uses of the component helper are bad and I wouldn’t want people to start refactoring everything until we can provide a more pleasant solution to refactor toward.
Today it is somewhat laborious and awkward. Using public API, you can only get a handle on a component definition from inside a template, not from inside Javascript. Which means your mapping of allowed components needs to be expressed in a template:
{{! app/templates/components/feed-items.hbs }}
{{yield (hash
// these uses of the component helper are all
// statically analyzable, because they use
// string literal arguments.
post={{component "feed-components/post"}}
photo={{component "feed-components/photo"}}
comment={{component "feed-components/comment"}}
)}}
Which you can then use something like:
<FeedItems as |feedItems|>
{{#let (get feedItems this.itemType) as |ItemComponent|>
// angle brackets can invoke component definitions
// and aren't supposed to resolve strings into
// definitions, so this is statically safe (it
// can't access a component that wasn't
// already loaded by somebody else).
<ItemComponent />
{{/let}}
</FeedItems>
This would all be much nicer if we were allowed to directly import components into Javascript and then invoke them. Because that, combined with the rules already laid out in RFC 507 would allow:
import { importSync } from '@ember/macros';
export default class extends Component {
get ItemComponent() {
return importSync(`./feed-components/${this.itemType}`).default;
}
}
<this.ItemComponent />
Which still lets you write very dynamic code, but in a way that allows us to statically know what set of components might get used by that spot.
It’s also even possible to lazy load the components in a data-driven way, as long as you don’t mind dealing with the asynchrony:
import { restartableTask } from 'ember-concurrency-decorators';
export default class extends Component {
init(...args) {
super.init(...args);
this.loadComponent.perform(this.itemType);
}
@restartableTask
loadComponent = function*(itemType) {
let module = yield import(`/feed-components/${itemType}`);
this.set('ItemComponent', module.default);
}
}
{{#if this.loadComponent.isRunning }}
<LoadingSpinner />
{{else}}
<this.ItemComponent />
{{/if}}
This kind of behavior is basically implied by the strict mode RFC, since it implies that components can be imported in JS and then provided in the ambient scope of a template. That RFC also suggests an alternative way to solve the problem we’re solving here (importing components into templates rather than importing them into Javascript), but I have emphasized the Javascript case because it composed so well with dynamic imports.