Skip to content

Latest commit

 

History

History
903 lines (610 loc) · 29.1 KB

prez-Angular.md

File metadata and controls

903 lines (610 loc) · 29.1 KB
title filename tags description lang slideOptions
Présentation de Angular JS, EPFL VPSI — Décembre 2019
prez-Angular.md
AngularJS, EPFL
View the slide with "Slide Mode".
fr
transition theme
fade
white

Workshop IDEV

Angular.js 8

logo Angular

Décembre 2019


Telegram

Tous les garçons cool l'ont !


Demandez le programme...

  • AngularJS par la pratique
  • Programme / courbe d'apprentissage
  • The occasional diatribe

On plonge !

Si vous aimez... vous aimerez...
☁️ les nuages ☁️ stackblitz.com
Votre laptop node.js + cli.angular.io
Emacs M-x package-install
tide
... vous ne savez pas Essayez logo Visual Studio Code Visual Studio Code

Exercice 1 : “Hello world”

Vous connaissez la musique... Je saisis mon prénom, l'affichage se met à jour


Exercice 1

💡

  • Commencer par la vue «mock» (avec le HTML souhaité, mais aucun comportement)
  • Pas besoin de <form>

Exercice 1

«Moustacher» le mot Angular de Hello, Angular depuis une variable d'instance du contrôleur.

🔧 Avec Chrome et Augury, c'est suffisant pour bidouiller «à la main» :


Exercice 1

Google est votre ami.

<input type="text" (input)="console.log('bah!')">

... Mais Angular ? Pas toujours.

@Component{...}
export class AppComponent {
    console = window.console
}

Exercice 1

Comment savoir quoi mettre dans (input)="..." ?

Le débogueur du navigateur est aussi votre ami.

<input type="text" (input)="inputChanged()">
@Component{...}
export class AppComponent {
    inputChanged() {
        debugger;
    }
}

Exercice 1

💡

Tous les «tuyaux» vus pour l'exercice 1 restent valables dans la vraie vie.

  • Division du travail (HTML, CSS, JS)
  • Buts intermédiaires
  • Outillage : débogueur du navigateur + Augury

Exercice 1 : solution

https://stackblitz.com/edit/epfl-angular-exercice1


Exercice 1 : décortiquage

Note:

  • app.component.ts : le Component
  • app.module.ts : I is serious framwerk. I haz modules.
  • Une brève plongée dans les templates et les événements
  • Les dépendances dans package.json, et en particulier les @types

Exercice 2

refactorer pour créer HelloComponent

<h1>Hello, {{ name }}!</h1>

Pour le JavaScript TypeScript, à vous de jouer !


Exercice 2 : solution

Si vous aviez deviné que la solution serait à l'adresse

https://stackblitz.com/edit/epfl-angular-exercice2

... alors vous avez gagné !


Fin de l'étape

  • Révisions JS (npm, ES2015, import / export)
  • TypeScript : types... et décorateurs
  • Angular : templates, événements... Et bien plus encore !

Tests avec
Karma logo

ng test Interdit aux nuages

Note:

  • À présent chacun doit installer Angular sur son laptop, si pas déjà fait

Exercice 3

Pré-requis : ng test est «vrai rouge» (pas de crash)

→ Si nécessaire, se resynchroniser depuis go/workshop-angular-exercices


Exercice 3

app.component.spec.ts

// ...
describe('AppComponent', () => {
  // ...
  it('updates the title', () => {
  })
})

app.component.spec.ts

// ...
describe('AppComponent', () => {
  // ...
  it('updates the title', () => {
    // ...
  })
})

describe, it ← le BDD de Jasmine Jasmine logo


// ...
describe('AppComponent', () => {
  // ...
  fit('updates the title', () => {
        // ...
  })
})

💡 fdescribe existe également (f comme “focused”)


// ...
describe('AppComponent', () => {
  // ...
  it('updates the title', () => {
    const fixture = TestBed.createComponent(AppComponent);
  })
})

Cargo cult


// ...
describe('AppComponent', () => {
  // ...
  it('updates the title', () => {
    const fixture = TestBed.createComponent(AppComponent);

    const input = fixture.nativeElement.
          querySelector('input');
    // ...
  })
})

... Wait wat ?



  it('updates the title', () => {
      const fixture = TestBed.createComponent(AppComponent);

      debugger;
      // ...
  })
  • fixture
  • fixture.nativeElement
  • ... est un élément DOM
  • ... Donc logo MDN MDN est votre ami


  it('updates the title', () => {
      const fixture = TestBed.createComponent(AppComponent);

      const input = fixture.nativeElement.
          querySelector('input');


      input.value = 'World';
      input.dispatchEvent(new Event('input'));

      // ...
})

