Collecting Data with Forms and Validation
Check code at: https://github.com/wghglory/angular2-fundamental
Using Models for Type Safety
- create events/shared/event.model.ts
export interface IEvent {
id: number
name: string
date: Date
time: string
price: number
imageUrl: string
location?: {
address: string
city: string
country: string
},
onlineUrl?: string
sessions: ISession[]
}
export interface ISession {
id: number
name: string
presenter: string
duration: number
level: string
abstract: string
voters: string[]
}
- export in shared index.ts
export * from './event.service'
+ export * from './event.model'
- event.service.ts
+ import { Subject, Observable } from 'rxjs/RX'
+ import { IEvent } from './event.model'
@Injectable()
export class EventService {
+ getEvents(): Observable {
//simulate async loading data operation, return subject. Need to change events-list.component.ts
+ let subject = new Subject()
setTimeout(() => {
subject.next(EVENTS);
subject.complete();
}, 100)
return subject;
}
+ getEvent(id: number): IEvent {
return EVENTS.find(e => e.id === id)
}
}
+ const EVENTS: IEvent[] = ...
- update
event:any
toevent:IEvent
in event-detail.component, event-list.component, event-thumbnail.component
Creating Your First Template-based Form
- user/login.component.ts
import {Component} from '@angular/core'
import {AuthService} from './auth.service'
@Component({
templateUrl: 'app/user/login.component.html'
})
export class LoginComponent {
constructor(private authService: AuthService) { }
login(formValues) { ////will be object of userName and password
this.authService.loginUser(formValues.userName, formValues.password)
}
}
- login.component.html
Login
- Register component in UserModule
import { NgModule } from '@angular/core'
import { RouterModule } from '@angular/router'
import { CommonModule } from '@angular/common'
import {userRoutes} from './user.routes'
import {ProfileComponent} from './profile.component'
+ import {LoginComponent} from './login.component'
+ import {FormsModule} from '@angular/forms'
@NgModule({
imports: [
CommonModule,
+ FormsModule,
RouterModule.forChild(userRoutes)
],
declarations: [
ProfileComponent,
+ LoginComponent
],
})
export class UserModule { }
- Add user.routes.ts path
import {ProfileComponent} from './profile.component'
+ import {LoginComponent} from './login.component'
export const userRoutes = [
{ path: 'profile', component: ProfileComponent },
+ { path: 'login', component: LoginComponent }
]
- create user/user.model.ts:
export interface IUser {
id: number
firstName: string
lastName: string
userName: string
}
- create user/auth.service.ts:
import {Injectable} from '@angular/core'
import {IUser} from './user.model'
@Injectable()
export class AuthService {
currentUser: IUser
loginUser(userName: string, password: string) {
this.currentUser = {
id: 1,
userName: userName,
firstName: 'guanghui',
lastName: 'wang'
}
}
isAuthenticated() {
return !!this.currentUser;
}
}
- register service in app.module.ts
...
+ import {AuthService} from './user/auth.service'
@NgModule({
...
providers: [
...
+ AuthService
],
bootstrap: [AppComponent]
})
export class AppModule { }
- inject AuthService in nav.component.ts
import {Component} from '@angular/core'
+ import {AuthService} from '../user/auth.service'
@Component({
selector: 'nav-bar',
templateUrl: 'app/nav/nav.component.html',
styles: [`
.nav.navbar-nav {font-size:15px;}
#searchForm {margin-right:100px;}
@media(max-width:1200px) {#searchForm {display:none;}}
li>a.active{color:#f97924;}
`]
})
export class NavBarComponent {
+ constructor(private authService: AuthService) { }
}
- modify nav.component.html
Login
Welcome {{authService.currentUser.userName}}
Now when you type username, password and then click login, the rightmost area in navbar's "login" will become the userName.
We will finish up the other features below, like when we click login, or cancel, navigate to all events page
So login.component.ts:
import {Component} from '@angular/core'
import {AuthService} from './auth.service'
+ import {Router} from '@angular/router'
@Component({
templateUrl: 'app/user/login.component.html'
})
export class LoginComponent {
+ constructor(private authService: AuthService, private router:Router) { }
login(formValues) {
this.authService.loginUser(formValues.userName, formValues.password)
+ this.router.navigate(['events'])
}
+ cancel(){
+ this.router.navigate(['events'])
+ }
}
add cancel click in login.component.html
Validating Template-based Forms (LoginForm)
login.component.html
Login
+
login.component.ts add styles:
@Component({
templateUrl: 'app/user/login.component.html',
styles:[
`em {float:right;color:#e05c65;padding-left:10px;}`
]
})
Creating Your First Reactive Form (Profile Form)
- create profile.component.html and update profile.component.ts
import { Component, OnInit } from '@angular/core'
import { FormControl, FormGroup } from '@angular/forms'
import { AuthService } from './auth.service'
import { Router } from '@angular/router'
@Component({
templateUrl: 'app/user/profile.component.html'
})
export class ProfileComponent implements OnInit {
constructor(private authService: AuthService, private router: Router) { }
profileForm: FormGroup
ngOnInit() {
let firstName = new FormControl(this.authService.currentUser.firstName)
let lastName = new FormControl(this.authService.currentUser.lastName)
this.profileForm = new FormGroup({
firstName: firstName,
lastName: lastName
})
}
cancel() {
this.router.navigate(['events'])
}
saveProfile(formValues) {
this.authService.updateCurrentUser(formValues.firstName, formValues.lastName)
this.router.navigate(['events'])
}
}
Edit Your Profile
- import ReactiveFormsModule into user.module.ts
...
+ import {FormsModule,ReactiveFormsModule} from '@angular/forms'
@NgModule({
imports: [
CommonModule,
FormsModule,
+ ReactiveFormsModule,
RouterModule.forChild(userRoutes)
],
...
})
export class UserModule { }
- add updateCurrentUser method in authService
export class AuthService {
...
updateCurrentUser(firstName: string, lastName: string) {
this.currentUser.firstName = firstName;
this.currentUser.lastName = lastName;
}
}
Validating Reactive Forms
- add required Validators, error style, validateFirstName, validateLastName functions in profile.component.ts
import { Component, OnInit } from '@angular/core'
+ import { FormControl, FormGroup, Validators } from '@angular/forms'
import { AuthService } from './auth.service'
import { Router } from '@angular/router'
@Component({
templateUrl: 'app/user/profile.component.html',
+ styles: [
`
em {float:right;color:#e05c65;padding-left:10px;}
.error input{background-color:#e3c3c5;}
.error ::-webkit-input-placeholder {color:#999;}
.error ::-moz-placeholder {color:#999;}
.error :-ms-input-placeholder {color:#999;}
`
]
})
export class ProfileComponent implements OnInit {
constructor(private authService: AuthService, private router: Router) { }
profileForm: FormGroup
+ private firstName: FormControl
+ private lastName: FormControl
ngOnInit() {
+ this.firstName = new FormControl(this.authService.currentUser.firstName, Validators.required)
+ this.lastName = new FormControl(this.authService.currentUser.lastName, Validators.required)
this.profileForm = new FormGroup({
+ firstName: this.firstName,
+ lastName: this.lastName
})
}
cancel() {
this.router.navigate(['events'])
}
saveProfile(formValues) {
+ if (this.profileForm.valid) {
this.authService.updateCurrentUser(formValues.firstName, formValues.lastName)
this.router.navigate(['events'])
}
}
+ validateFirstName() {
+ return this.firstName.valid || this.firstName.untouched
+ }
+ validateLastName() {
+ return this.lastName.valid || this.lastName.untouched
+ }
}
- update profile template:
Edit Your Profile
Using Multiple Validators in Reactive Forms
Let's say we want firstName starts with a letter, so we add Validators.pattern
import { FormControl, FormGroup, Validators } from '@angular/forms'
@Component({
templateUrl: 'app/user/profile.component.html',
styles: [
`
em {float:right;color:#e05c65;padding-left:10px;}
.error input{background-color:#e3c3c5;}
.error ::-webkit-input-placeholder {color:#999;}
.error ::-moz-placeholder {color:#999;}
.error :-ms-input-placeholder {color:#999;}
`
]
})
export class ProfileComponent implements OnInit {
constructor(private authService: AuthService, private router: Router) { }
profileForm: FormGroup
firstName: FormControl
lastName: FormControl
ngOnInit() {
+ this.firstName = new FormControl(this.authService.currentUser.firstName, [Validators.required, Validators.pattern('[a-zA-Z].*')])
this.lastName = new FormControl(this.authService.currentUser.lastName, Validators.required)
this.profileForm = new FormGroup({
firstName: this.firstName,
lastName: this.lastName
})
}
...
}
profile template:
Edit Your Profile
Diving Deeper into Template-based Forms (Create New Event)
add create-event.component.html
create-event.component.ts: Add styles, saveEvent
//click cancel will use router to navigate to /events (load all events)
import {Component} from '@angular/core'
import {Router} from '@angular/router'
import {EventService} from './shared/event.service'
@Component({
templateUrl: 'app/events/create-event.component.html',
styles: [
`
em {float:right;color:#e05c65;padding-left:10px;}
.error input{background-color:#e3c3c5;}
.error ::-webkit-input-placeholder {color:#999;}
.error ::-moz-placeholder {color:#999;}
.error :-ms-input-placeholder {color:#999;}
`
]
})
export class CreateEventComponent {
isDirty: boolean = true
constructor(private router: Router, private eventService: EventService) { }
cancel() {
this.router.navigate(['/events'])
}
saveEvent(formValues) {
this.eventService.saveEvent(formValues)
this.isDirty = false
this.router.navigate(['/events'])
}
}
we can now add a temporary method in event.service.ts
saveEvent(event){
event.id = 999;
event.sessions = [];
EVENTS.push(event);
}
import FormsModule, ReactiveFormsModule in app.module.ts
now when we submit the form, formValues is a object that has city, address, country, name, etc. They are at same level. Our model structure actually has location property which includes address, city, country. So we can use ngModelGroup to wrap up these 3 fields.
+
+
data is like {name: "fdsa", date: "1010/2/2", time: "8:00", price: 34432, location: {address: "fds", city: "fdasfdas", country: "fdsa"}, …}
Diving Deeper into Reactive Forms (Event-detail shows session)
- create /events/event-detail/create-session.component.ts, its template. Don't forget to export in index.ts and register in app.module.ts
import { Component, OnInit } from '@angular/core'
import { FormControl, FormGroup, Validators } from '@angular/forms'
import { ISession } from '../shared/index'
@Component({
templateUrl: 'app/events/event-detail/create-session.component.html',
styles: [
`
em {float:right;color:#e05c65;padding-left:10px;}
.error input, .error select, .error textarea {background-color:#e3c3c5;}
.error ::-webkit-input-placeholder {color:#999;}
.error ::-moz-placeholder {color:#999;}
.error :-ms-input-placeholder {color:#999;}
`
]
})
export class CreateSessionComponent implements OnInit {
newSessionForm: FormGroup
name: FormControl
presenter: FormControl
duration: FormControl
level: FormControl
abstract: FormControl
ngOnInit() {
this.name = new FormControl('', Validators.required)
this.presenter = new FormControl('', Validators.required)
this.duration = new FormControl('', Validators.required)
this.level = new FormControl('', Validators.required)
this.abstract = new FormControl('', [Validators.required, Validators.maxLength(400)])
this.newSessionForm = new FormGroup({
name: this.name,
presenter: this.presenter,
duration: this.duration,
level: this.level,
abstract: this.abstract
})
}
saveSession(formValues) {
// console.log(formValues);
let session: ISession = {
id: undefined,
name: formValues.name,
duration: +formValues.duration, //convert
presenter: formValues.presenter,
level: formValues.level,
abstract: formValues.abstract,
voters: []
}
console.log(session)
}
}
Create Session
- register it in routes.ts
import {CreateSessionComponent} from './events/event-detail/create-session.component'
export const appRoutes: Routes = [
//order matters
{ path: 'events/new', component: CreateEventComponent, canDeactivate: ['canDeactivateCreateEvent'] }, //Guarding Against Route De-activation using function, canDeactivateCreateEvent is provider name which points to a function
{ path: 'events', component: EventsListComponent, resolve: { events1: EventListResolver } }, //call EventListResolver before using the component, bind resolver result to a property events1, and this property will be passed to the component
{ path: 'events/:id', component: EventDetailComponent, canActivate: [EventRouteActivator] }, //Guarding Against Route Activation using service
+ { path: 'events/session/new', component: CreateSessionComponent },
{ path: '404', component: Error404Component },
{ path: '', redirectTo: '/events', pathMatch: 'full' }, //pathMatch: prefix or full
// user prefix, localhost/user/x, will load module here: app/user/user.module and the module name is UserModule, concat '#'
{ path: 'user', loadChildren: 'app/user/user.module#UserModule' }
]
Custom Validation
create-session.component.ts
import { Component, OnInit } from '@angular/core'
import { FormControl, FormGroup, Validators } from '@angular/forms'
import { ISession, restrictedWords } from '../shared/index'
@Component({
templateUrl: 'app/events/event-detail/create-session.component.html',
styles: [
`
em {float:right;color:#e05c65;padding-left:10px;}
.error input, .error select, .error textarea {background-color:#e3c3c5;}
.error ::-webkit-input-placeholder {color:#999;}
.error ::-moz-placeholder {color:#999;}
.error :-ms-input-placeholder {color:#999;}
`
]
})
export class CreateSessionComponent implements OnInit {
newSessionForm: FormGroup
name: FormControl
presenter: FormControl
duration: FormControl
level: FormControl
abstract: FormControl
ngOnInit() {
this.name = new FormControl('', Validators.required)
this.presenter = new FormControl('', Validators.required)
this.duration = new FormControl('', Validators.required)
this.level = new FormControl('', Validators.required)
this.abstract = new FormControl('', [Validators.required, Validators.maxLength(400), restrictedWords(['foo', 'bar'])])
this.newSessionForm = new FormGroup({
name: this.name,
presenter: this.presenter,
duration: this.duration,
level: this.level,
abstract: this.abstract
})
}
saveSession(formValues) {
// console.log(formValues);
let session: ISession = {
id: undefined,
name: formValues.name,
duration: +formValues.duration, //convert
presenter: formValues.presenter,
level: formValues.level,
abstract: formValues.abstract,
voters: []
}
console.log(session)
}
//custom validation
// private restrictedWords(control: FormControl): { [key: string]: any } {
// return control.value.includes('foo')
// ? { 'restrictedWords': 'foo' }
// : null
// }
// this will be put into shared folder
// private restrictedWords(words) {
// return (control: FormControl): { [key: string]: any } => {
//
// if (!words) return null;
//
// var invalidWords = words.map(w => control.value.includes(w) ? w : null)
// .filter(w => w != null);
//
// return invalidWords && invalidWords.length > 0
// ? { 'restrictedWords': invalidWords.join(', ') }
// : null
// }
// }
}
shared/restricted-words.validator.ts, and don't forget export in index.ts barrel
import {FormControl} from '@angular/forms'
export function restrictedWords(words) {
return (control: FormControl): { [key: string]: any } => {
if (!words) return null;
var invalidWords = words.map(w => control.value.includes(w) ? w : null)
.filter(w => w != null);
return invalidWords && invalidWords.length > 0
? { 'restrictedWords': invalidWords.join(', ') }
: null
}
}