Запустите проверку ControlValueAccessor после обновления и проверки дочерних компонентов

Я столкнулся с серьезной проблемой в Angular. Я хочу создать компонент настраиваемой формы, который «связывает» несколько дочерних элементов ввода и действителен, когда допустимы все дочерние элементы.

Я думал, что просто создам компонент, реализующий интерфейсы ControlValueAccessor и Validator. Я бы ввел входные данные с помощью декоратора ViewChildren как NgModels, чтобы я мог перебирать их и собирать любые ошибки проверки в методе проверки компонента. Вот пример реализации компонента ввода адреса, который я сделал, который делает это для двух входов (улица и номер улицы), для которых требуется значение:

import { AfterViewInit, Component, forwardRef, OnInit, QueryList, ViewChildren } from '@angular/core';
import { AbstractControl, ControlValueAccessor, NG_VALIDATORS, NgModel, NG_VALUE_ACCESSOR, ValidationErrors, Validator } from '@angular/forms';
import { Address } from './address';

@Component({
    selector: `address-input`,
    template: `
        <label for="street">Street</label>
        <input id="street" name="street" type="text" [(ngModel)]="Street" (blur)="onTouchedAField()" required /><br />
        <label for="number">Number</label>
        <input id="number" name="number" type="number" [(ngModel)]="Number" (blur)="onTouchedAField()" required />`,
    providers: [
        {
            provide: NG_VALUE_ACCESSOR,
            useExisting: AddressInputComponent,
            multi: true,
        },
        {
            provide: NG_VALIDATORS,
            useExisting: AddressInputComponent,
            multi: true,
        },
    ],
})
export class AddressInputComponent implements ControlValueAccessor, Validator {
    @ViewChildren(NgModel) public validatedFields!: QueryList<NgModel>;

    private address: Address = new Address(null, null);

    private onFormCtrlChanged!: (_: any) => void;
    private onFormCtrlTouched!: (_: any) => void;

    public get Street(): string | null {
        if (this.address) {
            return this.address.Street;
        } else {
            return null;
        }
    }
    public set Street(val: string) {
      this.address = new Address(val, this.address.Number);

        this.emitChanged();
    }

    public get Number(): number | null {
        if (this.address) {
            return this.address.Number;
        } else {
            return null;
        }
    }
    public set Number(val: number | null) {
        this.address = new Address(this.address.Street, val);

        this.emitChanged();
    }

    public onTouchedAField(): void {
        this.emitTouched();
    }

    public writeValue(newAddress: Address): void {
        if (newAddress !== undefined) {
            this.address = newAddress;
        }
    }

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

    public registerOnTouched(fn: (_: any) => void): void {
        this.onFormCtrlTouched = fn;
    }

    public validate(control: AbstractControl): ValidationErrors | null {
        let validationErrors: ValidationErrors | null = null;
        this.validatedFields.forEach((ngm: NgModel) => {
            console.log(`ValidationErrs for ${ngm.name} with value ${ngm.value}`, ngm.errors);
            if (ngm.errors !== null) {
                if (validationErrors === null)
                    validationErrors = {};
                validationErrors[ngm.name] = ngm.errors;
            }
        });

        console.log(`validationErrors `, validationErrors);
        return validationErrors;
    }

    private emitChanged(): void {
        this.onFormCtrlChanged(this.address);
    }

    private emitTouched(): void {
        this.onFormCtrlTouched(this.address);
    }
}

В основном это отлично работает, когда я встраиваю компонент адреса в такую ​​форму:

<form (ngSubmit)="onSubmit()" #myForm="ngForm">
    <address-input name="address" [(ngModel)]="address" #addressInput="ngModel"></address-input>

    <button type="submit">SUBMIT</button>
</form>

Единственная, но раздражающая проблема заключается в том, что, когда компонент ввода адреса будет правильно заполнен через привязку модели, компонент ввода адреса, тем не менее, останется в недопустимом состоянии.

Т.е. когда компонент приложения, содержащий форму, которая содержит компонент настраиваемой формы, определяет поле адреса, которое он привязывает к компоненту настраиваемой формы, например:

