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

Cec picture Cec · Apr 10, 2018 · Viewed 11.6k times · Source

I have a series of forms (each managed by 1 component).

There is a pattern of inputs in these forms (e.g. many of them require to allow input of an address) that I have to refactor into re-usable components as they are used in multiple forms and I don't want to duplicate neither their logic nor their templates.

Each re-usable component would have to

  1. have its logic
  2. have its template containing input tags and no <form> tag
  3. have its client validation constraints
  4. possibly receive initial values from its parent
  5. be able to return the value of its fields to the parent as an object (e.g. address: {street: "...", "city": "...", ...})
  6. make the parent form invalid if its validation constraints are not satisfied
  7. make the parent form "touched" once its values have been changed by the user

From this tutorial for Angular2, I understand how to achieve objectives 1, 2 and 4.

The solution in the tutorial allows to achieve also the other objectives, but it does so by doing everything from the parent (see app.component.ts#initAddress).

How can I achieve 3, 5, 6 and 7, while declaring controls and their constraints within the child?

Answer

Tomasz Kula picture Tomasz Kula · Apr 10, 2018

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.