Note: Le fait que nous utilisons les API MDN rend la compatibilité des tests sous IE / Edge nettement plus... hasardeuse ? À vrai dire je n'en sais rien, nous sommes quand même en 2019.


  it('updates the title', () => {
    const fixture = TestBed.createComponent(AppComponent);

    const input = fixture.nativeElement.
          querySelector('input') as HTMLInputElement;
    input.value = 'World';
    input.dispatchEvent(new Event('input'));

    // ...

    expect(fixture.nativeElement.
           querySelector('h1').textContent).toContain('World');
  })

expect, toContain ← moar Jasmine Jasmine logo



      // ...    // ???



      fixture.detectChanges();


Pour finir...

// ...
describe('AppComponent', () => {
  // ...
  it('updates the title', () => {
    const fixture = TestBed.createComponent(AppComponent);

    const input = fixture.nativeElement.
          querySelector('input') as HTMLInputElement;
    input.value = 'World';
    input.dispatchEvent(new Event('input'));

    fixture.detectChanges();

    expect(fixture.nativeElement.
           querySelector('h1').textContent).toContain('World');
  })
})

Fin de l'étape

  • logo Karma Karma
  • logo Jasmine Jasmine et le BDD
  • logo MDN L'API du DOM selon Mozilla
  • logo AngularLe mécanisme¹ de détection des changements

¹ Ou plus précisément, son absence


Quelques derniers conseils...

  • Peaufinez vos recherches sur Google ("2019", "Angular 7", "Angular 8")

Quelques derniers conseils...

  • Ne vous éloignez pas trop des sentiers battus ! Votre app risquerait (par exemple) de ne plus compiler en prod'
  • Malgré cela, attendez-vous à des changements incompatibles (ex : adoption de async / await et Puppeteer pour les tests unitaires)

Exercice 4 : chercher-en-tapant

  • Test-first¹
  • logo RxJS RxJS

Exercice 4

stackblitz.com/edit/epfl-angular-exercice4-jquery


Exercice 4

Logs d'arrivée des réponses dans le désordre

... Ça ne marche pas très bien


Exercice 4

  1. ng new --defaults --no-routing search-as-you-type
    cd search-as-you-type
    ng test
    
  2. ng generate component search-as-you-type
    ng test
    

    💡 ng create change les dépendances, il faut donc stopper/redémarrer les tests en continu (commande ng test).


Exercice 4 : révisions

Faisons une mini-app pour les tests manuels :

  1. Tout effacer dans src/app/app.component.html; remplacer par
    <app-search-as-you-type></app-search-as-you-type>
  2. ng serve @ http://localhost:4200/
  3. ng test : revenir à la barre verte (solution)

Exercice 4 : diviser pour régner

  • Une «carrosserie» graphique
    • Fournit : les mots-clefs de recherche
    • Reçoit : les résultats
  • Un service de recherche
  • La tuyauterie pour les relier : RxJS

Carrosserie graphique


Test first second

... À ce stade de notre apprentissage, commençons par un test manuel, et voyons en second lieu comment l'automatiser.

<input type="text" #searchterm
    (keyup)="search(searchterm.value)">

Suite et fin


Test second


Test second

Solution


Test pas du tout maintenant

it("shows results");

RxJS

  search(value: string) {
    console.log(value);  // TODO: peut mieux faire.
  }

RxJS et les Observables

... et les Subjects

import { Subject } from 'rxjs';
// ...
  private searchString = new Subject<string>();

// ...
  search(value: string) {
    this.searchString.next(value);
  }

Marble diagram


RxJS et les Subscriptions

export class SearchAsYouTypeComponent implements OnInit {
  // ...
  ngOnInit() {
    this.searchString.subscribe(console.log);
  }

}

RxJS et les fuites de mémoire

import { debounceTime, tap } from 'rxjs/operators';
// ...

export class SearchAsYouTypeComponent implements OnInit {
  // ... 

  private searchString = new Subject<string>();
  searchStringDebounced = searchString.pipe(
      debounceTime(400),
      tap(console.log)
  );
}

RxJS : au-delà de console.log

.pipe()

import { tap, debounceTime,
         distinctUntilChanged } from 'rxjs/operators';

export class SearchAsYouTypeComponent implements OnInit {
  searchStringDebounced = this.searchString.pipe(
    debounceTime(400),
    distinctUntilChanged(),
    tap(console.log)
  );
}

🚑 Le diff


... Plus rien.

Les Observables sont paresseux. Tant qu'on n'y .subscribe() pas, ils ne font rien.


&lt;p&gt;Current search term:&lt;/p&gt;
&lt;pre&gt;{{searchStringDebounced | async }}&lt;/pre&gt;

| async rend la dernière valeur d'un Observable, n'a pas besoin d'être importé, et est auto-unsubscribe.


