I have created some custom components, each of which implements ControlValueAccessor, 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 {
}