Pluralsight - Angular Reactive Forms
Pluralsight - Angular Reactive Forms
-
-
Template-driven vs. Reactive Forms
Deborah Kurata
CONSULTANT | SPEAKER | AUTHOR | MVP | GDE
@deborahkurata | blogs.msmvps.com/deborahk/
Template-
Reactive
driven
Module
Angular Form Building Blocks
Overview - FormGroup
- FormControl
FormControl FormGroup
Form Model
- Retains form state
- Retains form value
- Retains child controls
• FormControls
• Nested FormGroups
Template-driven Forms
Template
- Form element
- Input element(s)
- Data binding
- Validation rules (attributes)
- Validation error messages
- Form model automatically generated
Component Class
- Properties for data binding (data model)
- Methods for form operations,
such as submit
Reactive Forms
Component Class
- Form model
- Validation rules
- Validation error messages
- Properties for managing data (data
model)
- Methods for form operations,
such as submit
Template
- Form element
- Input element(s)
- Binding to form model
Directives
Template-driven
(FormsModule)
FormGroup
• ngForm
• ngModel
• ngModelGroup
<form (ngSubmit)="save()">
</form>
Directives
Template-driven
(FormsModule)
FormGroup
• ngForm
• ngModel
• ngModelGroup
<form (ngSubmit)="save()"
#signupForm="ngForm">
</form>
Directives
Template-driven
(FormsModule)
FormGroup
• ngForm
• ngModel
• ngModelGroup
<form (ngSubmit)="save()"
#signupForm="ngForm">
<button type="submit"
[disabled]="!signupForm.valid">
Save
</button>
</form>
Directives
Template-driven
(FormsModule)
FormGroup
• ngForm
• ngModel
• ngModelGroup FormControl
<form (ngSubmit)="save()">
<input id="firstNameId" type="text"
[(ngModel)]="customer.firstName"
name="firstName"
#firstNameVar="ngModel"/>
</form>
Directives
Template-driven Reactive
(FormsModule) (ReactiveFormsModule)
• ngForm • formGroup
• ngModel • formControl
• ngModelGroup • formControlName
• formGroupName
• formArrayName
HTML Form
customer.component.html
<form>
<fieldset>
<div>
<label for="firstNameId">First Name</label>
<input id="firstNameId" type="text"
placeholder="First Name (required)"
required
minlength="3" />
</div>
...
<button type="submit">Save</button>
</fieldset>
</form>
Template-driven Form
customer.component.html
<form (ngSubmit)="save()">
<fieldset>
<div [ngClass]="{'has-error': firstNameVar.touched && !firstNameVar.valid }">
<label for="firstNameId">First Name</label>
<input id="firstNameId" type="text"
placeholder="First Name (required)"
required
minlength="3"
[(ngModel)]="customer.firstName"
name="firstName"
#firstNameVar="ngModel" />
<span *ngIf="firstNameVar.touched && firstNameVar.errors">
Please enter your first name.
</span>
</div>
...
<button type="submit">Save</button>
</fieldset>
</form>
Reactive Form
customer.component.html
<form (ngSubmit)="save()" [formGroup]="signupForm">
<fieldset>
<div [ngClass]="{'has-error': formError.firstName }">
<label for="firstNameId">First Name</label>
<input id="firstNameId" type="text"
placeholder="First Name (required)"
formControlName="firstName" />
<span *ngIf="formError.firstName">
{{formError.firstName}}
</span>
</div>
...
<button type="submit">Save</button>
</fieldset>
</form>
Demo
Template-driven Form
Complex Scenarios
Template-driven Reactive
Generated form model Manually created form model
HTML validation Validation in the class
Two-way data binding No two-way data binding
Angular Form Building Blocks
Summary - FormGroup
- FormControl
Deborah Kurata
CONSULTANT | SPEAKER | AUTHOR | MVP | GDE
@deborahkurata | blogs.msmvps.com/deborahk/
Module
Overview The Component Class
The Angular Module
The Template
Using setValue and patchValue
Simplifying with FormBuilder
Template-driven vs. Reactive Forms
Form Model
- Root FormGroup
- FormControl for each input element
- Nested FormGroups as desired
- FormArrays
Creating a FormGroup
customer.component.ts
...
import { FormGroup } from '@angular/forms';
...
export class CustomerComponent implements OnInit {
customerForm: FormGroup;
customer: Customer = new Customer();
ngOnInit(): void {
this.customerForm = new FormGroup({ });
}
}
Creating FormControls
customer.component.ts
...
import { FormGroup, FormControl } from '@angular/forms';
...
export class CustomerComponent implements OnInit {
...
ngOnInit(): void {
this.customerForm = new FormGroup({
firstName: new FormControl(),
lastName: new FormControl(),
email: new FormControl(),
sendCatalog: new FormControl(true)});
}
}
Demo
Customer- Reactive -
Component FormsModule
Angular Module
Binding to the Form Model
Reactive Forms Directives
Reactive Forms
• formGroup
• formControl
• formControlName
• formGroupName
• formArrayName
formGroup
customer.component.html
<form (ngSubmit)="save()" [formGroup]="customerForm">
...
</form>
formControlName
customer.component.html
<form (ngSubmit)="save()" [formGroup]="customerForm">
<fieldset>
<div ... >
<label for="firstNameId">First Name</label>
<input id="firstNameId" type="text"
placeholder="First Name (required)"
formControlName="firstName" />
<span ... >
...
</span>
</div>
...
</fieldset>
</form>
Accessing the Form Model Properties
customerForm.controls.firstName.valid
customerForm.get('firstName').valid
firstName = new FormControl();
ngOnInit(): void {
this.customerForm = new FormGroup({
firstName: this.firstName,
...
});
}
firstName.valid
Using setValue and patchValue
this.customerForm.setValue({
firstName: 'Jack',
lastName: 'Harkness',
email: 'jack@torchwood.com'
});
this.customerForm.patchValue({
firstName: 'Jack',
lastName: 'Harkness'
});
FormBuilder
Import
FormBuilder
Inject the
Import
FormBuilder
FormBuilder
instance
Inject the
Import Use the
FormBuilder
FormBuilder instance
instance
this.customerForm = this.fb.group({
firstName: null,
lastName: null,
email: null,
sendCatalog: true
});
FormBuilder's FormControl Syntax
this.customerForm = this.fb.group({
firstName: '',
sendCatalog: true
});
this.customerForm = this.fb.group({
firstName: {value: 'n/a', disabled: true},
sendCatalog: {value: true, disabled: false}
});
this.customerForm = this.fb.group({
firstName: [''],
sendCatalog: [{value: true, disabled: false}]
});
Checklist: Component Class
Create a property for the root FormGroup
Create the FormGroup instance
Pass in each FormControl instance
ngOnInit(): void {
this.customerForm = new FormGroup({
firstName: new FormControl(),
lastName: new FormControl(),
email: new FormControl(),
sendCatalog: new FormControl(true)
});
}
Checklist: FormBuilder
Import FormBuilder
Inject the FormBuilder instance
Use that instance
ngOnInit(): void {
this.customerForm = this.fb.group({
firstName: '',
lastName: '',
email: '',
sendCatalog: true
});
}
Checklist: Angular Module
Import ReactiveFormsModule
Add ReactiveFormsModule to the imports
array
@NgModule({
imports: [
BrowserModule,
ReactiveFormsModule
],
declarations: [
AppComponent,
CustomerComponent
],
bootstrap: [AppComponent]
})
export class AppModule { }
Checklist: Template
Bind the form element to the FormGroup
property
<form class="form-horizontal"
(ngSubmit)="save()"
[formGroup]="customerForm">
Deborah Kurata
CONSULTANT | SPEAKER | AUTHOR | MVP | GDE
@deborahkurata | blogs.msmvps.com/deborahk/
Module
Overview Setting Built-in Validation Rules
Adjusting Validation Rules at Runtime
Custom Validators
Custom Validators with Parameters
Cross-field Validation
Creating the Root FormGroup
ngOnInit(): void {
this.customerForm = this.fb.group({
firstName: '',
lastName: '',
email: '',
sendCatalog: true
});
}
Creating the FormControls
this.customerForm = this.fb.group({
firstName: '',
sendCatalog: true
});
this.customerForm = this.fb.group({
firstName: {value: 'n/a', disabled: true},
sendCatalog: {value: true, disabled: false}
});
this.customerForm = this.fb.group({
firstName: [''],
sendCatalog: [true]
});
Setting Built-in Validation Rules
this.customerForm = this.fb.group({
firstName: ['', Validators.required],
sendCatalog: true
});
this.customerForm = this.fb.group({
firstName: ['',
[Validators.required, Validators.minLength(3)]],
sendCatalog: true
});
Adjusting Validation Rules at Runtime
Adjusting Validation Rules at Runtime
myControl.setValidators(Validators.required);
myControl.setValidators([Validators.required,
Validators.maxLength(30)]);
myControl.clearValidators();
myControl.updateValueAndValidity();
Custom Validator
<div formGroupName="availability">
...
<input formControlName="start"/>
...
<input formControlName="end"/>
</div>
Cross-field Validation
Cross-field Validation: Custom Validator
function dateCompare(c: AbstractControl):
{[key: string]: boolean} | null {
let startControl = c.get('start');
let endControl = c.get('end');
if (startControl.value !== endControl.value) {
return { 'match': true };
}
return null;
}
this.customerForm = this.fb.group({
firstName: ['', [Validators.required, Validators.minLength(3)]],
lastName: ['', [Validators.required, Validators.maxLength(50)]],
availability: this.fb.group({
start: ['', Validators.required],
end: ['', Validators.required]
}, { validator: dateCompare })
});
Checklist: Setting Built-in Validation Rules
Import Validators
Pass in the validator or array of validators
ngOnInit(): void {
this.customerForm = this.fb.group({
firstName: ['', Validators.required],
lastName: ['', [Validators.required,
Validators.maxLength(30)]],
email: '',
sendCatalog: true
});
}
Checklist: Adjusting Validation Rules
Determine when to make the change
Use setValidators or clearValidators
Call updateValueAndValidity
Deborah Kurata
CONSULTANT | SPEAKER | AUTHOR | MVP | GDE
@deborahkurata | blogs.msmvps.com/deborahk/
Module
Overview
Watching
Reacting
Reactive Transformations
Watching
valueChanges is an Observable<any>
this.myFormGroup.valueChanges.subscribe(value =>
console.log(JSON.stringify(value)));
this.customerForm.valueChanges.subscribe(value =>
console.log(JSON.stringify(value)));
Reacting
Validation rules
Validation messages
Automatic suggestions
And more …
Demo
debounceTime
a b @ c
a
a b
a b @
a b @ c
debounceTime
a b @ c d e
1000 ms 1000 ms a
b
a @
b c
@ d
c e
Reactive Transformations
throttleTime
distinctUntilChanged
https://github.com/ReactiveX/rxjs/tree/master/src/operator
Checklist: Watching
Use the valueChanges Observable property
Subscribe to the Observable
this.myFormControl.valueChanges
.subscribe(value => console.log(value));
Checklist: Reacting
this.myFormControl.valueChanges
.subscribe(value =>
this.setNotification(value));
Deborah Kurata
CONSULTANT | SPEAKER | AUTHOR | MVP | GDE
@deborahkurata | blogs.msmvps.com/deborahk/
Module
Overview
Steps
Perform Each Step
- FormArrays
Steps to Dynamically Duplicate Input Elements
Duplicate
the input
Loop element(s)
through the
Create a FormArray
FormArray
Refactor to
make copies
Define a
FormGroup,
Define the if needed
input
element(s)
to duplicate
Steps to Dynamically Duplicate Input Elements
Duplicate
the input
Loop element(s)
through the
Create a FormArray
FormArray
Refactor to
make copies
Define a
FormGroup,
Define the if needed
input
element(s)
to duplicate
Steps to Dynamically Duplicate Input Elements
Duplicate
the input
Loop element(s)
through the
Create a FormArray
FormArray
Refactor to
make copies
Define a
FormGroup,
Define the if needed
input
element(s)
to duplicate
FormGroup
FormGroup
FormControl FormControl
FormGroup
FormControl FormControl
FormGroup
FormControl
FormGroup
FormControl FormControl
Benefits of a FormGroup
Match the
Check Watch for
value of the
touched, dirty, changes and
form model to
and valid state react
the data model
Dynamically
Perform cross
duplicate the
field validation
group
Steps to Dynamically Duplicate Input Elements
Duplicate
the input
Loop element(s)
through the
Create a FormArray
FormArray
Refactor to
make copies
Define a
FormGroup,
Define the if needed
input
element(s)
to duplicate
Creating a FormGroup in a Method
buildAddress(): FormGroup {
return this.fb.group({
addressType: 'home',
street1: '',
street2: '',
city: '',
state: '',
zip: ''
});
}
this.customerForm = this.fb.group({
...
addresses: this.buildAddress()
});
Steps to Dynamically Duplicate Input Elements
Duplicate
the input
Loop element(s)
through the
Create a FormArray
FormArray
Refactor to
make copies
Define a
FormGroup,
Define the if needed
input
element(s)
to duplicate
FormArray
FormArray FormArray
FormControl FormGroup
FormGroup FormGroup
FormControl FormControl FormControl FormControl
FormGroup FormGroup
FormControl FormControl FormControl FormControl
FormControl
Creating a FormArray
this.myArray = this.fb.array([...]);
Steps to Dynamically Duplicate Input Elements
Duplicate
the input
Loop element(s)
through the
Create a FormArray
FormArray
Refactor to
make copies
Define a
FormGroup,
Define the if needed
input
element(s)
to duplicate
Looping Through a FormArray
<div formArrayName="addresses"
*ngFor="let address of addresses.controls; let i=index">
<div [formGroupName]="i">
...
</div>
</div>
Steps to Dynamically Duplicate Input Elements
Duplicate
the input
Loop element(s)
through the
Create a FormArray
FormArray
Refactor to
make copies
Define a
FormGroup,
Define the if needed
input
element(s)
to duplicate
Duplicate the Input Elements
addAddress(): void {
this.addresses.push(this.buildAddress());
}
Duplicate
the input
Loop element(s)
through the
Create a FormArray
FormArray
Refactor to
make copies
Define a
FormGroup,
Define the if needed
input
element(s)
to duplicate
Summary
Steps
Perform Each Step
- FormArrays
Reactive Form in Context
Deborah Kurata
CONSULTANT | SPEAKER | AUTHOR | MVP | GDE
@deborahkurata | blogs.msmvps.com/deborahk/
Module
Overview Sample Application
Routing to the Form
Reading a Route Parameter
Setting a canDeactivate Guard
Refactoring to a Custom Validation Class
APM Sample Application Architecture
Welcome Product
Component Filter Pipe
ProductList -
AppComponent ProductService StarComponent
Component
ProductDetail -
WelcomeComponent ProductDetailGuard CommonModule
Component
Imports
ProductEditGuard ProductFilterPipe FormsModule
Exports
Declarations
Providers ProductEdit-
Component
Bootstrap
Routing Steps
Place
result
Activate
routes
Configure
routes
Configuring Routes
[
{ path: 'products', component: ProductListComponent },
{ path: 'product/:id', component: ProductDetailComponent },
{ path: 'productEdit/:id', component: ProductEditComponent }
]
Route Guards
[
{ path: 'products', component: ProductListComponent },
{ path: 'product/:id',
canActivate: [ ProductDetailGuard],
component: ProductDetailComponent },
{ path: 'productEdit/:id',
canDeactivate: [ ProductEditGuard ],
component: ProductEditComponent }
]
Tying Routes to Actions
app.component.ts
...
@Component({
selector: 'pm-app',
template: `
<ul class='nav navbar-nav'>
<li><a [routerLink]="['/welcome']">Home</a></li>
<li><a [routerLink]="['/products']">Product List</a></li>
<li><a [routerLink]="['/productEdit', '0']">Add Product</a></li>
</ul>
`
})
Placing the Views
app.component.ts
...
@Component({
selector: 'pm-app',
template: `
<ul class='nav navbar-nav'>
<li><a [routerLink]="['/welcome']">Home</a></li>
<li><a [routerLink]="['/products']">Product List</a></li>
<li><a [routerLink]="['/productEdit', '0']">Add Product</a></li>
</ul>
<router-outlet></router-outlet>
`
})
Reading Parameters from a Route
{ path: 'productEdit/:id', component: ProductEditComponent }
@Injectable()
export class ProductEditGuard
implements CanDeactivate<ProductEditComponent> {
Deborah Kurata
CONSULTANT | SPEAKER | AUTHOR | MVP | GDE
@deborahkurata | blogs.msmvps.com/deborahk/
Web
Web Browser Server
index.html index.html
JavaScript JavaScript
(http://mysite/api/products/5)
Web
Response Service
Data
DB
Web
Web Browser Server
index.html index.html
JavaScript JavaScript
(http://mysite/api/products/5)
Web
Data Service
Response
Data
DB
Module Prerequisites
@Injectable()
export class ProductService { } constructor(private http: Http) { }
... ...
import { ProductService } import { Observable }
from './product.service'; from 'rxjs/Observable';
import 'rxjs/add/operator/do';
@NgModule({ import 'rxjs/add/operator/catch';
imports: [ ... ], import 'rxjs/add/operator/throw';
providers: [ ProductService ] import 'rxjs/add/operator/map';
}) ...
export class ProductModule { }
getProducts(): Observable<IProduct[]> {
return this.http.get(this.baseUrl)
.map(this.extractData);
}
Module
Overview Data Access Service
Creating Data
Reading Data
Updating Data
Deleting Data
APM Sample Application Architecture
Welcome Product
Component Filter Pipe
Separation of Concerns
Reusability
Data Sharing
Sending an HTTP Request
Request Request
(Get) (GET)
Write the
code to
Import issue each
Observable Http request
Inject the and the
Angular observable
Create and Http Service operators
register the
Register the data access
Angular service
Http Service
Demo
Build the
Select a Define the
server-side
technology API
code
Faking a Backend Server
Directly return
Use a JSON
hard-coded
file
data
@Injectable()
export class ProductService {
private baseUrl = 'www.myWebService.com/api/products';
@Injectable()
export class ProductService {
private baseUrl = 'www.myWebService.com/api/products';
constructor(private http: Http) { }
editProduct: void {
this.productService.updateProduct(p)
.subscribe(
() => this.onSaveComplete(),
(error: any) => this.errorMessage = <any>error
);
}
Demo
Saving Edits
Creating New Items
Initializing an Object
product.service.ts
initializeProduct(): IProduct {
return {
id: 0,
productName: null,
productCode: null,
tags: [''],
releaseDate: null,
price: null,
description: null,
starRating: null,
imageUrl: null
}
}
HTTP Post Request
product.service.ts
...
@Injectable()
export class ProductService {
private baseUrl = 'www.myWebService.com/api/products';
constructor(private http: Http) { }
@Injectable()
export class ProductService {
private baseUrl = 'www.myWebService.com/api/products';
constructor(private http: Http) { }
deleteProduct: void {
this.productService.deleteProduct(this.product.id)
.subscribe(
() => this.onSaveComplete(),
(error: any) => this.errorMessage = <any>error
);
}
Demo
app.module.ts
...
import { HttpModule }
from '@angular/http';
@NgModule({
Add HttpModule to the imports array of
imports: [ HttpModule ], one of the application's Angular Modules
...
})
export class AppModule { }
CRUD Checklist: Data Access Service
Import what we need
product.service.ts
...
import { Http, Response} from '@angular/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
CRUD Checklist: Data Access Service
Import what we need
Define a dependency for the http client
service
product.service.ts - Use a constructor parameter
...
@Injectable()
export class ProductService {
product-edit.component.ts
Inject the Data Access Service
...
Call the subscribe method of the returned
this.ps.getProduct(id)
.subscribe();
observable
CRUD Checklist: Using the Service
product-edit.component.ts
Inject the Data Access Service
...
Call the subscribe method of the returned
this.ps.getProduct(id)
.subscribe(
observable
(product: IProduct) =>
Provide a function to handle an emitted
this.onRetrieved(product)
); item
CRUD Checklist: Using the Service
product-edit.component.ts
Inject the Data Access Service
...
Call the subscribe method of the returned
this.ps.getProduct(id)
.subscribe(
observable
(product: IProduct) =>
Provide a function to handle an emitted
this.onRetrieved(product),
(error: any) => item
this.errorMessage = error
); Provide an error function to handle any
returned errors
Summary Data Access Service
Creating Data
Reading Data
Updating Data
Deleting Data
Final Words
Deborah Kurata
CONSULTANT | SPEAKER | AUTHOR | MVP | GDE
@deborahkurata | blogs.msmvps.com/deborahk/
Template-
Reactive
driven
Angular Forms
Template-driven Reactive
Easy to use More flexible ->
more complex scenarios
Similar to Angular 1
Immutable data model
Two-way data binding ->
Minimal component code Easier to perform an action
on a value change
Automatically tracks form and
input element state Reactive transformations ->
DebounceTime or DistinctUntilChanged
Easily add input elements dynamically
Easier unit testing
Template-driven
Reactive
Request Request
(Get) (GET)
Angular Documentation
- Angular.io
@deborahkurata
Template-
Reactive
driven