Check code at: https://github.com/wghglory/angular2-fundamental
Using Models for Type Safety 1) create events/shared/event.model.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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[] }
2) export in shared index.ts
1 2 export * from './event.service' + export * from './event.model'
3) event.service.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 + import { Subject, Observable } from 'rxjs/RX' + import { IEvent } from './event.model' @Injectable() export class EventService {+ getEvents(): Observable<IEvent[]> { + let subject = new Subject<IEvent[]>() setTimeout(() => { subject.next(EVENTS); subject.complete(); }, 100 ) return subject; } + getEvent(id: number): IEvent { return EVENTS.find(e => e.id === id) } } + const EVENTS: IEvent[] = ...
4) update event:any
to event:IEvent
in event-detail.component, event-list.component, event-thumbnail.component
1) user/login.component.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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) { this .authService.loginUser(formValues.userName, formValues.password) } }
2) login.component.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <h1 > Login</h1 > <hr > <div class ="col-md-4" > <form #loginForm ="ngForm" (ngSubmit )="login(loginForm.value)" autocomplete ="off" > <div class ="form-group" > <label for ="userName" > User Name:</label > <input (ngModel )="userName" name ="userName" id ="userName" type ="text" class ="form-control" placeholder ="User Name..." /> </div > <div class ="form-group" > <label for ="password" > Password:</label > <input (ngModel )="password" name ="password" id ="password" type ="password" class ="form-control" placeholder ="Password..." /> </div > <button type ="submit" class ="btn btn-primary" > Login</button > <button type ="button" class ="btn btn-default" > Cancel</button > </form > </div >
3) Register component in UserModule
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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 { }
4) Add user.routes.ts path
1 2 3 4 5 6 7 import {ProfileComponent} from './profile.component' + import {LoginComponent} from './login.component' export const userRoutes = [ { path : 'profile' , component : ProfileComponent }, + { path : 'login' , component : LoginComponent } ]
5) create user/user.model.ts:
1 2 3 4 5 6 export interface IUser { id: number firstName: string lastName: string userName: string }
6) create user/auth.service.ts:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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; } }
7) register service in app.module.ts
1 2 3 4 5 6 7 8 9 10 11 12 ... + import {AuthService} from './user/auth.service' @NgModule({ ... providers: [ ... + AuthService ], bootstrap: [AppComponent] }) export class AppModule { }
8) inject AuthService in nav.component.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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) { } }
9) modify nav.component.html
1 2 3 4 <li > <a *ngIf ="!authService.isAuthenticated()" [routerLink ]="['user/login']" > Login</a > <a *ngIf ="authService.isAuthenticated()" [routerLink ]="['user/profile']" > Welcome {{authService.currentUser.userName}}</a > </li >
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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
1 <button type ="button" (click )="cancel()" class ="btn btn-default" > Cancel</button >
login.component.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <h1 > Login</h1 > <hr > <div class ="col-md-4" > + <form #loginForm ="ngForm" (ngSubmit )="login(loginForm.value)" autocomplete ="off" novalidate > <div class ="form-group" > <label for ="userName" > User Name:</label > + <em *ngIf ="loginForm.controls.userName?.invalid && (loginForm.controls.userName?.touched || mouseoverLogin)" > Required</em > + <input required (ngModel )="userName" name ="userName" id ="userName" type ="text" class ="form-control" placeholder ="User Name..." /> </div > <div class ="form-group" > <label for ="password" > Password:</label > + <em *ngIf ="loginForm.controls.password?.invalid && (loginForm.controls.password?.touched || mouseoverLogin)" > Required</em > + <input required (ngModel )="password" name ="password" id ="password" type ="password" class ="form-control" placeholder ="Password..." /> </div > + <span (mouseenter )="mouseoverLogin=true" (mouseleave )="mouseoverLogin=false" > + <button [disabled ]="loginForm.invalid" type ="submit" class ="btn btn-primary" > Login</button > + </span > <button type ="button" (click )="cancel()" class ="btn btn-default" > Cancel</button > </form > </div >
login.component.ts add styles:
1 2 3 4 5 6 @Component({ templateUrl: 'app/user/login.component.html' , styles:[ `em {float:right;color:#e05c65;padding-left:10px;}` ] })
1) create profile.component.html and update profile.component.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 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' ]) } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <div > <h1 > Edit Your Profile </h1 > <hr > <div class ="col-md-4" > <form [formGroup ]="profileForm" (ngSubmit )="saveProfile(profileForm.value)" autocomplete ="off" novalidate > <div class ="form-group" > <label for ="firstName" > First Name:</label > <input formControlName ="firstName" id ="firstName" type ="text" class ="form-control" placeholder ="First Name..." /> </div > <div class ="form-group" > <label for ="lastName" > Last Name:</label > <input formControlName ="lastName" id ="lastName" type ="text" class ="form-control" placeholder ="Last Name..." /> </div > <button type ="submit" class ="btn btn-primary" > Save</button > <button (click )="cancel()" type ="button" class ="btn btn-default" > Cancel</button > </form > </div > </div >
2) import ReactiveFormsModule into user.module.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 ... + import {FormsModule,ReactiveFormsModule} from '@angular/forms' @NgModule({ imports: [ CommonModule, FormsModule, + ReactiveFormsModule, RouterModule.forChild(userRoutes) ], ... }) export class UserModule { }
3) add updateCurrentUser method in authService
1 2 3 4 5 6 7 8 export class AuthService { ... updateCurrentUser(firstName: string, lastName : string) { this .currentUser.firstName = firstName; this .currentUser.lastName = lastName; } }
1) add required Validators, error style, validateFirstName, validateLastName functions in profile.component.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 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 + } }
2) update profile template:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <div > <h1 > Edit Your Profile </h1 > <hr > <div class ="col-md-4" > <form [formGroup ]="profileForm" (ngSubmit )="saveProfile(profileForm.value)" autocomplete ="off" novalidate > + <div class ="form-group" [ngClass ]="{'error': !validateFirstName()}" > <label for ="firstName" > First Name:</label > + <em *ngIf ="!validateFirstName()" > Required</em > <input formControlName ="firstName" id ="firstName" type ="text" class ="form-control" placeholder ="First Name..." /> </div > + <div class ="form-group" [ngClass ]="{'error': !validateLastName()}" > <label for ="lastName" > Last Name:</label > + <em *ngIf ="!validateLastName()" > Required</em > <input formControlName ="lastName" id ="lastName" type ="text" class ="form-control" placeholder ="Last Name..." /> </div > <button type ="submit" class ="btn btn-primary" > Save</button > <button (click )="cancel()" type ="button" class ="btn btn-default" > Cancel</button > </form > </div > </div >
Let’s say we want firstName starts with a letter, so we add Validators.pattern
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <div > <h1 > Edit Your Profile </h1 > <hr > <div class ="col-md-4" > <form [formGroup ]="profileForm" (ngSubmit )="saveProfile(profileForm.value)" autocomplete ="off" novalidate > <div class ="form-group" [ngClass ]="{'error': !validateFirstName()}" > <label for ="firstName" > First Name:</label > + <em *ngIf ="!validateFirstName() && profileForm.controls.firstName.errors.required" > Required</em > + <em *ngIf ="!validateFirstName() && profileForm.controls.firstName.errors.pattern" > firstName starts with a letter</em > <input formControlName ="firstName" id ="firstName" type ="text" class ="form-control" placeholder ="First Name..." /> </div > ... </form > </div > </div >
1) add create-event.component.html
2) create-event.component.ts: Add styles, saveEvent
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 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
1 2 3 4 5 saveEvent(event){ event.id = 999 ; event.sessions = []; EVENTS.push(event); }
3) import FormsModule, ReactiveFormsModule in app.module.ts
4) 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 + <div ngModelGroup ="location" > <div class ="form-group" > <label for ="address" > Event Location:</label > <input (ngModel )="address" name ="address" id ="address" type ="text" class ="form-control" placeholder ="Address of event..." /> </div > <div class ="row" > <div class ="col-md-6" > <input (ngModel )="city" name ="city" id ="city" type ="text" class ="form-control" placeholder ="City..." /> </div > <div class ="col-md-6" > <input (ngModel )="country" name ="country" id ="country" type ="text" class ="form-control" placeholder ="Country..." /> </div > </div > + </div >
data is like {name: "fdsa", date: "1010/2/2", time: "8:00", price: 34432, location: {address: "fds", city: "fdasfdas", country: "fdsa"}, …}
1) create /events/event-detail/create-session.component.ts, its template. Don’t forget to export in index.ts and register in app.module.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 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) { let session: ISession = { id: undefined , name: formValues.name, duration: +formValues.duration, presenter: formValues.presenter, level: formValues.level, abstract: formValues.abstract, voters: [] } console .log(session) } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 <div class ="col-md-12" > <h3 > Create Session</h3 > </div > <div class ="col-md-6" > <form [formGroup ]="newSessionForm" (ngSubmit )="saveSession(newSessionForm.value)" autocomplete ="off" > <div class ="form-group" [ngClass ]="{'error': name.invalid && name.dirty}" > <label for ="sessionName" > Session Name:</label > <em *ngIf ="name.invalid && name.dirty" > Required</em > <input formControlName ="name" id ="sessionName" type ="text" class ="form-control" placeholder ="session name..." /> </div > <div class ="form-group" [ngClass ]="{'error': presenter.invalid && presenter.dirty}" > <label for ="presenter" > Presenter:</label > <em *ngIf ="presenter.invalid && presenter.dirty" > Required</em > <input formControlName ="presenter" id ="presenter" type ="text" class ="form-control" placeholder ="presenter..." /> </div > <div class ="form-group" [ngClass ]="{'error': duration.invalid && duration.dirty}" > <label for ="duration" > Duration:</label > <em *ngIf ="duration.invalid && duration.dirty" > Required</em > <select formControlName ="duration" class ="form-control" > <option value ="" > select duration...</option > <option value ="1" > Half Hour</option > <option value ="2" > 1 Hour</option > <option value ="3" > Half Day</option > <option value ="4" > Full Day</option > </select > </div > <div class ="form-group" [ngClass ]="{'error': level.invalid && level.dirty}" > <label for ="level" > Level:</label > <em *ngIf ="level.invalid && level.dirty" > Required</em > <select formControlName ="level" class ="form-control" > <option value ="" > select level...</option > <option value ="Beginner" > Beginner</option > <option value ="Intermediate" > Intermediate</option > <option value ="Advanced" > Advanced</option > </select > </div > <div class ="form-group" [ngClass ]="{'error': abstract.invalid && abstract.dirty}" > <label for ="abstract" > Abstract:</label > <em *ngIf ="abstract.invalid && abstract.dirty && abstract?.errors.required" > Required</em > <em *ngIf ="abstract.invalid && abstract.dirty && abstract?.errors.maxlength" > Cannot exceed 400 characters</em > <textarea formControlName ="abstract" id ="abstract" rows ="3" class ="form-control" placeholder ="abstract..." > </textarea > </div > <button type ="submit" [disabled ]="newSessionForm.invalid" class ="btn btn-primary" > Save</button > <button type ="button" class ="btn btn-default" > Cancel</button > </form > </div >
2) register it in routes.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import {CreateSessionComponent} from './events/event-detail/create-session.component' export const appRoutes: Routes = [ { path : 'events/new' , component : CreateEventComponent, canDeactivate : ['canDeactivateCreateEvent' ] }, { path : 'events' , component : EventsListComponent, resolve : { events1 : EventListResolver } }, { path : 'events/:id' , component : EventDetailComponent, canActivate : [EventRouteActivator] }, + { path : 'events/session/new' , component : CreateSessionComponent }, { path : '404' , component : Error404Component }, { path : '' , redirectTo : '/events' , pathMatch : 'full' }, { path : 'user' , loadChildren : 'app/user/user.module#UserModule' } ]
Custom Validation create-session.component.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 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) { let session: ISession = { id: undefined , name: formValues.name, duration: +formValues.duration, presenter: formValues.presenter, level: formValues.level, abstract: formValues.abstract, voters: [] } console .log(session) } }
shared/restricted-words.validator.ts, and don’t forget export in index.ts barrel
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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 } }