Exercice 4 : l'exercice

Implémenter le search-as-you-type et présenter votre approche.

  • Ne pas s'attarder sur la présentation des résultats (console.log() ou bien <pre>{{ searchResults | async | json}}</pre>)
  • Rappel : la version jQuery contient une fonction searchEPFL() prête à copier-coller.

L'approche RxJS

Avec l'opérateur switchMap (démo sur rxmarbles.com), on peut

  • créer un nouvel Observable pour chaque valeur de l'Observable «parent»;
  • fusionner les valeurs.

💡 La fonction «switchMappée» peut renvoyer une Promise à la place d'un Observable.

Note: Ce n'est pas la solution parfaite pour l'instant.


L'approche RxJS

En savoir plus sur switchMap et son utilité pour le search-as-you-type : blog.angular-university.io/rxjs-higher-order-mapping/ (§ “Search TypeAhead”)


L'approche RxJS

Corrigé


Fin de l'étape

  • RxJS : Observables, opérateurs, Observables d'Observables
  • Functional reactive programming
  • Mieux que les Promises rien (jQuery et asynchronie incontrôlée)
  • Beaucoup de progrès à faire
    • Rendu des résultats
    • Limitation / annulation des requêtes
    • Couverture de tests

Chercher-en-tapant : on améliore

Au cours de cette partie,

  • nous explorerons l'API HTTP d'Angular, qui propose des requêtes annulables
  • nous verrons comment séparer (et tester séparément) le code «graphique» et le client de l'API

Injection de dépendances

constructor(private http : HttpClient) {}


Exercice 5

Réécrire la fonction searchEPFL(q) en termes de @angular/common/http.

blog.angular-university.io/angular-http


Exercice 5

Solution

  • Meilleur style
  • Les requêtes sont à présent annulables
Les tests sont cassés (solution)

Fin d'étape

Du point de vue du consommateur, l'injection de dépendances

  • fournit automagiquement des singletons à tout endroit de la hiérarchie des composants,
  • permet d'utiliser des instances séparées pour le test et la production (⇒ tests hermétiques)

Pour aller plus loin...

  • Couverture de tests
  • Injection de dépendances en tant que producteur (ex : I18NService)

Note:

  • Sujets potentiellement intéressants pour la journée 3

Le décor


Exercice 6 : ng-select + chercher-en-tapant

  • Partir du premier exemple de la doc de ng-select
  • Écouter l'événement search (plutôt que chercher une seule fois dans ngOnInit())

Exercice 6 : ng-select + chercher-en-tapant

  • Tuyauter ce qui doit l'être pour rechercher dans l'API de l'EPFL
    • .pipe(map()) pour transformer les .sciper en .id

Solution


Exercice 7

Même chose que l'exercice 6, mais sans le cloud.

  • Raccrochage au debounceTime etc. des exercices précédents
  • (Optionnel) Réutiliser l'idée d'un DataService injecté comme fait dans le StackBlitz
<style> .reveal { background-color: #fff; background-image: url('https://epfl-idevelop.github.io/elements/svg/epfl-logo.svg'); background-repeat: no-repeat; background-position: 5px 5px; color: #707070; } .reveal h1, .reveal h2, .reveal h3, .reveal h4, .reveal h5, .reveal h6 { color: #212121; } .reveal a { color: #f009; } .reveal a:hover { color: #f00; } .reveal code { padding-top: 0.2em; padding-bottom: 0.2em; margin: 0; font-size: 85%; background-color: rgba(255, 255, 255, 0.46); border-radius: 3px; } .reveal code mark { font-weight: bold; background-color: yellow; } img[alt$=">"] { float: right; } img[alt$="<"] { float: left; } img[alt$="><"] { display: block; max-width: 100%; height: auto; margin: auto; float: none!important; } .reveal [data-background] > div { background-color: #ffffff94; border-radius: 20px; } .reveal [data-opacity="over9k"] > div { background-color: #fffffff0; } .reveal [data-opacity="full"] > div { background-color: white; } .reveal section .logo { border: none; box-shadow: none; background: none; } .reveal section img.inline.logo { max-width: 60pt; margin: unset; } #interdit-aux-nuages { vertical-align: text-bottom; max-width: 200pt; } #telegram-qrcode { max-width: 40%; border: none; } .reveal [data-slide-id="home"] p, .reveal [data-slide-id="home"] img { margin: unset; } .reveal [data-slide-id="home"] img { max-width: 30%; } .reveal .huge.fragment { font-size: 400%; padding-bottom: 0.5em; } .reveal .huge.fragment code { background-color: white; } #cargo-cult { max-width: 60%; } #karma-derniere-etape { font-size: 200%; } #logs-jquery-desordre { max-width: 50%; } </style>