Formation Angular
Florent Berthelot
- EFREI
- 7 ans d'expériences pro en développement JavaScript
- 2 SSII (ESN), puis ma société (WeFacto)
- Postes:
- Consultant
- Consultant et Formateur
- Référent Technique
- Consultant à mon compte, entrepreneur
Craftsmanship JS
Déroulé du cours
TP fil rouge
Jeu d'arène de Pokémon
Notation
2 Notes :
- Somme des notes d'attentions sur le cours
Individuel
- Projet Angular
Équipe de 3 personnes
Ce n'est pas nécessaire de travailler à la maison
Notation
0 si le projet ne démarre pas
- Outillage
- L'utilisation de l'ensemble des notions vu en cours
- La qualité du code (TypeScript, Angular, HTML, CSS)
- La qualité des tests
- Méthodologie
- Fun (bonus)
Formation TypeScript
Angular
Pourquoi un framework JS ?
Angular
- Framework créé par Google et annoncé en 2014
- Réécriture totale du framework
- Reprend certains concepts d'AngularJS
- Première version beta annoncée en octobre 2014
- Version finale 2.0.0 officielle sortie en septembre 2016
- Programmation orientée Composant
Angular Versionning
Angular Platform
Getting started
Git
NPM
TypeScript
WebPack
Jasmine + Karma
Test E2E
TSLint
...
Angular CLI
- Bootstrap un projet
- Evite de maintenir le tooling d'un projet
- Créer automatiquement des composants d'Angular
$ npm install -g @angular/cli
Angular CLI
$ ng new Application
$ ng serve
$ ng build
Création de l'application :
Démarrage de l'application (pour développer) :
Construction de l'application (pour production) :
Démarrage des tests (unitaire) :
$ ng test
Exercice 6
Formez des groupes de 3
Initialisez le projet Angular grâce à Angular-cli
Regardez la structure de fichier puis changez le titre de la page
Test front
Exercice 7
Migrez les tests Jasmine + karma vers Jest
Lancez les tests Jasmine + Karma
Observez la différence
Templates Angular
Templates Angular
@Component({
selector: 'css-selector',
template: `<div>Hello World</div>`,
styles: [`
div { color: red; }
`]
})
export class AppComponent { }
Composant
Vue
Templates Angular
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
}
Composant
Vue
Templates Angular
@Component({
selector: 'like-button',
template: `<button>Like</button>`,
})
export class LikeButton { }
Composant
Vue
<div>
<h1>A tweet</h1>
<like-button></like-button>
</div>
<!-- attention, <like-button/> ne fonctionne pas -->
Interpolation
@Component({
selector: 'like-button',
template: `<button>{{nbLikes}}</button>`,
})
export class LikeButton {
nbLikes = 0;
}
Composant
Vue
État
Interpolation
@Component({
selector: 'like-button',
template: `<button>{{nbLikes++}}</button>`,
})
export class LikeButton {
nbLikes = 0;
}
Composant
Vue
État
Ne jamais modifier l'état dans une expression !
PROPRIÉTÉS
@Component({
selector: 'like-button',
template: `<button type="button" [disabled]="isLiked">{{nbLikes}}</button>`,
})
export class LikeButton {
nbLikes = 0;
isLiked = true;
}
Composant
Vue
État
[PROPERTY-NAME]="VALUE"
Propriétés
PROPRIÉTÉS
@Component({
selector: 'like-button',
template: `
<button
type="button"
[disabled]="isLiked"
[class.liked]="isLiked"
[style.color]="isLiked ? 'red' : 'grey'"
>
{{nbLikes}}
</button>`,
})
export class LikeButton {
nbLikes = 0;
isLiked = true;
}
Composant
Vue
État
Propriétés
PROPRIÉTÉS
@Component({
selector: 'cell-table',
template: `<td [colspan]="Math.floor(Math.random() * 10)">help</td>`,
})
export class CellTable {
}
Composant
Vue
État
Attention, DOM !== attributs HTML
Propriétés
PROPRIÉTÉS
@Component({
selector: 'cell-table',
template: `<td [attr.colspan]="Math.floor(Math.random() * 10)">help</td>`,
})
export class CellTable {
}
Composant
Vue
État
Attributs HTML accessible via [attr.atributHTML]
Propriétés
PROPRIÉTÉS
@Component({
selector: 'tweet',
template: `
<div>
<p>{{tweet.message?.text}}</p>
<like-button [nbLikes]="tweet.likes"></like-button>
</div>`,
})
export class TweetComponent {
tweet: any = {
likes: 10
};
}
Composant
Vue
État
[PROPERTY-NAME]="VALUE"
Propriétés
PROPRIÉTÉS
@Component({
selector: 'like-button',
template: `<button type="button" [disabled]="isLiked">{{nbLikes}}</button>`,
})
export class LikeButton {
@Input() nbLikes = 0;
isLiked = true;
}
Composant
Vue
État
[PROPERTY-NAME]="VALUE"
Propriétés
PROPRIÉTÉS
@Component({
selector: 'like-button',
template: `
<button
type="button"
[disabled]="isLiked"
[class.liked]="isLiked"
[style.color]="isLiked ? 'red' : 'grey'"
>
{{likes}}
</button>`,
})
export class LikeButton {
@Input('nbLikes') likes = 0;
isLiked = true;
}
Composant
Vue
État
Surcharge des noms de propriétés
Propriétés
PROPRIÉTÉS
import { Input, Component } from "@angular/core";
Composant
Vue
État
import viennent de @angular/core
Propriétés
Module Angular
Là où se passe l'inversion de contrôle
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { LikeButtonComponent } from './like-button/like-button.component';
@NgModule({
declarations: [
AppComponent,
LikeButtonComponent
],
imports: [BrowserModule],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Angular/cli
Création d'un composant avec la commande
$ ng generate component my-component-name
Création d'un composant dans un dossier avec la commande
$ ng generate component folder/my-component-name
TDD composants
Composant
Vue
État
Propriétés
Paramètre d'une fonction
Fonction
Retour d'une fonction
Live coding !
Exercice 8
- Affichez les deux pokemons qui doivent se battre
- Affichez les dégats qu'ils font durant le combat
- Une attaque maximum par seconde
Template Angular
éVÉNEMENTS
@Component({
selector: 'like-button',
template: `
<button type="button" (click)="handleClick()>
{{likes}}
</button>
`,
})
export class LikeButton {
@Input('nbLikes') likes = 0;
handleClick() {
this.likes++;
}
}
Composant
Vue
État
Propriétés
éVÉNEMENTS
@Component({
selector: 'like-button',
template: `
<button type="button" (click)="handleClick()>
{{likes}}
</button>
`,
})
export class LikeButton {
@Input('nbLikes') likes = 0;
handleClick() {
this.likes++;
}
}
Composant
Vue
État
Propriétés
éVÉNEMENTS
@Component({
selector: 'like-button',
template: `
<button type="button" (click)="handleClick()>
{{likes}}
</button>
`,
})
export class LikeButton {
@Input('nbLikes') likes = 0;
@Output() onLike = new EventEmitter<undefined>();
handleClick() {
this.onLike.emit();
}
}
Composant
Vue
État
Propriétés
éVÉNEMENTS
@Component({
selector: 'tweet',
template: `
<div>
<p>{{tweet.message?.text}}</p>
<like-button [nbLikes]="tweet.likes" (onLike)="handleLike()"></like-button>
</div>`,
})
export class TweetComponent {
tweet: any = {
likes: 10
};
handleLike():void {
this.tweet.likes++;
}
}
Composant
Vue
État
Propriétés
éVÉNEMENTS
@Component({
selector: 'like-button',
template: `
<button type="button" (click)="handleClick()>
{{likes}}
</button>
`,
})
export class LikeButton {
@Input('nbLikes') likes = 0;
@Output('like') onLike = new EventEmitter<undefined>();
handleClick() {
this.onLike.emit();
}
}
Composant
Vue
État
Propriétés
On peut renommer les événements
éVÉNEMENTS
@Component({
selector: 'like-button',
template: `
<button type="button" (click)="handleClick()>
{{likes}}
</button>
`,
})
export class LikeButton {
@Input('nbLikes') likes = 0;
@Output('like') onLike = new EventEmitter<number>();
handleClick() {
this.onLike.emit(this.likes);
}
}
Composant
Vue
État
Propriétés
On peut passer des informations
éVÉNEMENTS
@Component({
selector: 'tweet',
template: `
<div>
<p>{{tweet.message?.text}}</p>
<like-button [nbLikes]="tweet.likes" (onLike)="handleLike($event)"></like-button>
</div>`,
})
export class TweetComponent {
tweet: any = {
likes: 10
};
handleLike(initialLike: number):void {
this.tweet.likes = initialLike + 1;
}
}
Composant
Vue
État
Propriétés
Double Way Data Binding
<input
[value]="currentHero.firstName"
(input)="currentHero.firstName = $event.target.value"
/>
Composant
Vue
État
Propriétés
éVÉNEMENTS
Composant
Vue
État
Propriétés
Le test des événements sert à observer le comportement du composant lors de modification de l'état du composant
Exercice 9
- Ajouter un bouton play/pause pour le combat
- Par default, le combat est arrêté
Une dernière chose sur les Composants
cycle de vie des composants
import { Component, OnInit } from '@angular/core';
@Component({ selector: 'user', /* ... */ })
export class UserComponent implements OnInit {
@Input() data: User;
products: Product[];
ngOnInit(): void {
this.products = this.getProducts(this.data.id);
}
getProducts(id){ ... }
}
cycle de vie des composants
Transclusion
Ou
Content projection
@Component({
selector: "app-post",
template: `
<article>
<ng-content></ng-content>
</article>
`
})
export class PostComponent {}
<!-- HTML Du Composant Parent -->
<app-post>
<h2>Title</h2>
<p>Content</p>
</app-post>
Directive Angular
Directive Angular
Directive = Composant sans template
Ajoute un comportement
Directive Angular
import { Directive, ElementRef, Renderer2 } from '@angular/core';
@Directive({
selector: '[myHighlight]'
})
export class HighlightDirective {
constructor(element: ElementRef, renderer: Renderer2) {
//element.nativeElement.style.backgroundColor = 'yellow';
renderer.setStyle(element.nativeElement, 'backgroundColor', 'yellow');
}
}
<p myHighlight>
Highlight me!
</p>
Directive Angular
import { Directive, HostListener, HostBinding } from '@angular/core';
@Directive({ selector: '[myHighlight]' })
export class HighlightDirective {
@HostBinding('style.backgroundColor') color = 'red';
constructor() { ... }
@HostListener('mouseenter') onMouseEnter() { this.color = 'blue'; }
@HostListener('mouseleave') onMouseLeave() { this.color = 'red'; }
}
<p myHighlight>
Highlight me!
</p>
Directive Angular
import { Directive } from '@angular/core';
@Directive({
selector: '[myHighlight]',
host: {
'[style.backgroundColor]': 'color',
'(mouseenter)': 'highlight()',
'(mouseleave)': 'restoreColor()',
}
})
export class HighlightDirective {
color = ''
highlight() { this.color = 'yellow'; }
restoreColor() { this.color = ''; }
}
<p myHighlight>
Highlight me!
</p>
Directive Angular
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HighlightDirective } from './highlight.directive';
@NgModule({
declarations: [
HighlightDirective
],
imports: [
BrowserModule
]
})
export class AppModule {}
Même chose que pour les composant
Il faut déclarer la directive dans le module
Common Module
Ensemble de directives utilitaire
NG Style
import { Component } from '@angular/core';
@Component({
selector: 'ngStyle-example',
template: `
<h1 [ngStyle]="{'font-size': size}">
Title
</h1>
<label>Size:
<input type="text" [value]="size" (input)="size = $event.target.value">
</label>
`
})
export class NgStyleExample {
size = '20px';
}
NG Class
import { Component } from '@angular/core';
@Component({
selector: 'toggle-button',
template: `
<div [ngClass]="{'highlight': isHighlighted}"></div>
<button (click)="toggle(!isHighlighted)">Click me!</button>
`,
styles: [
`.highlight { ... }`
]
})
class ToggleButton {
isHighlighted = false;
toggle(newState) {
this.isHighlighted = newState;
}
}
D'autres syntaxes :
[ngClass]="'class class1'"
[ngClass]="['class', 'class1']"
[ngClass]="{'class': isClass, 'class1': isClass1}"
NG For
<ng-template ngFor [ngForOf]="items" let-item>
<li> {{ item.label }} </li>
</ng-template>
<ng-template ngFor [ngForOf]="items" let-item let-i="index" let-isOdd="odd" let-first="first">
<li [ngClass]="{odd: isOdd, first}"> {{i}} : {{ item.label }}</li>
</ng-template>
D'autres variables sont disponible : index, first, last, even et odd
NG For Short
<li *ngFor="let item of items; let i = index">
{{i}} : {{ item.label }}
</li>
Impossible de mettre la classe comme avant avec cette syntaxe
<li *ngFor="let item of items; let i = index; let first = first;" [ngClass]="{first}" >
{{i}} : {{ item.label }}
</li>
NG IF
<div *ngIf="condition">...</div>
<ng-template [ngIf]="condition">
<div>...</div>
</ng-template>
NG ELSE
<div *ngIf="condition; else elseBlock">...</div>
<ng-template #elseBlock>No data</ng-template>
Hidden
<div [hidden]="condition">...</div>
NG Switch
<div [ngSwitch]="value">
<p *ngSwitchCase="0">0, increment</p>
<p *ngSwitchCase="1">1, stop incrementing</p>
<p *ngSwitchDefault>> 1, STOP!</p>
</div>
Exercice 10
- Affichez l'ensemble des log de la bataille grâce à un ng-for
- Affichez un message en rouge lorsqu'un pokemon est vaincu
- Faire en sorte que le Pokemon vaincu soit mort (graphiquement)
- Créer une directive pour afficher les logs de la couleur du Pokemon qui attaque (BONUS)
Les services
C
C
C
C
C
C
C
State
State
State
State
State
State
State
Les services
MVC à la rescousse
Les services
C
C
C
C
C
C
C
State
Service
Dans angular, les services sont des singletons
Les services
import { UserService } from './user.service'
@Component({
/* ... */
})
export class AppComponent {
constructor(private userService: UserService) {
console.log(userService.getUser());
}
}
Les services
import { Injectable } from '@angular/core';
import { Logger } from './logger-service';
@Injectable()
export class UserService {
constructor(private logger: Logger) { }
getUsers(): Promise<User> {
this.logger.log('getUsers called!');
// ...
}
}
Les services
import { AppComponent } from './application.components';
import { UserService } from './user.service';
@NgModule({
declarations: [ AppComponent ],
providers: [ UserService ]
})
export class AppModule { }
Les services
// fichier application.component.ts
import { UserService } from './user.service'
@Component({
providers: [ UserService ]
})
export class AppComponent {
constructor(private userService: UserService) {
console.log(userService.getUser());
}
}
Les services
export function serverConfigFactory(appService: AppService){
return appService.getConfig();
}
@NgModule({
providers: [
UserService, // Le plus simple et le plus courant : une classe
{
provide: LoginService, // Pour un élément de ce type
useClass: LoginServiceImpl // Utiliser cette classe (ou implémentation)
},
{
provide: ServerConfig, // Pour un élément de ce type
useFactory: serverConfigFactory, // Utiliser une fonction factory
deps: [ AppService ] // La factory peut elle même avoir des injections
}
]
})
export class AppModule { }
Les différentes manières de déclarer un services
Les services
// Fichier app.module.ts
const apiUrl: string = 'api.heroes.com';
const env: string = 'dev';
@NgModule({
declareations: [ AppComponent ],
providers: [
{ provide: 'apiUrl', useValue: apiUrl },
{ provide: 'env', useValue: env }
]
})
export class AppModule { }
// Fichier app.component.ts
@Component({/* ... */})
class AppComponent {
constructor( @Inject('apiUrl') private api: string ) { ... }
}
Les différentes manières de déclarer un services
Les services
En réalité, les services sont des singleton au niveau de leur injecteur
Les services
Les services
@Injectable({
providedIn: 'root'
})
export class UserService {
constructor() { }
}
Angular 6, une syntaxe raccourci pour ne pas avoir à injecter manuellement un service
Les services
import {TestBed, async} from '@angular/core/testing';
import {UserService} from './user.service';
describe('UserService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
UserService,
{ provide: LoggerService, useValue: { log: jasmine.createSpy() } }
]
});
});
it('should return 1 user', async(() => {
const service = TestBed.get(UserService);
service.getUsers().then(users => {
expect(users.length).toBe(1);
});
}));
});
Ça se test !
Exercice 11
Déplacez la logique de vos composants (app.component.ts) vers vos services
Pipes
Pipes
Idem que les Pipes Unix
Sert à transformer des données
S'utilise côté template
{{ variable | pipeName | pipe2Name:pipeArg1:pipeArg2 }}
Pipes
- LowerCasePipe, UpperCasePipe, TitleCasePipe
- CurrencyPipe, DecimalPipe, PercentPipe
- DatePipe, JSONPipe, SlicePipe
- I18nPluralPipe, I18nSelectPipe
- AsyncPipe
Pipes
{{ new Date() | date }}
// Friday, april 15, 1988
{{ 42 | currency:'EUR':'symbol' | upperCase }}
// 42,00 €
Pipes - Création
import { isString, isBlank } from '@angular/core/src/facade/lang';
import { PipeTransform, Pipe } from '@angular/core';
@Pipe({ name: 'randomCasePipe' })
export class RandomCasePipe implements PipeTransform {
transform(value: any, param1:string, param2:string): string {
if (isBlank(value)) {
return value;
}
if (!isString(value)) {
throw new Error('MyLowerCasePipe value should be a string');
}
return value
.split('')
.map(letter => Math.random() > 0.5 ? letter.toLowerCase() : letter.toUpperCase())
.join('');
}
}
Pipes - Création
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { randomCasePipe } from './randomcase.pipe';
@NgModule({
declarations: [
randomCasePipe
],
imports: [
BrowserModule
]
})
export class AppModule {}
Pipes
Utilisation alternative
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { randomCasePipe } from './randomcase.pipe';
@NgModule({
declarations: [
randomCasePipe
],
providers: [
randomCasePipe
],
imports: [
BrowserModule
]
})
export class AppModule {}
Pipes
Utilisation alternative
import { Component } from '@angular/core';
import { randomCasePipe } from './randomCasePipe';
@Component({
selector: 'app',
})
class App {
name: string;
constructor(randomCase: randomCasePipe) {
this.name = randomCase.transform('Hello Angular');
}
}
Quel est la différence entre :
Une fonction pure et une fonction impure ?
Une pipe pure et une pipe impure ?
Pipe Impure
@Pipe({
name: 'myImpurePipe',
pure: false
})
export class MyImpurePipe implements PipeTransform {
transform(value: any): any {
value.piped = true;
return value;
}
}
Pipe Impure
@Component({
selector: 'pipes',
template: '{{ promise | async }}'
})
class PipesAppComponent {
promise: Promise;
constructor() {
this.promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Hey, this is the result of the promise");
}, 2000);
});
}
}
exercice 12
- Affichez la date (avec les secondes) de début de combat (en utilisant les pipes)
- Utilisez le DecimalPipe pour afficher les points de dégâts
Services HTTP
La programmation fonction
Ça vous parle ?
La programmation Reactive
Ça vous parle ?
La programmation Reactive
Du coup, ajoutons aux streams les mêmes fonctions qu'il y a pour les tableaux !
La programmation Reactive
Les observables
import { Observable } from "rxjs";
new Observable(subscriber => {
subscriber.next('Rxjs'); // string
subscriber.next(2018); // number
subscriber.next('training'); // string
});
La programmation Reactive
Les observables, le raccourci
import { from } from 'rxjs';
const myArray = [1, 2, 3]
const myObservableArray = from(myArray)
const myPromise = new Promise(resolve => resolve('Hello RxJS!'))
const myObservablePromise = from(myPromise)
La programmation Reactive
Souscription
import { Observable } from "rxjs";
const myObservable = new Observable(subscriber => {
subscriber.next('Hello');
subscriber.next('Observable');
subscriber.next(2);
});
myObservable.subscribe(response => {
console.log(response)
})
// => 'Hello'
// 'Observable'
// 2
La programmation Reactive
Souscription
import { Observable } from "rxjs";
const myObservable = new Observable(subscriber => {
subscriber.next('Hello');
subscriber.next('Observable');
subscriber.next(2);
});
myObservable.subscribe({
next: next => console.log('onNext: %s', next),
error: error => console.error('onError: %s', error),
complete: () => console.log('onCompleted')
})
La programmation Reactive
unsubscribe
import { Component, OnDestroy } from "@angular/core";
import { Observable, Subscriber } from "rxjs";
@Component({ ... })
export class AppComponent implements OnDestroy {
private subscriber: Subscriber;
constructor() {
const source = new Observable(observer => {
const interval = setInterval(() => observer.next('TICK'), 1000);
return () => {
observer.complete();
clearInterval(interval);
};
});
this.subscriber = source.subscribe(value => console.log(value));
}
ngOnDestroy() { this.subscriber.unsubscribe(); }
}
La programmation Reactive
Les opérateurs - map
import { of } from "rxjs";
import { map } from "rxjs/operators";
const myObservable = of(1, 2, 3);
myObservable.pipe(map(x => x * 10)).subscribe(console.log);
La programmation Reactive
Les opérateurs - filter
import { from } from "rxjs";
import { filter } from "rxjs/operators";
const myArray = [1, 2, 3, 4, 5];
from(myArray)
.pipe(filter(element => element > 3))
.subscribe(console.log)
La programmation Reactive
Les opérateurs - error
import { interval, of } from "rxjs";
import { map, catchError } from "rxjs/operators";
const source = from([1, 2, 3, 4, 5, 6]).pipe(
map(value => {
if (value > 5) {
throw new Error("Error detected!");
}
return value;
}),
catchError(error => of(5))
);
source.subscribe({
next: value => console.log(value),
error: err => console.error(err.message),
complete: () => console.log(`We're done here!`)
});
La programmation Reactive
Un exemple complet
function getDataFromNetwork(): Observable<SomeClass> {
/* ... */
}
function getDataFromAnotherRequest(arg: SomeClass): Observable<SomeOtherClass> {
/* ... */
}
getDataFromNetwork()
.pipe(
debounce(300),
filter((rep1) => rep1 !== null),
mergeMap((rep1) => {
return getDataFromAnotherRequest(rep1);
}),
map((rep2) => rep2.data)
)
.subscribe((value) => console.log(`next => ${value}`));
La programmation Reactive
Avec Angular
- Router
- Service HTTP
- Formulaire
- ...
La programmation Reactive
Ça se test
function race(cars: string[], numberOfLap: number): Observable<string> {
return interval(1000)
.pipe(
map(lap => {
if(lap < numberOfLap) {
return 'race still running';
}
return `${cars[0]} won`;
})
)
}
it('should return winner when battle is over', (done) => {
const subscriber = race(['mario', 'luigi'], 0)
.subscribe(
(raceLog) => {
expect(racelog).toBe('mario won')
subscriber.unsubscribe();
done();
}
)
});
Exercice 13
Transformez vos setInterval ou setTimeout vers un Observable
Service HTTP
Service HTTP
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { LikeButtonComponent } from './like-button/like-button.component';
@NgModule({
declarations: [
AppComponent,
LikeButtonComponent
],
imports: [BrowserModule, HttpClientModule],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Avant de commencer
Service HTTP
Avant de commencer
Service HTTP
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Person } from './model/person';
@Injectable()
export class ContactService {
constructor(private http: HttpClient){ }
getContacts(): Observable<Person[]> {
return this.http.get<Person[]>('people.json');
}
}
Service HTTP
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Contact } from './model/contact';
@Injectable()
export class ContactService {
constructor(private http: HttpClient) { }
save(contact: Contact): Observable<Contact> {
const headers = new HttpHeaders();
headers.set('Authorization', 'xxxxxxx');
const requestOptions: RequestOptionsArgs = {
headers
};
return this.http.put(`rest/contacts/${contact.id}`, contact, requestOptions);
}
}
Service HTTP
import {Component} from '@angular/core';
import {ContactService} from './contact.service';
@Component({
selector: 'app',
template: '{{ displayedData | json }}'
})
export class AppComponent {
displayedData: Array<Contact>;
constructor(private contactService: ContactService) {
contactService.getContacts().subscribe(contacts => {
this.displayedData = contacts;
});
}
}
Service HTTP
import {
HttpInterceptor,
HttpRequest,
HttpHandler,
HttpEvent } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable()
export class HeaderInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>>{
const clone = req.clone({ setHeaders: {'Authorization': `token ${TOKEN}`} });
return next.handle(clone);
}
}
Les intercepteurs
Service HTTP
import { NgModule } from '@angular/core';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { HeaderInterceptor } from './header.interceptor';
@NgModule({
providers: [{
provide: HTTP_INTERCEPTORS,
useClass: HeaderInterceptor,
multi: true,
}],
})
export class AppModule {}
Les intercepteurs
Service HTTP
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { TestBed, async } from '@angular/core/testing';
describe('UserService', () => {
beforeEach(() => TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [UserService]
}));
it('should return 1 user', async(() => {
const userService = TestBed.get(UserService);
const http = TestBed.get(HttpTestingController);
const mockedUsers = [{ name: 'Florent' }];
userService.getUsers().subscribe((users: User[]) => {
expect(users.length).toBe(1);
});
http.expectOne('/api/users').flush(mockedUsers);
});
});
Les tests
Exercice 14
Récupérez les information sur vos pokemons depuis une API (seulement 2 pokemons)
Une forte adaptation des données est nécessaire, utilisez le pattern adapter si vous en sentez le besoin
Router
Avant propos
Qu'est ce qu'une SPA ?
Pourquoi faire une SPA ?
Quels sont les limites de cette architecture applicative ?
Avant propos
Client
Page web
Serveur
html + css + js + Data
Avant propos
Client
APP Angular
Serveur
Statique
Ngnix, GH-page, surge.sh, ...
html + css + js
API
Java, Node, ...
data (Json)
Router Angular
- Développement difficile
- Fonctionne pour AngularJS et Angular
- Orientée composant
Router Angular
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent, ContactsComponent, ContactComponent } from './pages';
const routes: Routes = [
{ path: '', component: HomeComponent }, // path: '/'
{ path: 'contacts', component: ContactsComponent },
{ path: 'contact/:id', component: ContactComponent }
];
@NgModule({
imports: [
RouterModule.forRoot(routes)
]
})
export class AppModule { }
Où s'affichera le composant HomeComponent ?
Router Angular
import { Component } from '@angular/core';
@Component({
template: `
<header><h1>Title</h1></header>
<router-outlet></router-outlet>
`
})
export class AppComponent { }
Dans le composant Router Outlet !
Router Angular
@Component({
template: `
<nav>
<ul>
<li><a routerLink="contacts">Link 1</a></li>
<li><a [routerLink]="['contact', 1]">Link 2</a></li>
<li><a [routerLink]="['contact', id]">Link 3</a></li>
</ul>
</nav>
<router-outlet></router-outlet>
`
})
export class AppComponent {
id = 2;
}
Les liens ne doivent plus être des href !
Router Angular
import { RouterModule, Routes } from '@angular/router';
import { ContactComponent, EditComponent, ViewComponent } from './pages';
const routes: Routes = [
{
path: 'contact/:id', component: ContactComponent, children: [
{path: 'edit', component: EditCmp},
{path: 'view', component: ViewCmp}
]
}
];
const routing = RouterModule.forRoot(routes);
Hiérarchie des routes
Router Angular
@Component({
template: `
<nav>
<ul>
<button type="button" (click)="handleClick()">Link</button>
</ul>
</nav>
<router-outlet></router-outlet>
`
})
export class AppComponent {
constructor(public router: Router) {}
handleClick() {
this.router.navigate(['/contact/edit/1']);
}
}
Navigation côté TypeScript
Quel est la mauvaise pratique dans cette exemple ?
Router Angular
import { HashLocationStrategy, LocationStrategy } from '@angular/common';
@NgModule({
providers: [{ provide: LocationStrategy, useClass: HashLocationStrategy }]
})
export class AppModule { }
HashLocationStrategy
// Inclure <base href="/"> dans la balise head de votre html
// Ou alors :
import { Component } from '@angular/core';
import { APP_BASE_HREF } from '@angular/common';
@NgModule({
providers: [{ provide: APP_BASE_HREF, useValue: '/' }],
})
export class AppModule { }
PathLocationStrategy
Router Angular
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router';
@Component({
template: "userid : {{id}}"
})
export class ProductComponent implements OnInit {
id: string = "Musique d'ascenseur";
constructor(private route: ActivatedRoute) { }
ngOnInit() {
this.route.params
.subscribe((params: Params): void => {
this.id = Number(params.id);
});
}
}
Récupérer les paramètres d'URL
Router Angular
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router';
@Component({
template: "userid : {{id}}"
})
export class ProductComponent implements OnInit {
id: string = "Musique d'ascenseur";
constructor(private route: ActivatedRoute) { }
ngOnInit() {
const snapshot: ActivatedRouteSnapshot = this.route.snapshot;
this.id = Number(snapshot.params.id);
}
}
Récupérer les paramètres d'URL
Router Angular
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, UrlTree} from '@angular/router';
import { AuthService } from './auth.service';
import { AdminComponent } from './admin.component';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private authService: AuthService, private router: Router) { }
canActivate(route: ActivatedRouteSnapshot): boolean | UrlTree {
if(this.authService.isLoggedIn()) return true;
return this.router.parseUrl( '/login' );
}
}
Guards
import { CanActivate, Router, Routes } from '@angular/router';
const routes: Routes = [
{ path: 'admin', component: AdminComponent, canActivate: [ AuthGuard ] }
];
Exercice 15
- Ajoutez un écran qui permet de sélectionner deux Pokemons
- Les deux Pokemons sélectionné sont envoyé en paramètre d'url à l'écran de combat
Point vérité
Les templates en .html sont en réalité du JS
Point vérité
Just In Time -> Compilation directement chez le client (dev)
Ahead Of Time -> pré compilation au build (prod)
Formulaires
La force d'angular
Formulaires
2 manières différentes :
- Template Driven Form
- Reactive Form
Template Driven Form
<input
[value]="currentHero.firstName"
(input)="currentHero.firstName = $event.target.value"
>
<input
[ngModel]="currentHero.firstName"
(ngModelChange)="currentHero.firstName=$event"
>
<input
[(ngModel)]="currentHero.firstName"
>
Banana In the Box
Template Driven Form
@Component({
selector: 'contact-form',
template: `
<form (submit)="saveForm()">
<label>
Name:
<input type="text" [(ngModel)]="contact.name" name="name">
</label>
<button type="submit">Save</button>
</form>
`
})
export class ContactFormComponent implements OnInit {
contact: Contact = new Contact;
constructor(private contactService: ContactService) { }
ngOnInit(): void {
this.contactService.load().subscribe(contact => this.contact = contact);
}
saveForm(): void {
this.contactService.save(this.contact).subscribe();
}
}
Template Driven Form
<form (submit)="saveForm()" novalidate>
<label>
Name:
<input type="text" [(ngModel)]="contact.name" name="name">
</label>
<button type="submit">Save</button>
</form>
Désactive la validation du navigateur
Template Driven Form
<form (submit)="saveForm()" novalidate>
<label>
Name:
<input
type="text"
[(ngModel)]="contact.name"
name="name"
#nameInput="ngModel"
>
</label>
<span [hidden]="nameInput.valid">Error</span>
<button type="submit">Save</button>
</form>
Validation des composants
Template Driven Form
<form (submit)="saveForm()" novalidate>
<label>
Name:
<input
type="text"
[(ngModel)]="contact.name"
name="name"
required
#nameInput="ngModel"
>
</label>
<span [hidden]="nameInput.pristine && nameInput.valid">Error</span>
<button type="submit">Save</button>
</form>
L'état d'un champ
- pristine / dirty
- untouched / touched
- valid / invalid
Template Driven Form
<form (submit)="saveForm()" novalidate>
<label>
Name:
<input
type="text"
[(ngModel)]="contact.name"
name="name"
required
#nameInput="ngModel"
>
</label>
<span [hidden]="!nameInput.errors?.required">Name is required</span>
<button type="submit">Save</button>
</form>
Un message différent par erreur
- required
- min / max
- min-length /max-length
- pattern
Template Driven Form
<form (submit)="saveForm()" #contactForm="ngForm" novalidate>
<label>
Name:
<input
type="text"
[(ngModel)]="contact.name"
name="name"
required
#nameInput="ngModel"
>
</label>
<span [hidden]="!nameInput.errors?.required">Name is required</span>
<button type="submit" [disabled]="contactForm.invalid" >Save</button>
</form>
Validation du formulaire
Template Driven Form
@Directive({
selector: '[pattern][ngModel]',
providers: [
{ provide: NG_VALIDATORS, useExisting: PatternValidator, multi: true }
]
})
export class PatternValidator implements Validator {
@Input('pattern') pattern: string;
validate(control: AbstractControl): { [key: string]: any } {
if (control.value && control.value.match(new RegExp(this.pattern))) {
return null;
}
return { pattern: true };
}
}
Custom Validator
Reactive Form
Data driven form
Reactive Form
import {NgModule} from "@angular/core";
import {ReactiveFormsModule} from "@angular/forms";
@NgModule({
/* ... */
imports: [ReactiveFormsModule],
})
export class AppModule {}
Reactive Form
@Component({ /* ... */ })
export class SignupReactiveComponent implements OnInit {
signupForm: FormGroup;
user: User;
ngOnInit() {
this.signupForm = new FormGroup({
username: new FormControl('', Validators.required)
});
}
save() {
if (this.signupForm.valid) {
/* save user */
}
}
}
Reactive Form
<form novalidate [formGroup]="signupForm" (submit)="save()">
<div
[ngClass]="{
'has-error': (
signupForm.get('username').touched ||
signupForm.get('username').dirty)
&& signupForm.get('username').invalid
}"
>
<label>Username *</label>
<div>
<input type="text" name="username" formControlName="username"/>
<span *ngIf="(
signupForm.get('username').touched ||
signupForm.get('username').dirty) &&
signupForm.get('username').errors"
>
<span *ngIf="signupForm.get('username').errors?.required">Username is required</span>
</span>
</div>
</div>
<div>
<button [disabled]="signupForm.invalid">Submit</button>
</div>
</form>
Dynamic Form
<form (ngSubmit)="onSubmit()" [formGroup]="form">
<div *ngFor="let question of questions">
<label [attr.for]="question.key">{{question.label}}</label>
<div [ngSwitch]="question.controlType">
<input
*ngSwitchCase="'textbox'"
[formControlName]="question.key"
[id]="question.key"
[type]="question.type"
>
<!-- other types -->
</div>
<div *ngIf="!isValid">{{question.label}} is required</div>
<div>
<button type="submit" [disabled]="!form.valid">Save</button>
</div>
</form>
Exercice 16
Ajouter un formulaire pour créer et faire combattre votre Pokemon
Pour aller plus loin
MVC Vs Redux, NGRX Store
SERVER SIDE RENDERING
Framework JS et le syndrome de la page blanche
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>AngularBdd</title>
<base href="/">
<meta
name="viewport"
content="width=device-width, initial-scale=1"
>
<link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
<app-root></app-root>
</body>
</html>
SERVER SIDE RENDERING
Client
APP Angular
Serveur
html + css + js
API
Java, Node, ...
data (JSON)
+ initial data
data (JSON)
Statique
SERVER SIDE RENDERING
Client
APP Angular
Serveur
Statique
Ngnix, GH-page, surge.sh, ...
html + css + js + initial data
API
Java, Node, ...
data (Json)
Architecture de fichiers
Pensez fonctionnel et pas "uniquement technique"
Ne pas faire des dossiers par composants technique, mais par composants fonctionnels
Design System
Travaillez avec vos collègues des différents métier !
- Material
- Bootstrap
- Bulma
- ...
Choisissez le bon !
Week End
@berthel350
FBerthelot @Github
florent@berthelot.io
https://berthelot.io
Angular
By Florent Berthelot
Angular
- 2,504