Angular gives us the mechanism to render components dynamically through View Container using ComponentFactory. To do this, we need to know the Component Type at the compile time.

The most dynamic component rendering mechanism would be the one where we don't know what component will be rendered at the compile time. This article talks about rendering an Angular component based on its selector, which is available only at run-time in the browser.

When do we need this

Generally this capability is helpful when we define what components to render at run-time based on some events or as per user actions. There could be multiple use-cases:

  1. Components to be rendered are known through some external source like a metadata JSON or an API response.
  2. Micro-front ends based on iframe and interacting through inter-iframe communication (JSON format) that needs to trigger component rendering.
  3. Any other cases where the component that would be rendered is not known upfront.

Rendering components using component selector and module path

To be able to render components by specifying the component selector, we need a way to get access to ComponentFactory of these components on-demand.

Getting component factory by component selector

Before Ivy (version 8), Angular had a mechanism to define entryComponents in modules. Adding the component to the entryComponents array would make the factories for these dynamic components available at runtime. It was needed to make sure TreeShaking does not remove these components from the final production bundle of the module.

Angular doesn't have a documented way of getting access to factories but a lot of people have used undocumented API like this to get it working:

const module: NgModuleFactory<unknown> = loadMyModule();
const factoryResolver = module.componentFactoryResolver;

factoryResolver['_factories'].forEach(componentFactory => {
	// componentFactory.selector
});
Getting Component Factories from ComponentFactoryResolver in Angular 8

As this was never officially supported or documented by Angular team, this stopped working with Angular 9 and above and people are left to find alternatives to keep supporting this. There is an open request on Angular project to support this, but they don't seem to be wanting to cater to this use case officially.

Dynamic Components by selector with Ivy

The suggested approach works by providing an interface similar to entryComponents through a custom field at the module level:

@NgModule({
  imports: [CommonModule],
  declarations: [Dynamic1Component]
})
export class Child1Module extends BaseModule {
  dynamicComponents = [Dynamic1Component];

  constructor(componentFactoryResolver: ComponentFactoryResolver) {
    super(componentFactoryResolver);
  }
}
Specifying dynamic components similar to entryComponents (Angular 8)

We need some application-level infrastructure to be able to define dynamic components then lazy load and render them when needed. Here's what we need to do:

  1. Create a base class BaseModule that each module exposing dynamic components needs to extends from
  2. Add all the components that need to be exposed to be available via component selectors as an array in dynamicComponents field
  3. Provide ComponentFactoryResolver to the base class

As we have the references to components via dynamicComponents field, these become part of chunks that are built and are not removed as part of TreeShaking.

We also have type safety here to make sure if a module is extending BaseModule, then dynamicComponents and provide ComponentFactoryResolver are also defined.

What all is BaseModule doing?

Since we need ComponentFactory for rendering a component at runtime, we create a map of component selectors and their corresponding ComponentFactory instances for all components specified in the dynamicComponents field. This is done per module level through inheritance:

export abstract class BaseModule {

  private selectorToFactoryMap: { [key: string]: ComponentFactory<any> } = null;
  
  protected abstract dynamicComponents: Type<any>[]; // similar to entryComponents

  constructor(protected componentFactoryResolver: ComponentFactoryResolver) { }

  public getComponentFactory(selector: string): ComponentFactory<any> {
    if (!this.selectorToFactoryMap) {
      // lazy initialisation
      this.populateRegistry();
    }
    return this.selectorToFactoryMap[selector];
  }

  private populateRegistry() {
    this.selectorToFactoryMap = {};
    if (
      Array.isArray(this.dynamicComponents) &&
      this.dynamicComponents.length > 0
    ) {
      this.dynamicComponents.forEach(compType => {
        const componentFactory: ComponentFactory<
          any
        > = this.componentFactoryResolver.resolveComponentFactory(compType);
        this.selectorToFactoryMap[componentFactory.selector] = componentFactory;
      });
    }
  }
}
BaseModule: Storing map of selectors and corresponding ComponentFactories

Here, we have a public function getComponentFactory that takes the component selector and returns a ComponentFactory for that component.

We have a hash map selectorToFactoryMap that holds module level selector to factory mapping. We make sure this map is lazily initialized when first requested for a dynamic component in this module.

Consumption

We will create a helper service that we will be injected and used where we need to get components for dynamic rendering:

export class DynamicComponentService {
  constructor(private injector: Injector) {}

  getComponentBySelector(
    componentSelector: string,
    moduleLoaderFunction: () => Promise<any>
  ): Promise<ComponentRef<unknown>> {
    return this.getModuleFactory(moduleLoaderFunction).then(moduleFactory => {
      const module = moduleFactory.create(this.injector);
      if (module.instance instanceof BaseModule) {
        const compFactory: ComponentFactory<
          any
        > = module.instance.getComponentFactory(componentSelector);
        return compFactory.create(module.injector, [], null, module);
      } else {
        throw new Error('Module should extend BaseModule to use "string" based component selector');
      }
    });
  }

