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
- Template driven ← inferred from DOM (html)
- Reactive ← form created programmatically and synchronized with the DOM → more controls
Everything you wanna do → do it in the template ← template-driven!!!!
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>
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
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.
- Read more in official doc.
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
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>
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";
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>
- 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
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
)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'];
(video) With this HTML file
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});
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}
Create the form programmatically (not from scratch).
FormGroup
→ a form, in the end, it's just a group of controls.
- We don't need
FormsModule
inapp.module.ts
(it's for template-driven form) → NEEDReactiveFormsModule
- 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}
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>
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"
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>
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>
(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 === '[email protected]') {
12 resolve({'emailIsForbidden': true});
13 } else {
14 resolve(null);
15 }
16 }, 1500);
17 });
18 return promise;
19}
.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}
1this.signupForm.setValue({
2 'userData': {
3 'username': 'Max',
4 'email': '[email protected]'
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}