Как разрешить родительским компонентам отслеживать вложенные компоненты и получать значения от них в Angular?

У меня есть несколько форм (каждая из которых управляется одним компонентом).

В этих формах есть шаблон ввода (например, многие из них требуют ввода адреса), который мне нужно реорганизовать в повторно используемые компоненты, поскольку они используются в нескольких формах, и я не хочу дублировать ни их логику. ни их шаблоны.

Каждый повторно используемый компонент должен

  1. иметь свою логику
  2. иметь свой шаблон, содержащий теги ввода, а не тег <form>
  3. иметь свои ограничения проверки клиента
  4. возможно получить начальные значения от своего родителя
  5. иметь возможность возвращать значение своих полей родителю как объект (например, address: {street: "...", "city": "...", ...})
  6. сделать родительскую форму недействительной, если ее ограничения валидации не выполнены
  7. сделать родительскую форму "затронутой" после того, как ее значения были изменены пользователем

Из этого руководства для Angular2, я понимаю, как достичь целей 1, 2 и 4.

Решение в руководстве позволяет достичь и других целей, но это достигается за счет выполнения всего из родительского (см. app.component.ts#initAddress).

Как достичь 3, 5, 6 и 7, объявляя элементы управления и их ограничения в ребенок?


person Cec    schedule 10.04.2018    source источник
comment
Это должно вам помочь: telerik.com/blogs/nested-forms-in -угловая-6   -  person Neel    schedule 23.10.2018


Ответы (2)


Если вы хотите предоставить все в дочернем компоненте, вы можете попробовать что-то вроде этого.

import { Component, Input } from '@angular/core';
import { FormGroupDirective, ControlContainer, Validators, FormGroup, FormControl } from '@angular/forms';

@Component({
  selector: 'address',
  template: `
    <div formGroupName="address">
      <input formControlName="city" placeholder="city" (blur)="onTouched" />
      <input formControlName="country" placeholder="country" (blur)="onTouched" />
      <input formControlName="zipCode" placeholder="zipCode" (blur)="onTouched" />
    </div>
  `,
  styles: [`h1 { font-family: Lato; }`],
  viewProviders: [
    { provide: ControlContainer, useExisting: FormGroupDirective }
  ]
})
export class AddressComponent {
  private form: FormGroup;

  constructor(private parent: FormGroupDirective) { }

  ngOnInit() {
    this.form = this.parent.form;

    const city = new FormControl('', Validators.required);
    const country = new FormControl('', Validators.required);
    const zipCode = new FormControl('', Validators.required);

    const address = new FormGroup({ city, country, zipCode });

    this.form.addControl('address', address);
  }
}

Использование:

import { Component } from '@angular/core';
import { FormGroup } from '@angular/forms';

@Component({
  selector: 'my-app',
  template: `
  <form [formGroup]="form">
    <address></address>
  </form>

  {{ form.value | json }}
  `,
  styleUrls: ['./app.component.css'],

})
export class AppComponent {
  form: FormGroup;

  constructor() {
    this.form = new FormGroup({});
  }
}

Обратите внимание, что я использую ReactiveFormsModule.

Живая демонстрация

Также не забудьте ознакомиться с Angular Forms - Kara Erickson. В презентации показано, как создать компонент адреса многократного использования с реализациями как для форм на основе шаблонов, так и для реактивных форм.

person Tomasz Kula    schedule 10.04.2018
comment
Привет, Томаш, не смотрите на меня из-за отрицательного голоса, но все же это сценарий, которого я бы хотел избежать, так как я не хочу, чтобы родитель определял ребенка - person Cec; 10.04.2018
comment
@Cec зацени мое редактирование. Все объявлено в дочернем компоненте. - person Tomasz Kula; 10.04.2018
comment
Привет @Tomasz, кто будет вводить formGroup? Правильно ли я полагаю, что это допускает только один уровень вложенности? - person Cec; 10.04.2018
comment
Он внедрит директиву ближайшей группы формы. В этом примере это тот, который находится в компоненте my-app. - person Tomasz Kula; 10.04.2018
comment
Спасибо за это решение. Я думаю, что это самый простой способ не знать все элементы управления формами в самом родительском компоненте. Таким образом, проверка работает снизу вверх - person Mcanic; 24.11.2018

Не стоит использовать такую ​​реализацию. Намного проще использовать ControlValueAccessor.

ControlValueAccessor - это интерфейс, который позволяет модулю угловой формы (классическому или реактивному) записывать значение или состояние и регистрировать обратный вызов для получения изменений и событий.

writeValue(value: Address): void { } // Allows angular to set a default value to the component (used by FormControl or ngModel)

registerOnChange(fn: (_: any) => void): void {} // Callback to be called when the component value change.

registerOnTouched(fn: (_: any) => void): void { } // Callback to be called when a "touch" event occurs on the component

setDisabledState(isDisabled: boolean): void { } // Allows angular to update the component disable state.

Но вам также необходимо предоставить NG_VALUE_ACCESSOR

Я написал этот быстрый и грязный пример для компонента адреса:

import { Component, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Address } from './address';

@Component({
  selector: 'app-address-input',
  template: `
    <label for="number">Num: </label>
    <input type="number" [disabled]="disabled" name="number" id="number" (change)="numberUpdate($event)" value="{{value.num}}"/><br />
   <label for="street">Street: </label>
   <input type="text" [disabled]="disabled" (change)="streetUpdate($event)"name="street" id="street" value="{{value.street}}" /><br />
   <label for="city">City: </label>
   <input type="text" [disabled]="disabled" name="city" id="city" value="{{value.city}}" (change)="cityUpdate($event)" /><br />
   <label for="zipCode">Zip Code: </label>
   <input type="text" [disabled]="disabled" name="zipCode" id="zipCode" value="{{value.zipCode}}" (change)="zipCodeUpdate($event)" /><br />
  <label for="state">State: </label>
  <input type="text" [disabled]="disabled" name="state" id="state" value="{{value.state}}" (change)="stateUpdate($event)" /><br />
  <label for="country">Country: </label>
  <input type="text" [disabled]="disabled" name="country" id="country" value="{{value.country}}" (change)="countryUpdate($event)" />`,
  providers: [{
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() => AddressInputComponent) // forward the reference,
    multi: true // allow multiple component in the same form
    }]
})
export class AddressInputComponent implements ControlValueAccessor {