  async getModuleFactory(
    moduleLoaderFunction: () => Promise<NgModuleFactory<any>>
  ) {
    const ngModuleOrNgModuleFactory = await moduleLoaderFunction();
    let moduleFactory;
    if (ngModuleOrNgModuleFactory instanceof NgModuleFactory) {
      // AOT
      moduleFactory = ngModuleOrNgModuleFactory;
    } else {
      // JIT
      moduleFactory = await this.injector
        .get(Compiler)
        .compileModuleAsync(ngModuleOrNgModuleFactory);
    }
    return moduleFactory;
  }
}
Helper service for getting component's ComponentRef by its selector

The helper service exposes a utility function getComponentBySelector that takes component selector and dynamic import statement for the module. The service works in the following manner:

  1. Loads the module and checks if it extends BaseModule or not
  2. Creates an instance of the module and calls the method getComponentFactory exposed by BaseModule to get the component factory. The baseModule now initialises the map and populates all selectors and ComponentFactory mapping for the module

We will use the helper service in the following manner:

  1. We have a placeholder container div in our template where we need a dynamic component. We get ViewContainerRef for the same.
  2. We request DynamicComponentService for the ComponentRef of a component with selector 'app-dynamic1' that is part of child1.module file.
  3. We insert the ComponentRef inside our container div

Here's the code demonstrating the approach:

@ViewChild("container", { read: ViewContainerRef, static: true })
  container: ViewContainerRef;

  constructor(private componentService: DynamicComponentService) {}

  addDynamicComponent() {
    this.componentService
      .getComponentBySelector("app-dynamic1", () =>
        import("./child1/child1.module").then(m => m.Child1Module)
      )
      .then(componentRef => {
        this.container.insert(componentRef.hostView);
      });
  }

Inputs to the component

One problem with dynamic rendering in Angular is that component inputs are not automatically bound to the fields of the parent component. To solve that problem and set up automatic inputs propagation, we will add some custom binding functionality to a wrapping component.

The component will use the DynamicComponentService we created above and set the inputs to the component instance. It also makes sure any updates to inputs are updated in the instance or if the selector is changed, a new component is created:

export interface DynamicComponentInputs { [k: string]: any; };

@Component({
  selector: 'app-dynamic-selector',
  template: `
  <ng-container #componentContainer></ng-container>
  `
})
export class DynamicSelectorComponent implements OnDestroy, OnChanges {
  @ViewChild('componentContainer', { read: ViewContainerRef, static: true })
  container: ViewContainerRef;

  @Input() componentSelector: string;
  @Input() moduleLoaderFunction;
  @Input() inputs: DynamicComponentInputs;

  public component: ComponentRef<any>;

  constructor(private componentService: DynamicComponentService) { }

  async ngOnChanges(changes: SimpleChanges) {
    if (changes.componentSelector) {
      await this.renderComponentInstance();
      this.setComponentInputs();
    } else if (changes.inputs) {
      this.setComponentInputs();
    }
  }

  ngOnDestroy() {
    this.destroyComponentInstance();
  }

  private async renderComponentInstance() {
    this.destroyComponentInstance();

    this.component = await this.componentService.getComponentBySelector(this.componentSelector, this.moduleLoaderFunction);
    this.container.insert(this.component.hostView);
  }

  private setComponentInputs() {
    if (this.component && this.component.instance && this.inputs) {
      Object.keys(this.inputs).forEach(p => (this.component.instance[p] = this.inputs[p]));
    }
  }

  private destroyComponentInstance() {
    if (this.component) {
      this.component.destroy();
      this.component = null;
    }
  }
}

Limitations

We should also keep in mind that dynamism brings in some limitations.

1. TypeSafety
As the component to be created is dynamically decided, it is not type safe while consuming it.

2. Errors at Runtime
It will be difficult to find errors at compile time. If the component is not exposed as a dynamic component by the module, there will be exceptions seen at runtime. Though this was the case even when using entryComponents.

Conclusion

The approach shared in the article is useful in cases where the requirement is to be very dynamic components and no criteria is available at build time to decide which component should be rendered where.

I have personally used this approach in projects where we have iframe based integrations and dialogs render components dynamically by getting component the selector name by using Window.postMessage API.

Try it

The sample Angular application that render dynamic components via selector is available here: https://github.com/tarangkhandelwal/components-by-selector

Credits

Thanks to Suresh Nagar and Shrinivas Parashar with whom I collaborated to come up with this solution.