public address: Address = new Address('Baker Street', 123);

компонент настраиваемой формы должен быть действительным, но остается недействительным.

Когда я затем добавляю букву или цифру в поле, например, компонент настраиваемой формы сразу становится действительным.

После некоторого расследования я заметил, что это связано с тем, что Angular вызывает метод проверки интерфейса Validator сразу после того, как он вызывает метод writeValue интерфейса ControlValueAccessor в компоненте настраиваемой формы. Таким образом, значения дочерних входов компонента настраиваемой формы и их состояние проверки еще не обновляются, когда Angular вызывает метод проверки в компоненте настраиваемой формы.

Чтобы решить мою проблему, мне интересно, есть ли способ заставить Angular обновлять значение и состояние проверки ngModels или снова запускать проверку при обновлении дочерних входов / компонентов. Или есть другой способ заставить эту работу работать. В более широком смысле, мне также интересно, есть ли лучший способ реализовать компонент формы, который действителен, когда есть все его дочерние элементы, потому что я чувствую, что для исправления этой проблемы потребуются некоторые «взломы» или неэффективный код.

Здесь вы можете найти Stackblitz, содержащий простое воспроизведение проблемы.

Заранее спасибо за ваше время и помощь, Джошуа


person Joshua Schroijen    schedule 24.07.2019    source источник


Ответы (4)


Ваш код был бы намного проще, если бы вы использовали ReactiveForms, в ReactiveForms источником истины является модель, и у вас всегда есть доступ к состоянию вашего отдельного элемента управления или группы элементов управления без сканирования компонента с помощью @ViewChildren, вот очень хороший пример того, как это реализовано из угловых документов https://angular.io/guide/form-validation#cross-field-validation

person Alexandr Mihalciuc    schedule 24.07.2019
comment
Я совершенно уверен, что проблема не в QueryList. Я использую аксессоры для привязки модели в примере. Когда я ввожу вход в геттеры, я вижу, что значения модели доступны для обновления только после того, как проверка была вызвана в моем компоненте ... Я понимаю, что реактивные формы, вероятно, здесь были бы лучше, но я не могу их использовать, так как мои команде они почему-то очень не нравятся ... - person Joshua Schroijen; 24.07.2019
comment
Вы правы. QueryList - это не проблема, проблема в порядке событий в шаблонных формах, которые, очевидно, у вас не работают. Вы можете попробовать взломать его, проверив, является ли вызов проверки первым или нет, но зачем это делать, если Angular уже предоставил лучший вариант. Фактически TemplatedForms внутренне построены поверх реактивных, поэтому вы, вероятно, используете его, не осознавая этого :)) - person Alexandr Mihalciuc; 24.07.2019
comment
Вы были правы, в данном случае лучше всего использовать реактивные формы. Я реализовал свой компонент с дочерними входами вместе в FormGroup, и теперь все отлично работает. Если кто-то хочет знать, как именно я его реализовал сейчас, ознакомьтесь с этим Stackblitz - person Joshua Schroijen; 24.07.2019

Вы можете попробовать использовать элементы управления FormGroup для вложения форм: https://angular.io/guide/reactive-forms#step-1-creating-a-nested-group

person Eli    schedule 24.07.2019

спасибо за такое подробное объяснение ... У меня была аналогичная проблема, когда значение по умолчанию, установленное для custom-component, не проверяло компонент-оболочку. После отладки понял, что onChangeCb () все еще тот, который я инициализировал, а не тот, который назначает registerOnChange. Так что изменения не распространялись. Ниже представлено изменение, которое я пробовал и, похоже, работает. пожалуйста, проверьте

...
public registerOnChange(fn) {
            this._onChange = fn;
            /* value is model */
            this.value && setTimeout(()=>{this._onChange(this.value)},0);
        }
...
person Subodh Kumar    schedule 22.01.2020

В дополнение к приведенному выше, я чувствую, что приведенное ниже решение лучше, поскольку оно не затрагивает какой-либо setTimeout

public writeValue(val) {
// EXISING CODE
...

(val!==this.value) && this._onChange(this.value);

}
person Subodh Kumar    schedule 23.01.2020