Angular Dynamic Components - aakash14goplani/FullStack GitHub Wiki

Dynamic components are essentially components which you create (programatically) dynamically at runtime. Example, let's say you want to show an alert, a modal or some overlay which should only be loaded upon a certain action, for example you have an error and you want to show an overlay on the entire screen or something like that, could be done using dynamic components.

  • Lets add an Alert pop-up that displays if we encounter any error in our app:
    AlertSharedComponent.ts

    TypeScript
    ...
    @Input() message: string = '';
    @Output() closePopUp = new EventEmitter<void>();
    ...
    closeAlertBox(): void {
       this.closePopUp.emit();
    }
    HTML
    <div class="backdrop" (click)="closeAlertBox()">
       <div class="alert-box">
          <p>{{ message }}</p>
          <div class="alert-box-action">
             <button class="btn btn-danger" (click)="closeAlertBox()">Close</button>
          </div>
       </div>
    </div>
    CSS
    .backdrop {
       position: fixed;
       top: 0;
       left: 0;
       width: 100vw;
       height: 100vw;
       background: rgba(0, 0, 0, 0.75);
       z-index: 50;
    }
    .alert-box {
       position: fixed;
       top: 30vw;
       left: 20vw;
       width: 60vw;
       padding: 16px;
       z-index: 100;
       background: white;
       box-shadow: 0 2px 8px rgba(0, 0, 0, 0.26);
    }
    .alert-box-action {
       text-align: right;
    }
  • We can use this Alert functionality in 3 different ways:

Approach 1: Not to use external component and display error message using local property variable

errorMessage: string = '';
...
this.authObservable.subscribe(
   responseData => {
      this.errorMessage = '';
      ...
   },
   errorMsg => {
      this.errorMessage = errorMsg;
      ...
   }
);
<div class="alert alert-danger center-align">
    <p>An error occured:</p>
    <span>{{ errorMessage }}</span>
</div>

Approach 2: Using *ngif to display dynamic components. RECOMMENDED.

// same lines from approach-1 plus
closePopUp(): void {
    this.errorMessage = '';
}
<app-alert-shared [message]="errorMessage" *ngIf="errorMessage.length > 0" (closePopUp)="closePopUp()"></app-alert-shared>

Approach 3: Creating component programatically

  1. Create a placeholder/dummy directive

    import { Directive, ViewContainerRef } from '@angular/core';
    @Directive({
       selector: '[appPlaceholder]'
    })
    export class PlaceholderDirective {
       constructor(public viewContainerRef: ViewContainerRef) { }
    }
  2. Create component programatically:

    • You need to use ComponentFactoryResolver to create component programatically and use resolveComponentFactory(<component-name>) method to create desired component.
    • This method will return a component factory, not the component itself.
    • Now with that factory, we can use that factory to create a concrete component but for this, we also need a place where we can attach it in our DOM.
      • we can think of adding div with a local reference add here and with @ViewChild, we could get access to that but this is not how it works.
      • Angular needs a ViewContainerRef which is essentially an object managed internally by Angular, which gives Angular a reference/pointer to a place in the DOM with which it can interact
    • We can create a helper directive and now this directive needs to do one important thing, it needs to inject the ViewContainerRef and this automatically gives you access to the reference/pointer at the place where this directive is then used.
      • Note: we need to turn this into a public property, so that we can access that ViewContainerRef from outside.
    • So we get access to that directive we use in the template and we store that in alertHost reference that can be used in show error alert.
    • We clear anything that might have been rendered there before by simply calling clear on this ViewContainerRef, it simply clears all Angular components that have been rendered in that place before
    • Now we can use our component factory to create a new alert component in that host ViewContainerRef and this is done by simply now using our host ViewContainerRef and calling createComponent() and pass in that alert component factory we created earlier and this will now create a new component in that place.
    constructor(
     private authService: UserAuthService, private router: Router, private componentFactoryResolver: ComponentFactoryResolver) { }
    ...
    @ViewChild(PlaceholderDirective, { static: false }) alertHost: PlaceholderDirective;
    alertSubscription: Subscription;
    ...
    this.authObservable.subscribe(
       responseData => {
          this.errorMessage = '';
          ...
       },
       errorMsg => {
          this.errorMessage = errorMsg;
          ...
       }
    );
    private showAlertError(errorMessage: string): void {
       const alertComponentFactory = this.componentFactoryResolver.resolveComponentFactory(AlertSharedComponent);
       const hostViewContainerRef = this.alertHost.viewContainerRef;
       hostViewContainerRef.clear();
       const componentRef = hostViewContainerRef.createComponent(alertComponentFactory);
       componentRef.instance.message = errorMessage;
       this.alertSubscription = componentRef.instance.closePopUp.subscribe(() => {
          this.alertSubscription.unsubscribe();
          hostViewContainerRef.clear();
       });
    }
    
    ngOnDestroy(): void {
      if (this.alertSubscription) {
         this.alertSubscription.unsubscribe();
      }
    }
  3. Register Component in app.module

    • You need to understand how Angular actually works behind the scenes when it comes to creating components.
      • Any component as well as directives and pipes you plan on working with, you need to add them to your declarations array, this is important for Angular to understand what's a component or which components and directives and so on you have in your app because it does not automatically scan all your files, you need to tell it which components exist.
      • Still, this alone only makes Angular aware of it, so that it is able to create such a component when it finds it in one of two places.
        • The first place would be in your templates. If in your templates, let's say of the recipe-list component, if it find something like <app-recipe-item>, like this selector, it basically looks into the declarations array, finds it there and then is able to create that component.
        • The other place where Angular will look for this component is in your route, in your route config when you point at a component there, Angular will also check that in the declarations array and if it finds it there, it is able to create such a component and load it.
      • Now one place that does not work by default is when you want to create a component manually in code, which is the exact thing we're trying to do here. Angular does not automatically reach out to the declarations array, you instead deliberately need to inform Angular that in this case, the alert component will need to be created at some place and that Angular basically should be prepared for this.
      • Now to tell Angular to be prepared for the creation of that component, you need to add a special property to the object you pass to NgModule. Besides declarations, imports and so on, there is a property entryComponents. Entry components also is an array and it's an array of components types but only of components that will eventually need to be created without a selector or the route config being used.
      • So whenever a component is created by selector or you use it with the route configuration, you don't need to add it here to entry components. For custom component, it's different and there you simply need to add component to entry components.
    @NgModule({
       declarations: [
          ...
          CustomAlertElementComponent
       ],
       imports: [...],
       providers: [...],
       bootstrap: [AppComponent],
       entryComponents: [CustomAlertElementComponent]
    })
  4. Use this in template

    • Note: Adding ng-template anywhere in template won't work, it has to be at top of HTML file i.e. line # 1
    <ng-template appPlaceholder></ng-template>
⚠️ **GitHub.com Fallback** ⚠️