Skip to content

Instantly share code, notes, and snippets.

@jiyoon-koo
Last active May 19, 2019 19:43
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save jiyoon-koo/e115ebf850b7b372406a1865b1abe85c to your computer and use it in GitHub Desktop.
Save jiyoon-koo/e115ebf850b7b372406a1865b1abe85c to your computer and use it in GitHub Desktop.
Angular Form Presentation

All about forms in Angular

✏️ by Jiyoon Koo


What are forms? πŸ“°

  • Take many different types of user input
  • Allow user interaction
  • Enforce validation before data can even be submitted
  • 2 types of forms in Angular - Template and Reactive

Template-driven forms (steps 1/4)

  • Import Angular's FormsModule into your @NgModule
    • This activates Angular Form API.
import { NgModule } from '@angular/core';

@NgModule({
  imports: [
    ...
    FormsModule
  ]
  ...
})
class AppModule {}
  • Let's look at a very simple form
<form>

  <label>Your name:</label>
  <input type="text">
  
  <button type="submit">Submit</button>
  
</form>

Template-driven forms (steps, cont'd 2/4)

  • Angular's ngForm is already 'hooked into' <form> selector!
  • To access this, we simply have to declare a local form variable as ngForm
<form #form="ngForm">

  <label>What's your name?</label>
  <input type="text">
  
  <button type="submit">Submit</button>
  
</form>
  • This allows access to...
    • A JSON representation of the form value
    • Validity state of the entire form (eg. ..controls.isDirty, ..controls.hasError, etc)
    • Delegates submission events

Template-driven forms (steps, cont'd 3/4)

  • How to submit? Here's the typical way...
<form #form="ngForm" (submit)="submitMyStuff(form.value)">

  <label>What's your name?</label>
  <input type="text">
  
  <button type="submit">Submit</button>
  
</form>
  • ⚠️ : Using (submit) will cause page reload when error is thrown!
  • Instead, use (ngSubmit) to prevent this behavior.
<form #form="ngForm" (ngSubmit)="submitMyStuff(form.value)">

Template-driven forms (steps, cont'd 4/4)

  • Register form controls using ngModel directive name attribute
  <input name="firstname" ngModel>
  • if you want to group multiple inputs together, use ngModelGroup like this:
<div ngModelGroup="address">

  <label>Firstname:</label>
  <input name="firstname" ngModel>

  <label>Lastname:</label>
  <input name="lastname" ngModel>

</div>

Built-in Validation πŸŽ‰

  • Angular comes equipped with following validation directives
  • required, requiredTrue, email, minLength, maxLength, pattern
  • Use it like this
<input required minlength="3">

Accessing validation state of built-in validators

  • If a validation error exists, you can access it in a FormControl's errors property
<input #firstname="ngModel" required minlength="3">

<!-- IF minlength validation error state is "true" -->
<p *ngIf="firstname.errors.minlength">
  oops!
</p>
  • Hint: this can also be applied as a class, using [ngClass]..!
  • firstname.errors object might look like this:
firstname.errors = {

  minlength: {
    actualLength: 2,
    requiredLength: 3
  },
  
  required: true
  
}

Other validation states βœ…

  • ngForm, ngModelGroup, ngModel keeps record of the following states, which are boolean:
  • Dirty / Pristine (did user change value of control?)
  • Touched / Untouched (did user focus on control and move away?)
  • Valid / Invalid (did user input pass all validation ?)
  • Access them like this:
<form #form="ngForm">

  <input ngModel name="firstname" required>
  <div *ngIf="!firstname.valid && !firstname.pristine">
	show this div if firstname field is either invalid or dirty
  </div>
  
  <button [disabled]="!form.valid">Save</button>
</form>

Custom Validators! πŸ”§

  • A validator is a function
  • It takes FormControl as an input
  • ... and returns null (successful validation)
  • ... or returns an error object (contains validation error)

Custom Validator (for template driven form) example

Steps:

  • ng generate directive [YOUR_VALIDATOR_NAME]
  • Write your validator function in the directive and reference it in providers attribute
  • Import and declare the directive in your @NgModule declarations array
import { YOUR_VALIDATOR_NAME } from '...';

@NgModule({

  declarations: [
  YOUR_VALIDATOR_NAME
  ...
  ], ..
})
  • Use it in template like this
  <input type="email" name="email" ngModel validateEmail>
  • See example directive on next page

import { Directive } from '@angular/core';
import { FormControl } from '@angular/forms/src/model';
import { NG_VALIDATORS } from '@angular/forms';

const VALID_EMAIL = [SOME_REGEX_FOR_EMAIL_VALIDATION];

// 1. export the function by itself
export function validateEmail(c: FormControl) {

  var valid = VALID_EMAIL.test(c.value);
  
  // 1a. if INVALID: return error message
  // 1b. if VALID: return null
  return (!valid) ? {
    isEmailValid: { value: false }
  } : null;
}

// 2. Reference the exported function in useValue attribute
@Directive({
  // Angular will look for input tags with both
  selector: '[validateEmail][ngModel]', 
  providers: [{
      provide: NG_VALIDATORS,
      useValue: validateEmail,
      // this is called multi provider
      // it allows multiple deps for a single token
      // NG_VALIDATORS is a built-in multi provider
      multi: true
    }]
})
export class EmailValidatorDirective {
  constructor() { }
}

Custom Async Validators! βœ¨πŸ”§βœ¨

  • Used when you need validation, but you require some service (or some external dependency) to perform it
  • Returns an Observable (or Promise)
  • Similar beginnings as writing custom sync validators, but has a couple big differences.

Steps:

  • ng generate directive [VALIDATOR_NAME]
  • Write a function that returns a function(!)
  • Do a depedency injection in the constructor of the directive
  • Implement AsyncValidator in the class and return the function above inside the validate(c: FormControl) method that's implemented.
  • Import and declare the directive in your @NgModule declarations array
  • Use it in the template by calling the selectors chosen in the directive
  • See example in next page...

import { ContactsService } from './contacts.service';
import { Directive, forwardRef } from '@angular/core';
import { 
  NG_ASYNC_VALIDATORS, 
  FormControl,  
  AsyncValidator 
} from '@angular/forms';
import { map } from 'rxjs/operators';

// factory function: a function that returns a function
// it requires injection of contactsService
export function checkEmailAvail(ctSvc: ContactsService) {

  return (c: FormControl) => {
    return ctSvc.isEmailAvailable(c.value)
      /*
      ctSvc.isEmailAvailable() response body:
      { msg?: string, error?: string }
      
      if response body does NOT contain error
      --> return null, ELSE return { emailTaken: true }
      */
      .pipe(map(response => !response.error 
      			    ? null 
                            : { emailTaken: true }));
      
  };
}
@Directive({
  selector: '[emailAvailValidator][ngModel]',
  providers: [{
      provide: NG_ASYNC_VALIDATORS,
      useExisting: 
      	forwardRef(() => EmailAvailValidatorDirective),
      multi: true
    }]
})
export class EmailAvailValidatorDirective 
	implements AsyncValidator {
  _validate: Function;

  // service injection in the constructor
  constructor(contactSvc: ContactsService) {
    this._validate = checkEmailAvail(contactSvc);
  }

  validate(c: FormControl) {
    return this._validate(c);
  }

}

Q&A about example in previous slide πŸ™‹

  • Q: What's that forwardRef() thing in the provider attribute of @Directive?
  • A: It's kind of like hoisting. We need to do this because we want to have a class injected that we created in the same file.
  • Q: Is there an order of execution for different validators?
  • A: Yes. Angular executes sync validators before async validators.

Reactive forms πŸ™Œ

  • Another way to implement forms in Angular.
  • Preferred -- easier to test!
  • Uses a few APIs in Angular
  • FormControl, FormGroup, FormBuilder, FormArray

Reactive forms (steps)

  • Import ReactiveFormsModule into your @NgModule
import { ReactiveFormsModule } from '@angular/forms';

@NgModule({
  imports: [
    ...
    ReactiveFormsModule
  ]
  ...
})
export class ContactsModule {}
  • Import FormControl for each input value you want to keep track of
import { FormControl } from '@angular/forms';

@Component(...)
export class ContactCreatorComponent {
  firstname = new FormControl();
}
  • Bind to formControl in the template
<input [formControl]="firstname">

Reactive forms (steps, cont'd)

  • Q: What if you want to group several FormControls?
  • A: Use FormGroup in the component
import { FormControl, FormGroup } from '@angular/forms';

@Component(...)
export class ContactCreatorComponent {

  form = new FormGroup({
  
    firstname: new FormControl(),
    lastname: new FormControl()
    
  })
}
  • ...And call it in the template as formGroup for group and formControlName for "child" FormControl inputs
<form [formGroup]="form">

  <input formControlName="firstname">
  <input formControlName="lastname">
  
</form>

This is way too much typing.

I am far too lazy to type all that out πŸ˜’

Let's use the FormBuilder API instead! ✨


Reactive forms using FormBuilder API

  • Import FormBuilder and FormGroup
  • Inject it in component constructor
  • Declare component variable form as type of FormGroup
  • Build your form using .group() !
  • NB: you can build inner groups as well, see example below
import { FormBuilder, FormGroup } from '@angular/forms';

@Component(...)
export class ContactsCreatorComponent implements OnInit {
  form : FormGroup;

  constructor(private fb: FormBuilder) {}

  ngOnInit() {
 
    this.form = this.fb.group({
      firstname: '',
      lastname: '',
      email: '',
      address: this.fb.group(
      	{ 
            street: '', 
            zip: '', 
            city: '', 
            country: '' 
        })
    });
  }
}
  • NB: Make sure the template [formGroup] and formControlName attribute matches what you are composing in the component! They still need to correlate to each other...

Let's apply (Sync) validators to our Reactive Form! πŸ’¨

  • Import Validators into component
  • Edit each FormControl to be an array, pass it in as the second argument.
  • If you have multiple validators, compose it via an array
  • If you are using a custom validator, then import the Validator function (not the directive)
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { validateEmail } from '../email-validator.directive';

@Component(...)
export class ContactsCreatorComponent implements OnInit {
  form : FormGroup;

  constructor(private fb: FormBuilder) {}

  ngOnInit() {
 
    this.form = this.fb.group({
      firstname: ['', Validators.required],
      email: ['', 
        [
          Validators.required, 
          Validators.minlength(3), 
          validateEmail
        ]
      ]
    });

  }
}

Let's apply (Async) validators to our Reactive Form! πŸ’¨πŸ’¨

  • Edit each FormControl to be an array, pass it in as the third argument.
  • Again, import the Custom Async Validator function (not the directive)
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { validateEmail } from '../email-validator.directive';

@Component(...)
export class ContactsCreatorComponent implements OnInit {
  form : FormGroup;

  constructor(private fb: FormBuilder, private contactsService: ContactsService) {}

  ngOnInit() {
 
    this.form = this.fb.group({
      firstname: ['', Validators.required],
      email: ['', 
        [
          Validators.required, 
          Validators.minlength(3), 
          validateEmail
        ],
        checkEmailAvail(this.contactsService)
      ]
    });

  }
}

What about dynamic Form Fields (FormGroup of unknown size)? 😨

Example: Multiple (optional) phone fields

Requires the use of FormArray API (which we can call from FormBuilder.array())

FormArray API exposes push(c: FormControl) and removeAt(index: Number)


FormArray (steps)

  • Assign desired field as FormBuilder.array()
  • Use .push() and .removeAt() as desired
export class ContactsCreatorComponent implements OnInit {
  form: FormGroup;
  
  constructor(private fb: FormBuilder) {}
  
  ngOnInit() {
    this.form = this.fb.group({
      ...
      phone: this.fb.array([''])
    });
  }
  
  addPhoneField() {
    const phoneFieldFormArray = <FormArray>this.form.get('phone');
    phoneFieldFormArray.push(new FormControl(''));
  }
  
  removePhoneField(index) {
    const phoneFieldFormArray = <FormArray>this.form.get('phone');
    phoneFieldFormArray.removeAt(index);
  }
}
<div formArrayName="phone">
  <div 
  *ngFor="let phone of form.get('phone').controls; let i = index; let l = last;">

  <mat-input-container>
  	<input [formControlName]="i" matInput placeholder="Phone">
  </mat-input-container>

  <button mat-icon-button type="button" 
  	*ngIf="i >= 1" 
  	(click)="removePhoneField(i)"><mat-icon>highlight_off</mat-icon>
  </button>

  <button mat-icon-button type="button" 
  	*ngIf="l && phone.value != '' && i <= 3" 
    (click)="addPhoneField()"><mat-icon>add_circle_outline</mat-icon>
  </button>
 
  </div>
</div>

Thank you ✌️ πŸ™ 😎 🎊

Resources:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment