Angular Dynamic Component in Material Dialog with Dependencies

In my previous article I discussed how to dynamically lazy-load a standalone component at runtime into an Angular Material dialog. But, what if that standalone component has service dependencies, needs external configuration, or needs to communicate with the host dialog in some way? Let’s see several ways of solving those challenges.

Here is the starting point from the last article if you want to follow along on StackBlitz. I highly recommend reading the preceding article for context. And here you can find the complete, working example. Thanks to Jason Warner for reviewing this code for me.

Before We Begin

As I worked through this example it became more and more evident that even though you can solve some unique instances in this way, there is often greater clarity and value in simply creating non-dynamic, static components/dialogs that directly satisfy a use case. Although some of these approaches may reduce the total number of lines of code, they may also make your code harder to understand, debug, and maintain. But, there are always multiple solutions to complex problems. This is one possible solution. Caveat emptor.

Setup

To start, we want to modify the following files to use an interface for the dialog data. We want to do this because we want the dialog to stay generic, while the callers send in the specifics about what the dialog will instantiate.

Create the new interface dynamic-dialog-data.ts in the services folder. We’ll be extending this soon.

Modify the showDialog method in dynamic-dialog.service.ts to accept and use the new interface:

Modify the constructor in dynamic-dialog.component.ts to use the new interface:

Modify dynamic-dialog.component.html to use the interface:

Everything should be working as before. When you click the Alpha button you see dynamic ALPHA content in the dialog; Beta, BETA.

Scenario: Service Dependency

Let’s change our Alpha component to show a superscript number that increments once a second. We’ll implement that in a service in order to show how to leverage a dependency. Note that this has nothing to do with the dialog in any way. It’s just standalone component patterns.

Here is our service:

To leverage this service in the alpha component we simply add it to the providers in the component metadata (line 8), add a constructor (15), and update the template (11) using the async pipe:

The standalone APIs make importing services into standalone components very easy. Now, if you click the Alpha button in the UI it will show a superscripted number that updates every second.

Do the same changes to the Beta component and verify that it shows the counter too.

But, what if we want our standalone component to be extensible, allowing the consumer to provide configuration or services?

Configuring Dynamic Component From Host

For simple values that we want to pass in to a dynamically created component we can do one of two things:

Create an InjectionTokenCreate an Injectable class (i.e. service)

Both ways have merit. For simple values an InjectionToken is slightly simpler. For multiple values or for behaviors, it might be best to favor a service. We will examine both approaches.

Scenario: Injection Token

First, let’s say we want to pass a multiplier into our components so that the superscript value is multiplied by this value. It’s a simple value so we will use an InjectionToken.

Next, we modify the alpha component’s constructor to accept this token:

What we have done is made the alpha component dependent on the token MULTIPLIER, not on any specific instance of it. It will depend on the dependency injector that is used to create this component at runtime.

Do the same to the beta component. I’ll wait… 😉

Let’s configure the instance that we want to fulfill the token. Let’s do it so that if the user clicks the Alpha button the multiplier is 2, and if they click the Beta button it will be 10.

Let’s start by updating the DynamicDialogData interface with a new multiplier property:

We’ll modify the click handlers for the buttons in app.component.ts to pass in the appropriate multipliers:

Now, we will use the injection token combined with the multiplier value to inject the value into the dynamically created components. We will do this in the DynamicDialogComponent, since that is where our ngComponentOutlet is declared.

On line 12 we’ve added a property to expose the injector to the template.
On line 17 we’re asking Angular to inject the current Injector instance as a local variable to the constructor. (Be sure you understand the Dependency Injection hierarchy to know which instance you are actually getting. In many cases you won’t have to worry, but in advanced cases it can catch you if you’re not careful.)
On line 20 we are setting the providers for our new injector instance, and telling that injector that when anything using the injector asks for an MULTIPLIER to provide it with the multiplier value from the dialog data.
On 21 we are telling the new instance to resolve dependencies using the injector instance that was injected into the constructor.

