Handling Angular 2+ Forms Without Losing Your Sanity

Jennifer Wadella

Thanks to All Our Sponsors

Jennifer Wadella

@likeOMGitsFEDAY

  • 9-5 Remote Software Engineer
  • Nonprofit Founder/Director
  • International Speaker
  • Aspiring crazy plant lady

This talk may contain strong language, harsh truths, and serious passion.

image/svg+xml
Paul Rudd

“Why’d you have to go and make things so complicated?”

- Avril Lavigne, implementing Angular forms in 2018

Quick Overview of the Basics

Template-driven Forms vs. Reactive Forms

Template Driven Forms Imports

							
	import { NgModule } from '@angular/core';
	import { BrowserModule } from '@angular/platform-browser';
	import { FormsModule } from '@angular/forms';

	@NgModule({
	  imports: [BrowserModule, FormsModule],
	  declarations: [AppComponent],
	  bootstrap: [AppComponent]
	})
	export AppModule {}
							
						

Template Driven Forms Markup

							
<form>
  <label for="name">Name</label>
  <input class="form-control" name="name" ngModel>

  <label for="description">Description</label>
  <input class="form-control" name="description" ngModel>

  <label for="source">Source</label>
  <input class="form-control" name="source" ngModel>

  <label for="url">Url</label>
  <input class="form-control" name="url" ngModel>
</form>
							
						

Template Driven Forms ...

Reactive Forms Imports

							
	import { NgModule } from '@angular/core';
	import { BrowserModule } from '@angular/platform-browser';
	import { ReactiveFormsModule } from '@angular/forms';

	@NgModule({
	  imports: [BrowserModule, ReactiveFormsModule],
	  declarations: [AppComponent],
	  bootstrap: [AppComponent]
	})
	export AppModule {}
							
						

Reactive Forms Component

							
import { Component } from '@angular/core';
import { FormGroup, FormControl } from '@angular/forms';

@Component({
  selector: 'recipe',
  templateUrl: './recipe.component.html',
  styleUrls: ['./recipe.component.less']
})
export class RecipeComponent {
  recipeForm = new FormGroup({
    name: new FormControl(''),
    description: new FormControl(''),
    source: new FormControl(''),
    url: new FormControl(''),
  });
}
							
						

Reactive Forms Markup

							
<form [formGroup]="recipeForm">
  <label for="name">Name</label>
  <input class="form-control" formControlName="name">

  <label for="description">Description</label>
  <input class="form-control" formControlName="description">

  <label for="source">Source</label>
  <input class="form-control" formControlName="source">

  <label for="url">Url</label>
  <input class="form-control" formControlName="url">
</form>
							
						

Reactive Forms ...

Complicated Problems

  1. I need dynamic required fields
  2. I need custom validation rules
  3. I need to display one thing in an input field
    for the user but submit something else
  4. I have a bunch of checkboxes and need to
    submit an array
  5. I need to dynamically create and remove
    form controls & form groups

Demo Code Available:

https://github.com/tehfedaykin/complicated-forms-app

Problem 1:

I need dynamic required fields

Let's say we only want the subcategories
field to be required if categories has a value

							
<form [formGroup]="recipeForm" (ngSubmit)="onSubmit()">
  <div class="form-group">
    <label for="category">Category</label>
    <div>
      <ng-container *ngFor="let category of categories">
        <div class="form-check form-check-inline">
          <input class="form-check-input" type="radio" [value]="category.id" formControlName="category">
          <label class="form-check-label">{{category.name}}</label>
        </div>
      </ng-container>
    </div>
  </div>
  <div class="form-group" *ngIf="recipeForm.controls.category.value">
    <label for="category">Subcategory</label>
    <my-checkboxes formControlName="subcategory" [data]="subcategories" ></my-checkboxes>
  </div>
</form>
							
						

k, so initially subcategory is unrequired

							
this.recipeForm = this.fb.group({
  name: [null, Validators.required],
  description: [null],
  source: [null],
  url: [null],
  category: [null],
  subcategory: [[]],
  ingredients: this.fb.array([])
});
this.onChanges();
							
						

Step One

