Programmatic Component Creation Using ViewContainerRef
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.
ViewContainerRef:
- What it is:
ViewContainerRefprovides 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
ViewContainerRefallows 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
ViewContainerRefhas properties like_hostLView,_hostTNode, and_lContainer. These are lower-level representations of the view container's state: _hostLViewand_hostTNoderepresent the internal structure of Angular's view engine, providing context and metadata about the current view._lContaineris 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:
ElementRefprovides 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:
ElementRefis 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
ElementRefin our screenshot contains anativeElementproperty that refers to the actual DOM element (app-rootin 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
appendChildorinsertAdjacentElementmethods to add elements after an existing one. So it would create component next toapp-root, but not inside it. That is why we usually usingng-containerto create a spot for our component. So if you need to create a component insideapp-rootyou have to addng-containeras 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
parentproperty in theInjector.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 theUserCardComponentwill be dynamically inserted.headerandfooter: These references point to theng-templateelements that contain the custom header and footer content we want to project into theUserCardComponent.
In the loadContent() method:
- We use
createEmbeddedView()to render theng-templatecontent (header and footer) and obtain a list of DOM nodes (rootNodes). - We then use
createComponent(UserCardComponent, { projectableNodes: contentList }), whereprojectableNodesallows us to specify dynamic content to be projected into<ng-content>slots inside theUserCardComponent.
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
headertemplate (<div class="footer">Header from APP</div>) is projected into the<mat-card-header>section of theUserCardComponent. - The
footertemplate (<div class="footer">Footer from APP</div>) is projected into the<mat-card-footer>section of theUserCardComponent.
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
*ngTemplateOutletdirective to render either the customheaderandfooterprovided 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-labelOptionextracts thelabelOptionobject passed from the parent.let-index="index"extracts the index of the item in the list.- Here, the
labelOptionis passed as the$implicitvalue, meaning it is the default value in the context, whileindexis 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 theUserCardComponentdynamically and usessetInputto pass theheader,footer, anduserLabeltemplates as inputs to the child component. - The
userLabeltemplate is passed along with the other templates, allowing theUserCardComponentto 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 theuserLabeltemplate viangTemplateOutletContext. - The
userLabeltemplate useslet-labelOptionandlet-index="index"to receive and display this data. - The
$implicitvalue 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 likeindexare 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!









