Dynamic rendering in Angular sounds like a fairly narrow technical problem:
“I don't know which component I need until runtime.”
Angular already gives us several good tools for that.
But there is a big difference between dynamically choosing a component and dynamically constructing an entire UI from a runtime specification.
And that difference becomes especially important with Server-Driven UI and Generative UI.
1. ngComponentOutlet: when the problem is really just component selection
For simple cases Angular already gives us:
<ng-container *ngComponentOutlet="componentType" />
This works very well when the application already knows its possible components and runtime logic only decides which one to display.
componentType =
condition ? UserCardComponent : AdminCardComponent;
The advantages are obvious: very little infrastructure, normal Angular lifecycle, AOT-compatible components and a relatively declarative template.
But this approach starts becoming uncomfortable when the runtime input is no longer:
UserCardComponent
and instead becomes:
{
"type": "Card",
"children": [
{
"type": "Input",
"props": {
"label": "Name"
}
}
]
}
Now we are no longer selecting a component.
We are interpreting a UI description.
2. ViewContainerRef.createComponent(): more control, more responsibility
Angular also allows components to be instantiated programmatically:
const ref =
viewContainerRef.createComponent(componentType);
ref.setInput('label', 'Name');
This is a powerful primitive.
We control where the component is created, which component is used, how inputs are assigned and when the component is destroyed.
For relatively contained dynamic behavior, this can be exactly what we need.
But once a runtime specification controls many components, application code often starts evolving into something like:
switch (node.type) {
case 'input':
...
case 'select':
...
case 'button':
...
case 'dialog':
...
}
Then we add input mapping.
Then events.
Then nested components.
Then state.
Then validation.
Soon the difficult part isn't createComponent() anymore.
It is everything around it.
3. Component Registry: separating runtime intent from Angular implementation
A natural next step is a registry:
const registry = {
Card: CardComponent,
Input: InputComponent,
Button: ButtonComponent
};
Now the runtime specification doesn't need to know anything about Angular classes.
It only says:
{
"type": "Input"
}
and the application decides:
"Input"
↓
InputComponent
This separation is more important than it initially appears.
The external system describes intent.
The frontend controls implementation.
And the registry also starts becoming a security boundary: only explicitly registered components can be instantiated.
4. Recursive rendering: when UI becomes a tree
Once a specification can describe nested content, recursion becomes the obvious model.
For example:
Card
└─ Form
├─ Input
├─ Select
└─ Button
A renderer can conceptually do:
render(node) {
const component = registry[node.type];
create(component);
for (const child of node.children ?? []) {
render(child);
}
}
This is where dynamic rendering becomes significantly more powerful.
Forms, dashboards, dialogs, layouts and even entire workflows can now be represented as data.
But this is also where I think an important architectural mistake can happen:
A recursive renderer should not blindly render whatever tree it receives.
The UI tree should be validated before it is trusted.
AOT doesn't compile the runtime UI tree
There is sometimes confusion around this part.
If the structure is dynamic, how can Angular's AOT compiler know what to render?
The answer is: it doesn't need to know the future structure.
AOT compiles the building blocks.
For example:
BUILD TIME
CardComponent
InputComponent
ButtonComponent
DialogComponent
↓
AOT
↓
compiled Angular components
At runtime, a JSON specification only decides how those already compiled components are composed:
RUNTIME
JSON specification
↓
Component Registry
↓
Recursive Renderer
↓
Card + Input + Button
So the important distinction is:
The UI composition is dynamic. The Angular component implementations are not.
The runtime is not compiling new Angular components.
It is assembling already compiled components.
This also means we don't need to ship Angular's JIT compiler just to support dynamic UI.
What about sending HTML or Angular templates from the backend?
This is another tempting approach.
Why not return something like:
<app-user-card [user]="user"></app-user-card>
from the server?
Because injecting that HTML into the page does not make it an Angular template.
Angular doesn't suddenly compile arbitrary HTML received from an API into AOT components.
And trying to introduce runtime template compilation changes the trust model completely.
There is a major architectural difference between:
Server describes UI
and:
Server sends executable Angular templates
I strongly prefer the first model.
Especially when AI becomes part of the system.
Dynamic UI also creates a trust problem
This is the part I find more interesting than the rendering itself.
Imagine that the UI specification comes from a backend, CMS or AI model.
That specification is now external input.
Even if it is “just JSON”, it controls what the application creates and potentially what the user can do.
So I think dynamic UI should be treated similarly to any other untrusted runtime input.
Unknown components
The specification should not be able to instantiate arbitrary Angular classes.
This:
{
"type": "AdminPanel"
}
should only work if AdminPanel belongs to an explicitly controlled catalog.
The registry therefore isn't only a convenience.
It is an allowlist.
JSON
↓
"Input"
↓
Component Registry
↓
InputComponent
No registry entry?
Nothing gets instantiated.
Props are also external input
Even a trusted component can expose dangerous inputs.
Imagine a component receiving:
html
url
redirect
resourceUrl
The fact that the component itself is trusted does not automatically mean every possible value supplied to it should be trusted.
So a component catalog should ideally define not only:
"Button" → ButtonComponent
but also the valid shape of its props.
For example:
Button: {
props: z.object({
label: z.string(),
disabled: z.boolean().optional()
})
}
Now the runtime contract becomes much stronger.
Actions are probably the most important boundary
Rendering UI is one thing.
Allowing generated UI to execute application behavior is another.
I would never want a specification like:
{
"click": "deleteUser()"
}
and definitely not anything remotely resembling:
{
"click": "eval(...)"
}
Instead, the specification should only describe an action:
{
"action": "saveProfile"
}
and the application resolves it through another controlled registry:
"saveProfile"
↓
Action Registry
↓
known application code
The external specification can request a capability.
It cannot invent one.
This gives us a very useful separation:
The Component Registry controls what UI can exist.
The Action Registry controls what that UI is allowed to do.
Validate the UI tree before recursively rendering it
This is one part I think deserves much more attention.
Suppose a backend or model produces:
Container
└─ Container
└─ Container
└─ ...
Thousands of levels deep.
Or maybe the tree contains hundreds of thousands of nodes.
There may be no XSS.
No JavaScript injection.
The JSON may even be structurally valid.
But rendering it can still freeze the browser.
And if the specification uses references:
A → B → C → A
a malformed graph may create recursive cycles.
So validation shouldn't stop at:
JSON.parse(...)
or even basic schema validation.
A robust runtime boundary may eventually need to reason about component types, props, missing references, illegal parent/child combinations, cycles, maximum depth, maximum node count and potentially maximum repeat expansion.
The architecture I prefer is:
Backend / AI
↓
Untrusted UI specification
↓
Schema validation
↓
Structural validation
↓
Catalog / props validation
↓
Runtime limits and policies
↓
Trusted specification
↓
Angular renderer
Only the last step should create Angular views.
Streaming makes this even more interesting
Generative UI adds another complication.
The model may not send the entire UI at once.
Instead:
patch
patch
patch
patch
gradually builds the screen.
That means a half-generated UI can naturally contain temporary inconsistencies: a parent may reference a child that simply hasn't arrived yet.
So validating every intermediate state with exactly the same rules as a completed UI can produce false errors.
I think the model should instead be:
stream patches
↓
build partial UI
↓
generation completes
↓
validate completed specification
↓
accept / reject / persist
Potentially with additional lightweight limits while streaming to prevent a generation from growing without bounds.
This becomes much more than “dynamic component rendering”.
It is a small UI runtime.
This is the problem that led me to ngx-json-render
While exploring these patterns, I realized that Angular itself already solves the lowest-level problem very well.
Angular knows how to create components.
What I wanted was the layer above that.
That became ngx-json-render.
The library takes a different approach from runtime Angular template generation.
An external system produces a JSON UI specification.
The Angular application provides a catalog of components and actions.
The renderer connects the two.
Conceptually:
AI / Backend
↓
JSON UI specification
↓
Catalog / Schema
↓
Component Registry
↓
Action Registry
↓
Angular Renderer
↓
AOT-compiled Angular components
There is no need to send executable Angular templates, use innerHTML as a component mechanism or evaluate generated JavaScript.
The external system generates UI intent, not frontend code.
The current library already provides the main building blocks around this model: a controlled component registry, catalog-defined component props, registered actions, recursive composition, state/bindings, streaming JSON patches and optional structural validation that can reject a malformed completed specification before it is accepted.
But I don't think the security story should be overstated.
There are still boundaries worth strengthening.
In particular, catalog-level validation and structural validation are different concerns, validation currently has to be explicitly enabled, and resource limits such as maximum graph depth, maximum rendered nodes or explicit cycle protection are areas I consider important for a hardened dynamic UI runtime.
And maybe that's the larger point.
The interesting question is no longer:
“Can Angular dynamically render components?”
Of course it can.
The more interesting question is:
How do we safely turn an untrusted runtime UI description into a predictable Angular component tree?
As Generative UI moves from demos into real applications, I suspect this boundary will matter much more than the component creation API itself.
Dynamic UI does not have to mean dynamic trust.
The structure may be generated at runtime.
The capabilities should still belong to the application.
Top comments (0)