Angular 4 - Forms

20-03-2021 / Edit on Github

This is my note for the course "Angular - The Complete Guide (2021 Edition)" which is taught by Maximilian SchwarzmΓΌller on Udemy. This is an incredible course for you to start to learn Angular from basic. The official Angular docs is good but it's not for the beginner.

πŸ„ PART 1 β€” Angular 1 - Basics & Components & Databinding & Directives
πŸ„ PART 2 β€” Angular 2 - Services & Dependency Injection & Routing
πŸ„ PART 3 β€” Angular 3 - Observable
πŸ„ PART 4 β€” Angular 4 - Forms
πŸ‘‰ Github repo
πŸ‘‰ Other note (taken before this course): Angular 101

This note contains only the important things which can be look back later, it cannot replace either the course nor the official document!

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

Angular_4_-_Forms_4258fc3ce29b46deb5f5b0492c50c7bb/Untitled.png
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 #

πŸ‘‰ Codes for this section.

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!

// make sure
// app.module.ts
import { FormsModule } from '@angular/forms';

@NgModule({
imports: [
FormsModule // with this, angular will auto create form based on <form> in html
]
})
export ...

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?

<!-- app.component.ts -->
<form>
<input type="text"> <!-- normal input (without "ngModel") -->
<input <!-- tell angular to control this input -->
type="text"
ngModel <!-- looks like 2-way binding, ie. [(ngModel)] -->
name="username"> <!-- must have <- registered in JS representation of the form -->
</form>

Submitting & Using the Form #

πŸ‘‰ Codes for this section.

We can actually see what users entered.

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

Angular_4_-_Forms_4258fc3ce29b46deb5f5b0492c50c7bb/Untitled_1.png

Get the values user entered: form.value

Form State #

// .html
<form (ngSubmit)="onSubmit(f)" #f="">...</form>

// .component.ts
onSubmit(form: HTMLFormElement) {
console.log(form);
}
  • 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.
  • Read more in official doc.

Access Form via @ViewChild() #

πŸ‘‰ Codes for this section.

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!

// .html
<form (ngSubmit)="onSubmit()" #f="ngForm">...</form>
// ^don't have "f" here

// .component.ts
export class ... {
@ViewChild('f') signupForm: NgForm;

onSubmit() {
console.log(this.signupForm);
}
}

Validation #

πŸ‘‰ Codes for this section.

<form (ngSubmit)="onSubmit()" #f="ngForm">
<input
type="text"
ngModel
name="username"
required>

<!-- ^default HTML attribute <- angular see it as a built-in directive -->

<input
type="email"
ngModel
required
email // angular's directive, not html attribute -> make sure it's a valid email
#email="ngModel">
<!-- ^ expose some additional info abt the controls -->

<span *ngIf="!email.valid && email.touched">Please enter valid email!</span>

<button
type="submit"
[disabled]="!f.valid">
Submit</button>
</form>

πŸ‘‰ List of all built-in validators.
πŸ‘‰ List of directives which can be added to your template.

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

// if user touched in input and leave
// it but it's invalid
input.ng-invalid.ng-touched{
...
}
// patterns (eg. number > 0)
<input
type="number"
name="amount"
ngModel
pattern="^[1-9]+[0-9]*$"
>

Set default values #

πŸ‘‰ Codes for this section.

Using 1-way binding / property binding ([ngModel]) to set the default value.

<select
id="secret"
class="form-control"
[ngModel]="defaultQuestion"
name="secret">
<option value="pet">Your first Pet?</option>
<option value="teacher">Your first teacher?</option>
</select>

// component.ts
defaultQuestion = "pet";

Instantly react to changes #

Before, the check only performed after clicking "Submit" β†’ If you wanna check "lively" β†’ Using two-way binding [(NgModel)]

<div class="form-group">
<textarea
name="questionAnswer"
rows="3"
class="form-control"
[(ngModel)]="answer">
</textarea>
</div>
<p>Your reply: </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.

<div
ngModelGroup="userData" <!-- ^the key name for this group -->

#userData="NgModelGroup">
<!-- input fields -->
</div>
<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 #

πŸ‘‰ Codes for this section.

<div class="radio" *ngFor="let gender of genders">
<label>
<input
type="radio"
name="gender"
ngModel
[value]="gender"
required>


</label>
</div>

<!-- .component.ts -->
genders = ['male', 'female'];

Set values to input fields #

(video) With this HTML file,

// .component.ts
this.signupForm.setValue({
userData: {
username: suggestedName,
email: ''
},
secret: 'pet',
questionAnswer: '',
gender: 'male'
});
// down side -> overwrite all fields whenever we click "Suggest an Username".

// If we wanna set value TO A SINGLE ONE FIELD?
this.signupForm.form.patchValue({
// ^patchValue is only available with .form
userData: {
username: suggestedName
}
});

Using Form Data #

πŸ‘‰ Codes for this section.

// html
<div *ngIf="submitted">
<h3>Your Data</h3>
<p>Username: </p>
<p>Mail: </p>
<p>Secret Question: Your first </p>
<p>Answer: </p>
<p>Gender: </p>
</div>
// .component.ts
export class AppComponent {
user = {
username: '',
email: '',
secretQuestion: '',
answer: '',
gender: ''
};
submitted = false;

onSubmit() {
this.submitted = true;
this.user.username = this.signupForm.value.userData.username;
this.user.email = this.signupForm.value.userData.email;
this.user.secretQuestion = this.signupForm.value.secret;
this.user.answer = this.signupForm.value.questionAnswer;
this.user.gender = this.signupForm.value.gender;

this.signupForm.reset(); // to reset the form
}
}

