Dynamic Components with Content Projection in Angular

Reading Time: 4 minutesIn this article, we are going to learn how to apply content projection to dynamically generated components.

The idea is this: imagine you have a modal window component — you want to encapsulate the idea of a popup message that can be reused in many different situations.

But there’s a problem — how do you customize the modal for each individual situation? In this example, we’ll show how to create a dynamically generated component that also supports content projection.

You can run the final example on StackBlitz here.

We’ll create a modal component and expose a modal service that will give our consumers the ability to open the modal in one of the following methods:

Let’s start by creating the modal component:

We have wrapping elements for styling purposes and ng-content tag that will be replaced with a custom component, template or a string that will get from the consumer.

Let’s continue with the service.

The resolveComponentFactory() method takes a component and returns a ComponentFactory. You can think of ComponentFactory as an object that knows how to create a component. Once we have a factory, we can use the create() method to create a componentRef instance, passing the current injector.

A componentRef exposes a reference to the native DOM element which we append to the body.

In this stage, we can call the service open() method, and we’ll get an empty working modal.

What we want now is to give our consumers the ability to pass a custom component, a template or a string and inject it as ng-content. Let’s see how can we do it.

The factory create() method excepts as the second parameter projectableNodes, which is a two-dimensional array of DOM elements that Angular will pass as ng-content to the current component.

Let’s see how can we get a reference to the DOM elements in each of the cases mentioned above.

Handling Strings

Strings are the most straightforward case. If the content is a string, we create a text node.

Now we can call the open() method and pass a string that’ll be displayed in the modal.

Handling Templates

By template, we mean a TemplateRef. For example, we want to be able to pass a reference to the template:

Let’s update our resolveNgContent() method and add support for this functionally.

We instantiate an embedded view based on the TemplateRef which returns a viewRef instance. This instance exposes a rootNodes property, which is an array of DOM nodes that are extracted from the template.

Handling Components

The last and most powerful option is to be able to pass a custom component that will be injected as ng-content. For example:

Let’s update our resolveNgContent() method and add support for this functionally:

The same process follows: get the factory, create the component and pass the reference to the native DOM element.

Pay attention that every dynamic component must be declared as entryComponent.

You may have wondered why the projectableNodes parameter is a two-dimensional array. The reason is that we can pass more than one ng-content. For example:

Complete Code Example

You can view the complete code example here

I want to take this opportunity to mention that we recently came out with Akita, which offers simple and effective state management for Angular applications. Check it out here.

About the Author

Netanel is a Frontend Architect who works at Datorama, blogs at netbasal.com, open source maintainer, creator of Akita and Spectator, Husband, Father and the Co-founder of HotJS.

  • http://www.groupe-sma.fr Alain Boudard

    Oh that’s really handy !
    Very clever use of content projection !

    BTW, didn’t you mean “expects” ?
    “The factory create() method excepts as the second parameter projectableNodes”

    Thanks for sharing.

    • NetanelBasal

      Yes, thanks Alain!

  • Fatih Emre

    Cool solution! Thank you but how do we close the modal? Esc button or backdrop click not working like other modals. What is best practise for this problem?

    • Fatih Emre

      Sorry, this is not problem. This is a feature

      • NetanelBasal

        You should emit an event from the modal and listen to it from the open() method and call destroy.

        • sakthi

          how to do it? Please help me. i am stuck on this

          • sakthi

            After a lot of struggle. I understand your solution. but i tried with remove child . Please explain using destroy method. componentRef.instance.closeModalEvent.subscribe(data => {

          • Francisco Javier RodrĂ­guez Ace

            Hello, I have solved it by creating a class property (natElem) to which I assign nativeElement in the open method. I created a close method with the following code:

            this.document.body.removeChild (this.natElem);

          • Basil Bear

            Hi Francisco – I tried that approach, but I don’t think that it holds together without calling destroy (if you project a component). I am getting unsubscription errors when I open the modal more than once.

  • guyn

    Thanks for taking the time to write this post – however there’s a bug / problem with your implementation.
    the ngOnInit of the child component (login-modal) isn’t being called which I suspect points at a larger problem.

    I believe your just missing after line 54

    • minchul chae

      Same here. It has a binding issue….

      • Basil Bear

        I am a little puzzled by this, as I don’t see where addComponentEl is being called.

    • Cin

      I ran into the same issue trying to implement this, and after much investigation, I found a solution. My resolveNgContent function looks like this:

      if (content instanceof TemplateRef) {
      // Template reference
      const viewRef = content.createEmbeddedView(null);
      return [viewRef.rootNodes];

      } else if (content instanceof Type) {
      // Component
      const factory = this,resolver.resolveComponentFactory(content);
      const componentRef = factory.create(this.injector);
      return [[componentRef.location.nativeElement]];

      Note the addition and placement of the attachView calls here. Once I added that, angular began scaffolding the child components and content correctly.

      • Basil Bear

        Thanks Cin, that put the iCINing on the cake for me! 🙂

      • http://www.maximelafarie.com/ Maxime Lafarie

        Man, you just save me from hours of headaches, many thanks!

  • Rajesh Patil

    The modal dialog popups uses the complete width of the screen.
    How one can customize the style for individual component (different modal popups with different size (width/height))

  • devfcr

    Hi, this is really a great solution.
    I wonder what the expression const { nativeElement } = ... means?

  • Gary
  • Alberto GarcĂ­a Alvarez

    Thanks for your code and time. Very usefull.

  • Basil Bear

    Thanks for this really useful post – I have been wanting to achieve this for a long time!

  • Mubarak Ahmed

    I would be project content when using select with ng-content?