Skip to content

Dynamic component loader

@flyze/lib-core-angular 1.0.0-alpha.34· latest

[loadComponent] renders a component chosen at runtime into the host’s view container, hands it a configuration object, and swaps it when the configuration names a different component. It is what lets the flyze platform build a view out of stored data: a dashboard is a list of { component, config } pairs, and neither the container nor the loaded components know each other at compile time.

Exported by DynamicComponentModule (dynamic-component.module.ts), which also provides DynamicComponentResolverService.

A loadable component implements nothing mandatory. DynamicComponent<K> declares an optional config property (dynamic-component.model.ts:1) and the directive discovers the rest by inspecting the instance:

The directive looks forAnd then
setConfig(config)calls it
configassigns to it, when setConfig does not exist
environment (a method)calls it with the environment
environment (a field)assigns to it

Config is only applied when the loader config object actually has a config property (dynamic-component-loader.directive.ts:148); { component: X } alone leaves the component’s own default untouched.

Wiring by convention rather than @Input() is deliberate — createComponent() produces a component that has no template binding to receive inputs through, and requiring a shared base class would couple every consumer to this library. Rationale: ADR 0005.

The property the environment is written to is configurable via environmentKey (default 'environment'), and empty or non-string values are rejected so the default survives (dynamic-component-loader.directive.ts:82). setEnvironment only writes when the key already exists on the instance (dynamic-component-loader.directive.ts:168) — a component that does not declare the property is skipped silently, so the environment cannot be used to bolt a property onto a component that never asked for it. setConfig has no such guard: it assigns config whether or not the component declares it.

The directive tracks whether ngOnInit has run and refuses to create anything before it (dynamic-component-loader.directive.ts:118). Input setters fire before ngOnInit, and in any order, so a create in the config setter could run before environment was ever set. The first create therefore always happens in ngOnInit, with every input in place.

Afterwards, the config setter decides between two paths by comparing the component identity (dynamic-component-loader.directive.ts:50):

  • Different componentinitialize(): destroy the previous ComponentRef, create the new one, then apply config and environment.
  • Same componentsetConfig() only. The instance is kept and its config updated, so component state survives a config change.

initialize() destroys the previous ref explicitly (dynamic-component-loader.directive.ts:122) — ViewContainerRef.createComponent() appends rather than replaces, so without the destroy the old component would stay in the DOM alongside the new one.

A falsy config, or one without component, is reported with console.error and otherwise ignored (dynamic-component-loader.directive.ts:38) — the previously loaded component stays.

componentLoaded emits the new ComponentRef from the private setter, so it fires on every create before config and environment are applied (dynamic-component-loader.directive.ts:27). A subscriber reading ref.instance.config in that handler sees the previous value or undefined.

config.component may also be a string. DynamicComponentResolverService looks it up in DynamicComponentLookupRegistry before creating (dynamic-component-resolver.service.ts:14) — the path used when the component name comes out of stored data.

Components register themselves with @DynamicComponentDecorator('MyComponent'). The key must be passed explicitly. Deriving it from the class name looks obvious and does not work: minification rewrites class names, so a production build would register keys nobody can look up (dynamic-component.decorator.ts:14).

The registry is a module-level Map, so a component only registers once its file has been evaluated — a component in a lazy chunk is unknown until that chunk loads. An unknown key logs [loadComponent] Component not found and then calls createComponent(undefined), which throws.

Note — sample code in the published bundle

Section titled “Note — sample code in the published bundle”

dynamic-component.model.ts ends with a // #region EXAMPLES block (dynamic-component.model.ts:26:70) declaring FooComponent, BarComponent and a fooBarConfig value that demonstrate the generic types. None of it is exported through public-api.ts, but it is real code in a file that is: it ships in the bundle. Useful as documentation of the type gymnastics, not something to imitate or extend.