B

In reactive forms, suppose if you create some fields and later, if need arises that you want to use these same fields in other parts of the application then need to manage them on multiple places. So, you have to create all the related form fields into their own group in separate component and use the same component on multiple reactive forms.

 

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

 

In this case, all the child forms can have their own logic such as validations, events, and other stuff. Hence they are detached from the parent component. Once the child component form is attached to the parent, 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 below code. First, have a look at our child component.

 

typescript:

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, I have used @Output property which will emit the addressForm once it is created.

 

Html:

<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 in our parent component:

 

typescript:

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

 

Html:

<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 form then, here is how our child component looks like:

 

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.

 

That's it. Click here to view the full source code.

Tags:
  • Reactive Forms

Comments

Leave a Reply