  private _onChange = (_: any) => {};
  private _onTouched = (_: any) => {};
  disabled = false;

  private _value: Address = {num: undefined, street: undefined, city: undefined, state: undefined, zipCode: undefined, country: undefined}; // current value (Address is just an interface)

  set value(value: Address) { // interceptor for updating current value
    this._value = value;
   this._onChange(this._value);
  }

  get value() {
    return this._value;
  }

  writeValue(value: Address): void {
    if (value && value !== null) {
      this._value = value;
    }
  }

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

  registerOnTouched(fn: (_: any) => void): void {
    this._onTouched = fn; // didn't used it but you should for touch screen enabled devices.
  }

  setDisabledState(isDisabled: boolean): void {
    this.disabled = isDisabled;
  }

  numberUpdate(event: any) {
    // additional check or process
    this._updateValue(event.target.value, 'num')
  }

  streetUpdate(event: any) {
    // additional check or process
    this._updateValue(event.target.value, 'street')
  }

  cityUpdate(event: any) {
    // additional check or process
    this._updateValue(event.target.value, 'city')
  }

  zipCodeUpdate(event: any) {
    // additional check or process
    this._updateValue(event.target.value, 'zipCode')
  }

  stateUpdate(event: any) {
    // additional check or process
    this._updateValue(event.target.value, 'state')
  }

  countryUpdate(event: any) {
    // additional check or process
    this._updateValue(event.target.value, 'country');
  }

  private _updateValue(value: any, field: string) {
    const newValue = this._value;
    newValue[field] = value;
    this.value = newValue;
  }
}

Затем в форме используйте его как любой другой элемент формы:

<form [formGroup]="registerForm">
  <app-address-input formControlName="address"></app-address-input>
</form>

Вы можете добавить в компонент больше логики. Вот рабочий пример. Имейте в виду, что это быстрый пример, и его следует переработать для более чистого кода.

https://stackblitz.com/edit/angular-4dgxqh

person JEY    schedule 10.04.2018
comment
Привет, в этом решении родительский элемент предоставляет ограничения проверки для дочернего адреса. Можно ли определить их в дочернем элементе, реализующем ControlValueAccessor? (Я не хочу повторять эти ограничения везде, где я использую дочерний компонент) - person Cec; 10.04.2018
comment
Да, вы можете просто добавить его в метод обновления и вернуть null или undefined, пока адрес не станет действительным. - person JEY; 10.04.2018
comment
Не могли бы вы подробнее рассказать об этом? Я имею в виду, что есть много методов обновления в atm компонент адреса. Или, если бы вы могли обновить stackblitz, чтобы я буквально мог видеть, что вы имеете в виду :) - person Cec; 10.04.2018
comment
Я обновил stackblitz, чтобы изменить место проверки. - person JEY; 10.04.2018
comment
Возврат null будет работать только в том случае, если адрес требуется, но в тех случаях, когда это необязательно ... - person Cec; 11.04.2018
comment
Тогда проверка должна происходить в определении формы, а не в самом компоненте. Созданный мной ранее валидатор можно было повторно использовать в любой форме. Обязательно на основе null и undefined, поэтому я вернул null. - person JEY; 11.04.2018
comment
вот быстрый пример, stackblitz.com/edit/angular-eu5zns, который вам все известно. - person JEY; 11.04.2018