Reactive Form #

Create the form programmatically (not from scratch).

πŸ‘‰ Codes for this section + Video.

Setting up #

πŸ‘‰ Codes for this section.

  • 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)
// app.module.ts
import { ReactiveFormsModule } from '@angular/forms';

@NgModule({
imports: [
ReactiveFormsModule
]
})
export ...
// html
<form [formGroup]="signupForm">
// ^hey, don't treat this form as normal or create form for me, just
// use my formgroup "signupForm" (in component.ts)
<input
formControlName="username">

// ^the name given in component.ts of this input field
</form>
// app.component.ts
export ... OnInit {
signupForm: FormGroup;

ngOnInit() {
this.signupForm = new FormGroup({
'username': new FormControl(null);
// | | ^initial value
// | ^each field is a FormControl
// ^the same name for "formControlName" in .html <- that's the link
});
}
}

Grouping Controls #

πŸ‘‰ Codes for this section.

FormGroup inside FormGroup.

// .component.ts
this.signupForm = new FormGroup({
'userData': new FormGroup({
'username': new FormControl(...),
'email': new FormControl(...)
}),
'gender': new FormControl('male'),
});
// html
// need to put 'username' and 'email' inside a div
<form [formGroup]="signupForm">
<div formGroupName="userData">
<input formControlName="username">

<span *ngIf="!signupForm.get('userData.username').valid>
// ^new here
Please enter a valid username!
</span>

<input formControlName="email">
</div>
</form>

FormArray #

πŸ‘‰ Codes for this section.

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,

// .component.ts
this.signupForm = new FormGroup({
...
'hobbies': new FormArray([])
});
// html
<div formArrayName="hobbies">
<button
(click)="onAddHobby()">Add Hobby
</button>
<div
*ngFor="let hobbyControl of signupForm.get('hobbies').controls; let i = index">
<input type="text" class="form-control" [formControlName]="i">
</div>
</div>
// 1st way
// .ts
getControls() {
return (<FormArray>this.signupForm.get('hobbies')).controls;
}

// .html
*ngFor="let hobbyControl of getControls(); let i = index"
// 2nd way (using getter)
// .ts
get controls() {
return (this.signupForm.get('hobbies') as FormArray).controls;
}

// .html
*ngFor="let hobbyControl of controls; let i = index"

Validation #

πŸ‘‰ Codes for this section.

this.signupForm = new FormGroup({
'username': new FormControl(null, Validators.required);
// ^it's actually .required() but in
// this case, we wanna add a ref
// to this method, angular'll know
// to call it whenever we make changes
'email': new FormControl(null, [Validators.required, Validators.email]);
// ^multiple validations
});

Get access directly to the FormControl using .get()

<span
*ngIf="!signupForm.get('username').valid && signupForm.get('username').touched">
Please enter a valid username!
</span>
// for the overall form
<span
*ngIf="!signupForm.valid && signupForm.touched">
Please enter valid data!
</span>

Custom validation #

πŸ‘‰ Codes for this section.

Suppose that we don't want users use some specific names.

// .ts
forbiddenUsernames = ['Chris', 'Anna'];

ngOnInit() {
this.signupForm = new FormGroup({
'username': new FormControl(
null, [
Validators.required,
this.forbiddenNames.bind(this)]),
// ^need this 'cause angular will call .forbiddenNames()
// (not current class -> cannot use only "this." directly
// -> let angular knows (binds) to current class as "this"
}
}

forbiddenNames(control: FormControl): {[s: string]: boolean} {
// ^a return type
if (this.forbiddenUsernames.indexOf(control.value) !== -1) {
// ^return of .indexOf
// (if not contains) is "-1"
return {'nameIsForbidden': true};
}
return null;
// ^we don't return ... "false" here because for angular, returning null
// means "valid"
}
// use error message with "nameIsForbidden"
// can use Inspect of the browser to check the location of "nameISForbidden"
<span *ngIf="signupForm.get('userData.username').errors['nameIsForbidden']">
This name is invalid!
</span>

Custom async validation #

πŸ‘‰ Codes for this section.

(video) Suppose we wanna check "already-taken" emails from the server.

this.signupForm = new FormGroup({
'email': new FormControl(
null, [Validators.required, Validators.email], this.forbiddenEmails)
// ^can be an array,
// this position is for async
});

forbiddenEmails(control: FormControl): Promise<any> | Observable<any> {
const promise = new Promise<any>((resolve, reject) => {
setTimeout(() => {
if (control.value === '[email protected]') {
resolve({'emailIsForbidden': true});
} else {
resolve(null);
}
}, 1500);
});
return promise;
}

Form Status: statusChanges, valueChanges #

.statusChanges β†’ gives the status of the form (INVALID, VALID, PENDING,...)

.valueChanges β†’ change something in the form (eg. typing something).

ngOnInit() {
this.signupForm.valueChanges.subscribe(
(value) => console.log(value);
);
}

Set values to input fields #

this.signupForm.setValue({
'userData': {
'username': 'Max',
'email': '[email protected]'
},
'gender': 'male',
'hobbies': []
});
// down side -> overwrite all fields whenever we click "Suggest an Username".

// If we wanna set value TO A SINGLE ONE FIELD?
this.signupForm.patchValue({
'userData': {
'username': 'Anna',
}
});

Reset the form,

onSubmit() {
this.signupForm.reset();
// ^you can put object to reset specific values
}