Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to dynamically add NG_VALUE_ACCESSOR component to reactive form?

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.

like image 337
Julien Avatar asked May 25 '17 12:05

Julien


2 Answers

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;
    }
}
like image 55
Nathan Avatar answered Oct 05 '22 23:10

Nathan


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

like image 22
yurzui Avatar answered Oct 05 '22 23:10

yurzui