An Angular dashboard must render widgets described by a JSON layout from the server; how do you design the dynamic creation, updates and teardown?
answer
- strings to classes through a registry
- lazy import per widget type
- one container, cleared on relayout
- validate input names against the component
- stale async loads after a new layout
basics
~20 sMap each JSON type to a component class through an explicit registry with lazy imports, create widgets in a ViewContainerRef with inputBinding/outputBinding, clear the container on relayout, guard against stale async loads, and validate input names with reflectComponentType.
solid answer
~50 sJSON can only name widgets, so keep an explicit **registry** from type strings to loaders, `'revenue': () => import('./revenue').then(m => m.Revenue)`, which whitelists what the server can instantiate and code-splits each widget. Render into one anchor (`viewChild.required('grid', { read: ViewContainerRef })`): for each item, await its loader, then `createComponent(type, { bindings: [...] })`, turning the item's settings into `inputBinding(name, () => value)` and wiring events with `outputBinding`. Before binding, check names against `reflectComponentType(type).inputs`, since an unknown name throws NG0315 in development and is ignored in production. On a new layout, `vcr.clear()` destroys the old widgets, and a generation counter discards loads that finish after a newer layout arrived. Unknown types are skipped or get a fallback tile. Widgets are torn down with the dashboard automatically; only overlays made with the standalone `createComponent` need manual destruction.
code
ts · 40 linesimport {
Component, DestroyRef, Type, ViewContainerRef, inject, inputBinding,
outputBinding, reflectComponentType, viewChild,
} from '@angular/core';
interface WidgetConfig { id: string; type: string; inputs: Record<string, unknown>; }
const REGISTRY: Record<string, () => Promise<Type<unknown>>> = {
revenue: () => import('./widgets/revenue').then((m) => m.RevenueWidget),
alerts: () => import('./widgets/alerts').then((m) => m.AlertsWidget),
};
@Component({ selector: 'app-dashboard', template: `<ng-container #grid />` })
export class Dashboard {
private readonly grid = viewChild.required('grid', { read: ViewContainerRef });
private readonly destroyRef = inject(DestroyRef);
private generation = 0;
async render(layout: WidgetConfig[]): Promise<void> {
const gen = ++this.generation;
this.grid().clear();
for (const item of layout) {
const loader = REGISTRY[item.type];
if (!loader) { console.warn(`Unknown widget type ${item.type}`); continue; }
const type = await loader();
if (gen !== this.generation || this.destroyRef.destroyed) return; // stale
const mirror = reflectComponentType(type);
const known = new Set(mirror?.inputs.map((i) => i.templateName));
const bindings = Object.entries(item.inputs)
.filter(([name]) => known.has(name))
.map(([name, value]) => inputBinding(name, () => value));
if (mirror?.outputs.some((o) => o.templateName === 'refresh')) {
bindings.push(outputBinding('refresh', () => this.reload(item.id)));
}
this.grid().createComponent(type, { bindings });
}
}
private reload(id: string): void { /* refetch data for one widget */ }
}go deeper
Recall that a JSON type string must be mapped to a component class before ViewContainerRef.createComponent or NgComponentOutlet can render it.
Explain the view-container flow: anchor query, loader, createComponent with bindings, clear() on relayout, and why components are destroyed with their container.
Design the registry as a whitelist and code-splitting boundary, validate inputs with reflectComponentType, handle the stale-load race, and isolate widget failures.
Frame the widget platform: who may add widget types, how the registry is versioned against server layouts, and the contract widgets must honour for inputs and events.
## The scenario The server returns a layout such as `[{ "id": "w1", "type": "revenue", "inputs": { "range": "30d" } }, { "id": "w2", "type": "alerts", "inputs": {} }]`. The dashboard must render the matching components in order, pass them their settings, react to their events, and replace everything when the user switches layouts. Every part of Angular's dynamic-creation API gets exercised. ## 1. From strings to classes: a registry JSON cannot contain a component class, only a name. Map names to classes explicitly: - A `Record<string, () => Promise<Type<unknown>>>` of **loader functions**, each a dynamic `import()`, gives one lazily loaded chunk per widget type, so users download only the widgets their layout uses. - The registry is a **whitelist**: the server can only choose among components you registered, never an arbitrary class. - An unknown type is skipped with a warning, or mapped to a fallback component such as an unavailable-widget tile, rather than breaking the whole dashboard. ## 2. Where widgets live: one view container Put an `<ng-container #grid />` anchor in the dashboard's template and query it with `viewChild.required('grid', { read: ViewContainerRef })`. Creating widgets there means they are part of the dashboard's view tree: they are checked with it, resolve dependencies from its location, and are destroyed with it. The standalone `createComponent` would force you to attach views to `ApplicationRef` and destroy them by hand, which buys nothing here. ## 3. Creating a widget 1. Await the loader to get the class. 2. Read the class's metadata with `reflectComponentType(type)`; keep only JSON keys that match an entry in `inputs` (its `templateName`). 3. Call `vcr.createComponent(type, { bindings })` with `inputBinding(name, () => value)` for each valid key and `outputBinding('refresh', ...)` for events the dashboard handles. 4. Keep the returned `ComponentRef` in a map keyed by widget `id` if single widgets must be removed later. Validation matters because a misspelled input in `inputBinding` throws NG0315 in development, while in production the binding simply has no target. Filtering against the reflected inputs gives the same behaviour in both, and lets you log the bad key. ## 4. Relayout and the stale-load race When a new layout arrives: - `vcr.clear()` destroys every existing widget, running their destroy hooks. - Loaders are asynchronous, so a slow import from the previous layout can finish **after** the new one started. Increment a **generation counter** per render and drop any creation whose generation is no longer current. - Check the dashboard's `DestroyRef.destroyed` before creating, in case the dashboard itself was destroyed while imports were pending. For incremental changes, such as one widget's settings, prefer updating inputs over recreation: bind inputs to signals held per widget, so changing a signal updates the widget without losing its internal state. ## 5. Errors - **Construction errors** (a throwing constructor) surface synchronously from `createComponent`; wrap each creation in `try/catch` so one bad widget does not stop the rest. - **Rendering errors** can be routed to an `onError` callback on the creation options, a developer-preview API in 22.2, which lets you swap in an error tile. ## 6. Alternatives and when they win | Approach | Good for | Limits | |---|---|---| | `@for` + `NgComponentOutlet` | simple layouts, inputs only | no outputs option; recreation on class change | | `ViewContainerRef.createComponent` | ordering, outputs, host directives, fine control | more imperative code | | static components inside `@defer` | a known, fixed set of widgets | not driven by server data | `@for (w of widgets(); track w.id) { <ng-container *ngComponentOutlet="w.type; inputs: w.inputs" /> }` is a perfectly good first version once the classes are resolved. Move to the view-container design when widgets must emit events to the dashboard or when you need explicit control over creation order and teardown. ## Testing the design - Unit-test the registry mapping and the input filtering without rendering anything. - Render the dashboard with a small fake registry whose loaders resolve in a controlled order, to prove the generation guard drops stale widgets. - Assert that destroying the dashboard runs each widget's destroy hooks, which also catches leaks from any standalone-created overlays. ## What to show in the interview The registry as a security and bundling boundary, reflected-input validation, the relayout race, and a clear statement of who owns each widget's lifetime.
- Why not map the JSON type straight to a component with a lookup on window or a global object?Resolving arbitrary names lets server data choose any reachable class, which is a security and stability risk, and it defeats code-splitting because every candidate must already be loaded. An explicit registry of lazy loaders whitelists what can render and gives one chunk per widget type.
- How do you update one widget's settings without losing its internal state?Back that widget's inputs with signals kept per widget id and pass them through `inputBinding`. Changing a signal updates the input on the next check of the host view, while the same instance stays alive. Recreating the widget, or changing its class in an outlet, would discard its state.
saying these in an interview costs you the question
- Resolve the JSON type by looking up any exported class by its name.
- Import every widget eagerly so createComponent can find them synchronously.
- Widgets created in a ViewContainerRef must each be destroyed manually when the dashboard goes away.
- An unknown input name in inputBinding is harmless in every build mode.
- Awaiting imports in order makes a relayout race impossible.