Angular handles a lot of things for us and stays out of the way when something custom is required — behaviors can be swapped out or extended with little friction. That last part is what this article is about: a small library called Polymorpheus, written years ago and used throughout Taiga UI wherever dynamic content has to be rendered. Before getting into it, it is worth recapping how Angular deals with dynamic content on its own, and where those default paths start to feel limiting.
How Angular renders dynamic content
Take a simple case: an object — a User — that needs to appear on the page. There are several built-in ways to do this, each with its own capabilities and its own constraints.
Interpolation
The simplest approach is to drop the object straight into the template:
{{ user }}
What actually happens is that toString() is called and [object Object] ends up on screen. Making this useful requires a different toString() implementation:
class User {
constructor(
readonly name: string,
readonly surname: string,
) {}
toString(): string {
return `${this.name} ${this.surname}`;
}
}
That is not a practical way to render data. Either every key has to be interpolated by hand, or a custom toString() has to be written — neither is something developers normally reach for. The next technique is more capable.
Function
A function call sidesteps both manual key interpolation and baking a string representation into the data itself:
{{ stringify(user) }}
function stringify({ name, surname }: User): string {
return `${name} ${surname}`;
}
Calling functions from templates has a bad reputation, though that reputation was never really deserved. The concern is that a function runs on every change detection cycle, which only matters if the work inside it is expensive. Extensive benchmarking done years ago, back when this question was more urgent because signals did not exist and the default change detection was Eager, showed that strings, math, Boolean logic, and even small loops run fast enough that no perceptible slowdown comes from template function calls. That holds especially true with OnPush and properly split views, where clicking somewhere does not trigger a check of the whole application.
One rule applies here: never create new arrays, objects, or classes inside those function calls. That is a far heavier operation, and it also produces a new value on every run, which means inputs fire, loops re-execute, and whole DOM subtrees can be rebuilt.
This case looks similar to the previous one, but it differs in one important way — it introduces context. The stringify function takes a user as an argument, so the same representation logic works for any user and the output is determined by the actual object passed in. That leads to the next option.
Template
For anything beyond primitives, ng-template is the tool to use. Context is no longer an idea — it is official terminology. The User example looks like this with templates:
<ng-container
[ngTemplateOutlet]="template"
[ngTemplateOutletContext]="{ $implicit: user }"
/>
<ng-template #template let-user>
{{ user.name }} {{ user.surname }}
</ng-template>
This is the approach to take when, for instance, an avatar needs to sit next to a user name, or when other Angular components and directives are involved. For genuinely simple HTML — bold text and the like — binding to [innerHTML] can work, but anything more complex calls for templates.
Component
Components sit at the far end of the complexity scale for dynamic content. Templates are fine until logic needs to be encapsulated and that block reused somewhere unrelated in the app; then a dynamic component becomes the answer. These are instantiated through the ngComponentOutlet directive, similarly to templates, and values can be passed in via the ngComponentOutletInputs dictionary. Before inputs existed, another mechanism fit the mental model being built here — dependency injection. With components, the example above becomes:
const CONTEXT = new InjectionToken();
@Component({
template: `{{ user.name }} {{ user.surname }}`
})
class UserComponent {
protected readonly user = inject(CONTEXT, { self: true });
}
@Component({
template: `
<ng-container
[ngComponentOutlet]="component"
[ngComponentOutletInjector]="injector"
/>
`,
})
class App {
protected readonly component = UserComponent;
protected readonly injector = Injector.create({
providers: [{
provide: CONTEXT,
useValue: { name: 'Alex', surname: 'Inkin' },
}],
parent: inject(INJECTOR),
});
}
Dependency injection acts as the context in which the component is created: a dedicated CONTEXT token holds the values the component cares about.
That covers the overview, so it is time for the actual subject.
The universal outlet
As should be clear by now, there is no qualitative difference between these methods. Each takes some "content" and instantiates it with some "context" — plain interpolation simply always carries an empty context. Yet in stock Angular they must all be handled differently. Defining a component input forces a decision about whether a string suffices or whether a TemplateRef might be wanted later. Type checks go inside @if and template logic branches accordingly. That decision is always somewhere in the back of the mind. What if it could work more like generics in TypeScript — a Content<Context> type that behaves identically in every situation, no matter what content is supplied?
That is the origin of Polymorpheus. Low-level components have to stay maximally flexible in both how data is consumed and how it is presented, and Taiga UI is no exception. It became clear early on that usable APIs plus complete visual freedom required a universal outlet, so it was published as a standalone library — before Taiga UI itself was open-sourced.
Using it resembles working with regular templates, but it accepts any of the following content types:
type PolymorpheusContent<C> =
| Type<unknown>
| TemplateRef<C>
| ((context: C) => number | string)
| number | string;
That sketch is simplified; in practice Type (a constructor) and a plain function have to be told apart. Detecting with instanceof requires a PolymorpheusComponent class, which holds the actual constructor along with an optional Injector carrying extra providers when needed.
Rendering the User with it looks like this:
<ng-container
*polymorpheusOutlet="content as primitive; context: { $implicit: user }"
>
{{ primitive }}
</ng-container>
The
$implicitkey follows the official Angular convention for template context. Any context key can be pulled in with<ng-template let-key="key">, while the default$implicitkey applies when writing<ng-template let-user>.
Once a component receives its content as an input, callers can hand it a function, a template, a component, or a plain primitive — and the behavior is identical every time.
Some additional capabilities
Mostly this behaves like a switch statement over Angular's built-in content handlers, but the library includes a few extra affordances.
Default template
Strings had to be declared with as primitive before being placed on the page with {{ primitive }}. The reason is that Angular does not allow arbitrary DOM insertion — everything must be a ViewRef attached to a ViewContainerRef. Processing a primitive, whether provided directly or returned by a provided function, therefore requires a template, and that template is what sits under *polymorpheusOutlet. This makes it possible to branch logic according to the kind of content supplied. Consider this situation:
<tui-icon
*polymorpheusOutlet="content as icon; context: { $implicit }"
[icon]="icon"
/>
Here the default template is the <tui-icon> component and the primitive content is handed to it as [icon]. In practice, callers can pass an icon name and be done, or pass an entire template containing an avatar, button, or badge. Wherever a primitive carries a clear meaning — which happens surprisingly often — it can be handled in its own dedicated way. Taiga UI dialogs are an example: passing just a string yields an HTML paragraph plus an "OK" button for closing. Notifying a user with bold or italic text and having them acknowledge it needs no templates at all.
PolymorpheusTemplate
Anyone adopting this library will come across a directive named PolymorpheusTemplate. Compared to a plain TemplateRef, it brings two advantages.
- Context can be typed by passing its type as an input — a common workaround for a long-standing issue with Angular templates
- It keeps track of the
ChangeDetectorRefof the original template
Templates in Angular follow the change detection of the definition view rather than the instantiation view. The consequence is that change detection can fail to run after a change, contrary to what one would expect. This matters far less now that everything is signal-based, but it used to be a significant problem. PolymorpheusTemplate in place of TemplateRef addresses it, since PolymorpheusOutlet triggers change detection when necessary.
PolymorpheusComponent
As mentioned earlier, this is the special class that makes it possible to tell functions apart from component constructors. It also carries a custom Injector. Since PolymorpheusOutlet hides the actual Injector creation behind an abstraction in order to add the context token, supplying a different injector than the one at the instantiation view is done with new PolymorpheusComponent(component, injector).
Within dynamic components created by PolymorpheusOutlet, the context can be accessed in two ways:
- Inject it from DI with the
injectContext<T>()helper or thePOLYMORPHEUS_CONTEXTtoken - Declare inputs named the same as the context keys
Both approaches trigger change detection when values in the context change, and neither recreates the component.
The payoff
On its own, none of this looks like much. But once the library is in place everywhere custom content gets passed around, the reduction in mental overhead becomes obvious — you stop juggling mechanisms and start thinking only about content and context. That pays off in DX for the person writing the component and for everyone consuming it.
A few places where the difference shows up most clearly:
- Modals. Often a quick dialog with a handful of HTML blocks is all you need, and templates handle that fine. Other times the same dialog has to work across several pages, like a generic confirmation prompt — that calls for a component. Your modal infrastructure doesn't need to distinguish between the two.
- Tooltips. Usually plain text is sufficient, a nicer stand-in for the native title attribute. Sooner or later, though, you want a link or a button inside it, and a string won't get you there —
TemplateRefsupport is required. Nothing about the behavior has to be rethought when that day comes. - Errors. An Angular error is an object whose keys are validators and whose values describe what each validator reported. To render validation errors on a form, a common approach is to inject a dictionary of human-readable strings through DI. But what happens when you need to surface the specifics, like the maximum length from
Validators.maxLength? A function is the natural fit:
({requiredLength}) => `Maximum length — <b>${requiredLength}</b>`
More examples surely exist. The pattern holds: a plain string covers many cases, a function covers context, a template covers whole DOM blocks, and a component covers reuse. Quit deliberating over which one to pick, think in higher-order terms, and reach for Polymorpheus.

