Concepts avancés
D'aprés la documentation :
"Redux is a predictable state container for JavaScript apps"
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.
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
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,
});
Implémentation de redux utilisant rxjs et prévu pour angular2
npm i @ngrx/store @ngrx/effect -S
4 bibliothèques :
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 {}
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 });
}
}
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'
}
];
@NgModule({
imports: [
CommonModule,
FeatureOneRoutingModule
],
declarations: [FeatureOneComponent]
})
export class FeatureOneModule { }
const routes: Routes = [
{
path: '',
component: FeatureOneComponent
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class FeatureOneRoutingModule { }
Sans Route Guards, l'utilisateur peut naviguer vers n'importe quel composant comme bon lui semble
Plusieurs problèmes possibles :
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
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;
}
}
Demander confirmation avant de quitter un composant
TP : Demander à l'utilisateur de confirmer la sortie du composant AdminComponent
TP : Vérifier le mot de passe de l'utilisateur avant d'afficher le composant AdminComponent
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
@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.
@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)
}
}
@Component({
selector: 'parent',
template: `
<p>I am the parent.</p>
<ng-content></ng-content>
`
})
export class ParentComponent {
@ContentChild() projectedChild
}
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
}
}
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.
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
Télécharger l'application de mise en situation d'Angular
ng test
(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();
});
});
});
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 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);
});
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')
);
});
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');
});
});
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();
});
});
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();
});
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 ...
});
Angular simplifie certains aspects de l'internationalisation :
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');
<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