

Angular
Concepts avancés
@ngrx
Angular + Redux

Redux ?
D'aprés la documentation :
"Redux is a predictable state container for JavaScript apps"
- Facile à comprendre
- Cohérent
- Uniquement les données et la manière de les modifier
- Rien à voir avec le rendu des données (vue)
3 Principes de redux
Single Source of Truth
L'état de toute votre application est stocké dans un arbre d'objets se trouvant lui même dans un store unique.
State is read-only
L'état de votre application est "immutable", la seule manière de le modifier est d’émettre une action (dispatching).
Changes are made with pure functions
L'état sera modifié par des "pure functions", ces fonctions doivent toujours retourner le même output pour un même input.
Architecture

Unidirectional Data Flow
Reducer
Redux passe les actions a travers le "reducer" et fournit le nouveaux state à la vue.
Le reducer peut être divisés en plusieurs reducers spécialisés
Un store unique
Single Source of Truth ?
- Facilite le debug
- Les données venant du serveur n'ont pas à se trouver dans plusieurs endroits
State is read-only ?
- La vue ou les requêtes http ne peuvent modifier directement le state
- Toutes les mutations du state sont centralisées et se produisent une par une dans un/des reducer/s
- Les actions décrivent explicitement ce qui se passe
- Les actions sont des "plain old object" qui peuvent être serialisés, loggués ou utilisés pour le debug
- La librairie immutable.js peut vous être utile, ou son équivalent moderne seamless-immutable.
Changes are made with pure functions ?
- une fonction "pure" retournera toujours le même output pour un input donné, ce qui évite/interdit les effets de bords indésirables
- Composable
- combineReducers({filters: filtersReducer, list: listReducer})
- Réutilisable
- reducer spécialisé pour les formulaires par exemple
Un peu de code
reducer/counter.js
export const counter = (state = 0, action) => {
switch(action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
case default:
return state;
}
}
reducer/index.js
import { combineReducers } from 'redux';
import counter from './todos';
export default combineReducers({
counter,
});
ngRx
Implémentation de redux utilisant rxjs et prévu pour angular2
npm i @ngrx/store @ngrx/effect -S
4 bibliothèques :
- Store
- Effects
- DevTools
- RouterStore
Store
import { ActionReducer, Action } from '@ngrx/store';
export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT';
export const RESET = 'RESET';
export const counterReducer: ActionReducer<number> = (state: number = 0, action: Action) => {
switch (action.type) {
case INCREMENT:
return state + 1;
case DECREMENT:
return state - 1;
case RESET:
return 0;
default:
return state;
}
}
import { NgModule } from '@angular/core'
import { StoreModule } from '@ngrx/store';
import { counterReducer } from './counter';
@NgModule({
imports: [
BrowserModule,
StoreModule.provideStore({ counter: counterReducer })
]
})
export class AppModule {}
Dans le module
import { Store } from '@ngrx/store';
import { INCREMENT, DECREMENT, RESET } from './counter';
interface AppState {
counter: number;
}
@Component({
selector: 'my-app',
template: `
<button (click)="increment()">Increment</button>
<div>Current Count: {{ counter | async }}</div>
<button (click)="decrement()">Decrement</button>
<button (click)="reset()">Reset Counter</button>
`
})
class MyAppComponent {
counter: Observable<number>;
constructor(private store: Store<AppState>){
this.counter = store.select('counter');
}
increment(){
this.store.dispatch({ type: INCREMENT });
}
decrement(){
this.store.dispatch({ type: DECREMENT });
}
reset(){
this.store.dispatch({ type: RESET });
}
}
Au niveau du component
Effets secondaires
Lazy
Loading
Lazy loading ?
- Chargement plus rapide
- Un module par fonctionnalité
- Découpage du code en "chunks"
Avant
- Le navigateur demande le code au serveur
- Il lit le code de toute l'application
- Il génère les templates et les affiche
Après
- Le navigateur demande le code au serveur
- Il lit le code du module principal
- Il génère un template et l'affiche
- Il répète les actions 2. et 3. seulement quand l'utilisateur demande à voir une fonctionnalité supplémentaire
Mise en place du lazy loading
- Créer un module de fonctionnalité
- Créer un module de routage correspondant
- Configurer les routes
1. Créer un module de fonctionnalité
ng generate module feature --routing
L'option --routing permet de créer un module de routage automatiquement pour notre module.
Elle est également utilisable lorque l'on crée une application à l'aide de
ng new app --routing
Structure d'un module "Lazy loadé"

