Angular Partial Reactive Forms

The basic idea here is to have our parent component handle their own fields and child components create their own forms. Once the child forms are created, they will inform about their forms to parent component.

In this case, all the child forms can have their own logic such as validations, events, and other things. So, they are detached from the parent component. Once the child component form is attached to the parent component, it will have all the logic as if it was created in the parent component itself. Hence, when the form is submitted we will have whole combined form group containing fields of both parent and child.

For this, let’s create our form in the child component, when it is created we will inform the parent component about the form group created by the child component and parent will receive that and set it in its own form.

Let's check the below code. First, have a look at our child component.

This is our typescript code:

import { Component, OnInit, Output, Input, EventEmitter } from '@angular/core';
import { Validators, FormGroup, FormArray, ValidatorFn, FormControl, FormBuilder } from '@angular/forms';

@Component({
  selector: 'app-user-address-info',
  templateUrl: './user-address-info.component.html',
  styleUrls: ['./user-address-info.component.css']
})
export class UserAddressInfoComponent implements OnInit {
  addressForm: FormGroup;
  @Output() formCreated = new EventEmitter<FormGroup>();
  @Input() isFormSubmitted;

  get street() { return this.addressForm.get('street') }
  get city() { return this.addressForm.get('city') }
  get state() { return this.addressForm.get('state') }
  get zip() { return this.addressForm.get('zip') }

  constructor(private formBuilder: FormBuilder) { 
      this.buildForm();
  }

  buildForm(){
    this.addressForm = this.formBuilder.group({
      'city': [null, Validators.required],
      'state': [null, Validators.required],
      'street': [null, Validators.required],
      'zip': [null],
    });
  }

  ngOnInit() {
    this.formCreated.emit(this.addressForm);
  }

}

Here, we have used @Output property which will emit the addressForm once it is created.

This is the child component Html template:

<div [formGroup]="addressForm">
  <h4>Address Info</h4>
  <div class="row">
    <div class="col col-xs-6">
      <div class="form-group">
          <label class="control-label">State</label>
          <input type="text" formControlName="state" class="form-control" placeholder="State" />

          <div *ngIf="state.invalid && (isFormSubmitted || state.dirty || state.touched)" class="invalid-feedback" style="display: block">
            <span *ngIf="state.errors.required">State is required</span>
          </div>
        </div>
    </div>
    <div class="col col-xs-6">
      <div class="form-group">
        <label class="control-label">City</label>
          <input type="text" formControlName="city" class="form-control" placeholder="City" />

          <div *ngIf="city.invalid && (isFormSubmitted || city.dirty || city.touched)" class="invalid-feedback" style="display: block">
            <span *ngIf="city.errors.required">City is required</span>
          </div>
        </div>

    </div>
    
  </div>
  <div class="row">
    <div class="col col-xs-6">
      <div class="form-group">
          <label class="control-label">Street</label>
          <input type="text" formControlName="street" class="form-control" placeholder="Street" />

          <div *ngIf="street.invalid && (isFormSubmitted || street.dirty || street.touched)" class="invalid-feedback" style="display: block">
            <span *ngIf="street.errors.required">Street is required</span>
          </div>
        </div>
    </div>
    <div class="col col-xs-6">
      <div class="form-group">
          <label class="control-label">Zip</label>
          <input type="text" formControlName="zip" class="form-control" placeholder="Zip" />

          <div *ngIf="zip.invalid && (isFormSubmitted || zip.dirty || zip.touched)" class="invalid-feedback" style="display: block">
            <span *ngIf="zip.errors.required">Street is required</span>
          </div>
      </div>
    </div>
  </div>
</div>

Let's handle this child component in our parent component.

Typescript code:

import { Component, Output, EventEmitter } from '@angular/core';
import { Validators, FormGroup, FormArray, ValidatorFn, FormControl, FormBuilder } from '@angular/forms';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: [ './app.component.css' ]
})
export class AppComponent  {
  userForm: FormGroup;
  addressFormGroupKey = 'address';
  isFormSubmitted = false;

