7

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.

Julien
  • 73
  • 1
  • 3
  • Here's our static test case (the result we want to reach) -> http://plnkr.co/edit/XIkZe8xkoXXr9kEO6a9h?p=preview And the dynamic one (which is using the directive. We can't assign formControlName to this component) ->][2]`http://plnkr.co/edit/hDHOu96hFQuJSUXy3XMV?p=preview – Julien May 25 '17 at 12:56

2 Answers2

9

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;
    }
}
Nathan
  • 268
  • 2
  • 7
  • 1
    Thank you for the brilliant solution. Hope you don't mind that I referred to it in an answer to a similar SO question: https://stackoverflow.com/a/67861569/2879716. I provided a link to your code and an explanation of how it actually works. The only inconvenience I'm having with it is that I can't simply use standard HTML `` or ` – Alexey Grinko Jun 06 '21 at 16:57
  • Your code looks and works great, but I tested it and it doesn't work with [formGroup] and [formControlName] directives, I get an error: "ngModel cannot be used to register form controls with a parent formGroup directive. Try using formGroup's partner directive formControlName instead". Could be your code adapted to work with this reactive forms directives?. – LeonardoX Sep 19 '21 at 22:19
  • Alexey, i havent tested it yet but you can include a simple input by providing an ngtemplate to the Wrapper Component and use the ViewContainerRef to embed said template. However you have to include a @ViewChild and query the input – LookForAngular Apr 09 '22 at 10:46
7

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

yurzui
  • 205,937
  • 32
  • 433
  • 399
  • Hello, @yurzui, and thank you for your post. We succeeded to dynamically add the formControlName to the component but now we face another issue. Our component also implements Validator interface and we want to check the validity of the component in the main form. We edited our static plunker with the result we want to reach -> http://plnkr.co/edit/XIkZe8xkoXXr9kEO6a9h?p=preview and our dynamic plunker with your directive. We just added validators to it -> http://plnkr.co/edit/hDHOu96hFQuJSUXy3XMV?p=preview. As you see the panel validity is false and the main form validity is always true. – Julien May 26 '17 at 11:50
  • As i said the solution might be more complex. Here is example for sync validators http://plnkr.co/edit/osU3IUTIvvMkUTyMovmt?p=preview – yurzui May 26 '17 at 12:23
  • Thank you very much. You saved us hours. We implemented your directive in our solution and everything is fine now :) – Julien May 26 '17 at 15:08
  • 2
    why most of the plunker examples from this and other posts which are 1 year old or more are not working anymore? is there any way to do a quick fix or move them to stackblitz?. thanks – LeonardoX Sep 13 '21 at 08:23