DEV Community

Maxim Berenshtein
Maxim Berenshtein

Posted on AI-assisted

Dynamic Rendering in Angular Is Easy. Trusting Dynamic UI Is Not.

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" />
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

and instead becomes:

{
  "type": "Card",
  "children": [
    {
      "type": "Input",
      "props": {
        "label": "Name"
      }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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':
    ...
}
Enter fullscreen mode Exit fullscreen mode

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
};
Enter fullscreen mode Exit fullscreen mode

Now the runtime specification doesn't need to know anything about Angular classes.

It only says:

{
  "type": "Input"
}
Enter fullscreen mode Exit fullscreen mode

and the application decides:

"Input"
   ↓
InputComponent
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

A renderer can conceptually do:

render(node) {
  const component = registry[node.type];

  create(component);

  for (const child of node.children ?? []) {
    render(child);
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

At runtime, a JSON specification only decides how those already compiled components are composed:

RUNTIME

JSON specification
       ↓
Component Registry
       ↓
Recursive Renderer
       ↓
Card + Input + Button
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

and:

Server sends executable Angular templates
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

but also the valid shape of its props.

For example:

Button: {
  props: z.object({
    label: z.string(),
    disabled: z.boolean().optional()
  })
}
Enter fullscreen mode Exit fullscreen mode

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()"
}
Enter fullscreen mode Exit fullscreen mode

and definitely not anything remotely resembling:

{
  "click": "eval(...)"
}
Enter fullscreen mode Exit fullscreen mode

Instead, the specification should only describe an action:

{
  "action": "saveProfile"
}
Enter fullscreen mode Exit fullscreen mode

and the application resolves it through another controlled registry:

"saveProfile"
      ↓
Action Registry
      ↓
known application code
Enter fullscreen mode Exit fullscreen mode

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
         └─ ...
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

a malformed graph may create recursive cycles.

So validation shouldn't stop at:

JSON.parse(...)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)