Outils pour utilisateurs

Outils du site


web:javascript:angular:fundamentals

Angular Fundamentals

Prend en compte la version 4/5 d'Angular.

Démarrer un projet avec Angular CLI

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.

Utiliser SSL

Dans package.json:

"start:ssl": "ng serve --port 8000 --hmr --ssl true --ssl-cert ssl/server.crt --ssl-key ssl/server.key"

Librairie

$ ng generate library

Exécuter les tests

$ npm test

Générer des fichiers

$ cd src/app
$ ng g component todo-list  

Ressources sur Angular CLI

Template

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

Component de base :

import { Component } from '@angular/core'
 
@Component({
    selector: 'events-app',
    template: '<h2>Hello World.</h2>'
})
 
export class EventsAppComponent {
 
}

Enregistrer un Component

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 {}

Communication inter-component avec @Input et @Output

Parent à enfant

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'
    }
}

Enfant à parent

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);
    }
}

Exemple avec enregistrement de données dans un component enfant

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;
}

Accéder à des méthodes publiques d'un composant enfant

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>

Titre

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")
    }
}

Styliser un composant

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.

Syntaxe des templates

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:

  • L'utilisation de {{expression}} est appelé Interpolation.
  • L'utilisation de [attribut]="expression" est appelé Property Binding.
  • L'utilisation de (event)="statement" est appelé Event Binding.

ngFor

<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'.
    ];
 
}

Gerer les valeurs null

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}}

ngIf et [hidden]

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>

ngSwitch

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>

ngClass

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>

ngStyle

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.

Services

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();
    }
 
}

Encapsuler une librairie tierce

Il y a une meilleure façon de faire, décrite dans la section Injection de dépendances.

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);
    }
}

Routing

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">.

Accéder au paramètre de la route

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']);
    }

Utiliser les liens

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']);
    }

Activer une route

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 },

Deactivate

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;
}

Pré-chargement des données avec des Observables

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.

Resolve Route

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'];
    }
}

Liens actifs

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>

Modules fonctionnels

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>

Barrels

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';

Formulaires et validation

Il y a deux types de formulaires dans Angular:

  • Template-based Form
  • Reactive Form (Model-based Form)

Template-Based Forms

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>
Bindings
  • () : 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);
}

Validation des formulaires Template-based

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.

ngModelGroup

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

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.

Validation de Reactive Forms

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.

Ajout de validateurs

On peut ajouter des validateurs par programmation à un FormControl.

  formControl.setValidators([Validators.required, Validators.maxLength(10)]);
  formControl.updateValueAndValidity();

Supprimer les validateurs:

  formControl.clearValidators();

Custom Validators

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>

Validateur avec paramètres

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'])]);

Content Projection

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>

Utilisation des Pipes

Les built-in:

{{event?.name | uppercase}}
{{event?.date | date:'shortDate'}}

{{event?.price | currency:'USD':true}}

Custom pipe

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.

Filtrer

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é.

Trier

event-details.component.html
<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>
event-details.component.ts
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;
}

Injection de Dépendance

Une meilleure façon d'enregistrer un service tel que toastr, est d'utiliser un OpaqueToken.

toastr.service.ts
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 :

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.');
    }
  }

useClass

Dans le provider, on utilise le raccourci :

app.module.ts
  providers: [
    AuthService,

C'est l'équivalent de dire:

app.module.ts
  providers: [
    {provide: AuthService, useClass: AuthService},

On pourrait dire:

app.module.ts
  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.

Reactive Programming

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.

Méthodes sur les observables

  • take(n) prend les n premiers élémnets
  • map(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.

EventEmitter

let emitter = new EventEmitter();

Débugger un component dans la console

  ng.probe($0).componentInstance

La référence $0 étant le node du DOM sélectionné dans DevTools/Elements.

Debugging Angular Applications from the Console

Tests unitaires

AAA: Arrange, Act, Assert.

Jasmine

  • describe():
  • beforeEach():
  • it():
  • expect():

Matchers:

  • toBe()
  • toContain()
  • toBeDefined()

Karma

  • Exécute les tests dans le navigateur.
  • Supporte pluisieurs navigateurs
  • Rapport d'exécution de tests

Installation des dépendances

  • npm i -g karma-cli
  • npm i karma karma-chrome-launcher karma-jasmine jasmine-core @types/jasmine -D

Ajouter le fichier karma-test-shim.js à la racine du projet.

karma.conf.js

voter.service.spec.ts
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');
    });  
  });
});

Sources

web/javascript/angular/fundamentals.txt · Dernière modification : 2022/02/02 00:42 de 127.0.0.1