Building Angular 2 Components on the Fly: A Dialog Box Example

Building Angular 2 Components on the Fly: A Dialog Box Example thumbnail

UPDATE 8-22-16: This post has been updated to use Angular 2 version 2.0.0-rc.5. It was originally written against the API provided by Angular 2 version 2.0.0-rc.4, but as Tom Nurkkala pointed out in the comments, the relevant API from version 2.0.0-rc.4 has been deprecated.

UPDATE 9-24-16: A more succinct variation of this example has been added here. For a brief explanation of this variation, see the Afterwards section below.

Building components on the fly at runtime is not uncommon, but you won’t find a recipe for it in Angular 2’s official cookbook just yet. In this post, we fill the gap by working through an example.

A dialog box is a good example of an Angular 2 component you may want to build on the fly. If your application has a fair number of them (like Lucidchart does), and you feel put out at the thought of writing one large ngSwitch, then building them dynamically is a good alternative.

We will build a simple dynamic dialog box for our example. (Check out the completed example.) Here is our initial app:

@Component({
    selector: 'my-app',
    template: `
        <div class="open-button" (click)='openDialogBox()'>Open dialog box</div>
    `,
    styles: [`
        :host {
            display: flex;
            justify-content: center;
        }
        .open-button {
            padding: 5px;
            border: 1px solid black;
            cursor: pointer;
        }
    `],
    directives: []
})
export class AppComponent {
    openDialogBox() {
        // TODO: Open up a dialog box
    }
}

Fire this up in a Plunker, and you will see the following:

Screenshot showing a simple button that says, 'Open dialog box'
Initial state of our demo app

Our goal is to open a dialog box when the user clicks the button aptly labeled “Open dialog box.” Our main tool in accomplishing this goal is Angular 2’s ViewContainerRef class, which provides a handy createComponent method. It is hard to describe a ViewContainerRef in familiar terms, but here is what we need to know about it:

  • The ViewContainerRef can be informally thought of as a location in the DOM where new components can be inserted.
  • Internally, this insertion location is specified by a DOM element referred to as the “anchor element.”
  • When a new component is created using the createComponent method, the resulting component’s DOM gets inserted as a sibling to the anchor element.

Given that rundown, here is how we plan to use ViewContainerRef in our example:

  1. We’ll pick a spot (i.e., an element) in the AppComponent‘s template where we would like to insert our dialog box, and we’ll set up a new ViewContainerRef there.
  2. We’ll use the ViewContainerRef‘s createComponent method to build and insert our dialog box at that location.

The plan is easily summarized in into two steps but actually takes a fair bit of work to implement. Let’s get started.

Setting up the insertion location

Looking at the AppComponent’s template, there aren’t many elements to pick from for our insertion location—there is only one div. Let’s just add another div and use it:

...
    template: `
        <div></div>
        <div class="open-button" (click)='openDialogBox()'>Open dialog box</div>
    `,
...

We can attach a ViewContainerRef to this div by adding a directive to it that has been injected with a ViewContainerRef. Here is our directive:

@Directive({ selector: '[dialogAnchor]' })
export class DialogAnchorDirective {
    constructor(
        private viewContainer: ViewContainerRef,
    ) {}
}

Here, we’ve added the directive to the div:

...
    template: `
        <div [dialogAnchor]></div>
        <div class="open-button" (click)='openDialogBox()'>Open dialog box</div>
    `,
...

When the DialogAnchorDirective is created, it gets injected with a ViewContainerRef whose anchor element is set to our newly added div. Our dialog box’s DOM will get inserted as a sibling to this div.

Using createComponent

We need to provide some way to actually use DialogAnchorDirective’s viewContainer to build our dialog box. We will do this by adding the following method to the DialogAnchorDirective:

...
    createDialog(dialogComponent: { new(): DialogComponent }): ComponentRef {
        this.viewContainer.clear();

        let dialogComponentFactory = 
          this.componentFactoryResolver.resolveComponentFactory(dialogComponent);
        let dialogComponentRef = this.viewContainer.createComponent(dialogComponentFactory);
        
        dialogComponentRef.instance.close.subscribe(() => {
            dialogComponentRef.destroy();
        });

        return dialogComponentRef;
    }
