【问题标题】:How to allow nested components to be tracked by their parent and get values from their parent in Angular?如何允许嵌套组件被其父级跟踪并在 Angular 中从其父级获取值?
【发布时间】:2018-04-10 06:58:15
【问题描述】:

我有一系列表单(每个表单由一个组件管理)。

这些表单中有一种输入模式(例如,其中许多需要允许输入地址),我必须将其重构为可重用的组件,因为它们以多种形式使用并且我不想复制既不是他们的逻辑,也不是他们的模板。

每个可重用的组件都必须

  1. 有其逻辑
  2. 其模板包含输入标签,但没有<form> 标签
  3. 有其客户端验证约束
  4. 可能从其父级接收初始值
  5. 能够将其字段的值作为对象返回给父级(例如address: {street: "...", "city": "...", ...}
  6. 如果不满足验证约束,则使父表单无效
  7. 一旦用户更改了父表单的值,就让其“触动”父表单

this tutorial for Angular2,我了解了如何实现目标124

本教程中的解决方案还允许实现其他目标,但它通过从父级做所有事情来实现(请参阅app.component.ts#initAddress)。

如何实现3567,同时声明控件和他们对孩子的约束?

【问题讨论】:

标签: angular angular5 nested-forms angular-forms angular-validation


【解决方案1】:

如果您想在子组件中提供所有内容,可以尝试这样的操作。

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

Live demo

还请务必查看Angular Forms - Kara Erickson。该演示文稿向您展示了如何创建一个可重用的地址组件,其中包含模板驱动和反应式表单的实现。

【讨论】:

  • 嗨 Tomasz,不要看着我投反对票,但这实际上是我想避免的情况,因为我不希望父母定义孩子
  • @Cec 查看我的编辑。一切都在子组件中声明。
  • 嗨@Tomasz,谁会是注入的formGroup?我认为这将只允许一个嵌套级别是正确的吗?
  • 它将注入最接近的表单组指令。在此示例中,它是 my-app 组件中的那个。
  • 感谢您提供此解决方案。我认为这是不必知道最父组件中的所有表单控件的最简单方法。这种方式验证从下到上进行
【解决方案2】:

你不应该使用这样的实现。使用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

【讨论】:

  • 嗨,在这个解决方案中,父地址为子地址提供验证约束。是否可以在实现 ControlValueAccessor 的孩子中定义它们? (我不想在使用子组件的任何地方重复这些约束)
  • 是的,您可以将其添加到更新方法中并返回 null 或 undefined 直到地址有效
  • 你能详细说明一下吗?我的意思是地址组件atm中有很多更新方法。或者,如果您可以更新 stackblitz,以便我可以从字面上“看到”您的意思:)
  • 我更新了 stackblitz 以更改验证发生的位置。
  • 返回 null 仅在需要地址时才有效,但在它是可选的情况下...
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-10-27
  • 2011-05-16
  • 1970-01-01
  • 2016-04-12
相关资源
最近更新 更多