Subscribe to changes on the field that
determines other fields' requiredness

							
onChanges() {
  this.recipeForm.get('category').valueChanges.subscribe(val => {
  });
}
							
						

Step Two

Get fields control and use setValidators method.

							
onChanges() {
  this.recipeForm.get('category').valueChanges.subscribe(val => {
    const subCategoryControl = this.recipeForm.get('subcategory');
    if (val) {
      subCategoryControl.setValidators(Validators.required);
    }
    else {
      subCategoryControl.setValidators(null);
    }
  });
}
							
						

Step Three

Update changed fields validity.

							
onChanges() {
  this.recipeForm.get('category').valueChanges.subscribe(val => {
    const subCategoryControl = this.recipeForm.get('subcategory');
    if (val) {
      subCategoryControl.setValidators(Validators.required);
      subCategoryControl.updateValueAndValidity();
    }
    else {
      subCategoryControl.setValidators(null);
      subCategoryControl.updateValueAndValidity();
    }
  });
}
							
						

What if...

an option was supposed to be preselected based on data?

							
onChanges() {
  this.recipeForm.get('category').valueChanges.subscribe(val => {
    const subCategoryControl = this.recipeForm.get('subcategory');
    subCategoryControl.patchValue('my prepopulated data here');
  });
}
							
						

How 'bout some validation ya'll?

Avril excited

Angular validation classes

							
.ng-valid
.ng-invalid
.ng-pending
.ng-pristine
.ng-dirty
.ng-untouched
.ng-touched
							
						

We'll Let Angular Do the Heavy Lifting

My Preferred Validation

							
.ng-valid[required], .ng-valid.required  {
	border-left: 5px solid #42A948; /* green */
}
.ng-touched.ng-invalid:not(form)  {
	border-left: 5px solid #a94442; /* red */
}
							
						

I like to highlight as the user touches fields, but then highlight required untouched fields on save.

This means we need to highlight invalid untouched fields on save

We can do this pretty easily by marking them as touched.

Highlight untouched fields

							
Object.keys(this.recipeForm.controls).forEach(field => {
  const control = this.recipeForm.get(field);
  control.markAsTouched({ onlySelf: true });
})
							
						

Problem 2:

I need custom validation rules

Basic required validation

							const control = new FormControl('some value', {
  validators: Validators.required
});
							
						

We have several built-in validators at our disposal:

min(), max(), required(), requiredTrue(), email(), minLength(),
maxLength(), pattern(), nullValidator(), compose(), composeAsync()

Pattern validation two ways!

							const control = new FormControl('some value', {
	validators: Validators.pattern('[0-9]{5}')
});

const myDatePattern = "^\\d{2}\/\\d{2}\/\\d{4}$";
const otherControl = new FormControl('some value', {
  validators: Validators.pattern(myDatePattern)
});
							
						

Note the double escaping - the Regex needs a string representation of \d.
This trolled the crap out of me at one point in time.

Who doesn't love a good date regex?

Regex Crossword games - enjoy

Function validation

							amountValidator(c: FormControl) {
  return c.value > 340 ? null : {
    validAmount: {
      valid: false
    }
  };
}
const control = new FormControl('some value', {
  validators: [Validators.required, amountValidator]
});

							
						

Problem 3:

I need to display one thing in an input field
for the user but submit something else

Likely use case: the library I’m
using doesn’t play nice with reactive forms.

Enter our hero -
the Control Value Accessor


Not as scary as it sounds - it's just a way to build a custom input.

From the docs: Defines an interface that acts as a bridge between the Angular forms API and a native element in the DOM.

CVA Interface


export interface ControlValueAccessor {
  writeValue(obj: any) : void
  registerOnChange(fn: any) : void
  registerOnTouched(fn: any) : void
}
						

write Value


export interface ControlValueAccessor {
  writeValue(obj: any) : void
  registerOnChange(fn: any) : void
  registerOnTouched(fn: any) : void
}
						


Writes new value to the element.

register On Change


export interface ControlValueAccessor {
  writeValue(obj: any) : void
  registerOnChange(fn: any) : void
  registerOnTouched(fn: any) : void
}
						


