Steve Paul Steve Paul - 1 month ago 32
TypeScript Question

How to register a dynamically added custom component as a form control within a formgroup

I have created some custom components, each of which implements

, in order that they may function as controls within a
FormGroup
. Usually, the only thing left to do would be to add the
formControlName
directive to each custom component in HTML.

However, I'm appending these components to the form at runtime using this technique.

My problem is that I cannot register these components with a containing
FormGroup
because I cannot declare the
formControlName
directive (or any directive, for that matter) with a dynamically added control.

Has anyone else discovered how this might be possible? At the moment, I'm wrapping each control inside another component so that I can use the
formControlName
directive but this is just too ugly and labor intensive.




Below is a stripped down example of what I've already implemented as a standalone Angular2 app, which shows a component (
CustomComponent
) being programatically added on startup. In order to bind
CustomComponent
to the
FormGroup
, I've had to create
CustomContainerComponent
which I'd prefer to avoid.

import {
Component, ReflectiveInjector, ViewContainerRef, Compiler, NgModule, ModuleWithComponentFactories,
OnInit, ViewChild, forwardRef
} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {NG_VALUE_ACCESSOR, ControlValueAccessor, FormGroup, FormBuilder, ReactiveFormsModule} from '@angular/forms';


export class AbstractValueAccessor<T> implements ControlValueAccessor {
_value: T;
get value(): T {
return this._value;
};

set value(v: T) {
if (v !== this._value) {
this._value = v;
this.onChange(v);
}
}

writeValue(value: T) {
this._value = value;
this.onChange(value);
}

onChange = (_) => {};
onTouched = () => {};

registerOnChange(fn: (_: any) => void): void {
this.onChange = fn;
}

registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}

}

export function MakeProvider(type: any) {
return {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => type),
multi: true
};
}

@Component({
selector: 'app-custom',
template: `<input type="text" [value]="value">`,
providers: [MakeProvider(CustomComponent)]
})
class CustomComponent extends AbstractValueAccessor<string> {

}

@Component({
selector: 'app-custom-container',
template: `<div [formGroup]="formGroup"><app-custom formControlName="comp"></app-custom></div>`
})
class CustomContainerComponent {
formGroup: FormGroup;
}

@NgModule({
imports: [BrowserModule, ReactiveFormsModule],
declarations: [CustomComponent, CustomContainerComponent]
})
class DynamicModule {
}

@Component({
selector: 'app-root',
template: `<h4>Dynamic Components</h4><br>
<form [formGroup]="formGroup">
<div #dynamicContentPlaceholder></div>
</form>`
})
export class AppComponent implements OnInit {

@ViewChild('dynamicContentPlaceholder', {read: ViewContainerRef})
public readonly vcRef: ViewContainerRef;

factory: ModuleWithComponentFactories<DynamicModule>;
formGroup: FormGroup;

constructor(private compiler: Compiler, private formBuilder: FormBuilder) {
}

ngOnInit() {
this.compiler.compileModuleAndAllComponentsAsync(DynamicModule)
.then((moduleWithComponentFactories: ModuleWithComponentFactories<DynamicModule>) => {
this.factory = moduleWithComponentFactories;
const compFactory = this.factory.componentFactories.find(x => x.selector === 'app-custom-container');
const injector = ReflectiveInjector.fromResolvedProviders([], this.vcRef.parentInjector);
let cmp = this.vcRef.createComponent(compFactory, this.vcRef.length, injector, []);
(<CustomContainerComponent>cmp.instance).formGroup = this.formGroup;
});

this.formGroup = this.formBuilder.group({
'comp': ['hello world']
})
}
}

@NgModule({
imports: [BrowserModule, ReactiveFormsModule],
declarations: [AppComponent],
bootstrap: [AppComponent]
})
export class AppModule {
}

Answer

If I do not misunderstand your needs...

Weird thing is that you attach your form group inside of the AppComponent. Instead you could create the full form dynamically. In order to do that you need to implement two functions:

  1. One that generates the template from JSON config
  2. Another one that generates the form from JSON config

Then simply build a big form container component that contains all your inputs. I would recommend using a service here:

@Injectable()
export class ModuleFactory {

  public createComponent(jsonConfig: SomeInterface) {
    @Component({
      template: `
        <div ngForm="form">${ /* and here generate the template containing all inputs */ }</div>
      `
    })
    class FormContainerComponent {
      public form: FormGroup = /* generate a form here */;
    };

    return FormContainerComponent;
  }

  public createModule(component: any) {
    @NgModule({
      imports: [
        CommonModule,
        ReactiveFormsModule
      ],
      declarations: [ component ]
    })
    class MyModule {};

    return MyModule;
  }

}

This is of course just a simplified version, but it works fine for my project not only for the plain form but for nested form groups / arrays / controls.

Once you inject the service you can use these two functions to generate the module and the component.