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>&gt; 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