Is it possible to dynamically set a components @Input at run-time?

Viewed 2996

Say I have a dynamic-component-wrapper that can instantiate any Component class that is passed into it.

// DRE013 DCOOKE 16/05/2017 - The component to instantiate.
@Input() componentType: Type<{}>;

// DRE013 DCOOKE 16/05/2017 - the component that will be created
private _cmpRef: ComponentRef<Component>;


// DRE013 DCOOKE 16/05/2017 - Creates a component ref in the view at #target
createComponent(){

    let factory = this.componentFactoryResolver.resolveComponentFactory(this.componentType);
    this._cmpRef = this.target.createComponent(factory);
    //this._cmpRef.instance.inputs >>>>>>>>> this is a string[] and I do not see how I can use this 
}

Example usage

<shared-dynamic-component [componentType]="TestComponent"></shared-dynamic-component>

Where TestComponent = TestComponent //class

This works as expected, and I can receive an instance of this component from within the dynamic-component-wrapper like so:

this._cmpRef.instance

The Angular docs are not clear on this instance object - simply stating that instance is of type C - with absolutely no reference to what C actually is.

Thankfully my IDE was able to tell me that :

ComponentRef.instance has the following properties:

  • inputs : string[]
  • outputs : string[]

However I do not understand how I am meant to use this information. I'd imagine this is just the names of the @Input fields - but I cannot think how I can pass in a complex object as an Input.

Question

Is it possible for me to set the @Inputs and other meta-data after dynamically creating a component with the componentFactoryResolver?

2 Answers

I ended up creating a class to do so... incase this helps anyone else.

Usage

const componentChangeBuilder = new DynamicComponentChangeBuilder(componentRef);
componentChangeBuilder.addInput('inputName', 'some string value');
componentChangeBuilder.addInput('someOtherInputName', 1234);
componentChangeBuilder.commit();

import { ComponentRef, OnChanges, SimpleChange, SimpleChanges } from '@angular/core';

/**
 * Use this to track and trigger ngOnChanges for dynamically loaded components.
 */
export class DynamicComponentChangeBuilder<TComponent = any> {
    private readonly componentRef: ComponentRef<TComponent>; 
    private readonly inputs = new Map<keyof TComponent, TComponent[keyof TComponent]>();
    private readonly changesHistory = {} as SimpleChanges;

    constructor(componentRef: ComponentRef<TComponent>) {
        this.componentRef = componentRef;        
    }

    public addInput(
        input: keyof TComponent,
        value: TComponent[typeof input],
    ): void {
        this.inputs.set(input, value);
    }

    public commit() {
        const currentChanges = {} as SimpleChanges;

        this.inputs.forEach((newValue, input) => {
            const previousChange = this.changesHistory[input as string];
            let currentChange: SimpleChange;
            if (!!previousChange) {
                currentChange = {
                    previousValue: previousChange.currentValue,
                    currentValue: newValue,
                    firstChange: false,
                } as SimpleChange;
            } else {
                currentChange = {
                    currentValue: newValue,
                    firstChange: true,
                } as SimpleChange;
            }

            this.changesHistory[input as string] = currentChange;
            currentChanges[input as string] = currentChange;
            this.componentRef.instance[input] = currentChange.currentValue;
        });

        if (implementsOnChanges(this.componentRef.instance)) {
            this.componentRef.instance.ngOnChanges(currentChanges);
        }
    }
}

function implementsOnChanges(component: any): component is OnChanges {
    return (component as OnChanges)?.ngOnChanges !== undefined;
}
Related