Registers a callback function that is called when
the control's value changes in the UI.

register On Touched


export interface ControlValueAccessor {
  writeValue(obj: any) : void
  registerOnChange(fn: any) : void
  registerOnTouched(fn: any) : void
}
						


Registers a callback function is called by the forms
API on initialization to update the form model on blur.

CVA is great for granular control of displaying
to the UI and communicating with the forms API

CVA Key Concepts

  • Keep your wrapper components dumb.
  • Just input and output form values!
  • Leave validation logic to the parent form component.

Back to our initial problem ...

Avril excited

I want to display the name in my typeahead and
submit the id, but my library doesn't allow for that.

							[{
  name: 'breakfast',
  id: 1
},{
  name: 'lunch',
  id: 2
},
{
  name: 'appetizer',
  id: 3
}];
							
						

Let's wrap it in a component
that implements the CVA interface

Building our wrapper

							import { Component, OnInit, Input, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';

@Component({
  selector: 'my-typeahead',
  templateUrl: './typeahead.component.html',
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => MyTypeaheadComponent),
      multi: true
    }
  ]
})
export class MyTypeaheadComponent implements ControlValueAccessor, OnInit {
  @Input() data: any[];
  @Input('value') _value = null;
  onChange: any = () => { };
  onTouched: any = () => { };

  get value() {
    return this._value;
  }

  set value(val) {
    this._value = val;
  }

  registerOnChange( fn : any ) : void {
    this.onChange = fn;
  }

  registerOnTouched( fn : any ) : void {
    this.onTouched = fn;
  }

  writeValue(value) {
    this.value = value;
  }
}

							
						

sum html

							<ng-template #itemTemplate let-model="item">
      {{model[displayVal]}}
    </ng-template>
<input
  class="form-control"
  [(ngModel)]="selected"
  [typeahead]="data"
  [typeaheadOptionField]="displayVal"
  [typeaheadItemTemplate]="itemTemplate"
  [typeaheadMinLength]="0"
  (typeaheadOnSelect)="onSelect($event)"
  (blur)="onBlur($event)"
>

							
						

(Using the ngx-bootstrap typeahead component.)

Hook into our form

							
<form [formGroup]="mealForm">
  <div class="form-group">
    <label for="recipe">Recipe</label>
    <my-typeahead formControlName="recipe" [data]="recipes"></my-typeahead>
  </div>
</form>
							
						

What if I need to prepopulate a value
because I'm editing an existing form?

We already have access to the
value in our wrapper!

							
export class MyTypeaheadComponent implements ControlValueAccessor, OnInit {
  @Input() data: any[];
  @Input('value') _value;
  onChange: any = () => { };
  onTouched: any = () => { };

  get value() {
    return this._value;
  }

  set value(val) {
    this._value = val;
  }

  registerOnChange( fn : any ) : void {
    this.onChange = fn;
  }

  registerOnTouched( fn : any ) : void {
    this.onTouched = fn;
  }

  writeValue(value) {
    this.value = value;
  }
}

							
						

Good for more than just
random library components!

You can use the CVA to create reusable form
elements instead of drowning in event emitter soup!

Problem 4:

I have a bunch of checkboxes and
need to submit an array of selected ids

Control Value Accessor to the rescue again!

Our component CVA implementation

							
export class MyCheckboxesComponent implements ControlValueAccessor {
  @Input() data: any[];

  @Input('value') _value = [];
  onChange: any = () => { };
  onTouched: any = () => { };

  get value() {
    return this._value;
  }

  set value(val) {
    this._value = val;
    this.onChange(val);
    this.onTouched();
  }

  registerOnChange( fn : any ) : void {
    this.onChange = fn;
  }

  registerOnTouched( fn : any ) : void {
    this.onTouched = fn;
  }

  writeValue(value:any) {
    this.value = value;
  }

  selectCheckbox(checkboxId) {
    const updatedArray = this.value;
    updatedArray.push(checkboxId);
    this.writeValue(updatedArray);
  }

  removeCheckbox(checkboxId) {
    var index = this.value.indexOf(checkboxId);
    if (index > -1) {
      this.value.splice(index, 1);
    }
  }

}

							
						