Wow! That’s a lot of injecting! To simplify the mental model a bit, we are creating a new injector that uses an existing injector to resolve the dependencies for the new injector. The new injector instance is what we need to provide to the ngComponentOutlet to allow the dynamic components to leverage the MULTIPLIER token. Let’s add the injector to the outlet in the template:

What this means is that the dynamic components are now receiving the DynamicDialogData.multiplier value through the MULTIPLIER token via a dynamically created injector!

Let’s use the value in the alpha component’s template now.

Note: the + on line 13 prevents the error The left-hand side of an arithmetic operation must be of type ‘any’, ‘number’, ‘bigint’, or an enum type.

If you click the Show Dynamic Dialog With Alpha button you will see that the superscript increments by twos now!

Do the same changes for the beta component. On clicking the Beta button you should see the superscript increment by 10’s!

An InjectionToken works well for passing around a single value or object. But in many cases we want to inject more complex behaviors, perhaps even two-way communication. How do we accomplish that?

Shared services and NgRx or other state management are ways of accomplishing this when the behavior is concrete and known at compile time. But we want to consider that our dynamic components are open for extension. So, the instantiating component, the AppComponent in our case, is going to need to define at runtime the behavior it wants injected. Note that the dialog remains mostly agnostic to what the dynamic components are doing with the data or even what that data is!

Scenario: Injectable Class (i.e. Service)

We use interfaces to strongly type the public interface between two objects. So, let’s create an interface for handling the user clicking on the dynamically created component in the dialog.

We’ve defined an onClick handler and a click$ observable to notify subscribers.

Now we need a concrete implementation of a ClickBehavior. But let’s create two different ones so we can see what happens when we switch them out. First we’ll do an alert behavior that does not have any possible side effects:

Next we will create a toggling behavior that can notify other components so that they can respond to clicks:

Before we can specify which behavior to use we need to extend our interface for the items we pass to the dialog: (Line 7)

Now, we can pass that information into the dialog. We’re going to have the Alpha button use the alert behavior, and the Beta button use the toggle behavior. We set this up in app.component: (Lines 31 and 39)

The dialog now needs to inject the specified ClickBehavior into the dynamically instantiated component. We cannot inject an interface, so we will need to create an injection token this time as well. (Line 4)

I’ll now present the entirety of the dialog code rather than the piece-by-piece migration. Explanation follows…

We’ve moved the creation of the injector into it’s own method, createInjector, on line 32, and called it from the constructor. Note that we’ve added the provider for our new injection token on line 39 and set it’s value to whatever behavior the caller passed in.

This is all that we need to do for the Alpha component and the alert behavior. The alert never emits a value and has no side effect, so the dialog has no need to listen for it. But for the toggle behavior we needed to add a listener. We’ve added the method listenForClick on line 45 and called it from the constructor. For the toggle behavior we are going to toggle the text we show in the button on the dialog.

Everything is connected now. Try clicking in the empty space on either the Alpha or Beta versions of the dialog and see the different behaviors.

You could easily switch the behaviors in app.component, or even write entirely new behaviors and respond to them.

That gives us a working example. Do I think this is the best solution? That would depend entirely on your use case. It’s one possible solution. And it definitely shows off the power of dependency injection in Angular!

If you like my articles, please be sure to follow and leave some claps!

Join us at ng-conf 2023!

ng-conf | June 14–15, 2023
Workshops | June 12–13, 2023
Location | Salt Lake City, UT

ng-conf 2023 is a two-day single track conference focused on building the Angular community. Come be a part of this amazing event, meet the Angular Team and the biggest names in the community. Learn the latest in Angular, build your network, and grow your skills.

Angular Dynamic Component in Material Dialog with Dependencies was originally published in ngconf on Medium, where people are continuing the conversation by highlighting and responding to this story.

Leave a Comment

Your email address will not be published. Required fields are marked *