...

There is quite a bit going on here, so we will break it down, but before we do, this method has one small dependency we need to set up first. It is the componentFactoryResolver helper variable, which we need to add to our DialogAnchorDirective:

...
    constructor(
        private viewContainer: ViewContainerRef,
        private componentFactoryResolver: ComponentFactoryResolver
    ) {}
...

With that out of the way, let’s start with the method signature:

createDialog(dialogComponent: { new(): DialogComponent }):ComponentRef<DialogComponent> { ... }

We pass in the type for our dynamic dialog box component, DialogComponent, and we get back a ComponentRef for the new component. (Note that the DialogComponent is the custom dialog box component we keep referring to. We don’t provide a listing for it here, but you can take a look at it in the full demo. Just know that it requires no special setup to be dynamically built.)

This line removes any already-open dialog boxes:

this.viewContainer.clear();

These lines are where the component is actually created:

let dialogComponentFactory = 
    this.componentFactoryResolver.resolveComponentFactory(dialogComponent);
let dialogComponentRef = this.viewContainer.createComponent(dialogComponentFactory);

Notice that we resolve the DialogComponent type to a ComponentFactory using the newly added componentFactoryResolver. The factory then gets passed to ViewContainerRef’s createComponent method which builds, inserts, and returns a reference to the new dialog box component.

Next, we set up a listener to an event that gets fired when the user tries to close the dialog box:

dialogComponentRef.instance.close.subscribe(() => {
    dialogComponentRef.destroy();
});

Finally, we return the ComponentRef for the newly inserted dialog box component:

return componentCreated;

The last bit of setup is done in the AppComponent. First, we must indicate that the AppComponent dynamically builds DialogComponents. We do this using the entryComponents option in the @Component annotation:

@Component({
    selector: 'my-app',
    template: `...`,
    styles: [...],
    directives: [DialogComponent, DialogAnchorDirective],
    entryComponents: [DialogComponent]
})

Lastly, we query for AppComponent’s child DialogAnchorDirective and use its createDialog method to complete AppComponent’s openDialogBox method:

export class AppComponent {
    @ViewChild(DialogAnchorDirective) dialogAnchor: DialogAnchorDirective;

    openDialogBox() {
        this.dialogAnchor.createDialog(DialogComponent);
    }
}

Run the completed example, click on the button, and you will see the following:

Screenshot showing a simple button that says, 'Open dialog box'
Final state of our demo app

Afterwards

One of the main advantages of using a dedicated directive like DialogAnchorDirective is that it can be injected into subcomponents. For some use cases, this may not be necessary, so a pithier example, which eliminates this extra directive altogether, can be found here. In that example notice how we access a ViewContainerRef on a div using the @ViewChild‘s “read” API.

This is the second in a series of posts about Angular 2. The first is titled Angular 2 Best Practices: Change Detector Performance.