Our component markup

							
<ng-container *ngFor="let item of data">
  <div class="form-check form-check-inline">
    <ng-container *ngIf="value && value.includes(item.id)">
      <input class="form-check-input" type="checkbox" value="{{item.id}}" checked (click)=removeCheckbox(item.id)>
    </ng-container>
    <ng-container *ngIf="!value || !value.includes(item.id)">
      <input class="form-check-input" type="checkbox" value="{{item.id}}" (click)=selectCheckbox(item.id)>
    </ng-container>
    <label class="form-check-label" >{{item.name}}</label>
  </div>
</ng-container>
							
						

And In Our Form

							
<my-checkboxes formControlName="subcategory" [data]="subcategories"></my-checkboxes>
							
						

Seem familiar yet?

Problem 5:

I need to dynamically add and
remove form controls & form groups

Creating the nested form

							
this.recipeForm = this.fb.group({
  name: [null, Validators.required],
  description: [null],
  source: [null],
  category: [null],
  subcategory: [[]],
  ingredients: this.fb.array([]),
  calories: [null, [Validators.required, this.amountValidator]]
});
							
						

Adding a nested form group

							
public addIngredient() {
  const ingredientsFormArray = this.recipeForm.controls['ingredients'];
  const ingredientFormGroup = new FormGroup({
    name: new FormControl(null, [Validators.required]),
    amount: new FormControl(null, [Validators.required])
  });
  ingredientsFormArray.push(ingredientFormGroup);
}
							
						

Removing a nested form group

							
public removeIngredient(i) {
  const ingredientsFormArray = this.recipeForm.controls['ingredients'];
  ingredientsFormArray.removeAt(i);
}
							
						

The Markup

							
<label for="ingredients">Ingredients</label>
<ng-container *ngFor="let ingredient of recipeForm.controls['ingredients'].controls; let i = index" >
  <div [formGroup]="ingredient" class="row">
    <div class="form-group col-sm">
      <label for="name">Name</label>
      <input class="form-control" formControlName="name" aria-describedby="name">
    </div>
    <div class="form-group col-sm">
      <label for="amount">Amount</label>
      <input class="form-control" formControlName="amount" aria-describedby="amount">
    </div>
    <div class="col-sm">
      <button class="btn btn-danger remove-ingredient" (click)="removeIngredient(i)">
        <fa-icon icon="times"></fa-icon>
      </button>
    </div>
  </div>
</ng-container>
<div class="row">
  <div class="col-sm">
    <button type="button" class="btn btn-success" (click)="addIngredient()">Add ingredient</button>
  </div>
</div>
							
						

Make sure to pass the nested formGroup model in!

Uhhhh, howtf do I validate this?

							
Object.keys(this.recipeForm.controls).forEach(field => {
  const control = this.recipeForm.get(field);

  if (!control['controls']) {
    control.markAsTouched({ onlySelf: true });
  }
  else {
    let nestedFormArray = control['controls'];
    nestedFormArray.forEach(subcCtrlGp => {
      Object.keys(subcCtrlGp['controls']).forEach(subField => {
        const nestedControl = subcCtrlGp.get(subField);
        nestedControl.markAsTouched({ onlySelf: true });
      });
    });
    //can extract this to a recursive function for deeply nested forms
  }
})
							
						

Tips and Tricks

You can get into some sketchy situations
listening for formControl changes and acting on them.

									
patchValue(value: any, opts: {
  onlySelf?: boolean;
  emitEvent?: boolean;
} = {}): void
									
								

Take advantage of the options param
and don't emit an event on your changes.

Summary

  • Reactive forms - yay
  • Custom requiredness and validation is a breeze
  • Control Value Accessor solves almost any form issue
  • Maybe forms aren't so complicated
  • Avril Lavigne is timeless


Demo code: https://github.com/tehfedaykin/complicated-forms-app

Avril's New Single: Head Above Water

Questions?

Slides available at: tehfedaykin.github.io/complicatedNGforms

Don't forget to rate the session!

🎶 Pre-talk playlist 🎶

@likeOMGitsFEDAY #complicatedNGforms #RVAJavaScriptConf