Prend en compte la version 4/5 d'Angular.
Setup:
$ ng set defaults.styleExt scss
ou bien:
$ ng new my-new-app --style=scss
Démarrer un projet qui s'appellera todo
:
$ ng new todo
Cela crée un répertoire todo
dans le répertoire courant.
$ cd todo $ npm start
On peut visiter l'application avec http://localhost:4200.
Dans package.json:
"start:ssl": "ng serve --port 8000 --hmr --ssl true --ssl-cert ssl/server.crt --ssl-key ssl/server.key"
$ ng generate library
$ npm test
$ cd src/app $ ng g component todo-list
templateUrl : Le chemin indiqué est relative à la racine de l'application et cette racine est où se trouve le fichier index.html
. Par exemple, s'il y a un répertoire app
, le templateUrl
aura app/my-component.component.html
.
Component de base :
import { Component } from '@angular/core' @Component({ selector: 'events-app', template: '<h2>Hello World.</h2>' }) export class EventsAppComponent { }
Les component ajoutés dans l'applications doivent être enregistrés à un module (fichier app.module.ts
) :
import { NgModule } from '@angular/core' import { BrowserModule } from '@angular/platform-browser' import { EventsAppComponent } from './events-app.component' import { EventsListComponent } from './events-list.component' import { EventThumbnailComponent } from './events-thumbnail.component' @NgModule({ imports: [BrowserModule], declarations: [ EventsAppComponent, EventsListComponent, EventThumbnailComponent ], bootstrap: [EventsAppComponent] }) export class AppModule {}
Dans le component enfant EventThumbnailComponent
, on peut ajouter la directive @Input
dans la classe :
export class EventThumbnailComponent { @Input() event: any; }
Dans le component parent EventsListComponent
, on peut alors insérer la balise du component enfant et spécifier ce qui sera la valeur de event
:
<event-thumbnail [event]="event"></event-thumbnail>
Le premier [event]
(qui est le nom de l'attribut dans la balise), correspond à la propriété spécifiée à @Input()
. Le deuxième event
qui agit en tant que valeur dans la balise, correspond à un membre du component parent, par exemple:
export class EventsListComponent { event = { id: 1, name: 'Angular Connect', date: '9/26/2027', etc: 'etc' } }
La communication se fait avec un EventEmitter
et @Output
.
Dans le composant enfant, on peut ajouter un bouton:
<button class="btn btn-primary" (click)="handleClick()">Click</button>
Dans la classe, on ajoute la fonction:
import { Component, Input, Output, EventEmitter } from '@angular/core' // ... export class EventThumbnailComponent { @Input() event: any; @Output() eventClick = new EventEmitter(); handleClick() { this.eventClick.emit('clicked'); } }
Le composant parent doit écouter l'événement.
<event-thumbnail (eventClick)="handleEventClick($event)" [event]="event"></event-thumbnail>
Et dans la classe :
export class EventsListComponent { // ... handleEventClick(data) { console.log(data); } }
Ayant deux components, un EventDetailsComponent
et un CreateSessionComponent
, on peut envoyer les informations de la nouvelle session vers l'événement (le parent) qui contiendra la session.
CreateSessionComponent
@Output() saveNewSession = new EventEmitter(); saveNewSession(formValues) { let session: ISession = { id: undefined, name: formValues.name, presenter: formValues.presenter }; this.saveNewSession.emit(session) }
EventDetailsComponent
<create-session *ngIf="addMode" (saveNewSession)="saveNewSession($event)"></create-session>
saveNewSession(session: ISession) { const nextId = Math.max.apply(null, this.event.sessions.map(s => s.id)); session.id = nextId + 1; this.event.sessions.push(session); this.eventService.updateEvent(this.event); this.addMode = false; }
Dans le composant parent, si on utilise #
, ceci devient une référence à la classe du composant enfant. Par exemple, on utilise #thumbnail
pour référencer le composant enfant.
<event-thumbnail #thumbnail [event]="event"></event-thumbnail> <button class="btn btn-primary" (click)="thumbnail.logFoo()">Log Foo</button>
Dans le composant enfant, on peut définir une méthode publique logFoo()
:
export class EventThumbnailComponent { @Input() event: any; logFoo() { console.log('foo'); } }
Le même principe fonctionne pour les propriétés publiques du composant enfant.
export class EventThumbnailComponent { @Input() event: any; someProperty: any = 'Some value'; logFoo() { console.log('foo'); } }
Et dans le composant parent, on peut utiliser la syntaxe pour l'interpolation:
<h3>{{thumbnail.someProperty}}</h3>
Pour modifier le titre du navigateur par rapport à un composant, on peut utiliser Title
.
import { Title } from "@angular/platform-browser"@Component({ ... }) export class LoginComponent implements OnInit { constructor(private title: Title) {} ngOnInit() { title.setTitle("Login") } }
Les styles peuvent être appliqués à un composant.
@Component({ selector: 'event-details', templateUrl: 'app/event-details.component.html', styles: [` .outer { display:inline-block; } .inner { margin-top: 30px; } .values { margin-left:10px; } `] })
La portée du style est contrainte au composant, ce qu'on pourrait appeler l'encapsulation de la vue (View Encapsulation).
Pour plus d'info sur la stylisation des composants, voir Component Styles sur le site d'Angular.
import { Component } from '@angular/core'; @Component({ selector: 'user', template: ` <h2>{{user.name}}</h2> <img [src]="user.imageUrl" /> <button (click)="doSomething()">Do something</button> `, }) export class ProfileComponent { user = { name: 'John Doe', imageUrl: 'doe.com/profile.jpg' } doSomething() {} }
Question de vocabulaire:
{{expression}}
est appelé Interpolation.[attribut]="expression"
est appelé Property Binding.(event)="statement"
est appelé Event Binding.<event-thumbnail *ngFor="let event of events" [event]="event"></event-thumbnail>
import { Component } from '@angular/core'; @Component({ selector: 'events-list', template: ` <event-thumbnail *ngFor="let event of events" [event]="event"></event-thumbnail> `, }) export class EventsListComponent { events = [ // Ici des objets 'event'. ]; }
Si par exemple un objet event
est undefined
et qu'on accède ses propriétés, cela peut créer des erreurs (dans la console par exemple). Pour éviter cela, il faut utiliser le concept de Safe Navigation.
{{event?.name}}
On peut cacher des sections de la page avec ngIf
:
<div *ngIf="event?.location"> <span>Location: {{event?.location}}</span> </div>
Si l'expression de ngIf
est faux, les éléments impliqués ne seront pas dans le DOM. S'il faut afficher et cacher fréquemment ces éléments du DOM, il est préférable d'utiliser [hidden]
.
<div [hidden]="!event?.location"> <span>Location: {{event?.location}}</span> </div>
Exemple d'utilisation de ngSwitch
:
<h2>{{event.name}}</h2> <span [ngSwitch]="event.format"> <span *ngSwitchCase="'InPerson'" class="label label-warning">In-Person</span> <span *ngSwitchCase="'Online'" class="label label-warning">Online</span> <span *ngSwitchDefault class="label label-warning">TBD</span> </span>
On peut utiliser le Class Binding, si on a seulement un style à appliquer:
<div [class.selector]="expression"></div>
Exemple:
<div [class.green]="status === 'ok'"></div>
On peut aussi utiliser la syntaxe :
<div [ngClass]="{green: status === 'ok', bold: time === '10'}"></div>
Dans ce cas, le style green
et le style bold
seront appliqués si leur expression respective est vraie.
On peut aussi spécifier une méthode qui retournera soit un objet, soit une chaîne contenant les classes ou un tableau de classes.
<div [ngClass]="getElementClass()"></div>
export class EventThumbnailComponent { getElementClass(): any { const isEarlyStart = this.event && this.event.time === '8:00 am'; return { green: isEarlyStart, bold: isEarlyStart }; } // *Ou* getElementClass(): any { if (this.event && this.event.time === '8:00 am') { return 'green bold'; } return ''; } // *Ou* getElementClass(): any { if (this.event && this.event.time === '8:00 am') { return ['green', 'bold']; } return []; } }
L'attribut class
n'est pas incompatible avec ngClass
.
<div class="well" [ngClass]="{green: status === 'ok', bold: time === '10'}"></div>
Les mêmes principes s'appliquent avec ngStyle
, sauf que sur style
, on utilise les propriétés CSS:
<div [style.color]="status === 'ok' ? '#003300' : '#bbb'"></div>
<div [ngStyle]="{ 'color': expression ? valueiftrue : valueiffalse, 'font-weight': expression ? valueiftrue : valueiffalse }"></div>
ngStyle
peut aussi référer à une méthode qui retournera un objet avec la valeur des propriétés CSS.
Le nom des fichiers standardisé est <nom du service>.service.ts
.
import { Injectable } from '@angular/core' @Injectable() export class EventService { getEvents() { } }
Dans le module (app.module.ts
), il faut l'ajouter:
import { EventService } from './events/shared/event.service' @NgModule({ providers: [ EventService ] })
Utilisation du service:
import { Component, OnInit } from '@angular/core' import { EventService } from './shared/event.service' export class EventsListComponent implements OnInit { events: any[]; constructor(private eventService: EventService) {} ngOnInit() { this.events = eventService.getEvents(); } }
Télécharger la librairie JavaScript tierce. Dans cet exemple, on va utiliser toastr
.
$ npm install toastr --save
Référencer dans le fichier index.html
avec <script src=“node_modules/toastr/build/toaster.min.js”>
import { Injectable } from '@angular/core' declare let toastr: any; @Injectable() export class ToastrService { success(message: string, title?: string) { toastr.success(message, title); } info(message: string, title?: string) { toastr.info(message, title); } warning(message: string, title?: string) { toastr.warning(message, title); } error(message: string, title?: string) { toastr.error(message, title); } }
Ajouter le service dans le module.
Ensuite, on peut se servir du service :
import { ToastrService } from '../common/toastr.service' export class EventsListComponent { constructor(private toastr: ToastrService) {} someFunction() { toastr.success(message, title); } }
Routes dans routes.ts
:
import { Routes } from '@angular/router'; import { EventsListComponent} from './events/events-list.component'; import { EventDetailsComponent} from './event-details/events-details.component'; export const appRoutes: Routes = [ { path: 'events', component: EventsListComponent }, { path: 'events/:id', component: EventDetailsComponent }, { path: '', redirectTo '/events', pathMatch: 'full' } ];
Dans le composant app
, ajouter <router-outlet></router-outlet>
:
import { Component } from '@angular/core'; @Component({ selector: 'my-app', template: ` <router-outlet></router-outlet> ` }) export class AppComponent { }
Dans le fichier module (app.module.ts
), il faut inclure les routes avec RouterModule.forRoot(appRoutes)
:
import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { RouterModule } from '@angular/router'; import { AppComponent } from './app.component'; import { EventsListComponent } from './events-list.component'; import { EventService } from './event.service'; import { appRoutes } from './routes'; @NgModule({ imports: [ BrowserModule, RouterModule.forRoot(appRoutes) ], declarations: [ AppComponent, EventsListComponent ], providers: [ EventService ], bootstrap: [ AppComponent ] }) export class AppModule {}
Dans le fichier index.html
, il faut ajouter la mention <base>
dans la section <head>
:
<head> <base href="/"> <!-- ... autre code abbrégé --> </head>
Si l'URL de base de l'application est host:port/my-app
, le base sera donc <base href="/my-app">
.
Il y a une route dans l'exemple précédent qui a :id
. Ce paramètre peut être obtenu pour récupérer l'objet event
avec le id
correspondant.
Dans le composant, ajouter :
import { ActivatedRoute } from '@angular/router'
Ensuite, l'injecter dans le constructeur:
constructor(private eventService: EventService, private route: ActivatedRoute) {}
Utiliser le paramètre de la route:
ngOnInit() { this.event = this.eventService.getEvent(+this.route.snapshot.params['id']); }
On peut mettre [routerLink]
sur différents éléments :
<div [routerLink]="['/events', event.id]"> <!-- données de l'événement --> </div>
Ou
<a [routerLink]="['/events']">All Events</a>
Pour passer des paramètres:
<a [routerLink]="['/action']" [queryParams]="{someparam: '1'}">Action</a>
Sinon, avec le router
service:
this.router.navigate(['/product-list'], { queryParams: { page: pageNum } })
Dans le composant, il suffit d'injecter Router
dans le constructeur.
import { Router } from '@angular/router' //... constructor(private router: Router) {} redirectUser() { this.router.navigate(['/events']); }
Pour qu'un utilisateur puisse aller sur une page (route), on peut valider qu'elle est correcte avant.
Pour cela, il faut un Activator
:
import { ActivatedRouteSnapshot, CanActivate, Router } from "@angular/router" import { Injectable } from "@angular/core" import { EventService } from './event.service' @Injectable() export class EventDetailsActivator implements CanActivate { constructor(private eventService:EventService, private router:Router) {} canActivate(route:ActivatedRouteSnapshot) { const eventExists = !!this.eventService.getEvent(+route.params['eventId']); if (!eventExists) { this.router.navigate(['/404']); } return eventExists; } }
Ajouter cet EventDetailsActivator
dans la liste des Providers du module.
Dans les routes, après avoir importé le EventDetailsActivator
, on peut alors faire:
{ path: '/events/:id', component: EventDetailsComponent, canActivate: [EventDetailsActivator] }
Composant pour l'erreur 404, qui est le fichier app/errors/404.component.ts
:
import { Component } from '@angular/core' @Component({ template: ` <h1 class="errorMessage">404'd</h1> `, styles: [` .errorMessage { margin-top:150px; font-size: 170px; text-align: center; } `] }) export class Error404Component{ constructor() {} }
Dans le router, on ajoute la route:
{ path: '404', component: Error404Component },
Le deactivate
permet de ne pas laisser l'utilisateur sortir d'un état (ou d'une page, ou d'une route) sans que certaines conditions soient remplies. Par exemple, si des données ne sont pas sauvegardées, on doit en avertir l'utilisateur.
Dans la route, on utilise canDeactivate
:
{ path: '/events/new', component: CreateEventComponent, canDeactivate: ['canDeactivateCreateEvent'] },
On a spécifié le provider dans une châine, contrairement à ce qu'on avait fait pour le canActivate
.
Les providers dans la liste de providers
du module, sont simplement écrits avec leur nom. Ceci est un raccourcis. Donc, quand on voit :
@NgModule({ providers: [ EventService ] })
cela est l'équivalent de :
@NgModule({ providers: [ { provide: EventService, useValue: EventService } ] })
On peut donc écrire:
@NgModule({ providers: [ { provide: 'canDeactivateCreateEvent', useValue: checkDirtyState } ] }) // Dans un provider quelque part... function checkDirtyState() { return false; }
Dans le service, event.service.ts
:
import { Subject } from 'rxjs/Subject'; @Injectable() export class EventService { getEvents() { let subject = new Subject(); setTimeout(() => { subject.next(EVENTS); subject.complete(); }, 100); return subject; } } const EVENTS = [{}];
Dans le component :
/* Entête est omise pour concision. */ export class EventsListComponent implements OnInit { events: any; constructor(private eventServiceL EventService) {} rgOnInit() this.eventService.getEvents().subscribe(events => { this.events = events; }); } }
À ce moment, les données sont reçues par Observable.
Pour obtenir ces données avant que le component soit affiché, on doit utiliser un resolver qui sera utilisé pour resolver la route:
event-list-resolver.service.ts
:
import { Injectable } from '@angular/core'; import { Resolve } from '@angular/router'; import { EventService } from './shared/event.service'; @Injectable() export class EventListResolver implements Resolve<any> { constructor(private eventService: EventService) {} resolve() { return this.eventService.getEvents().map(events => events); } }
Dans routes.ts
:
{ path: 'events', component: EventListComponent, resolve: { events: EventListResolver } }
Dans le component, pour rgOnInit()
, on doit alors le changer pour récupérer les données résolvées par la route:
/* Entête est omise pour concision. */ import { ActivatedRoute } from '@angular/router'; export class EventsListComponent implements OnInit { events: any; constructor(private eventServiceL EventService, route: ActivatedRoute) {} rgOnInit() this.events = this.route.snapshot.data['events']; } }
On peut utilser routerLinkActive
pour ajouter une classe CSS (active
) au lien:
<a [routerLink]="['/events']" routerLinkActive="active">All Events</a>
Pour avoir les liens actifs pour exactement le lien de routerLink
, on peut utiliser routerLinkActiveOptions
:
<a [routerLink]="['/events']" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">All Events</a>
Si par exemple on veut développer une fonctionnalité enfant à notre application, disons app/user
pour ce qui est relatif aux utilisateurs, on peut développer un feature module.
Dans app
, créer le sous-répertoire user
.
Par la suite, créer le component user.component.ts
:
import { Component } from '@angular/core' @Component({ template: ` <h1>Edit Your Profile</h1> <h3>[Edit profile form will go here]</h3> `, }) export class ProfileComponent {}
Créer un fichier module user.module.ts
. La différence avec un module d'application, c'est que nous importons CommonModule
au lieu de BrowserModule
et on utilise RouterModule.forChild(childRoutes)
au lieu de RouterModule.forRoot(appRoutes)
.
import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterModule } from '@angular/router'; import { userRoutes } from './user.routes'; import { ProfileComponent } from './profile.component'; @NgModule({ imports: [ CommonModule, RouterModule.forChild(userRoutes) ], declarations: [ ProfileComponent ], providers: [ ] }) export class UserModule {}
Pour le fichier user.routes.ts
, nous avons:
import { ProfileComponent } from './profile.component'; export const userRoutes = [ { path: 'profile', component: ProfileComponent } ];
Dans le fichier routes.ts
, on peut ajouter une route pour notre module:
{ path: 'user', loadChildren: 'app/user/user.module#UserModule' }
Par la suite, on peut utiliser un lien tel que:
<a [routerLink]="['user/profile']">Profile</a>
Créer un fichier index.ts
qui exportera tout ce qui est dans une section, par exemple app/events
:
export * from './create-event.component'; export * from './event-thumbnail.component'; export * from './event-list-resolver.service'; export * from './events-list.component';
Dans le app.module.ts
, on peut donc faire:
import { EventsListComponent, EventThumbnailComponent, EventListResolver, EventsListComponent } from './events/index';
Il y a deux types de formulaires dans Angular:
Les formulaires template sont plus simples à utiliser. Par contre, ils sont moins testables.
Avant de commencer, on doit importer FormsModule
(@angular/forms
) dans le module de notre application.
<form #loginForm="ngForm" (ngSubmit)="login(loginForm.value)" autocomplete="off"> <div class="form-group" > <label for="userName">User Name:</label> <input (ngModel)="username" id="userName" type="text" class="form-control" placeholder="User Name..." /> </div> <div class="form-group" > <label for="password">Password:</label> <input (ngModel)="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>
()
: Vue vers le component[]
: Component vers la vue[()]
: Two-way binding, vue vers le component et component vers la vue (syntaxe banana in a box) Dans le Component, on peut avoir:
login(formValues) { this.authService.loginUser(formValues.userName, formValues.password); }
Certaines validations peuvent être nécessaires, tel qu'un champ requis, ou la longueur maximum.
Pour ne pas que la validation par HTML5 intervienne, il faut ajouter l'attribut novalidate
au niveau de la form
.
<form #loginForm="ngForm" (ngSubmit)="login(loginForm.value)" autocomplete="off" novalidate>
On peut utiliser les données de validation sur le formulaire:
{{loginForm.valid}}<br/> {{loginForm.invalid}}<br/> {{loginForm.dirty}}<br/> {{loginForm.pristine}}<br/> {{loginForm.touched}}<br/> {{loginForm.untouched}}<br/>
et sur un contrôle en particulier:
{{loginForm.controls.userName?.valid}}<br/> {{loginForm.controls.userName?.invalid}}<br/> {{loginForm.controls.userName?.dirty}}<br/> {{loginForm.controls.userName?.pristine}}<br/> {{loginForm.controls.userName?.touched}}<br/> {{loginForm.controls.userName?.untouched}}<br/>
On peut désactiver un bouton qui soumet le formulaire si ce dernier n'est pas valide:
<button type="submit" class="btn btn-primary" [disabled]="loginForm.invalid">Login</button>
Aussi, on peut indiquer à l'utilisateur pourquoi le formulaire est invalide:
<em *ngIf="loginForm.controls.userName?.invalid && loginForm.controls.userName?.touched">Required</em>
Il faut utiliser le safe navigator (?
) parce que les champs ne sont pas toujours présents dans le formulaire.
Pour avoir les valeurs dans un sous-objet, par exemple:
{ field1: value1, field2: value2, field3: { subField1: subValue1, subField2: subValue2 } }
On peut utiliser ngModelGroup
en donnant le nom du sous-groupe, qui est field3
dans notre cas.
<div ngModelGroup="field3"> <!-- Champs du sous-objet --> </div>
Reactive Forms peuvent être appelés Model-based Forms.
Avant de commencer, on doit importer ReactiveFormsModule
(@angular/forms
) dans le module de notre application.
import { FormControl, FormGroup } from '@angular/forms'; // ... export class ProfileComponent implements OnInit { public profileForm: FormGroup; ngOnInit() { let firstName = new FormControl(); let lastName = new FormControl(); this.profileForm = new FormGroup({ firstName: firstName, lastName: lastName }); } }
On peut mettre des valeurs initiales dans le FormControl
en les spécifiant en paramètre.
let firstName = new FormControl(this.authService.currentUser.firstName);
Le template HTML:
<div> <h1>Edit Your Profile </h1> <hr> <div class="col-md-4"> <form [formGroup]="profileForm" (ngSubmit)="saveProfile(profileForm.values)" 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 type="button" class="btn btn-default">Cancel</button> </form> </div> </div>
Les valeurs de formControlName
doivent correspondre aux noms de FormControl
qui sont dans le FormGroup
du Component.
Pour commencer, on doit importer Validators
(@angular/forms
) dans le component.
Ensuite, on peut donner des validateurs comme second argument dans nos contrôles:
let firstName = new FormControl(this.authService.currentUser.firstName, Validators.required); let lastName = new FormControl(this.authService.currentUser.lastName, [Validators.required, Validators.pattern('[a-zA-Z].*')]);
Ajouter dans le HTML entourant les contrôles:
<div class="form-group" [ngClass]="{'error': !validateLastName() }"> <em *ngIf="!validateLastName">Required</em> <!-- contrôles --> </div>
Dans le component, on déclare les FormControl
comme membres et on implémente validateLastName()
.
private firstName: FormControl; private lastName: FormControl; // ... validateLastName() { return this.lastName.valid || this.lastName.untouched; }
On pourrait mettre les FormControl
publiques et les utiliser directement dans la vue.
Pour d'autres validators, voir Validators de la documentation d'Angular.
On peut ajouter des validateurs par programmation à un FormControl
.
formControl.setValidators([Validators.required, Validators.maxLength(10)]); formControl.updateValueAndValidity();
Supprimer les validateurs:
formControl.clearValidators();
Une validation personnalisée est rien de moins qu'une fonction qui prend pour paramètre un contrôle et retourne une erreur s'il y en a une.
private restrictedWords(control: FormControl): {[key: string]: any} { return control.value.includes('foo') ? {'restrictedWords': 'foo' } : null; }
La clé dans le message de retour correspond au nom de la fonction.
Quand on instancie les FormControl, on peut alors ajouter notre validation:
this.description = new FormControl('', [Validators.required, Validators.maxLength(400), this.restrictedWords]);
Dans le template:
<em *ngIf="description.invalid && description.dirty && description?.errors.restrictedWords">Restricted words found: {{description.errors.restrictedWords}}</em>
On peut prendre des paramètres pour notre validateur. Essentiellement, on doit simplement retourner une fonction qui sera utilisée pour valider le contrôle.
Dans un fichier restrictedWords.validator.ts
, on peut implémenter le validateur:
import { FormControl } from '@angular/forms'; export function restrictedWords(words) { return (control: FormControl): {[key: string]: any} => { if (!words) return null; const invalidWords = words .map(w => control.value.includes(w) ? w : null) .filter(w => w != null); return invalidWords && invalidWords.length > 0 ? {'restrictedWords': invalidWords.join(', ') } : null; } }
Dans le component:
import { restrictedWords } from 'restrictedWords.validator.ts'; // ... this.description = new FormControl('', [Validators.required, Validators.maxLength(400), restrictedWords(['foo', 'bar'])]);
Le principe de Content Projection est semblable au transclusion dans AngularJS, c'est-à-dire que c'est une façon d'inclure du contenu inclus dans
<div class="row" *ngFor="let session of sessions"> <div class="col-md-10"> <collapsible-well [title]="session.name"> <h6>{{session.presenter}}</h6> <span>Duration: {{session.duration}}</span><br /> <span>Level: {{session.level}}</span> <p>{{session.abstract}}</p> </collapsible-well> </div> </div>
La magie se fait avec <ng-content>
qui prend le contenu qui sera à l'intérieur de <collapsible-well></collapsible-well>
et l'injectera à sa place.
import { Component } from '@angular/core'; @Component({ selector: 'collapsible-well', template: ` <div (click)="toggleContent()" class="well pointable"> <h4 class="well-title">{{title}}</h4> <ng-content *ngIf="visible"></ng-content> </div> ` }) export class CollapsibleWellComponent { @Input() title: string; visible: boolean = true; toggleContent() { this.visible = !this.visible; } }
On peut avoir du Multi-slot Content Projection avec select
sur le ng-content
. Dans le select
on peut passer n'importe quel sélecteur CSS.
<div class="row" *ngFor="let session of sessions"> <div class="col-md-10"> <collapsible-well> <div well-title> {{ session.name }} <i *ngIf="session.voters.length > 3" class="glyphicon glyphicon-fire" style="color: red"></i> </div> <div well-body> <h6>{{session.presenter}}</h6> <span>Duration: {{session.duration}}</span><br /> <span>Level: {{session.level}}</span> <p>{{session.abstract}}</p> </div> </collapsible-well> </div> </div>
Dans le template:
<div (click)="toggleContent()" class="well pointable"> <h4> <ng-content select="[well-title]"></ng-content> </h4> <ng-content *ngIf="visible" select="[well-body]"></ng-content> </div>
Les built-in:
{{event?.name | uppercase}} {{event?.date | date:'shortDate'}} {{event?.price | currency:'USD':true}}
duration.pipe.ts
import { Pipe, PipeTransform } from '@angular/core'; @Pipe({name: 'duration'}) export class DurationPipe implements PipeTransform { transform(value: number): string { switch(value) { case 1: return 'Half Hour'; case 2: return 'One Hour'; case 3: return 'Half Day'; case 4: return 'Full Day'; default: return value.toString(); } } }
On peut le mettre dans le fichier barrel.
Dans le module d'application, l'ajouter dans les declarations
de NgModule
.
Dans les détails d'un événement, on veut filtrer les sessions par leur niveau : beginner
, intermediate
ou advanced
. Il y a également l'option all
pour les afficher tous.
Dans la classe du component, on ajoute une propriété:
filterBy: string = 'all';
Dans le template:
<button class="btn btn-default" [class.active]="filterBy==='all'" (click)="filterBy='all'">All</button> <button class="btn btn-default" [class.active]="filterBy==='beginner'" (click)="filterBy='beginner'">All</button> <button class="btn btn-default" [class.active]="filterBy==='intermediate'" (click)="filterBy='intermediate'">All</button> <button class="btn btn-default" [class.active]="filterBy==='advanced'" (click)="filterBy='advanced'">All</button>
Sur la liste des sessions, toujours dans le template des événements:
<session-list [filterBy]="filterBy" *ngIf="!addMode" [sessions]="event?.sessions"></session-list>
Dans le component de la liste de sessions:
export class SessionListComponent implements OnChanges { @Input() sessions: ISession[]; @Input() filterBy: string; visibleSessions: ISession[] = []; ngOnChanges() { if (this.sessions) { this.filterSessions(this.filterBy); } } filterSessions(filter) { if (filter === 'all') { this.visibleSessions = this.sessions.slice(0); // On copie les sessions plutôt que d'y faire référence. } else { this.visibleSessions = this.sessions.filter(session => { return session.level.toLocaleLowerCase() === filter; }); } } }
On implémente OnChanges
. L'événement OnChanges
est lancé à chaque fois qu'une valeur d'un @Input()
est modifié.
<div class="btn-group btn-group-sm"> <button class="btn btn-default" [class.active]="sortBy==='name'" (click)="sortBy='name'">By Name</button> <button class="btn btn-default" [class.active]="sortBy==='votes'" (click)="sortBy='votes'">By Votes</button> </div>
sortBy: string = 'votes';
function sortByNameAsc(s1: ISession, s2: ISession) { if (s1.name > s2.name) { return 0; } else if (s1.name === s2.name) { return 0; } else { return -1; } } function sortByVotesAsc(s1: ISession, s2: ISession) { return s2.voters.length - s1.voters.length; }
Une meilleure façon d'enregistrer un service tel que toastr
, est d'utiliser un OpaqueToken
.
import { OpaqueToken } from '@angular/core'; export let TOASTR_TOKEN = new OpaqueToken('toastr'); export interface Toastr { success (msg: string, title?: string): void; info (msg: string, title?: string): void; warning (msg: string, title?: string): void; error (msg: string, title?: string): void; }
Dans les providers
du app.module.ts
:
import { TOASTR_TOKEN, Toastr } from './common/toastr.service'; //... declare let toastr: Toastr; @NgModule({ //... providers: [ EventService, {provide: TOASTR_TOKEN}, useValue: toastr}, //... ],
Le toastr
de useValue
est la variable globale de Toastr.
Utilisant un token pour enregistrer le toastr, nous n'avons plus de classe, seulement une interface. On ne peut donc pas l'injecter directement dans le constructeur comme c'était fait avant.
Là où c'est utilisé, comme dans profile.component.ts
, on doit:
import { Component, OnInit, Inject } from '@angular/core'; import { TOASTR_TOKEN, Toastr } from '../common/toastr.service'; //... constructor( private router: Router, private authService: AuthService, @Inject(TOASTR_TOKEN) private toastr: Toastr) { // ... } //... saveProfile(formValues) { if (this.profileForm.valid) { this.authService.updateCurrentUser(formValues.firstname, formValues.lastname); this.toastr.success('Profile saved.'); } }
Dans le provider, on utilise le raccourci :
providers: [ AuthService,
C'est l'équivalent de dire:
providers: [ {provide: AuthService, useClass: AuthService},
On pourrait dire:
providers: [ {provide: AuthService, useClass: EventService},
mais lorsqu'on injecte AuthService
, on aurait une instance de EventService
.
Il existe aussi useExisting
et useFactory
, mais ce sont pour des cas très spécifiques.
Dans la programmation réactive, tout est un flux et un flux est une séquence ordonnée d'événements. Ces événements peuvent représenter des valeurs, des erreurs ou des événements complétés.
take(n)
prend les n
premiers élémnetsmap(f)
va appliquer la fonction f
sur chaque événement et retourner le résultat.filter(f)
va filtrer les éléments selon la fonction f
.reduce(f)
va appliquer la fonction f
à chaque événement pour le réduire à une seule valeur.merge(f1, f2)
merge deux flux.subscribe(f)
va appliquer la fonction f
à chaque événement reçu.let emitter = new EventEmitter();
ng.probe($0).componentInstance
La référence $0
étant le node du DOM sélectionné dans DevTools/Elements.
AAA: Arrange, Act, Assert.
describe()
:beforeEach()
:it()
:expect()
:Matchers:
toBe()
toContain()
toBeDefined()
Ajouter le fichier karma-test-shim.js
à la racine du projet.
karma.conf.js
import { VoterService } from './voter.service'; import { ISession } from '../shared/event.model'; import { Observable } from 'rxjs/Observable'; describe('VoterService', () => { let voterService: VoterService; let mockHttp; beforeEach(() => { mockHttp = jasmine.createSpyObj('mockHttp', ['delete', 'post']); voterService = new VoterService(mockHttp); }); describe('deleteVoter', () => { it('should remove the voter from the list of voters', () => { const session = { id: 6, voters: ['joe', 'john'] }; mockHttp.delete.and.returnValue(Observable.of(false)); voterService.deleteVoter(3, <ISession>session, 'joe'); expect(session.voters.length).toBe(1); expect(session.voters[0]).toBe('john'); }); }); });