Note for course Angular 4: Forms

Anh-Thi Dinh

TIPS

👉 Assignment example of using Forms in Angular (video)+ codes.
👉 Project using Form :
codes.

Why Forms in Angular?

Because your app is SVC (single-view-component) → when you submit a form, how can web do it? → that's why angular need a special "things" for forms.
  • Get input values by the users
  • Check the valid of input
  • Styling conditionally the form
Left: HTML form → Right: key-value object can be submitted with the form.

2 approaches with forms

  • Template driven ← inferred from DOM (html)
  • Reactive ← form created programmatically and synchronized with the DOM → more controls

Template-driven form

Everything you wanna do → do it in the template ← template-driven!!!!

Create a TD form

The form here don't make any HTML request. ← <form> has no attribute!
1// make sure
2// app.module.ts
3import { FormsModule } from '@angular/forms';
4
5@NgModule({
6	imports: [
7		FormsModule // with this, angular will auto create form based on <form> in html
8	]
9})
10export ...
Angular doesn't recognize auto elements of <form> (label, input,...) because there may be some inputs which aren't served when submitting a form (their functions are different from a function of a form but they're inside <form>) → ==need to tell angular which ones to be extra controlled?==
1<!-- app.component.ts -->
2<form>
3	<input type="text">        <!-- normal input (without "ngModel") -->
4	<input                     <!-- tell angular to control this input -->
5		type="text"
6		ngModel   <!-- looks like 2-way binding, ie. [(ngModel)] -->
7		name="username">  <!-- must have <- registered in JS representation of the form -->
8</form>

Submitting & Using the Form

We can actually see what users entered.
1// If we use normal form
2<form>
3	<button type="submit">Submit</button> // normal behavior - sending http request
4</form>
1// We use ngSubmit
2<form (ngSubmit)="onSubmit(f)" #f="ngForm">
3														//   ^Hey, get me access to this form you created
4														//        automatically
5	<button type="submit">Submit</button>
6</form>
1// app.component.ts
2onSubmit(form: NgForm) {
3	console.log(form);
4	// Get the values user entered: form.value
5}
Get the values user entered: form.value

Form State

1// .html
2<form (ngSubmit)="onSubmit(f)" #f="">...</form>
3
4// .component.ts
5onSubmit(form: HTMLFormElement) {
6	console.log(form);
7}
  • form.controls → several properties to control the form.
  • form.dirty: true if we changed something in the form.
  • form.invalid / form.valid: if we add some invalidator / validator (to check the fields), this can be true or false.

Access Form via @ViewChild()

With #f (local ref), we can use @ViewChild. ⇒ Useful when you wanna access the form not only at the time you submit it but also earlier!
1// .html
2<form (ngSubmit)="onSubmit()" #f="ngForm">...</form>
3											//  ^don't have "f" here
4
5// .component.ts
6export class ... {
7	@ViewChild('f') signupForm: NgForm;
8
9	onSubmit() {
10		console.log(this.signupForm);
11	}
12

Validation

1<form (ngSubmit)="onSubmit()" #f="ngForm">
2	<input
3		type="text"
4		ngModel
5		name="username"
6		required>
7<!--    ^default HTML attribute <- angular see it as a built-in directive -->
8
9	<input
10		type="email"
11		ngModel
12		required
13		email  // angular's directive, not html attribute -> make sure it's a valid email
14		#email="ngModel">
15		   <!-- ^ expose some additional info abt the controls -->
16
17	<span *ngIf="!email.valid && email.touched">Please enter valid email!</span>
18
19	<button
20		type="submit"
21		[disabled]="!f.valid">Submit</button>
22</form>
There are 2 places for .valid information ← form.valid and form.controls.email.valid
When it's invalid (after clicking on submit) or valid → angular auto add classes to the html element ← we can use these class to style our element!
Enable HTML5 validation (by default, Angular disables it) → ngNativeValidate
1// if user touched in input and leave
2//   it but it's invalid
3input.ng-invalid.ng-touched{
4	...
5}
1// patterns (eg. number > 0)
2<input
3	type="number"
4	name="amount"
5	ngModel
6	pattern="^[1-9]+[0-9]*$"
7>

Set default values

Using 1-way binding / property binding ([ngModel]) to set the default value.
1<select
2  id="secret"
3  class="form-control"
4  [ngModel]="defaultQuestion"
5  name="secret">
6  <option value="pet">Your first Pet?</option>
7  <option value="teacher">Your first teacher?</option>
8</select>
9
10// component.ts
11defaultQuestion = "pet";

Instantly react to changes

Before, the check only performed after clicking "Submit" → If you wanna check "lively" → Using two-way binding [(NgModel)]
1<div class="form-group">
2  <textarea
3    name="questionAnswer"
4    rows="3"
5    class="form-control"
6    [(ngModel)]="answer"></textarea>
7</div>
8<p>Your reply: {{ answer }}</p>

Binding with NgModel

  • 0-way binding, NgModel → tell angular that this input is a control
  • 1-way binding, [NgModel] → get this control a default value
  • 2-way binding, [(NgModel)] → Instantly out / do whatever you want with that value

Grouping Form Controls

In a big form, we need to "group" some values into a group / validate some specific group of inputs.
1<div
2	ngModelGroup="userData" <!--  ^the key name for this group -->
3	#userData="NgModelGroup">
4	<!-- input fields -->
5</div>
6<p *ngIf="!userData.valid && userData.touched">User data is invalid!</p>
After submit: instead of getting form.value.email, but getting form.value.userData.email (and also form.controls.userData)

Radio buttons

1<div class="radio" *ngFor="let gender of genders">
2  <label>
3    <input
4      type="radio"
5      name="gender"
6      ngModel
7      [value]="gender"
8      required>
9    {{ gender }}
10  </label>
11</div>
12
13<!-- .component.ts -->
14genders = ['male', 'female'];

Set values to input fields

1// .component.ts
2this.signupForm.setValue({
3	userData: {
4	  username: suggestedName,
5	  email: ''
6	},
7	secret: 'pet',
8	questionAnswer: '',
9	gender: 'male'
10});
11// down side -> overwrite all fields whenever we click "Suggest an Username".
12
13// If we wanna set value TO A SINGLE ONE FIELD?
14this.signupForm.form.patchValue({
15						//  ^patchValue is only available with .form
16  userData: {
17    username: suggestedName
18  }
19});

Using Form Data

1// html
2<div *ngIf="submitted">
3  <h3>Your Data</h3>
4  <p>Username: {{ user.username }}</p>
5  <p>Mail: {{ user.email }}</p>
6  <p>Secret Question: Your first {{ user.secretQuestion }}</p>
7  <p>Answer: {{ user.answer }}</p>
8  <p>Gender: {{ user.gender }}</p>
9</div>
1// .component.ts
2export class AppComponent {
3	user = {
4    username: '',
5    email: '',
6    secretQuestion: '',
7    answer: '',
8    gender: ''
9  };
10  submitted = false;
11
12	onSubmit() {
13    this.submitted = true;
14    this.user.username = this.signupForm.value.userData.username;
15    this.user.email = this.signupForm.value.userData.email;
16    this.user.secretQuestion = this.signupForm.value.secret;
17    this.user.answer = this.signupForm.value.questionAnswer;
18    this.user.gender = this.signupForm.value.gender;
19
20    this.signupForm.reset(); // to reset the form
21  }
22}

Reactive Form

Create the form programmatically (not from scratch).

Setting up

  • FormGroup → a form, in the end, it's just a group of controls.
  • We don't need FormsModule in app.module.ts (it's for template-driven form) → NEED ReactiveFormsModule
  • We don't need local reference anymore.
  • We configure all things in the typescript code (component.ts)
1// app.module.ts
2import { ReactiveFormsModule } from '@angular/forms';
3
4@NgModule({
5  imports: [
6    ReactiveFormsModule
7  ]
8})
9export ...
1// html
2<form [formGroup]="signupForm">
3//     ^hey, don't treat this form as normal or create form for me, just
4//        use my formgroup "signupForm" (in component.ts)
5	<input
6		formControlName="username">
7//  ^the name given in component.ts of this input field
8</form>
1// app.component.ts
2export ... OnInit {
3	signupForm: FormGroup;
4
5	ngOnInit() {
6		this.signupForm = new FormGroup({
7      'username': new FormControl(null);
8		// |      				|				    ^initial value
9		// |              ^each field is a FormControl
10		// ^the same name for "formControlName" in .html <- that's the link
11    });
12	}
13}

Grouping Controls

FormGroup inside FormGroup.
1// .component.ts
2this.signupForm = new FormGroup({
3  'userData': new FormGroup({
4    'username': new FormControl(...),
5    'email': new FormControl(...)
6  }),
7  'gender': new FormControl('male'),
8});
1// html
2// need to put 'username' and 'email' inside a div
3<form [formGroup]="signupForm">
4	<div formGroupName="userData">
5		<input formControlName="username">
6
7		<span *ngIf="!signupForm.get('userData.username').valid>
8															//  ^new here
9			Please enter a valid username!
10		</span>
11
12		<input formControlName="email">
13	</div>
14</form>

FormArray

Let the user dynamically add their form controls (we don't know yet there are how many controls there) → using an array of form.
Get access to FormArray
1// .component.ts
2this.signupForm = new FormGroup({
3  ...
4  'hobbies': new FormArray([])
5});
1// html
2<div formArrayName="hobbies">
3  <button
4    (click)="onAddHobby()">Add Hobby</button>
5  <div
6    *ngFor="let hobbyControl of signupForm.get('hobbies').controls; let i = index">
7    <input type="text" class="form-control" [formControlName]="i">
8  </div>
9</div>
1// 1st way
2// .ts
3getControls() {
4	return (<FormArray>this.signupForm.get('hobbies')).controls;
5}
6
7// .html
8*ngFor="let hobbyControl of getControls(); let i = index"
1// 2nd way (using getter)
2// .ts
3get controls() {
4	return (this.signupForm.get('hobbies') as FormArray).controls;
5}
6
7// .html
8*ngFor="let hobbyControl of controls; let i = index"

Validation

1this.signupForm = new FormGroup({
2  'username': new FormControl(null, Validators.required);
3																					//   ^it's actually .required() but in
4																					//    this case, we wanna add a ref
5																					//    to this method, angular'll know
6																					//    to call it whenever we make changes
7	'email': new FormControl(null, [Validators.required, Validators.email]);
8															// ^multiple validations
9});
Get access directly to the FormControl using .get()
1<span
2	*ngIf="!signupForm.get('username').valid && signupForm.get('username').touched">
3	Please enter a valid username!
4</span>
1// for the overall form
2<span
3	*ngIf="!signupForm.valid && signupForm.touched">
4	Please enter valid data!
5</span>

Custom validation

Suppose that we don't want users use some specific names.
1// .ts
2forbiddenUsernames = ['Chris', 'Anna'];
3
4ngOnInit() {
5	this.signupForm = new FormGroup({
6		'username': new FormControl(
7			null, [
8				Validators.required,
9				this.forbiddenNames.bind(this)]),
10											//   ^need this 'cause angular will call .forbiddenNames()
11											//    (not current class -> cannot use only "this." directly
12											//    -> let angular knows (binds) to current class as "this"
13	}
14}
15
16forbiddenNames(control: FormControl): {[s: string]: boolean} {
17																		// ^a return type
18  if (this.forbiddenUsernames.indexOf(control.value) !== -1) {
19																							//     ^return of .indexOf
20																							//      (if not contains) is "-1"
21    return {'nameIsForbidden': true};
22  }
23  return null;
24		//   ^we don't return ... "false" here because for angular, returning null
25		//      means "valid"
26}
1// use error message with "nameIsForbidden"
2// can use Inspect of the browser to check the location of "nameISForbidden"
3<span *ngIf="signupForm.get('userData.username').errors['nameIsForbidden']">
4	This name is invalid!
5</span>

Custom async validation

(video) Suppose we wanna check "already-taken" emails from the server.
1this.signupForm = new FormGroup({
2  'email': new FormControl(
3		null, [Validators.required, Validators.email], this.forbiddenEmails)
4																								// ^can be an array,
5																								// this position is for async
6});
7
8forbiddenEmails(control: FormControl): Promise<any> | Observable<any> {
9  const promise = new Promise<any>((resolve, reject) => {
10    setTimeout(() => {
11      if (control.value === 'test@test.com') {
12        resolve({'emailIsForbidden': true});
13      } else {
14        resolve(null);
15      }
16    }, 1500);
17  });
18  return promise;
19}

Form Status: statusChanges, valueChanges

.statusChanges → gives the status of the form (INVALID, VALID, PENDING,...)
.valueChanges → change something in the form (eg. typing something).
1ngOnInit() {
2	this.signupForm.valueChanges.subscribe(
3		(value) => console.log(value);
4	);
5}

Set values to input fields

1this.signupForm.setValue({
2  'userData': {
3    'username': 'Max',
4    'email': 'max@test.com'
5  },
6  'gender': 'male',
7  'hobbies': []
8});
9// down side -> overwrite all fields whenever we click "Suggest an Username".
1// If we wanna set value TO A SINGLE ONE FIELD?
2this.signupForm.patchValue({
3  'userData': {
4    'username': 'Anna',
5  }
6});
Reset the form,
1onSubmit() {
2	this.signupForm.reset();
3										//  ^you can put object to reset specific values
4}