7
votes

We want to dynamically add NG_VALUE_ACCESSOR component to a reactive form using a custom directive with ComponentFactoryResolver and ViewContainerRef. The problem is that we can't assign a formControlName to the dynamically added component and we can't get the accessor value from the component.

We tried several different options but none of them worked for us (directly adding formControlName to the ngContainer throws an error, also an option with ngComponentOutlet but we can't provide parameters to the component).

We created a static test case in plunker (the result we want to reach) and a dynamic one which is using the directive where we can't assign formControlName to the component. We'll provide the links in the comment below.

2
Here's our static test case (the result we want to reach) -> plnkr.co/edit/XIkZe8xkoXXr9kEO6a9h?p=preview And the dynamic one (which is using the directive. We can't assign formControlName to this component) ->][2]`plnkr.co/edit/hDHOu96hFQuJSUXy3XMV?p=previewJulien

2 Answers

7
votes

You can try to extend NgControl. Here is simple implementation. But it might be more complex.

dynamic-panel.directive.ts

@Directive({
    selector: '[dynamic-panel]'
})
export class DynamicPanelDirective extends NgControl implements OnInit  {

    name: string;

    component: ComponentRef<any>;

    @Output('ngModelChange') update = new EventEmitter();

    _control: FormControl;

    constructor(
        @Optional() @Host() @SkipSelf() private parent: ControlContainer,
        private resolver: ComponentFactoryResolver,
        private container: ViewContainerRef) {
        super();
    }

    ngOnInit() {
        let component = this.resolver.resolveComponentFactory<GeneralPanelComponent>(GeneralPanelComponent);
        this.name = 'general';
        this.component = this.container.createComponent(component);
        this.valueAccessor = this.component.instance;
        this._control = this.formDirective.addControl(this);
    }

    get path(): string[] {
        return [...this.parent.path !, this.name];
    }

    get formDirective(): any { return this.parent ? this.parent.formDirective : null; }

    get control(): FormControl { return this._control; }

    get validator(): ValidatorFn|null { return null; }

    get asyncValidator(): AsyncValidatorFn { return null; }

    viewToModelUpdate(newValue: any): void {
        this.update.emit(newValue);
    }

    ngOnDestroy(): void {
        if (this.formDirective) {
            this.formDirective.removeControl(this);
        }
        if(this.component) {
            this.component.destroy();
        }
    }
}

Modified Plunker

So

How to dynamically add NG_VALUE_ACCESSOR component to reactive form?

this.valueAccessor = this.component.instance;

in my case

If you want to use validators then see this Plunker

7
votes

The current accepted answer works for the exact scenario in the original post, but a slightly different scenario led me to this post. Thanks to @yurzui, I was able to find a solution based on his answer.

My own solution allows for full integration (including ngModel, reactive forms and validators) into the Angular form ecosystem using the usual declarative bindings in the template. So I'm posting it here in case anybody else will come here looking for this.

You can check it out on StackBlitz.

import {
    Component,
    ComponentFactoryResolver,
    forwardRef,
    Host,
    Injector,
    SkipSelf,
    ViewContainerRef,
} from '@angular/core';
import { ControlContainer, NgControl, NG_VALUE_ACCESSOR } from '@angular/forms';

import { CustomInputComponent } from './custom-input.component';

@Component({
    selector: 'app-form-control-outlet',
    template: ``,
    providers: [
        {
            provide: NG_VALUE_ACCESSOR,
            useExisting: forwardRef(() => FormControlOutletComponent),
            multi: true,
        },
    ],
})
export class FormControlOutletComponent {
    constructor(
        public injector: Injector,
        private componentFactoryResolver: ComponentFactoryResolver,
        private viewContainerRef: ViewContainerRef,
    ) {}

    public ngOnInit(): void {
        const ngControl = this.injector.get(NgControl);
        const componentFactory = this.componentFactoryResolver.resolveComponentFactory(
            /**
              * Retrieve this component in whatever way you would like,
              * it could be based on an @Input or from a service etc...
              */
            CustomInputComponent,
        );

        const componentRef = this.viewContainerRef.createComponent(
            componentFactory,
        );

        ngControl.valueAccessor = componentRef.instance;
    }
}