In this tutorial, you will learn what dynamic components are and how they work in angular.
You will do so by creating a very flexible dialog system, that demonstrates how dynamic components are used.
We will learn how to create dynamic components and attach them to the DOM or use them in other components.
Also, you will discover how to provide objects to these dynamic components using dependency injection and custom angular injectors.
At the end, we will come up with a dialog system that is quite similar to the one provided by angular material.
Of course, everything is reduced to the core and split up, so we don’t lose the overview.
Ready to learn something new?
Let’s get started!
What are dynamic components in angular?
Before we start, let’s make sure we know what “dynamic” components actually are.
What dynamic components are
Dynamic means, that the components location in the application is not defined at buildtime. That means, that it is not used in any angular template.
Instead, the component is instantiated and placed in the application at runtime.
What dynamic components are not
Because of the broad use of the word “dynamic”, one could think that you could “dynamically” load just any component from the internet at runtime.
This is not the case though.
Because of the way angular works, it has to know about every component at buildtime.
That means that the component has to be defined in the same project or has to be imported into an angular module of the project.
Setting up a new angular project
Before we get started, we need an angular project to work on. In this tutorial, we are going to generate a new angular-cli project. If you want to use an existing project, that should be fine, too.
To generate a new project, make sure you have the angular-cli installed and use this command at the desired project destination:
ng new angular-dynamic-components
When the tool is done, we should be good to go.
Creating an empty dialog component
To get started, let’s create an empty dialog component. This component will consist of a white dialog area, while the rest of the screen is blurred.
We want the dialog to live in its own module. To do that, we generate a new module using the anuglar-cli.
ng generate module dialog
Afterward, we generate the dialog component like so:
ng generate component dialog
The cli will add the component to the same folder as the module. Also, make sure that the dialog component is declared in the dialog module instead of the app module.
The dialog effect
Achieving his dialog-effect is actually quite simple and does only require a little bit of HTML and CSS.
Basically, the components template does only consists of two div-elements:
The required CSS is nothing to fancy either:
Adding the callbacks to the component
Because we are already assigning click-callbacks in our template, let’s make sure they are defined in our component-class, as well.
Notice, that the component does implement the AfterViewInit and OnDestroy interfaces, as we will the hooks later.
While we will implement onOverlayClicked later, we make sure that the click event on the dialog itself is not propagated to the parent elements.
Otherwise, the onOverlayClicked callback would always fire, even if we hit the dialog itself and not the overlay (the overlay is the gray area around the dialog). We do so in the onDialogClicked method.
Also, the component has to know which type of component it will has to create later. For that, we are adding the property childComponentType.
Using a service to append the dialog to the HTML-body
Now that we have a working dialog component, we could just add it to the app-component and it would work just fine. Ok, maybe we would have to add an @Input to toggle it on and off, but that would not be an issue, right?
We will not do that in this tutorial!
Why?
Because this is all about dynamic components. Also, we want our component to live in the HTML-body to make sure that the dialog always fills the whole screen.
So how do we do that then?
We will use a service to instantiate the component and place it into the body. Doesn’t that sound crazy?
Actually, this approach is quite convenient to use. You maybe have used it already, as this is exactly the same approach as the guys from angular material are using for their dialog component.
The service
So let’s begin by creating a new service.
Again, we are using the angular-cli to do this:
ng generate service dialog/dialog
Inside of the service class, we create a new method called “appendDialogComponentToBody”.
Also, we need a property called dialogComponentRef, that will hold a reference to the instance of the DialogComponent that we will create.
Furthermore, we need to request a bunch of stuff via dependency injection.
Dynamically instantiating the dialog component using its factory
Angular is creating a factory for each component when building. At least when using ahead of time compilation (AOT).
Because this is the default configuration now, we are using AOT for this tutorial exclusively. When using just in time compilation (JIT) you may need to take additional steps.
To get the factory of our DialogComponent we can use the ComponentFactoryResolver provided by angular. This service is using the type of the component to look up the factory.
Once we have the factory, we can use it to create an instance of our DialogComponent.
We are passing in the injector we requested in the constructor. This enables the dynamic component to make use of dependency injection itself.
Afterward, we need to attach the new component to the angular component tree (which is separate from the DOM). We do so by using the ApplicationRef we requested.
Last but not least, we get the root DOM-element of our DialogComponent and attach it to the HTML-body. Also, we assign the componentRef to our property.
Destroying the component
We also need a way to remove the component once the dialog is closed.
For that, we are creating a method called “removeDialogComponentFromBody”. Basically, it is undoing the steps we did before.
Opening the dialog dynamically
Now that we are able to add the dialog to the DOM, all we need to do to open the dialog is to call our method.
To do that, we define a public method called “open”.
Inside of that method we call our appendDialogComponentToBody-method to open the empty dialog.
Because our DialogComponent is not used anywhere (in a template), angular will not include it in the final source code. To prevent that, we can add it to the entryComponents of our DialogModule:
Injecting other components into the dialog
Because empty dialogs are quite useless, we will enable our dialog to show any other component, next.
Doing that, we will pass the Type of the component we want to spawn inside of our dialog to the services “open”-method.
Inside of the method, we assign that type to our dynamically created DialogComponent.
Of course, our DialogComponent does not know what to do with that type, yet.
To change that, we need to modify our dialog to instantiate dynamic components and place them in itself.
This is done quite similar to how we dynamically created the DialogComponent. Except that we now don’t want to place our component inside the HTML-Body but in a certain place in our DialogComponent.
To tell angular the exact place to inject our component into, we need to create a custom directive.
Creating a custom directive to mark the insertion-point
Again, we are generating this directive using the angular-cli like so:
ng generate directive dialog/insertion
We call this directive “insertion” as it will mark the point where the dynamic component will be inserted.
All we have to do with that directive is to add a ViewContainerRef property.
Modifying the dialog to spawn a dynamic child-component
Now that we have our insertion-directive, we can mark the place for our dynamic child-component.
To do that, we simply add a ng-template and add the attribute “appInsertion” of our directive.
To get a reference to that insertion-directive in our component class, we add a @ViewChild decorated property called insertion point. Also, we need to request the CompoentFactoryResolver to our constructor.
To actually load the child-component, we are going to create a new method called “loadChildComponent”. This method takes the type of the child-component as a parameter. We then use this type to resolve the factory for this component. With the help of the factory and the insertion point, we then instantiate the dynamic child-component.
We do so by getting the ViewContainerRef of the directive (we added that property to the directive before) and using it to create the component.
While we are at it, we also need to make sure, the child-component is property destroyed with its parent. To do that, we call destroy on the componentRef on ngOnDestroy.
Loading the child component AfterViewInit
Because we are using a reference to the view in loadChildComponent, we need to make sure that the view is fully loaded before we use it. That is why we are calling the method in ngAfterViewInit.
There is only one problem: We are changing the view (by adding a child component) but angular thinks it is already done with the view-part. That’s why the hook is called AFTERViewInit.
This would result in an ExpressionChangedAfterItHasBeenCheckedError. To prevent that, we need to tell angular to re-run change detection after we have added the component.
For that, we need to add the ChangeDetectorRef to our constructor.
We then trigger change detection, once our dynamic child-component is loaded.
A small example component to test the whole thing
That’s it!
The DialogComponent is now able to load a dynamic child-component.
So why don’t we create a component to test that real quick?
ng generate component example
Here is what it looks like:
Template
Class
Styles
Using the example component
To test this, we import our DialogModule into the AppModule:
We can then use the DialogService to open a dialog with our ExampleComponent:
The result should look like this:
Passing data to the service using dependency injection
Great! We can now place any component inside of our dialog!
Most of the time that isn’t enough though…
We also need a way to communicate with the dialog. For example, we probably want to pass it some data.
There are many ways to achieve this. One very elegant one, used by the angular material library, is to pass a configuration object via dependency injection to the dialog.
We will do the same here, although in a much simpler and minimalist way.
But how do we dynamically provide an object via dependency injection?
The secret is called Injector. This thing controls which classes are available via dependency injection.
To extend the list of available classes, we need build to a custom injector.
How to build a custom injector
A custom injector is just a TypeScript class that implements the Injector interface.
So to create our own injector, we first need to create a new file called “dialog-injector.ts” inside of the dialog directory.
Inside of there, we define a class called “DialogInjector”.
Inside of there, we define a new property called _additionalTokens, which will hold our additional classes for DI.
We then override the get-method in a way, that it is not only searching for matches in the parent-injector, but also in our _additionalTokens map.
What does the data structure look like?
Next, we will use the custom injector to register our config-object.
But before we can do that, we need to define a class that describes that object.
To do that, create a new file called “dialog-config” inside of the dialog directory.
The class looks quite simple:
You can add many more properties to that class. For example, angular material is using this object to define the size of the dialog.
Modifying the DialogService to use the custom injector
Now we need to alter the DialogService to use the custom injector.
Do you remember this line from the “appendDialogComponentToBody”-method? Notice that we are already using an injector there.
We now have to replace that injector with our DialogInjector.
Also, the “appendDialogComponentToBody” now has to take a DialogConfig as a parameter, as well as the open method.
Our injector expects the old injector together with a WeakMap. We pass it the type of our DialogConfig together with the actual config-object.
Requesting the DialogConfig in the dialogs’ child-component
Now that we have fed our custom injector with the config-object, it is available to the DialogComponent and its children.
That enables us to use data of the config-object in our ExampleComponent:
For example, we could display the data in our template:
For that to work, we also have to change the call of the open-method and pass in a config-object:
Here is what it should look like:
How to get a result from the dialog
Often times, it is even more important to get a result from a dialog than it is to pass data in.
For example, to know which button the user pressed or what he typed into a form.
Also, we still need a way to close our dialog from within the dialogs’ child component.
Again, we will take the angular material library as a reference to solve this issue.
We will create a class called DialogRef which has a property called afterClosed which is an observable. We simply return that DialogRef with the call of the DialogServices’ open method.
We can that subscribe to that observable and get notified once the dialog closed. The result of the dialog is transferred with this observable as well.
The class does also contain a close-method, which we will use to close the dialog from within the child-component.
We do make this DialogRef object available in our child-component using our custom injector again, so we need to modify the appendDialogComponentToBody”-method of the DialogService again. Also, we want to return the DialogRef when calling the services’ open-method. So we need to modify that, as well.
Using a DialogRef to control the dialog
Now, the dialogRef is available inside of the child-component.
Because of that, it is possible to close the dialog from our example.component.
We simply request the DilaogRef via dependency injection in the constructor and then use it to close it when a button triggers the “onClose”-method. Using the close-method of DialogRef, we can also pass back the result of the dialog. For example “some value”.
The result of the dialog can then be received by subscribing to the afterClosed-observable of the DialogRef when openeing the dilaog itself:
Conclusion
In this tutorial, you have learned how you can utilize dynamic components to create a flexible and reusable dialog system.
I hope you stayed with til the end!
You can find the full source code at the corresponding GitHub Repository.
If you liked this article, please share it with your friends! It would mean a lot to me.
Happy coding!