1

I'm building a dynamic UI engine in Angular (v15+) where I need to load different components at runtime based on configuration. These components each have different @Input() and @Output() properties.

What I'm trying to achieve:

Render multiple dynamic components using ViewContainerRef.createComponent()

Bind @Input() values dynamically (even though each component has different inputs)

Subscribe to @Output() events generically

Cleanly destroy or replace components when needed

    const componentConfigs = [
        {
            component: ButtonComponent,
            inputs: { label: 'Click Me' },
            outputs: { clicked: () => alert('Button clicked') },
        },
        {
            component: CardComponent,
            inputs: { title: 'Card Title', content: 'Lorem ipsum' },
            outputs: { selected: data => console.log(data) },
        },
    ];

    loadComponent(config) {
      const componentRef = this.container.createComponent(config.component);

     // How to set `@Input()` and bind `@Output()` safely?
    }

My Questions:

How can I dynamically bind arbitrary @Input() and @Output() properties using createComponent()?

Is there a type-safe or generic way to handle components with different input/output contracts?

What's the best practice for cleaning up subscriptions from @Output() when destroying components?

This seems like a common requirement in CMS-style systems or dynamic form builders. Most examples show how to dynamically load one static component, but I’m looking for a flexible pattern that supports many components with unique bindings.

1 Answer 1

1

The gist of your problem is to loop through the inputs and outputs.

We use setInput method to set the inputs.

// loop through inputs
Object.entries(item.inputs).forEach((data: any) => {
  componentRef.setInput(data[0], data[1]);
});
item.subs = new Subscription();

For outputs, we need to unsubscribe on component destroy the subscriptions we create on the loop. For this we use the onDestroy callback available on the created component.

item.subs = new Subscription();

// loop through outputs
Object.entries(item.outputs).forEach((data: any) => {
  item.subs.add(
    (<any>componentRef.instance)[data[0]].subscribe((event: any) =>
      data[1](event)
    )
  );
});

// unsubscribe subscriptions on destroy
componentRef.onDestroy(() => item.subs.unsubscribe());

Using createComponent method:

This method is not deprected so favor this, we simply pass in the injectors required to create the component and then using renderer2 append the child elements, we need to trigger detectChanges on the inserted element, to have the component working fine.

add(): void {
  // create the component factory
  this.componentConfigs.forEach((item: any) => {
    // add the component to the view
    const componentRef = createComponent(item.component, {
      environmentInjector: this.envInjector,
      elementInjector: this.injector,
    });

    // loop through inputs
    Object.entries(item.inputs).forEach((data: any) => {
      componentRef.setInput(data[0], data[1]);
    });
    item.subs = new Subscription();

    // loop through outputs
    Object.entries(item.outputs).forEach((data: any) => {
      item.subs.add(
        (<any>componentRef.instance)[data[0]].subscribe((event: any) =>
          data[1](event)
        )
      );
    });

    // unsubscribe subscriptions on destroy
    componentRef.onDestroy(() => item.subs.unsubscribe());

    this.renderer.appendChild(
      this.container.nativeElement as HTMLElement,
      componentRef.location.nativeElement
    );
    componentRef.changeDetectorRef.detectChanges();
  });
  this.cdr.detectChanges();
}

Stackblitz Demo


Using container.createComponent (Deprecated):

We can use this method to create the components, only caveat is I get the deprecation warning from angular.

add(): void {
  // create the component factory
  this.componentConfigs.forEach((item: any) => {
    const componentFactory =
      this.componentFactoryResolver.resolveComponentFactory(item.component);
    // add the component to the view
    const componentRef = this.container.createComponent(componentFactory);

    // loop through inputs
    Object.entries(item.inputs).forEach((data: any) => {
      componentRef.setInput(data[0], data[1]);
    });
    item.subs = new Subscription();

    // loop through outputs
    Object.entries(item.outputs).forEach((data: any) => {
      item.subs.add(
        (<any>componentRef.instance)[data[0]].subscribe((event: any) =>
          data[1](event)
        )
      );
    });

    // unsubscribe subscriptions on destroy
    componentRef.onDestroy(() => item.subs.unsubscribe());
  });
}

Stackblitz Demo

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.