简体   繁体   中英

Angular 5 glue logic of components dynamically added to form

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 v alueChange @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.

It looks like my whole approach was unnecessarily convoluted. The correct way to handle this is very thorougly explained in this post, which also includes source code:

https://toddmotto.com/angular-dynamic-components-forms

Basically, what you need to do is to use an ngFor loop over an <ng-container YourAttribute [formControlName]="something"> . YourAttribute is a directive that will dynamically create the component. Notice the [] syntax for [formControlName], as it will inject the value (and the FormControlName directive!) into YourAttribute.

The linked project works beautifully, but I added ControlValueAccessor to my directive (as I don't use DefaultValueAccessor). Then my directive needs to chain the ControlValueAccessor method into the instantiated control through setTimeout to avoid " Error: Expression has changed after it was checked. Inside of nested ControlValueAccessor ".

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM