Angular 5 动态添加到表单的组件胶合逻辑

Angular 5 glue logic of components dynamically added to form

我正在使用 Angular 5 并且我需要创建一个组件 (dynform) 来实例化一组自定义组件 (dyncompA, dyncompB, 等等)。哪些是在 dynform.ngOnInit 决定的,因此它们不会在父模板中声明,而是动态添加。每个子组件都有一个 MyObjectAMyObjectB 等类型的值,派生自 MyObjectAbstract(不是字符串),我为其实现了一个 ControlValueAccessor 接口。

我遇到的问题是,父表单从未收到有关子组件的有效性状态或其已更改(!原始)状态的通知。我的自定义验证器也从未被调用过。此外,子组件不会从 AbstractControl 接收它的 属性 值。我可以看到从未调用 ComponentAregisterOnChange 并且没有人订阅组件的 valueChange @Output 事件。但是,如果我在模板中静态使用 ComponentA,所有这些都有效:调用验证器,正确传播更改等。我真的不知道我的问题是否来自 dynformcomponentA,或两者兼而有之。

对于 dynform 我从这个模板开始:

<form (ngSubmit)="test()" [formGroup]="fgroup">
    <div #container></div>
</form>

我的 dynform 代码有:

@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
    }
}

好吧,无论如何,这就是概念。一个典型的 ComponentA 就像:

@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
    }
}

我在 Whosebug 的某个地方读到 ControlValueAccessor 并不真正适用于动态加载的组件;这就是为什么我还实现了 valueChange @Output。但问题似乎来自于 ngForm 验证逻辑与 @FormControlName 指令相关联的事实,我不知道如何在创建动态控件之前将其 apply/generate

我关注了 this thread,但我无法让它发挥作用。实际上我很难理解一些概念,因为我是 Angular.

的新手

几天来我一直在努力使它工作,并阅读了很多关于验证器、自定义验证器、自定义组件、动态组件等的文章,但都无济于事。非常感谢您的帮助。

看来我的整个方法都不必要地令人费解。 post 中详细解释了处理此问题的正确方法,其中还包括源代码:

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

基本上,您需要做的是在 <ng-container YourAttribute [formControlName]="something"> 上使用 ngFor 循环。 YourAttribute 是一个动态创建组件的指令。注意 [formControlName] 的 [] 语法,因为它会将值(和 FormControlName 指令!)注入 YourAttribute。

链接的项目工作得很好,但我将 ControlValueAccessor 添加到我的指令中(因为我不使用 DefaultValueAccessor)。然后我的指令需要通过 setTimeout 将 ControlValueAccessor 方法链接到实例化控件中以避免“Error: Expression has changed after it was checked. Inside of nested ControlValueAccessor”。