Sitemap
Ivan Kudria

Ivan Kudria

Experienced Frontend Developer | Expert in Angular, TypeScript, and JavaScript.

Programmatic Component Creation Using ViewContainerRef

13 min readSep 14, 2024

--

Press enter or click to view image in full size
Angular 18
Angular 18

In the latest Angular version, working with components and dynamic templates has been significantly simplified. Gone are the days when we had to rely on component factories and manually create component instances. With the new approach, we can inject components into our templates programmatically, providing a more streamlined and intuitive developer experience.

In this article, we’ll dive deeper into these improvements, demonstrating how to dynamically render components using the updated Angular APIs.

In Angular 18, there are two main ways to dynamically render a component: in a template using NgComponentOutlet, or in your TypeScript code using ViewContainerRef.

Let’s start with ViewContainerRef, as it's a great way to begin exploring this topic. We'll build a simple app to test out ViewContainerRef and demonstrate its use in a real example.

Simple app component

<!-- app.component.ts -->
<section class="main">
<button>Load content</button>
</section>
@Component({
selector: 'app-root',
standalone: true,
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent implements AfterViewInit {
viewContainer = inject(ViewContainerRef);
el = inject(ElementRef);

ngAfterViewInit() {
console.log(this.viewContainer);
console.log(this.el);
}
}

The key to understanding ViewContainerRef is in its name — it provides a reference to the view container.

Let’s break down the code and the console output from the below screenshot to explain what’s happening in detail.

In this code, we’re using Angular’s dependency injection to get two key references: ViewContainerRef and ElementRef. Let's dive into what each of these does and how they appear in our console output.

Press enter or click to view image in full size

ViewContainerRef:

  • What it is: ViewContainerRef provides a reference to a view container, which is a logical structure in Angular where views (or templates) are attached. Essentially, it's a placeholder where you can dynamically insert components or templates.
  • How it’s used: The ViewContainerRef allows you to programmatically control the insertion, movement, or removal of views within the Angular component. This is particularly useful when you need to create or manipulate components dynamically during runtime.

In the console output:

  • The screenshot shows that ViewContainerRef has properties like _hostLView, _hostTNode, and _lContainer. These are lower-level representations of the view container's state:
  • _hostLView and _hostTNode represent the internal structure of Angular's view engine, providing context and metadata about the current view.
  • _lContainer is an array representing the embedded views within this container. This array contains references to the views that have been dynamically inserted or will be inserted.

ElementRef:

  • What it is: ElementRef provides a direct reference to the DOM element of the component, or the host element. This reference is useful when you need to interact with the DOM directly, bypassing Angular’s templating and change detection mechanisms.
  • How it’s used: ElementRef is primarily used for DOM manipulation, for example, if you need to modify styles or attributes directly on the element, or if you need to access properties or methods not covered by Angular's templating engine.

In the console output:

  • The ElementRef in our screenshot contains a nativeElement property that refers to the actual DOM element (app-root in this case). This is the element to which the component is attached.

Enough theory — let’s get hands-on and create our own component!

We’ll start with the following code:

// app.component.html
<section class="main">
<button (click)="loadContent()">Load content</button>
</section>
// app.component.ts
export class AppComponent {
viewContainer = inject(ViewContainerRef);

loadContent(): void {
this.viewContainer.createComponent(UserCardComponent);
}
}

Note: Even though it’s called a “container,” we don’t create the new component inside the existing container. Instead, we use the current component as a reference point to create the new component right after it. This approach is somewhat similar to how you might use JavaScript’s appendChild or insertAdjacentElement methods to add elements after an existing one. So it would create component next to app-root , but not inside it. That is why we usually using ng-container to create a spot for our component. So if you need to create a component inside app-root you have to add ng-container as a spot for you component.

Here is an example:

// app.component.html
<section class="main">
<ng-container #spot></ng-container>
<button (click)="loadContent()">Load content</button>
</section>
// app.component.ts
@Component({
selector: 'app-root',
standalone: true,
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent {
componentSpot = viewChild.required('spot', { read: ViewContainerRef });

loadContent(): void {
this.componentSpot().createComponent(UserCardComponent);
}
}

It’s that simple. To dynamically create a component, we just need to use ViewContainerRef and call createComponent.

But let’s take a look at the magic happening inside createComponent.

Here’s the function used to create a component:

abstract createComponent<C>(
componentType: Type<C>,
options?: {
index?: number;
injector?: Injector;
ngModuleRef?: NgModuleRef<unknown>;
environmentInjector?: EnvironmentInjector | NgModuleRef<unknown>;
projectableNodes?: Node[][];
},
): ComponentRef<C>;

There are some interesting options like injector and projectableNodes.

These options give us the ability to create a truly dynamic component. What if our component isn’t so simple and needs to include content and dependency injection?

These two options are designed to help with that.

Let’s create a service and a token for injection to see how this works:

@Injectable()
export class DataService {
data = {
name: 'DataServiceService'
}
}

export const DataServiceToken = new InjectionToken<{ data: { name: string } }>('Data Service Token');

Next, let’s add a dependency to our simple UserCardComponent. To create it, we need to pass the elementInjector and:

<p> {{ dataServiceToken.data.name }} </p>
<p> {{ dataService.data.name }} </p>
@Component({
selector: 'app-user-card',
standalone: true,
imports: [],
templateUrl: './user-card.component.html',
styleUrl: './user-card.component.scss'
})
export class UserCardComponent {
dataServiceToken = inject(DataServiceToken);
dataService = inject(DataService);
}

If we try to create our component now, we’ll get an error: NullInjectorError: No provider for InjectionToken Data Service Token!. This happens because we haven't added an injector or dependencies to our dynamic component yet.

To fix this, we need to create our own injector, add our dependencies to it, and pass it to the injector option when creating the dynamic component.

@Component({
selector: 'app-root',
standalone: true,
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent {
viewContainer = inject(ViewContainerRef);
injector = inject(Injector);

myInjector = Injector.create({
providers: [{ provide: DataServiceToken, useValue: { data: { name: 'DataServiceToken Hi!' } } }, DataService],
parent: this.injector,
});

loadContent(): void {
this.viewContainer.createComponent(UserCardComponent, { injector: this.myInjector });
}
}

As a result, when you click the button, you’ll see that our component is successfully created and receives the necessary dependencies.

What Does parent: this.injector Do?

  • Parent Injector: The parent property in the Injector.create() method specifies the injector that the newly created injector will fall back to if it can't resolve a dependency on its own. This means if the new injector can't find a provider for a particular token, it will delegate the lookup to its parent injector.
  • Default Behavior: By default, Angular provides a hierarchical dependency injection system. When you inject a service or a token into a component, Angular first checks the component’s injector. If it can’t find the requested service there, it looks in the parent injectors, moving up the injector tree until it either finds the provider or reaches the root injector.

Let’s Try It in Practice

Let’s modify our example by removing DataService from the providers of our custom injector and adding it to our parent component instead:

@Component({
selector: 'app-root',
standalone: true,
templateUrl: './app.component.html',
styleUrl: './app.component.scss',
providers: [DataService] // <-- MOVED DataService here
})
export class AppComponent {
injector = inject(Injector);
componentSpot = viewChild.required('spot', { read: ViewContainerRef });

myInjector = Injector.create({
providers: [{ provide: DataServiceToken, useValue: { data: { name: 'DataServiceToken Hi!' } } }],
// parent: this.injector,
});

loadContent(): void {
this.componentSpot().createComponent(UserCardComponent, { injector: this.myInjector });
}
}

If we attempt to load our component without passing the parent injector, it won’t be able to obtain dependencies from the parent, and we’ll encounter the same error: NullInjectorError: No provider for DataService!. This happens even though DataService is provided at the parent component level, and we might expect to be able to access an instance of this class from the parent.

By including parent: this.injector in the custom injector’s creation, we allow the new injector to fall back on the parent injector for unresolved dependencies. This will fix the NullInjectorError, allowing the DataService to be successfully injected into our dynamically created component.

Programmatically Creating Dynamic Content

In the previous section, we explored how to create components programmatically using ViewContainerRef and dynamically inject dependencies. Now, let's take this further and demonstrate how to dynamically create and project content into the UserCardComponent, such as custom headers and footers.

We can enhance the flexibility of our component by dynamically inserting content into the UserCardComponent through Angular’s content projection. This allows us to define custom templates in the parent component (such as a header and footer) and project them into the dynamically created component, while still using default content if nothing is passed.

Get Ivan Kudria’s stories in your inbox

Join Medium for free to get updates from this writer.

Here’s the updated code of AppComponent:

<section class="main">
<ng-container #spot></ng-container>
<button (click)="loadContent()">Load content</button>
</section>

<ng-template #header>
<div class="footer">Header from APP</div>
</ng-template>

<ng-template #footer>
<div class="footer">Footer from APP</div>
</ng-template>
export class AppComponent {
injector = inject(Injector);
componentSpot = viewChild.required('spot', { read: ViewContainerRef });

header = viewChild.required('header', { read: TemplateRef });
footer = viewChild.required('footer', { read: TemplateRef });

myInjector = Injector.create({
providers: [{ provide: DataServiceToken, useValue: { data: { name: 'DataServiceToken Hi!' } } }],
parent: this.injector,
});

loadContent(): void {
const contentList = [
this.componentSpot().createEmbeddedView(this.header()).rootNodes,
this.componentSpot().createEmbeddedView(this.footer()).rootNodes
];

this.componentSpot().createComponent(UserCardComponent, { injector: this.myInjector, projectableNodes: contentList });
}
}

Understanding the Code

In this updated example, we use three ViewChild references:

  • componentSpot: The placeholder (ng-container) where the UserCardComponent will be dynamically inserted.
  • header and footer: These references point to the ng-template elements that contain the custom header and footer content we want to project into the UserCardComponent.

In the loadContent() method:

  • We use createEmbeddedView() to render the ng-template content (header and footer) and obtain a list of DOM nodes (rootNodes).
  • We then use createComponent(UserCardComponent, { projectableNodes: contentList }), where projectableNodes allows us to specify dynamic content to be projected into <ng-content> slots inside the UserCardComponent.

Now, let’s take a look at how the UserCardComponent is structured to support content projection:

<mat-card>
<mat-card-header>
<ng-content></ng-content>
</mat-card-header>

<p> {{ dataServiceToken.data.name }} </p>
<p> {{ dataService.data.name }} </p>

<mat-card-footer>
<ng-content></ng-content>
</mat-card-footer>
</mat-card>

In this template, we use <ng-content> inside <mat-card-header> and <mat-card-footer> to define content projection slots. This allows us to insert dynamic content, such as headers and footers, into specific parts of the component.

How it Works

When the user clicks the “Load content” button, the following happens:

  • The header template (<div class="footer">Header from APP</div>) is projected into the <mat-card-header> section of the UserCardComponent.
  • The footer template (<div class="footer">Footer from APP</div>) is projected into the <mat-card-footer> section of the UserCardComponent.

If no custom header or footer was provided, the component would still function as usual, using default content, ensuring the component is always usable even without external templates.

To add even more flexibility and functionality, we can allow the dynamic component to accept custom content from the parent component while still providing default content when nothing is passed. This is particularly useful when you want to reuse a component but customize certain sections, such as headers or footers, depending on the context in which it’s used.

Customizing the Parent Component with Inputs

In the parent component, we can define custom header and footer templates and pass them to the dynamically created UserCardComponent. To do this, we use the createComponent method, which returns a ComponentRef. The ComponentRef gives us direct access to the dynamically created component instance, allowing us to use setInput to assign values to the component’s inputs.

Here’s how you can define the custom templates and pass them to the UserCardComponent:

<section class="main">
<ng-container #spot></ng-container>
<button (click)="loadContent()">Load content</button>
</section>

<ng-template #header>
<div class="header">Header from APP</div>
</ng-template>

<ng-template #footer>
<div class="footer">Footer from APP</div>
</ng-template>

In the corresponding AppComponent, we dynamically create the UserCardComponent and use the setInput method to assign the custom header and footer templates:

@Component({
selector: 'app-root',
standalone: true,
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
providers: [DataService]
})
export class AppComponent {
injector = inject(Injector);
componentSpot = viewChild.required('spot', { read: ViewContainerRef });

footer = viewChild.required('footer', { read: TemplateRef });
header = viewChild.required('header', { read: TemplateRef });

myInjector = Injector.create({
providers: [{ provide: DataServiceToken, useValue: { data: { name: 'DataServiceToken Hi!' }}}],
parent: this.injector,
});

loadContent(): void {
// Create the component and get a reference to it
const componentRef = this.componentSpot().createComponent(UserCardComponent, { injector: this.myInjector });

// Use setInput to pass custom header and footer templates to the component
componentRef.setInput('header', this.header());
componentRef.setInput('footer', this.footer());
}
}

In this example, createComponent() returns a ComponentRef object, which provides access to the newly created UserCardComponent. By calling setInput, we can programmatically set the header and footer inputs of the component.

Updating UserCardComponent

For this to work, we need to modify the UserCardComponent to accept these inputs and project the custom content. Here’s how we can extend the UserCardComponent to support these inputs:

@Component({
selector: 'app-user-card',
standalone: true,
templateUrl: './user-card.component.html',
styleUrls: ['./user-card.component.scss']
})
export class UserCardComponent {
header = input<TemplateRef<HTMLElement>>();
footer = input<TemplateRef<HTMLElement>>();

dataServiceToken = inject(DataServiceToken);
dataService = inject(DataService);
}

We add two input() properties (header and footer) to the component, which will accept the custom templates passed from the parent component. These inputs allow the component to dynamically receive content and render it.

Next, we update the template of the UserCardComponent to conditionally render the custom content:

export class UserCardComponent {
header = input<TemplateRef<HTMLElement>>();
footer = input<TemplateRef<HTMLElement>>();

userLabel = input.required<TemplateRef<unknown>>();
dataService = inject(DataService);
}
<mat-card>
<mat-card-header>
<ng-container *ngTemplateOutlet="header() || defaultHeader"></ng-container>
</mat-card-header>

@for (item of dataService.data; track item.label; let index = $index) {
<ng-container [ngTemplateOutlet]="userLabel()" [ngTemplateOutletContext]="{ $implicit: item, index }">
</ng-container>
}

<mat-card-footer>
<ng-container *ngTemplateOutlet="footer() || defaultFooter"></ng-container>
</mat-card-footer>
</mat-card>

<ng-template #defaultHeader>
<div>Default Header</div>
</ng-template>

<ng-template #defaultFooter>
<div>Default Footer</div>
</ng-template>

In this template:

  • We use the *ngTemplateOutlet directive to render either the custom header and footer provided by the parent component or fall back to the default header and footer if no custom templates are passed.

Using Template Variables and Passing Templates via Inputs

In Angular, you can further enhance the flexibility of your dynamic components by using template variables (let-var) and passing templates through component inputs. This approach allows the parent component to pass not only the templates but also contextual data to be rendered dynamically in the child component.

Let’s add some dummy data to our DataService for demonstration purpouse.

export interface UserLabel {
label: string;
value: number
}

@Injectable()
export class DataService {
data: UserLabel[] = [{ value: 1, label: 'Label1' }, { value: 2, label: 'Label2' }, { value: 3, label: 'Label3' }, { value: 4, label: 'Label4' }];
}

Defining and Passing Template Variables

In the AppComponent, we define a userLabel template with let-var syntax to extract data from the context provided by the parent component. Here’s how the template is structured:

<section class="main">
<ng-container #spot></ng-container>
<button (click)="loadContent()">Load content</button>
</section>

<ng-template #header>
<div class="header">Header from APP</div>
</ng-template>

<ng-template #footer>
<div class="footer">Footer from APP</div>
</ng-template>

<ng-template #userLabel let-labelOption let-index="index">
<mat-hint> {{ labelOption.label }} - {{ index | json }} </mat-hint>
<mat-hint> {{ labelOption.value }} </mat-hint>
</ng-template>

In this template:

  • let-labelOption extracts the labelOption object passed from the parent.
  • let-index="index" extracts the index of the item in the list.
  • Here, the labelOption is passed as the $implicit value, meaning it is the default value in the context, while index is explicitly passed via the context object.

Next, in the AppComponent class, we dynamically pass these templates and context to the UserCardComponent via setInput:

export class AppComponent {
injector = inject(Injector);
componentSpot = viewChild.required('spot', { read: ViewContainerRef });

footer = viewChild.required('footer', { read: TemplateRef });
header = viewChild.required('header', { read: TemplateRef });

userLabel = viewChild.required('userLabel', { read: TemplateRef });

myInjector = Injector.create({
providers: [{ provide: DataServiceToken, useValue: { data: { name: 'DataServiceToken Hi!' }}}],
parent: this.injector,
});

loadContent(): void {
// Create the component and get a reference to it
const componentRef = this.componentSpot().createComponent(UserCardComponent, { injector: this.myInjector });

// Use setInput to pass custom header and footer templates to the component
componentRef.setInput('header', this.header());
componentRef.setInput('footer', this.footer());
componentRef.setInput('userLabel', this.userLabel());
}
}

Here:

  • The loadContent() method creates the UserCardComponent dynamically and uses setInput to pass the header, footer, and userLabel templates as inputs to the child component.
  • The userLabel template is passed along with the other templates, allowing the UserCardComponent to use it to render contextual data dynamically

Rendering Templates and Using Context in the UserCardComponent

Now, in the UserCardComponent, we handle the incoming templates, including userLabel, which is used to render user-specific data passed from the parent:

@Component({
selector: 'app-user-card',
standalone: true,
imports: [
MatCard,
MatCardFooter,
NgTemplateOutlet,
MatCardHeader,
JsonPipe
],
templateUrl: './user-card.component.html',
styleUrl: './user-card.component.scss'
})
export class UserCardComponent {
header = input<TemplateRef<HTMLElement>>();
footer = input<TemplateRef<HTMLElement>>();
userLabel = input.required<TemplateRef<unknown>>();
dataService = inject(DataService);
}

In the template of UserCardComponent, we use ngTemplateOutlet to render the templates passed as inputs. Additionally, we use ngTemplateOutletContext to pass the dynamic data (e.g., labelOption and index) to the userLabel template:

<mat-card>
<mat-card-header>
<ng-container *ngTemplateOutlet="header() || defaultHeader"></ng-container>
</mat-card-header>

@for (item of dataService.data; track item.label; let index = $index) {
<ng-container [ngTemplateOutlet]="userLabel()" [ngTemplateOutletContext]="{ $implicit: item, index }">
</ng-container>
}

<mat-card-footer>
<ng-container *ngTemplateOutlet="footer() || defaultFooter"></ng-container>
</mat-card-footer>
</mat-card>

<ng-template #defaultHeader>
<div>Default Header</div>
</ng-template>

<ng-template #defaultFooter>
<div>Default Footer</div>
</ng-template>

Here’s how the userLabel template works inside the UserCardComponent:

  • For each item in dataService.data, we pass the item itself ($implicit) and its index to the userLabel template via ngTemplateOutletContext.
  • The userLabel template uses let-labelOption and let-index="index" to receive and display this data.
  • The $implicit value refers to data passed without being explicitly named. It is the default value that is available in the template context when no specific key is defined. On the other hand, named variables like index are explicitly declared in the context object, making them directly accessible by their name.

This allows the UserCardComponent to dynamically render customized content based on the context provided by the parent component, making it highly flexible and reusable.

Conclusion

In this article, we explored how to dynamically create components using Angular’s ViewContainerRef and Injector, while also passing templates and context dynamically via component inputs. We demonstrated how you can use template variables (let-var) in combination with ngTemplateOutlet and ngTemplateOutletContext to pass contextual data and render complex, customized content.

By using tools such as setInput, ngTemplateOutlet, and Angular’s powerful content projection capabilities, you can build highly flexible and reusable components that adapt to different contexts without sacrificing ease of use.

In the next article, we will explore an alternative approach to dynamic component rendering using NgComponentOutlet. This directive offers another powerful way to manage dynamic content in Angular applications. Stay tuned!

--

--