39 Comments

  1. Can this dynamically created component support 2 way data binding with @Input @Output

  2. @Vinod I do not know for sure, but I suspect that the @Input and @Output decorators provide metadata only relevant to template compilation. But that does not mean we can’t set variables on the component or subscribe to its event emitters. For example, if you look at the demo you will see we subscribe to the close event emitted by the DialogComponent. Also, here is an updated version of the example that illustrates setting data on the dynamic component: https://plnkr.co/edit/7y2PMR9AMAlpwHAQAUho?p=preview

  3. Hi Lewis, tks for sharing this. It helped a lot.
    I am wondering if it is possible load components dynamically without directives. I mean, we have app, dialogComponent and dialogDirective. Why don’t we user just dialogComponent to load it once Component is a directive as well.
    I really don’t know, I am pretty new in Angular2.
    Also, if it is possible, the best approach still creating a directive separated like your post?
    Cheers

  4. @Tiago I am glad you found this post helpful! It is true; the directive is an unnecessary middleman in our simple example. We could cut him out as in this plunker: https://plnkr.co/edit/R4olpIY8orKj9It7f9dK?p=preview One reason I made it a directive is that the directive method provides you the flexibility to put the DOM for the dynamic component where you want. Now, I may misunderstand your point. I just realized that you may be suggesting we make the DialogAnchorDirective into a DialogAnchorComponent. We could do that too: https://plnkr.co/edit/BimI2FROXK6mEVS7oCIi?p=preview (Note in that plunker, the DialogAnchorComponent has an empty template–kind of an Angular 2 design smell I would say maybe.) Let me know know if this does not answer your question.

  5. That is I was looking for! Tks a lot!

  6. Tom NurkkalaAugust 15, 2016 at 9:09 pm

    In the following line, what does the declaration of dialogComponent mean?

    createDialog(dialogComponent: { new(): DialogComponent }):Promise<ComponentRef> { … }

  7. Tom NurkkalaAugust 15, 2016 at 11:56 pm

    I’ve created an updated version of this example that uses non-deprecated Angular2 API calls and appears to work. Find it at https://plnkr.co/q98BLXAPeJ0pEaaQiRsQ. It looks to me like the component factory function is no longer asynchronous. Does that seem right? Am I on track with this example?

  8. @Tom I am guessing the type of the dialogComponent parameter is the unclear part. I had to do some reading, but it seems that TypeScript classes are considered objects containing “construct signature” member (i.e., they are objects with a constructor). I guess these are known as “constructor types.” Here and here are the relevant documentation from the spec. So, it is just the requisite type for passing a constructor into a function, which is what need to pass to componentResolver.resolveComponent.

    Now, if you look at the code for resolveComponent it uses Type as the type for its component parameter, but it seems this just an alias for a constructor type.

  9. @Tom Yep, that looks good! You are right; the ComponentResolver has since been deprecated in favor of the ComponentFactoryResolverolver. The API is changing so much these days! I will need to update this post.

  10. where can I find version with rc.4?

  11. T, in reply to Tiago’s comment, I posted several forks that still use rc.4’s API. I think the the following one stays truest to the original rc.4 version: https://plnkr.co/edit/7y2PMR9AMAlpwHAQAUho?p=info

  12. not working

  13. sourabh, sorry about that. It looks like there have been some changes with the CDNs and the external dependencies. It is back into a working state.

  14. Not sure if it’s due to changes in the core framework but I had to change the attribute annotation in the html template:

    should be :

    (no square brackets)

  15. Made it work with zone updated to the following version

  16. Thanks, Alex. The Zone dependency is all updated now. Hopefully these dependency issues will settle.

  17. Thanks for sharing this! Helps a lot. 🙂

  18. @NS, I updated that example to be in a working state now. (I need to update a few others from that comment where I listed a few other examples.) I updated it to use Angular v2.1.0’s API; hopefully that is OK for you.

  19. DILEEP KUMAR KOTTAKOTAOctober 18, 2016 at 11:51 pm

    Hi @Ty Lewis
    Thanks for the sharing. It helped us.
    I have a doubt , Can we add two components at a time like

    let dialogComponentFactory =
    this.componentFactoryResolver.resolveComponentFactory(DialogComponent);
    dialogComponentFactory =
    this.componentFactoryResolver.resolveComponentFactory(MYCOMPONENT);

    * where MYCOMPONENT is my another custom component.
    when clicks on ‘open dialog box’ button, dialog box and MYCOMPONENT should added.

    Thanks in advance.
    My actual intention is i need to add components dynamically to a div based on json data(which is having list of components to be added to div with row and column position details).

  20. Hi Dileep. I am not sure I understand the use case completely, but here is a fork that adds another custom component in addition to the dialog component. The additional component is the OverlayComponent. (It is probably overkill to make the overlay like this.) Let me know if this example does not quite capture your use case.

  21. DILEEP KUMAR KOTTAKOTAOctober 20, 2016 at 12:07 am

    Hi thanks for reply.
    My actual scenario is consider i have A(0-row,0-column),B(0,1),C(1,0),D(1,1),E(2,0),F(2-row,1-column) components.
    I have to display in div as per row and coluumn positions like
    A B
    C D
    E F

  22. Because of the way ViewContainer’s work, we are somewhat limited with how a dynamic component’s view can be inserted into the DOM. Nevertheless, here is an attempt at a general solution to your problem. Notice how we use multiple “anchor elements” to insert dynamic components in various locations.

  23. Hi Ty Lewis,

    thanks for the article, well explained. The only thing I wonder is … how would you approach dynamic templates for the dialog content. There is probably a better solution than creating multiple components with different templates for the dialog?

    Thanks

  24. AN, thank you for the feedback. For the dialog boxes in Lucidchart, we actually do build separate components for each dialog. The exact approach is similar to what can be found in this fork. There–as you might have guessed–we treat DialogComponent as a generic dialog container for arbitrary content. Specific dialog boxes, like the StopwatchDialogComponent, reuse this generic DialogComponent in their own templates, wrapping their content with the dialog box.

    My colleague, Tyler Davis, has been mentioning to me that the Angular 2 team may be building out better support for modals and dialogs. If I find out more, I will be sure to reply.

  25. Hi Ty Lewis,

    thanks for the fast response. Actually I did the same without watching the repo so I was on the right track at least 🙂

    Keep up the good work!

    Cheers

  26. Hello Ty Lewis,

    Thank you for this great article! It helped me a lot! Just a question. What if I want to load an array of dynamic components? How can I do that?

  27. Den, I am glad this helped. Could you provide a little more context for you question? What exactly are you looking to do?

  28. Awesome post! If you wanna go a bit more advanced you dynamically inject the modal into the body with something lile this service i wrote https://github.com/swimlane/swui/blob/master/src/services/injection.service.ts

  29. Awesome post Lewis, i tried to integrate this example https://plnkr.co/edit/GmOUPtXYpzaY81qJjbHp?p=preview in anthor project but, it’s crashed after changing the name of “DialogComponent” to “PictureDialogComponent” and “DialogAnchorDirective” to “PictureDialogDiractive” the error from typescript compiler : /home/habib/WebstormProjects/gbm_woqod/src/others/components/myAccount.component.ts:21:41
    Argument of type ‘typeof PictureDialogComponent’ is not assignable to parameter of type ‘new () => PictureDialogComponent’.
    [default] Checking finished with 1 errors

    in this line :
    “this.pictureDialog.createDialog(PictureDialogComponent);”

    in “Picture dialog directive” :
    “createDialog(pictureDialogComponent: { new(): PictureDialogComponent }): ComponentRef {“

  30. Hi Trimech. Thank you for the feedback. Could you send me a link to your fork? (ty@lucidchart.com)

  31. Hi, i recon that you’re compiling on the fly, am i right? what about if i want to use a component class to generate instances of it, like a list of those or a `tabs` that instantiate those components? what about AoT if you’re using the compiler, does it mean we can’t strip off the JiT compiler?

  32. Hey! Nice example, its almost exactly what I want to do, but with the addition that I want the message inside the DialogComponent to be made dynamic as well. In your example, the dialog component is created but the dialog it shows is set and cannot be changed. I would want to create a “template” dialog component into which I can set any message I want.

  33. Hi Mike. Try taking a look at my reply to AN and the fork I reference there. I think it might be what you are looking for, but let me know.

  34. Ty, thank you so much for the article. In the article you are mentioning that it is a good alternative to ngSwitch statements. Right the app I am supporting has a list view (with *ngFor) rendering different components, based on (item.type) of the list entries.. Right now the we use ngSwitch/ngIf to render them properly.

    In you examples though I see that you reference a specific component (this.componentFactoryResolver.resolveComponentFactory(dialogComponent);).. Does it mean that in order to load a different one, we would still need a switch statement or something of that kind to decide which component to create? Maybe a helper method with a switch statement returning a different component based on item.type.

    Any thoughts are much appreciated.

  35. Ty LewisMay 4, 2017 at 7:56 am

    Ilya, yes, it does sound like you might need to create some type of factory method/class. We settled on a similar approach for Lucidchart’s dialog box system–a factory class manages the construction of each type of dialog box component.

  36. This makes my head hurt:
    createDialog(dialogComponent: { new(): DialogComponent })

    For the record, in Angular 4, you can do:
    createDialog(dialogComponent: Type)

    Be sure to:
    import {Type} from ‘@angular/core’;

  37. Weird. The commend system took my generic angle brackets out. Let me try that again with html entities:

    createDialog<T> (dialogComponent: Type<T>)

Your email address will not be published.