Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

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

DZone's Guide to

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

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.

· Web Dev Zone
Free Resource

Get deep insight into Node.js applications with real-time metrics, CPU profiling, and heap snapshots with N|Solid from NodeSource. Learn more.

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
    }
}

This skeleton app can be run and forked here.

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);
    }
}

Be sure to run the completed example.

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.

Node.js application metrics sent directly to any statsd-compliant system. Get N|Solid

Topics:
angular 2 ,web dev ,dialog box

Published at DZone with permission of Ty Lewis, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}