Configurer les routes principales
const routes: Routes = [
{
path: 'featureOne',
loadChildren: 'app/feature-one/feature-one.module#FeatureOneModule'
},
{
path: 'featureTwo',
loadChildren: 'app/feature-two/feature-two.module#FeatureTwoModule'
},
{
path: '',
redirectTo: '',
pathMatch: 'full'
}
];
2. Configurer un module de routage
@NgModule({
imports: [
CommonModule,
FeatureOneRoutingModule
],
declarations: [FeatureOneComponent]
})
export class FeatureOneModule { }
3. Configurer les routes du modules
const routes: Routes = [
{
path: '',
component: FeatureOneComponent
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class FeatureOneRoutingModule { }
Lancement de l'app
Chargement d'un module "lazy loadé"
Route Guards
Sans Route Guards, l'utilisateur peut naviguer vers n'importe quel composant comme bon lui semble
Plusieurs problèmes possibles :
- l'utilisateur n'y est pas autorisé
- l'utilisateur doit être authentifié
- certaines données doivent être récupérées avant
- certaines données doivent être sauvegardées avant de quitter le composant actuel
- l'utilisateur doit confirmer la sortie du composant
Une Route Guard s'applique à la configuration d'une route
Si elle retourne true, le processus de navigation continue
Si elle retourne false, le processus de navigation est annulé
NB : il est possible d'y effectuer également une redirection
Problème
Dans la plupart des cas, les traitement effectués avant une navigation sont asynchrones.
Solution
Les Guards peuvent recevoir des données asynchrones (Promise ou Observable)
Dans ce cas, la réponse attendue par le routeur doit absolument être de type Observable<boolean> ou Promise<boolean>
Le routeur supporte de multiples interfaces de Guard
- CanActivate pour gérer la navigation vers une route
- CanActivateChild pour gérer la navigation vers une sous-route
- CanDeactivate pour gérer la navigation depuis une route (sortie d'un composant)
- CanLoad pour gérer la navigation vers un module "lazy loadé"
CanActivate
ng generate module ./modules/admin --routing
Générer un module de fonctionnalité admin
Demander l'authentification
ng generate component ./modules/admin/admin-dashboard
Et un composant à l'intérieur
Générer une Guard
ng generate guard auth
import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
@Injectable({
providedIn: 'root',
})
export class AuthGuard implements CanActivate {
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot): boolean {
console.log('AuthGuard#canActivate called');
return true;
}
}
Ajouter la propriété canActivate à la configuration de la route
const routes: Routes = [
{
path: 'admin',
component: AdminComponent,
canActivate: [AuthGuard]
}
];
Créer un service d'authentification
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { tap, delay } from 'rxjs/operators';
@Injectable({
providedIn: 'root',
})
export class AuthService {
isLoggedIn = false;
// store the URL so we can redirect after logging in
redirectUrl: string;
login(): Observable<boolean> {
return of(true).pipe(
delay(1000),
tap(val => this.isLoggedIn = true)
);
}
logout(): void {
this.isLoggedIn = false;
}
}
Mettre à jour la Guard
import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';
import { AuthService } from './auth.service';
@Injectable({
providedIn: 'root',
})
export class AuthGuard implements CanActivate {
constructor(private authService: AuthService, private router: Router) {}
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot): boolean {
let url: string = state.url;
return this.checkLogin(url);
}
checkLogin(url: string): boolean {
if (this.authService.isLoggedIn) { return true; }
// Store the attempted URL for redirecting
this.authService.redirectUrl = url;
// Navigate to the login page with extras
this.router.navigate(['/login']);
return false;
}
}
CanDeactivate
Demander confirmation avant de quitter un composant
TP : Demander à l'utilisateur de confirmer la sortie du composant AdminComponent
Guard asynchrone
TP : Vérifier le mot de passe de l'utilisateur avant d'afficher le composant AdminComponent
Manipulation du DOM
Projection de contenu
La projection de contenu est une syntaxe qu’on peut utiliser dans un template de composant Angular et qui permet de réafficher le contenu situé entre les balises d’un composant.
Composant sans projection
@Component({
selector: 'meteo',
template: '<p>Le temps est ensoleillé.</p>'
})
export class MeteoComponent {}
La projection de contenu permet à un composant de réafficher le texte contenu entre ses balises dans son propre template, grâce à la balise <ng-content></ng-content>.
@Component({
selector: 'meteo',
template: `
<p>Le temps est ensoleillé.</p>
<ng-content></ng-content>. <!-- Cette balise va permettre la projection de contenu. -->
`
})
export class MeteoComponent {}
Composant avec projection
ViewChild
@Component({
selector: 'child',
template: 'I am the child'
})
export class ChildComponent {}
// Ce composant est le parent : son template contient la balise de l'enfant
@Component({
selector: 'parent',
template: 'I am the parent: <child></child>'
})
export class ParentComponent {}
Si un composant A est affiché dans le template d’un composant B, alors A est un ViewChild de B.
Récupérér une référence dans la classe depuis le template
@Component({
selector: 'child',
template: 'I am the child'
})
export class ChildComponent {}
// Ce composant est le parent : son template contient la balise de l'enfant
@Component({
selector: 'parent',
template: 'I am the parent: <child #childComponent></child>'
})
export class ParentComponent {
@ViewChild('childComponent') childComponent
ngOnInit() {
console.log(this.childComponent)
}
}
@ContentChild : récupérer la référence d'un composant projeté
@Component({
selector: 'parent',
template: `
<p>I am the parent.</p>
<ng-content></ng-content>
`
})
export class ParentComponent {
@ContentChild() projectedChild
}
@ContentChild : récupérer la référence d'un composant dans une directive
import {AfterContentInit, ContentChild, Directive} from '@angular/core';
@Directive({selector: 'child-directive'})
class ChildDirective {
}
@Directive({selector: 'someDir'})
class SomeDir implements AfterContentInit {
// TODO(issue/24571): remove '!'.
@ContentChild(ChildDirective) contentChild !: ChildDirective;
ngAfterContentInit() {
// contentChild is set
}
}
Tests Unitaires
Pourquoi ?
On écrit un test pour confronter une réalisation à sa spécification. Le test permet de statuer sur le succès ou sur l'échec d'une vérification. Grâce à la spécification, on est en mesure de faire correspondre un état d'entrée donné à un résultat ou à une sortie. Le test permet de vérifier que la relation d'entrée / sortie donnée par la spécification est bel et bien réalisée.
Karma

Jasmine, framework de tests unitaires
Jasmine Cheatsheet

@angular/cli s'occupe de la configuration de Jasmine
Karma lance les fichiers de test jasmine
Tous les fichiers ayant l'extension .spec.ts
jasmine effectue les tests unitaires
Karma affiche les résultats
Par où commencer
Télécharger l'application de mise en situation d'Angular
Mise en place
ng test
Tester un service
(Sans les outils d'Angular)
// Straight Jasmine testing without Angular's testing support
describe('ValueService', () => {
let service: ValueService;
beforeEach(() => { service = new ValueService(); });
it('#getValue should return real value', () => {
expect(service.getValue()).toBe('real value');
});
it('#getObservableValue should return value from observable',
(done: DoneFn) => {
service.getObservableValue().subscribe(value => {
expect(value).toBe('observable value');
done();
});
});
it('#getPromiseValue should return value from a promise',
(done: DoneFn) => {
service.getPromiseValue().then(value => {
expect(value).toBe('promise value');
done();
});
});
});
Service avec dépendances
describe('MasterService without Angular testing support', () => {
let masterService: MasterService;
it('#getValue should return real value from the real service', () => {
masterService = new MasterService(new ValueService());
expect(masterService.getValue()).toBe('real value');
});
it('#getValue should return faked value from a fakeService', () => {
masterService = new MasterService(new FakeValueService());
expect(masterService.getValue()).toBe('faked service value');
});
it('#getValue should return faked value from a fake object', () => {
const fake = { getValue: () => 'fake value' };
masterService = new MasterService(fake as ValueService);
expect(masterService.getValue()).toBe('fake value');
});
it('#getValue should return stubbed value from a spy', () => {
// create `getValue` spy on an object representing the ValueService
const valueServiceSpy =
jasmine.createSpyObj('ValueService', ['getValue']);
// set the value to return when the `getValue` spy is called.
const stubValue = 'stub value';
valueServiceSpy.getValue.and.returnValue(stubValue);
masterService = new MasterService(valueServiceSpy);
expect(masterService.getValue())
.toBe(stubValue, 'service returned stub value');
expect(valueServiceSpy.getValue.calls.count())
.toBe(1, 'spy method was called once');
expect(valueServiceSpy.getValue.calls.mostRecent().returnValue)
.toBe(stubValue);
});
});
TestBed
TestBed est l'outil le plus important pour les tests avec Angular
Il crée pour nous dynamiquement un module de test qui simule un @NgModule
La méthode TestBed.configureTestingModule() reçoit un objet de métadonnées qui peut contenir presque toutes les propriétés d'un @NgModule
Dans cet objet de métadonnées, on remplit le tableau providers des services qu'on veut tester ou mocker
let service: ValueService;
beforeEach(() => {
TestBed.configureTestingModule({ providers: [ValueService] });
});
Puis on injecte ce service dans un test avec TestBed
it('should use ValueService', () => {
service = TestBed.get(ValueService);
expect(service.getValue()).toBe('real value');
});
On peut factoriser l'injection du service pour chaque test
beforeEach(() => {
TestBed.configureTestingModule({ providers: [ValueService] });
service = TestBed.get(ValueService);
});
Quand on test un service avec une dépendance, on ajoute un mock à la liste des providers
Dans cet exemple, un objet spy
beforeEach(() => {
TestBed.configureTestingModule({ providers: [ValueService] });
service = TestBed.get(ValueService);
});
it('#getValue should return stubbed value from a spy', () => {
const stubValue = 'stub value';
valueServiceSpy.getValue.and.returnValue(stubValue);
expect(masterService.getValue())
.toBe(stubValue, 'service returned stub value');
expect(valueServiceSpy.getValue.calls.count())
.toBe(1, 'spy method was called once');
expect(valueServiceSpy.getValue.calls.mostRecent().returnValue)
.toBe(stubValue);
});
Tester des services Http
let httpClientSpy: { get: jasmine.Spy };
let heroService: HeroService;
beforeEach(() => {
// TODO: spy on other methods too
httpClientSpy = jasmine.createSpyObj('HttpClient', ['get']);
heroService = new HeroService(<any> httpClientSpy);
});
it('should return expected heroes (HttpClient called once)', () => {
const expectedHeroes: Hero[] =
[{ id: 1, name: 'A' }, { id: 2, name: 'B' }];
httpClientSpy.get.and.returnValue(asyncData(expectedHeroes));
heroService.getHeroes().subscribe(
heroes => expect(heroes).toEqual(expectedHeroes, 'expected heroes'),
fail
);
expect(httpClientSpy.get.calls.count()).toBe(1, 'one call');
});
it('should return an error when the server returns a 404', () => {
const errorResponse = new HttpErrorResponse({
error: 'test 404 error',
status: 404, statusText: 'Not Found'
});
httpClientSpy.get.and.returnValue(asyncError(errorResponse));
heroService.getHeroes().subscribe(
heroes => fail('expected an error, not heroes'),
error => expect(error.message).toContain('test 404 error')
);
});
Tester un composant
describe('LightswitchComp', () => {
it('#clicked() should toggle #isOn', () => {
const comp = new LightswitchComponent();
expect(comp.isOn).toBe(false, 'off at first');
comp.clicked();
expect(comp.isOn).toBe(true, 'on after click');
comp.clicked();
expect(comp.isOn).toBe(false, 'off after second click');
});
it('#clicked() should set #message to "is on"', () => {
const comp = new LightswitchComponent();
expect(comp.message).toMatch(/is off/i, 'off at first');
comp.clicked();
expect(comp.message).toMatch(/is on/i, 'on after clicked');
});
});
Tester le DOM d'un composant
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { BannerComponent } from './banner.component';
describe('BannerComponent', () => {
let component: BannerComponent;
let fixture: ComponentFixture<BannerComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ BannerComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(BannerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeDefined();
});
});
Tester une directive d'attribut
beforeEach(() => {
fixture = TestBed.configureTestingModule({
declarations: [ AboutComponent, HighlightDirective],
schemas: [ NO_ERRORS_SCHEMA ]
})
.createComponent(AboutComponent);
fixture.detectChanges(); // initial binding
});
it('should have skyblue <h2>', () => {
const h2: HTMLElement = fixture.nativeElement.querySelector('h2');
const bgColor = h2.style.backgroundColor;
expect(bgColor).toBe('skyblue');
});
La méthode rapide
La meilleure méthode
beforeEach(() => {
fixture = TestBed.configureTestingModule({
declarations: [ HighlightDirective, TestComponent ]
})
.createComponent(TestComponent);
fixture.detectChanges(); // initial binding
// all elements with an attached HighlightDirective
des = fixture.debugElement.queryAll(By.directive(HighlightDirective));
// the h2 without the HighlightDirective
bareH2 = fixture.debugElement.query(By.css('h2:not([highlight])'));
});
// color tests
it('should have three highlighted elements', () => {
expect(des.length).toBe(3);
});
it('should color 1st <h2> background "yellow"', () => {
const bgColor = des[0].nativeElement.style.backgroundColor;
expect(bgColor).toBe('yellow');
});
it('should color 2nd <h2> background w/ default color', () => {
const dir = des[1].injector.get(HighlightDirective) as HighlightDirective;
const bgColor = des[1].nativeElement.style.backgroundColor;
expect(bgColor).toBe(dir.defaultColor);
});
it('should bind <input> background to value color', () => {
// easier to work with nativeElement
const input = des[2].nativeElement as HTMLInputElement;
expect(input.style.backgroundColor).toBe('cyan', 'initial backgroundColor');
// dispatch a DOM event so that Angular responds to the input value change.
input.value = 'green';
input.dispatchEvent(newEvent('input'));
fixture.detectChanges();
expect(input.style.backgroundColor).toBe('green', 'changed backgroundColor');
});
it('bare <h2> should not have a customProperty', () => {
expect(bareH2.properties['customProperty']).toBeUndefined();
});
Tester un pipe
- Instancier le pipe
- Tester le retour de sa méthode transform
describe('TitleCasePipe', () => {
// This pipe is a pure, stateless function so no need for BeforeEach
let pipe = new TitleCasePipe();
it('transforms "abc" to "Abc"', () => {
expect(pipe.transform('abc')).toBe('Abc');
});
it('transforms "abc def" to "Abc Def"', () => {
expect(pipe.transform('abc def')).toBe('Abc Def');
});
// ... more tests ...
});
Test debugging
- Ouvrir le navigateur de Karma
- Cliquer sur le bouton DEBUG
- Un onglet s'ouvre et Karma relance les tests
- Ouvrir les Outils Développeur du navigateur
- Ouvrir la section "Source"
- Ouvrir un fichier spec (Ctrl/Command-P)
- Appliquer un breakpoint dans les tests
- Rafraîchir le navigateur, il s'arrête sur un breakpoint

i18n
Internationalization
Angular simplifie certains aspects de l'internationalisation :
- Afficher des dates, des nombres et des devises dans un format local
- Préparer les textes dans les templates des composants à la traduction
- Prendre en charge la forme plurielle des mots
Configurer la locale
ng serve --configuration=fr
import { LOCALE_ID, NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from '../src/app/app.component';
@NgModule({
imports: [ BrowserModule ],
declarations: [ AppComponent ],
providers: [ { provide: LOCALE_ID, useValue: 'fr' } ],
bootstrap: [ AppComponent ]
})
export class AppModule { }
Importer les données d'un langage autre que l'anglais
import { registerLocaleData } from '@angular/common';
import localeFr from '@angular/common/locales/fr';
// the second parameter 'fr' is optional
registerLocaleData(localeFr, 'fr');
Marquer un élément pour la traduction
<h1 i18n>Hello i18n!</h1>
Ajouter des informations au traducteur
<h1 i18n="An introduction header for this sample">Hello i18n!</h1>
Une description
<h1 i18n="site header|An introduction header for this sample">Hello i18n!</h1>
Le sens du texte
Créer un fichier de traduction
ng xi18n
Générer un build traduit
ng build --prod --i18n-file src/locale/messages.fr.xlf --i18n-format xlf --i18n-locale fr
Servir l'application traduite
ng serve --configuration=fr
Angular - Concepts avancés
By AdapTeach
Angular - Concepts avancés
- 1,312