Angular is a great framework. It is also a very considerate one, lots of things are taken care of, and when you need something custom – behaviors are easy to override or extend. Today I want to talk about one such case and to show you a tool I created many years ago which I considered an Angular must-have ever since. I want to explore a tiny library called Polymorpheus that we use in Taiga UI absolutely everywhere when we need to display some dynamic content. But first, let’s recap how Angular does this and why we might consider going beyond the default ways.
Dynamic content in Angular
Assume we have an object – a User – that we want to display on the page. Let’s explore what options we have at our disposal out of the box, what are the capabilities and limitations.
Interpolation
The most basic way to display our user is to just print it on the page with interpolation:
{{ user }}
This would call its toString() method and display [object Object], so If we want to display it properly, we need a different toString() implementation:
class User {
constructor(
readonly name: string,
readonly surname: string,
) {}
toString(): string {
return `${this.name} ${this.surname}`;
}
}
But that’s not a very handy way to display things. It requires manual interpolation of object keys or a custom toString() method which is not something people usually do. Let’s move on to a more advanced technique.
Function
Instead of laying out our object keys or baking a stringified representation into the data itself, we can rely on a function call:
{{ stringify(user) }}
function stringify({ name, surname }: User): string {
return `${name} ${surname}`;
}
Calling functions from the template always had a bad rap. But that was always a fallacy. The idea was that the function gets called on each change detection cycle, so it’s bad. In reality, it is only bad when the actual computation is heavy. I did extensive benchmarking long ago, when this subject was more pressing since we didn’t have signals and the default change detection was Eager. All string operations, math, Boolean logic, or even small loops are so fast that you would never get any noticeable performance degradation from using functions in templates. Especially if you use OnPush and break down your views properly, so when you click somewhere – it’s not your entire app that gets checked for changes.
One important thing to keep in mind here – do not create new arrays, objects, or classes inside those function calls. Not only this is a much heavier operation, but that also effectively creates a new value each time, meaning the inputs get triggered, loops re-run, and entire DOM sections can get rebuilt.
While this case seems a lot like the previous one, it has a very important distinction – it introduces context. We have our representation function stringify that receives a user as an argument. So this approach works the same way for any user, and the result is determined by the context – actual user object. This moves us to the next step.
Template
Another way we can display our user, and the one we have to use if we want something more than primitives – ng-template. The idea of context is more than just an idea now, it is official terminology. Here’s how our simple situation plays out with templates:
<ng-container
[ngTemplateOutlet]="template"
[ngTemplateOutletContext]="{ $implicit: user }"
/>
<ng-template #template let-user>
{{ user.name }} {{ user.surname }}
</ng-template>
This is what we use if we need, for example, to display an avatar alongside a user name or some other Angular components and directives. Sometimes we can bind to [innerHTML] if we just want simple HTML like bold text etc., but for anything remotely complex templates is our goto solution.
Component
At the farthest end of complexity for dynamic content, we have components. Templates are great, but what if you want to encapsulate some logic and reuse this block in an unrelated part of the app? Dynamic components can be the solution. They are instantiated via ngComponentOutlet directive, akin to templates, and they can receive input values through ngComponentOutletInputs dictionary. But before inputs were added, there was another way that fits better in the mental model we’re building here – dependency injection. Here’s how we would handle the case above using components:
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),
});
}
We treat dependency injection as the context in which the component is created, providing a dedicated token CONTEXT that stores the values that concern the component.
This sums up the overview, and we can move on to the actual subject.
Universal outlet
By now you probably noticed that there is no qualitative difference between all the listed methods. They all take some "content" and instantiate it with some "context" (basic interpolation just always has empty context). But in plain Angular we have to treat them differently. When we define an input for a component we always have to decide whether just string would be enough or if later we would want to include TemplateRef. We have to do type checking inside @if and branch template logic. This is always a conscious decision in the back of our minds. But what if we could treat this kind of like we treat generics in TypeScript? What if we had a Content<Context> type that works the same way in every situation regardless of the actual content used?
This is how Polymorpheus was created. In low-level components you have to be as flexible as ever in both data consumption and data representation. Taiga UI is not an exception. We figured early on that if we want our APIs to be easy to use and allow complete visual freedom – we need to create a universal outlet. So we published it as a separate library even before we open-sourced Taiga UI itself.
The usage is a lot like regular templates, but it consumes any content of the following type:
type PolymorpheusContent<C> =
| Type<unknown>
| TemplateRef<C>
| ((context: C) => number | string)
| number | string;
This is a rough overview, in reality we need to be able to differentiate between Type (constructor) and regular function. So to use instanceof we also need a PolymorpheusComponent class that stores the actual constructor as well as optional Injector with additional providers if you need any.
Here’s how we would use it to handle the User display:
<ng-container
*polymorpheusOutlet="content as primitive; context: { $implicit: user }"
>
{{ primitive }}
</ng-container>
Note
$implicitkey – this is an official Angular convention for template context. You can query any context key using<ng-template let-key="key">but the default$implicitkey is used when you just write<ng-template let-user>.
Now if our component gets content as an input – people can pass anything, a function, template, component, or just a primitive value, and it works the same way each time.
A few more tricks
While in most cases you can view this as a switch case for built-in Angular content handlers – there are a few additional benefits built into the library.
Default template
You saw how we had to declare strings with as primitive to later put them into the page with {{ primitive }}. That’s because Angular does not allow arbitrary additions to the DOM. Everything is a ViewRef attached to a ViewContainerRef. Therefore, if we need to process a primitive, either because it was provided or because it was returned by a provided function – we have to use a template. And that template is what you saw under *polymorpheusOutlet. What this means is that we can branch our logic based on the kind of content that was provided. Consider this situation:
<tui-icon
*polymorpheusOutlet="content as icon; context: { $implicit }"
[icon]="icon"
/>
This means that the default template is the <tui-icon> component and the primitive content is fed to it as [icon]. Basically, this allows people to just pass an icon name as input, but if they want – they can pass a whole template with an avatar, button or badge. So in situations where a primitive content means something clear, which surprisingly often is the case – you can handle it in a dedicated way. For example, in Taiga UI dialogs, if you just pass a string, you would get an HTML paragraph plus an "OK" button to close it. So there’s no need for templates if you just want to notify a user about something, with bold or italic text, for example, and have them acknowledge it.
PolymorpheusTemplate
If you start using this library, you might find a directive called PolymorpheusTemplate. There are two benefits of using it against plain old TemplateRef.
- It allows you to type context by passing its type as an input, a common workaround to a long-standing issue with Angular templates
- It keeps track of
ChangeDetectorRefof original template
Templates in Angular follow change detection of the definition view, not instantiation view. This means that you can have change detection not running when you change something, although you would expect it to. This is much less relevant now, when everything is a signal, but it was a pretty big deal before. To take care of that, you could use PolymorpheusTemplate instead of TemplateRef and PolymorpheusOutlet would trigger change detection for you when needed.
PolymorpheusComponent
Like I said before, this is a special class that we need to be able to distinguish between functions and component constructors. It is also used to pass custom Injector. Since PolymorpheusOutlet abstracts actual Injector creation away to be able to add the context token – if you need to provide a different injector than the one located at the instantiation view, you could do that with new PolymorpheusComponent(component, injector).
Inside dynamic components, created with PolymorpheusOutlet you have two options for accessing the context:
- Inject it from DI using
injectContext<T>()helper orPOLYMORPHEUS_CONTEXTtoken - Have inputs that are named the same way context keys are named
Both approaches will trigger change detection if values inside context change, without recreating the component.
Takeaway
This might not seem that impressive at first. However, once you start using this library everywhere you pass custom content – you will quickly discover how good it feels when the cognitive load is alleviated, and you start thinking simply in terms of content and context. This significantly improves the DX of both you, the component creator, and other developers who are component consumers.
Common situations where this is particularly apparent:
- Modals. Sometimes you want just a quick dialog with some HTML blocks. Then templates work great. But sometimes you want to reuse the same dialog across multiple pages, like a generic confirmation prompt. Then you have to go for a component. Your modal infrastructure does not have to care.
- Tooltips. In most cases, you just want a simple text, like a fancy substitute for native title attribute. But you might want to have links or buttons inside it at some point, which means strings are not enough – you need to support
TemplateRef. No need to rethink behavior when that happens. - Errors. Angular errors are objects with keys representing validators, and values are what they had reported as the issue. When you display validation errors on your form – a great way to handle this is to provide a dictionary of user-readable strings with DI. But what if you want to convey the details, such as the maximum length allowed by
Validators.maxLength? Using a function feels natural here:
({requiredLength}) => `Maximum length — <b>${requiredLength}</b>`
I’m sure you can come up with more examples. The bottom line is, for many cases plain string input is enough. When you need context, that becomes a function. When you need whole DOM blocks, it becomes a template. When you need reusability – you go for components. Stop bothering yourself with choices, embrace higher order thinking, and use Polymorpheus.

