Why render components by selector?

Angular already supports dynamic component rendering via View Container and ComponentFactory, but that path requires the component type to be known ahead of time. For truly flexible scenarios, however, the component to render may not be determined until runtime. The technique described here shows how to instantiate a component purely from its selector string, resolved in the browser.

Where this comes in handy

Runtime-driven component selection is useful whenever the set of rendered components is decided by data or user interaction rather than by static imports. Typical situations include:

  1. Components are listed in external definitions, such as a metadata JSON payload or a response from an API.
  2. Micro-frontends communicate through iframes using JSON messages, and those messages trigger component rendering.
  3. Any architecture where component selection cannot be anticipated during the build phase.

Rendering via selector and module path

To load a component from a selector, you need access to its ComponentFactory on demand. This requires a dedicated lookup mechanism set up ahead of time.

Obtaining a factory from the selector

Before Ivy, Angular provided the entryComponents array in modules. Components in that array were excluded from tree-shaking so their factories remained available at runtime. Though not a documented public API, many developers accessed those factories with an approach like the following:

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

factoryResolver['_factories'].forEach(componentFactory => {
	// componentFactory.selector
});

The Angular 8 pattern for retrieving ComponentFactories via ComponentFactoryResolver

Since this technique was never officially supported, it effectively broke with Angular 9 and later upgrades, leaving developers to improvise workarounds. A related issue on the Angular repository remains open, though the team has not expressed interest in supporting this scenario natively.

Ivy-compatible dynamic components by selector

The proposed workaround introduces an entryComponents-like contract at the module level by adding a custom field:

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

  constructor(componentFactoryResolver: ComponentFactoryResolver) {
    super(componentFactoryResolver);
  }
}

Defining dynamic components similarly to entryComponents in Angular 8

To make this practical, a small application-level pattern is needed. The steps are:

  1. Define a BaseModule class that every module exposing dynamic components extends.
  2. Add the exposed components to a dynamicComponents array within each module.
  3. Provide ComponentFactoryResolver in the base class.

Because the components are referenced in the dynamicComponents field, they stay included in the app chunks and are not stripped out during TreeShaking. TypeScript also helps: any module extending BaseModule must define dynamicComponents and supply a ComponentFactoryResolver.

What the BaseModule does

To retrieve a ComponentFactory at runtime, the base class builds a map from selectors to factory instances for every component listed in dynamicComponents. This map is created lazily, once per module:

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: building a selector-to-factory map

A public method, getComponentFactory, accepts a selector and returns the matching ComponentFactory. The map itself, selectorToFactoryMap, is populated on first use for that module.

Using the approach

Next, we create a helper service to load the module and hand back a rendered component. The service is injected wherever dynamic rendering is needed:

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: resolving a ComponentRef from a selector

The service method, getComponentBySelector, takes the component selector and a dynamic import for the module. It then follows this process:

  1. Loads the module and verifies that it extends BaseModule.
  2. Instantiates the module, triggers getComponentFactory on it, and returns the factory. On first call, this also populates the selector-to-factory mapping.

In practice, the helper service is used like this:

  1. Place an empty container div in the template where the dynamic component should appear, and fetch its ViewContainerRef.
  2. Request a ComponentRef from DynamicComponentService using the selector 'app-dynamic1' and the path to the module file where it resides.
  3. Attach the returned ComponentRef to the container div.

The full example looks like the following:

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

Passing inputs to dynamic components

When components are rendered dynamically, Angular doesn’t automatically bind inputs from the parent. To handle that, we wrap the dynamic component in a small container that takes over input propagation.

This wrapper relies on the DynamicComponentService described earlier. It assigns input values to the component instance, reacts to input changes, and creates a fresh component whenever the selector changes:

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

Dynamic rendering also comes with certain constraints worth noting.

1. TypeSafety
Because the component type is decided at runtime, you lose static typing when consuming it.

2. Runtime errors
Detecting missing components is not possible at compile time. If a module hasn’t exposed a given component as dynamic, you will only see runtime errors during rendering. This was also the case with entryComponents.

Summary

This approach is valuable in cases where the components to render are not known up front, and there’s no predefined logic at build time to determine what appears where.

It’s a pattern that has been used in production integrations where modules communicate via iframes, with selectors passed dynamically through the Window.postMessage API to render dialog contents on demand.

Test it yourself

The sample Angular application demonstrating this selector-driven dynamic rendering is available at: https://github.com/tarangkhandelwal/components-by-selector

Acknowledgements

This solution was developed together with Suresh Nagar and Shrinivas Parashar.