  get firstName() { return this.userForm.get('firstName') }
  get lastName() { return this.userForm.get('lastName') }

  constructor(private formBuilder: FormBuilder) {     
    this.buildForm();
  }

  buildForm(){
    this.userForm = this.formBuilder.group({
      'firstName': [null, Validators.required],
      'lastName': [null, Validators.required]
    });
  }

  ngOnInit() {
    
  }

  saveUserForm(){
    this.isFormSubmitted = true;
  }

  formInitialized(name, form: FormGroup){
    this.userForm.setControl(name, form);
  }
}

This is our parent component HTML code:

<div class="container-fluid">
  <h2>User Details</h2>
  <small>(Click on save button without filling out the form and then fill the necessary details to see validations in action)</small>
  <br><br>
	<form [formGroup]="userForm" (ngSubmit)="saveUserForm()">
    <div class="row">
      <div class="col col-xs-12">
        <h4>Basic Info</h4>
      </div>
    </div>
		<div class="row">
			<div class="col col-xs-6">
				<div class="form-group">
					<label class="control-label">First Name</label>
          <input type="text" formControlName="firstName" class="form-control" placeholder="First Name" />

          <div *ngIf="firstName.invalid && (isFormSubmitted || firstName.dirty || firstName.touched)" class="invalid-feedback" style="display: block">
            <span *ngIf="firstName.errors.required">First Name is required</span>
          </div>
        </div>
      </div>
      <div class="col col-xs-6">
        <div class="form-group">
          <label class="control-label">Last Name</label>
          <input type="text" formControlName="lastName" class="form-control" placeholder="Last Name" />

          <div *ngIf="lastName.invalid && (isFormSubmitted || lastName.dirty || lastName.touched)" class="invalid-feedback" style="display: block">
            <span *ngIf="lastName.errors.required">Last Name is required</span>
          </div>
        </div>
      </div>
    </div>
    <app-user-address-info [isFormSubmitted]="isFormSubmitted" (formCreated)="formInitialized(addressFormGroupKey, $event)"></app-user-address-info>

    <div class="form-group">
      <button class="btn btn-primary">Save</button>
    </div>
  </form>
  <div >
      <div>
        Is Form Valid: {{userForm.valid}}
      </div>
      <pre>
        {{userForm.value | json}}
      </pre>
    </div>
</div>

We are almost done here.

If you need to load asynchronous data into our main form then, here is how our child component looks:

import { Component, OnChanges, OnInit, Output, Input, EventEmitter, SimpleChanges } from '@angular/core';
import { Validators, FormGroup, FormArray, ValidatorFn, FormControl, FormBuilder } from '@angular/forms';
import { User } from '../user';

@Component({
  selector: 'app-user-address-info',
  templateUrl: './user-address-info.component.html',
  styleUrls: ['./user-address-info.component.css']
})
export class UserAddressInfoComponent implements OnChanges, OnInit {
  addressForm: FormGroup;
  @Output() formCreated = new EventEmitter<FormGroup>();
  @Input() isFormSubmitted;
  @Input() user: User;

  get street() { return this.addressForm.get('street') }
  get city() { return this.addressForm.get('city') }
  get state() { return this.addressForm.get('state') }
  get zip() { return this.addressForm.get('zip') }

  constructor(private formBuilder: FormBuilder) { 
      this.buildForm();
  }

  buildForm(){
    this.addressForm = this.formBuilder.group({
      'city': [null, Validators.required],
      'state': [null, Validators.required],
      'street': [null, Validators.required],
      'zip': [null],
    });
  }

  ngOnInit() {
    this.formCreated.emit(this.addressForm);
  }

  ngOnChanges(changes: SimpleChanges) {
    if (changes.user) { 
      this.addressForm.patchValue({
        state: this.user.state,
        city: this.user.city,
        street: this.user.street,
        zip: this.user.zip
      }) 
    }
  }

}

You can see in the above code that we are accepting @Input property user which contains the relevant data we need to bind to addressForm

We can get async data from parent component and pass here in the child component.

You can click here to check the full source code with demo.