I'm using Angular 5 and I need to create a component (dynform
) that will instantiate a set of custom components (dyncompA
, dyncompB
, etc.). Which ones is decided at dynform.ngOnInit
, so they're not declared in the parent's template but added dynamically. Each child component holds a value of type MyObjectA
, MyObjectB
, etc. derived from MyObjectAbstract
(not string) for which I implemented a ControlValueAccessor
interface.
The problem I get is that the parent form is never notified about the validity status of the child components, or their changed (!pristine) status. Nor my custom validator is ever called. In addition, the child component doesn't receive it's property value from the AbstractControl
. I can see that ComponentA
's registerOnChange
is never called and nobody is subscribed to the component's valueChange @Output
event. However, if I use ComponentA
statically in a template, all of that works: validators are called, changes are properly propagated, etc. I don't really know if my problem comes either from the dynform
, componentA
, or both.
For the dynform
I started with this template:
<form (ngSubmit)="test()" [formGroup]="fgroup">
<div #container></div>
</form>
My dynform
code has:
@Component({
selector: 'dynform',
templateUrl: '<form (ngSubmit)="test()" [formGroup]="fgroup"><div #container></div></form>'
]
})
export class DynForm implements OnInit, OnDestroy {
constructor( private resolver: ComponentFactoryResolver,
private view: ViewContainerRef) {
}
private mappings: any = {
'compA': { type: ComponentA },
'compB': { type: ComponentB },
...
}
@Input valuecollection: MyObjectAbstract[]; // Set by instantiator
fgroup: FormGroup;
private getComponentFactory(value: compValue): ComponentFactory<{}> {
let entry = this.mappings[value.getCompType()];
return this.resolver.resolveComponentFactory(entry.type);
}
static myValidation(control: AbstractControl): ValidationErrors | null {
let err = {
myValidationError: {
given: control.value,
max: 10,
min: 0
}
}
// Never called anyway
return control.value.toString() == "error" ? err : null;
}
ngOnInit() {
this.valuecollection.( value => {
let name = value.name;
let ref = this.container.createComponent(
this.getComponentFactory(value)
);
ref.instance.value = value;
ref.instance.name = name; // IS THIS OK?
let control = new FormControl(value, [DynForm.myValidation]);
this.fgroup.addControl(name, control);
});
}
ngOnDestroy() {
// Call the created references' destroy() method
}
}
Well, that's the concept, anyway. A typical ComponentA
would be like:
@Component({
selector: 'component-a',
templateUrl: '<stuff></stuff>',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: ComponentA,
multi: true
},
]
})
export class ComponentA implements OnInit, DoCheck, ControlValueAccessor {
@Input() value: MyObjectAbstract;
@Input('formControlName') fname: string; // What?
@Output() valueChange = new EventEmitter<MyObjectAbstract>();
private propagateChange : (_: any) => {};
private _prevValue: string;
getFieldInstanceChange(): EventEmitter<FieldInstance> {
return this.fieldInstanceChange;
}
ngOnInit() {
// TODO: Connect inputFieldText in the view with the field instance (onblur?)
// console.log(`BizbookStringInputComponent()[${this.name}].ngOnInit()`);
if (this.fieldInstance && this.fieldInstance instanceof FieldInstance) {
this.inputFieldName = this.fieldInstance.base.description;
this.inputFieldText = (this.fieldInstance.value as string);
} else {
// this.inputFieldName = this.name;
this.inputFieldText = '(no field instance)';
}
}
ngDoCheck() {
if (this._prevValue == this.value.toString()) return;
if (this.propagateChange) {
// Never gets in here if added dynamically
this._prevValue = value.toString();
this.propagateChange(this.fieldInstance);
} else {
// Always gets in here if added dynamically
console.log(`propagateChange()[${this.name}].ngDoCheck(): change!: "${this.value.toString()}", but propagateChange not yet set.`);
this._prevValue = this.value.toString();
}
}
writeValue(value: any) {
if (value instanceof MyObjectAbstract && value !== this.value) {
this.value = (value as MyObjectAbstract);
}
}
registerOnChange(fn: any) {
// Never called if instantiated dynamically
this.propagateChange = fn;
}
registerOnTouched(fn: any) {
// Not used
}
}
I've read somehere in StackOverflow that ControlValueAccessor
doesn't really apply to dynamically loaded components; and that's why I also implemented the valueChange @Output
. But the problem seems to come from the fact that the ngForm validation logic is tied to the @FormControlName
directive which I don't know how to apply/generate to the dynamic control before its creation.
I've followed this thread but I couldn't get it to work. Actually I'm struggling to understand some of the concepts because I'm new to Angular.
I've been trying to make this work for days and read a lot of articles about validators, custom validators, custom components, dynamic components, etc. to no avail. I'd really appreciate your help.