How to allow nested components to be tracked by their parent and get values from their parent in Angular?

China☆狼群 提交于 2019-12-04 12:15:21
Tomasz Kula

If you want to provide everything in the child component you can try something like this.

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);
  }
}

Usage:

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({});
  }
}

Please note that I'm using the ReactiveFormsModule.

Live demo

Also make sure to check out Angular Forms - Kara Erickson. The presentation shows you how to create a reusable address component with implementations for both, template driven and reactive forms.

JEY

You shouldn't use such implementation. It much more clean to use ControlValueAccessor.

ControlValueAccessor is an interface that allows to angular form module (classic or reactive) to write value or state and register callback to retrieve changes and event.

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.

But you also need to provide a NG_VALUE_ACCESSOR

I wrote this quick and dirty example for an address component:

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

Then in a form use it like any other form element:

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

You could add more logic into the component. Here is a working example. Please keep in mind it's a quick example and should be rework for cleaner code.

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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!