cd /news/developer-tools/dynamic-rendering-in-angular-is-easy… Β· home β€Ί topics β€Ί developer-tools β€Ί article
[ARTICLE Β· art-120205] src=dev.to β†— pub= topic=developer-tools verified=true sentiment=Β· neutral

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

An engineer detailed the challenges of dynamic UI rendering in Angular, contrasting simple component selection with full runtime UI construction from specifications. The post advocates for component registries and recursive renderers, emphasizing validation as a security boundary for server-driven and generative UI.

read8 min views1 publishedSep 3, 2026

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

── more in #developer-tools 4 stories Β· sorted by recency
── more on @angular 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/dynamic-rendering-in…] indexed:0 read:8min 2026-09-03 Β· β€”