Question

Dynamically loading and rendering External files as component in angular

I have a use-case, where I should render an external angular component or web-component in an angular application.

I have created a sample application (stackblitz) to reproduce the scenario and below is the visual expecation.

Dynamic rendering expectation

In the sample code, to compile a angular component I use

<div style="margin:20px; display: flex; max-height: 500px;">
  <div style="max-width:250px; overflow-y: scroll;">
    <button (click)="onInternalControl()">Internal Component</button>

    <ng-template #internalTemplate>this is internal template</ng-template>
  </div>

  <div style="width:250px; height: 150px; overflow-y: scroll;">
    <button (click)="onExternalControl()">External Component</button>

    <ng-template #externalTemplate>this is external template</ng-template>
  </div>
</div>

The below is the component code

export class AppComponent implements OnInit {
  @ViewChild('internalTemplate', { read: ViewContainerRef })
  internalTemplate: any;

  @ViewChild('externalTemplate', { read: ViewContainerRef })
  externalTemplate: any;

  constructor() {}

  ngOnInit() {}
  onInternalControl() {
    this.internalTemplate.clear();
    this.internalTemplate.createComponent(LabelComponent);
  }

  onExternalControl() {
    this.externalTemplate.clear();
    // how to dynamically register and load web-component in the ng-template (externalTemplate)
    // e.g., assets/external-label.js
  }
}

The below code is the web component which is exepected to be render on click of EXTERNAL_COMPONENT button.

class ExternalLabel extends HTMLElement {
  // The browser calls this method when the element is
  // added to the DOM.
  connectedCallback() {
    let $p = document.createElement('p');
    $p.style.backgroundColor = 'red';
    $p.style.width = '250px';
    $p.style.height = '250px';
    $p.innerHTML = 'External Label Control';
    this._shadowRoot.appendChild($p);
  }
}

// Register the ExternalLabel component using the tag name <ext-label>.
customElements.define('ext-label', ExternalLabel);

How can I dynamically register and load the external label web component?

 2  26  2
1 Jan 1970

Solution

 0

I think there is some problem in your web component, here is my web component that works fine.

customElements.define(
  'fancy-tabs',
  class extends HTMLElement {
    constructor() {
      super(); // always call super() first in the constructor.

      // Attach a shadow root to <fancy-tabs>.
      const shadowRoot = this.attachShadow({ mode: 'open' });
      shadowRoot.innerHTML = `
      <style>#tabs { ... }</style> <!-- styles are scoped to fancy-tabs! -->
      <div id="tabs">...</div>
      <div id="panels">...</div>
  `;
    }
  }
);

When it comes to web components, make sure you import the script in the scripts array of angular.json.

      ...
      "options": {
        "assets": ["src/app/assets"],
        "index": "src/index.html",
        "browser": "src/main.ts",
        "outputPath": "dist/demo",
        "scripts": ["src/app/assets/externalLabel.js"],
        "styles": ["src/global_styles.css"],
        "tsConfig": "tsconfig.app.json"
      }
      ...

Then the web-component, is best to exist in a wrapper component, because we can use CUSTOM_ELEMENTS_SCHEMA to prevent the error that the selector is unknown. This can also be done at the module level, but why I recommend this is because the schema will block some errors, so your angular application will not show errors properly.

import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
@Component({
  selector: 'app-external',
  standalone: true,
  imports: [],
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  template: `test<fancy-tabs></fancy-tabs>`,
})
export class ExternalComponent {}

Finally we can render this wrapper component as the way we do our normal component.

import { Component, OnInit, ViewChild, ViewContainerRef } from '@angular/core';
import { LabelComponent } from './label.component';
import { ExternalComponent } from '../external/external.component';

@Component({
  selector: 'my-app',
  standalone: true,
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent implements OnInit {
  @ViewChild('internalTemplate', { read: ViewContainerRef })
  internalTemplate: any;

  @ViewChild('externalTemplate', { read: ViewContainerRef })
  externalTemplate: any;

  constructor() {}

  ngOnInit() {}
  onInternalControl() {
    this.internalTemplate.clear();
    this.internalTemplate.createComponent(LabelComponent);
  }

  onExternalControl() {
    this.externalTemplate.clear();
    this.externalTemplate.createComponent(ExternalComponent);
    // how to dynamically register and load web-component in the ng-template (externalTemplate)
    // e.g., assets/external-label.js
  }
}

Stackblitz Demo

2024-07-22
Naren Murali