Table des matières

Prerequis

Ma démo se trouve ici : releaseDemoAngular4/index.html

Le code source de ma démo est sur github aussi : https://github.com/newman79/DemoAngular4

Un tuto : http://awesome-angular.developpez.com/tutoriels/premier-pas-angular/

Plusieurs cours et lecture m'ont permis d'écrire ce tuto et l'application web

Mise en place

E1: Creation manuelle de l'application

+ Create application folder <AngularApplicationRootDirPath> + Add package definition (package.json) and configuration files

Vu le nombre de fichier de paramètres à écrire, il est préférable de télécharger une version déjà prête, comme celle ci https://github.com/angular/quickstart

ou encore mieux : d'utiliser le générateur angular-cli : https://github.com/angular/angular-cli

angular-cli (command-line-interface) est décrit plus bas

Pour le cours, on télécharge un squelette (starter files), ici : https://github.com/DeborahK/Angular-GettingStarted/tree/master/APM-Start

E2: Install the packages

$ cd <AngularApplicationRootDirPath>
$ npm install

→ Ca installe toutes les dépendances dans le dossier <AngularApplicationRootDirPath>/node_modules

Si tout s'est bien passé (même avec des Warnings), on doit voir dans les logs une structure d'arbre des dépendances installées, ou alors dans les nouvelles versions, on ne doit pas voir d'ERROR.

E3: Launch application

$ cd <AngularApplicationRootDirPath>
$ npm start
  1. ca lance le serveur web sur le port TCP 4200 par défaut
  2. ca ouvre un navigateur web sur le port 4200, qui affichera la page index.html
  3. ca lance un watch dog sur tous les fichiers dans src. Si un fichier est modifié, alors ca redeploie sur le serveur et ca raffraichit automatiquement le navigateur web (modifier par index.html)

E4: Create the app's angular module E5: Create main.ts files E6 Create the host web page

Structure d'un dossier angular

Le projet <AngularApplicationRootDirPath> contient notamment :

package.json

fichier contenant toutes les dépendances de l'application (dependencies) et celles pour le développement de l'application (devDependencies) et la définition d'alias npm pour lancer des scripts

dependencies

Ce sont ces dépendances que l'on va installer à l'étape suivante On retrouve des lignes comme ceci :

devDependencies

on y retrouve notamment :

–> A partir d'un bloc de phrase, jasmine est capable d'identifier les conditions de setup (Given), la ou les méthodes à tester (When), et les assertions à effectuer (Expect je crois).

Dans les alias npm pour lancer des scripts :

	"ng": "ng",						--> commande à tapper : npm ng
	"start": "ng serve -o",			--> commande à tapper : npm start
	"build": "ng build",			--> commande à tapper : npm build
	"test": "ng test",
	"lint": "ng lint --type-check",
	"e2e": "ng e2e"

tsconfig.json

config de base pour la transpilation de l'application. Cette config de base est héritée par des fichier de config dans le dossier src : celui pour l'appli (tsconfig.app.json) et celui pour les tests (tsconfig.spec.json).

Je crois que c'est lui définit dans quel dossier on déploie

typing.d.ts

fichier de détinition de type TypeScript

tslint.json

Définit les règles de codages (transpiler). Il permet aux IDE de vérifier le code.

Dossier e2e

End-To-End est une discipline permettant de réaliser des tests d'intégration ; il est possible de réaliser des tests en simulant un utilisateur final utilisant l'application dans un browser type chrome.

Dossier src, qui contient

  1. le dossier app, qui contient des fichier TypeScript (*.ts) et des fichiers html
  2. le fichier index.html : à savoir qu'angular4 est de type SPA (Single Page Application)
  3. le fichier main.ts : en fait c'est le point d'entrée de l'application angular 4. Il définit les composants et vues de l'application qu'il va utiliser, et les modules qu'il va importer (qui contiennent eux aussi des composants)

.angular-cli.json

Config du générateur angular-cli

Prerequis : Installer/Réinstaller correctement angular-cli :

npm install -g @angular/cli

angular-cli, c'est un package node.js qui permet de :

  1. générer un projet, ou des fichiers éléments angular4 component, class, service, interface, pipe, directive, enum, module (les fichiers ts, css, html, spec.js); Pour l'utiliser, on tape par exemple :
  $ ng g component MonComposant
  1. builder le projet complet, i.e transpiler et mettre tout ce qu'il faut dans le dossier de sortie
  2. lancer les tests “End to End” ou unitaire
  3. préparer l'application pour son déploiement

Pour pouvoir l'utiliser, il faut d'abord l'installer de manière globale :

  $ npm install -g @angular/cli

En fait, angular-cli contient le binaire ng Quelques commandes angular-cli :

 $ ng help
ng new

créer des fichiers TypeScripts et leur fichier associés (html, css, spec, etc …) en fonction du type d'élément créé

 $ ng new hello-word --> cree entièrement l'appli et installe les packages de node_modules
 $ ng new --help
ng serve

$ ng serve -o : fait la meme chose, en ouvrant un navigateur (chrome par défaut)

 $ ng serve --help
ng generate ou ng g

Permet de générer : directive, enum, guard, interface, module, pipe, service

 $ ng g --help  

Exemple :

 $ ng g c welcome
ng test et ng e2e

Pour les tests, voir plus bas

ng build

Pour constuire et déployer l'application web

karma.conf.js

Fichier de conf pour les test avec le moteur de test karma

Comment ca fonctionne, comme le code javascript est lancé ?

Pre connaissance

Avant tout, il faut savoir ceci : en fonction de l'environnement indiqué (dev ou prod) au moment du build, angular cli ne fait pas exactement la même chose. Dans les deux cas, il commence par générer le bootstrap et le lien avec main.js, il transpile tout les ts, puis il appelle webpack webpack est un mimifier de js. Il va mimifier tous les fichiers js de l'application : on les retrouve sur son navigateur en inspectant le DOM de la page web : on voit 5 scripts inline.bundle.js, polyfill.bundle.js, style.bundle.js, vendor.bundle.js, et surtout main.bundle.js. Pourtant ces fichiers ne sont pas présents sur le disque dur ; en fait ils sont générés en RAM.

Il faut bien comprendre que dans les 2 cas, de toute facon, les fichiers js produits ne servent à rien puisqu'ils seront réécrasés à la prochaine modif. d'un ts. Pour l'environnement dev, les fichiers js sont complètement cachés (en mémoire RAM) Pour l'environnement de prod, je ne suis pas certains que les fichiers js de chaque ts soit gardés après la mimification.

Analyse par interprétation

index.html fait appel à <pm-root>

Le code main.ts est appelé par la séquence de démarrage (main.bundle.js) :

main fait appel à platformBrowserDynamic().bootstrapModule(AppModule);

app.module.ts contient bootstrap : AppComponent

→ AppComponent est instancié puis initialisé

app.component.ts est identifié par pm-root contient un template : le titre et un body.

–> La balise <pm-root> dans index.html sera interprétée par angular, qui fera tout le travail escompté.

Reste à comprendre ce qui lance vraiment main.ts (la fameuse séquence de démarrage). J'ai l'impression que c'est dans la config de angular-cli ; en effet, le fichier de config angular-cli.json contient les propriétés :

  1. index:index.html
  2. main: main.ts

Je pense qu'angular-client se sert de ce fichier de conf pour générer le code javascript de démarrage (le main.bundle.js), qui fera en sorte d'appeler notre main.ts transpilé

Versions Javascript

Dernière version de javascript ou

TypeScript is a superset of javascript, and is object-oriented : classes, interfaces, et annotation with strong typing, by using type definition files : *.d.ts and then great ide tooling

Fichiers TypeScripts ou ES6 –> Transpiler –> fichier javascript ES 5

Les fichiers TypeScripts de l'application seront donc transpilés avant d'être exécutés.

Les modules

Modules angular JS

Une application contient au moins un module : le module AppModule, par convention de nommage.

Les autres modules sont des feature modules ou des shared modules.

Chacun de ces modules contient des Component.

→ 1 Component appartient à un et un seul module.

Un Component =

    une classe (attribut, méthodes) 
  
       + 
	 
    une Metadata (une annotation @Component annotant la classe), et 
        des annotations @ sur les propriétés ou méthodes de la classe
	  L'annotation @Component doit être qualifiée à minima avec : 
           - l'identification du composant pour pouvoir s'en servir (selector)
           - un template html : la vue

Cette syntaxe est valable pour tous les éléments angular : component, service, module, enum, interface, …

Mot clé export

Mot clé export pour définir un module et on importe le module dans un autre module avec le mot clé import

Debogguer

Tapper F12, aller dans Sources > webpack > leDossierContenantVotreApp

Interfaces

Par exemple, pour typer Product : export Interface IProduct

L'interface OnInit est fournie par angular. Elle déclare ngOnInitData(), qui est appelée juste après l'instanciation du composant, après que l'injection de dépendance aie setté ses éventuels propriétés “@autowired”

Il est possible de créer ses propres interfaces avec le mot clé interface. <sxh java>

 export interface IProduct {
   ...
 }

</sxh>

Une interface peut contenir :

Définition de @Pipe

Dans un module pour l'utiliser dans tous le module. Pipe peut jouer le role de filtre ou de transformation.

Par exemple : lowercase

On peut créer son propre Pipe.

Changer une propriété attribut de la classe en _attribut + setter + getter

→ Ca permet d'avoir un handler de modification

Exemple : <sxh html>

 <input type="text" (ngModel)='attribut'  />

</sxh> et <sxh java>

 _filteredByName: string;

 set filteredByName(value: string) {
    this._filteredByName = value;
    this.performProductFilter();
 }
 get filteredByName():string {
    return this._filteredByName;        
 }

</sxh>

Méthode constructor() et injection de dépendance

Méthode constructor(MonService)

  1. → injection de dépendance : retrouve le service MonService nécessaire pour instancier le composant

Créer un service

E1 : Creer la classe service <sxh java>

 @Injectable()
 export Service ProductService {
    getProducts() : IProduct[] {
    }
 }

</sxh>

E2: Register a provider. On peut registerer : * dans un composant, ca cloisonne et empêche l'injection dans un autre composant.

* dans un module =⇒ tous les composants du module pourront s'en servir

  dans AppComponent, on retrouve la propriété providers qui est un array de Service

E3: Finir de mettre en oeuvre l'injection de dépendance : les Components ou services qui en ont besoin le déclareront dans leur constructor().

<sxh java>

 export class PanelProduct {
     private _productService : ProductService;
     constructor(productService : ProductService) {
        this._productService = productService;
     }
 }

</sxh> Rqe : Il y a un raccourci créer directement la propriété _productService : la déclarer diectement dans le constructeur (ca fait l'injection de dépendance aussi).

Il est préférable d'appeler les services autre part que dans les constructeur, tout simplement parce que l'injection de dépendance n'a pas encore été faite sur le composant. Plutôt appeler les services au plus tôt dans ngOnInitData()

Requêtes http (Extensions Observable et Reactive)

* Observable : travaillent sur une FIFO d'événements

* Promise : fournit une et une seule valeur future, non annulable

Observable : fournit une FIFO de valeur futures, annuable, et mappable(map, filter, take, merge)

Envoyer une requête web

E1 : Importer le module HttpClientModule dans le module de l'application (dans imports)
E2 : Injecter le service HttpClient

Injecter le service HttpClient dans ProductService : constructor(private _http : HttpClient)

Remarque : les services d'angular sont en singleton.

E3 : Invocation

Définir la propriété _productUrl; Remarque : on peut mettre le path vers le fichier products.json, ca évitera d'implémenter un service directement.

Dans service en cours de développement implémenter la méthode d'appel

<sxh java>

  return this._http.get(this.productUrl); // ici, retourne un json (synchrone)

</sxh> ou encore <sxh java>

  getProducts() : Observable<IProduct[]> {
     return this._http.get<IProduct[]>(this.productUrl)
   .do(data => console.log(JSON.stringify(data)))
   .catch(handlerFunction(err: HttpErrorResponse)) ;
  }

 handlerFunction(...) {
    console.log();
    Observable.throw(somehing);
 }	

</sxh>

E4 : Souscrire à l'Observable dans l'appel du service

<sxh java> ProduitService.getProducts().suscribe(result ⇒ succesFn(result), error ⇒ errorFn(), completeFn); </sxh>

Remarque : Les fonctions x ⇒ f(x) sont des lambda :

  p => this.products = p

ou

  p => {  some code; another code  }

Le routing

A minima pour faire un routage simple, il faut :

<sxh javascript>

  RouterModule.forRoot([
     { path: 'products', component: PanelProductsComponent },
     { path: 'productDetail/:id', canActivate : [ProductGardService], component: PanelProductDetailComponent }  
   ...

</sxh>

<sxh javascript>

  <a [routerLink]="['/welcome']" [routerLinkActive]="['cssClass1', 'cssClass2']">Home</a>

</sxh> ou dans un handler de fonction <sxh javascript>

 this._router.navigate(['productDetail','' + p.productId]);

</sxh> _router est de type Router, il faut l'injecter

Remarque : [routerLinkActive]=“cssClassesArray est optionnel, ça permet de positionner des class css si la route active est bien celle correspondant au path indiqué.

Module

RouterModule

RouterModule.forRoot()

RouterModule.forChild()

Important : Dans le module d'application, l'ordre des modules contenant des mappings de route a une incidence sur la résolution du mapping.

Exemple :

 App définit le path1 dans Module1 puis path2 dans Module2
 App importe Mod1 puis Mod2
   ==> Les paths de Module1 seront matchés avant ceux de Module2
 

Si on créé un module juste pour le routing, il est sans doute préférable de le mettre à la fin, parce qu'il contiendra sans doute les mappings pour les pages par défaut.

Directives

permet d'indiquer qu'un clic doit invoquer le service de routing ou fournissant le path demandé

RouterLinkActive

Directive optionnel pour spécifier des classes css à positionner si le path visé est bien celle qui est actif

RouterOutler

Permet de définir une base pour placer les composants

ActivatedRoute

Permet de lire les paramètres de l'url passée au routing

Paramètre dans les path

Route parameter

Ils sont de la forme :IdentifiantVariable

Exemple : products/:id/edit/subcomponent/:idSubComponent

Optional Parameters

⇒ il n'affectent pas la stratégie de routing

Exemple pour un path du genre products/:id : <sxh html>

  <a [routerLink]=['products',5, {option1: val1, option2: val2}]> DetailProduit</a>

</sxh> On les lit ensuite de la même manière : <sxh javascript> this.route.snapshot.params['option1'] </sxh>

Query Parameters

Ils permettent surtout de conserver l'historique de la page sur laquelle on était avant. Ils ne font pas partie de la notion de routage. <sxh html>

  <a [routerLink]=['products',5, {option1: val1, option2: val2}]
  [queryParams]= "{filterBy: 'er', showImage:true}"
  [preserveQueryParams] = "true"
>
DetailProduit
  </a>

</sxh> ou <sxh html>

  <a [routerLink]=['products',5, {queryParamsHandling : 'preserve'}]>
DetailProduit
  </a>

</sxh> ou <sxh javascript>

onclickOnButton:void() {
	this.router.navigate(['products'], {preserveQueryParams : true});	
}    

</sxh>

Avec preserveQueryParams à true ou queryParamsHandling : 'preserve', quand on passe à l'écran suivant, les paramètres de requêtes qui étaient dans la barre d'adresse restent toujours là

Mais ça ne suffit pas, après, il faut les lire pour réinitialiser correctement son composant. Ca se fait avec ça :

<sxh javascript>

 this.monParamId = this.route.snapshot.queryParams['paramsId'];

</sxh>

Prefetching data using route resolvers

Parfois, avant de passer à l'écran suivant, il peut être bien de charger les données avant.

Avec angular, on peut aussi passer des données au Router, et ce de plusieurs facons :

avec les route parameters et optional route parameters et les query parameters

C'est ceux qu'on a vu au dessus

route's data property

<sxh javascript>

 RouterModule.forChild([
      path:'products',
      component: ProductComponent,
      data: { pageTitle: 'Product List'}		
 ])

</sxh>

Pour les lire : <sxh javascript>

  this.route.snapshot.data['pageTitle']

</sxh>

route resolver service

Il est chargé de faire des traitements avant de passer à la page demandée

E1 : Register a route resolver service <sxh javascript>

 @Injectable()
 export class ProductResolver implements Resolve<IProduct> {
  resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) : Observable<IProduct> {
	  ...
    }
 }

</sxh>

 Dans le module qui va l'utiliser
 providers [ ... , ProductResolver ]

E2 : Add resolve to the route configuration <sxh javascript>

 RouterModule.forChild([
    path:'products/:id',
    component: ProductDetailComponent,
    resolve: { product : ProductResolver, autresData : autresDataResolver }
 ])

</sxh>

product sera la clé qui permettra au composant de récupérer la valeur

E3 : Read data from ActivatedRoute <sxh javascript>

this.route.snapshot.data['product']

</sxh>

ou mieux : 

<sxh javascript>

this.route.data.suscribe();

</sxh>

Routing et aiguillage (router-oulet)

Au début on a utilisé un <router-oulet> pour afficher des composants représentants quasiment tout l'écran. A chaque fois qu'on changeait de route, on changeait le composant entier : 1 seul composant affiché, donc.

Il est aussi possible d'utiliser le routing pour choisir tous les composants à afficher, notamment si un sous-composant souhaiterait utiliser un mécanisme d'onglet Un composant peut lui aussi avoir dans son template un <router-oulet>. On parle de child routing

Dans la config des routes : <sxh javascript>

  RouterModule.forChild([
{
      path:'products',
      component:ProductListComponent,
      children: [
          { path:'', redirectTo : 'info', pathMatch : 'full' }
		{ path:'info',component : ProductListInfoComponent },
		{ path:'tags',component : ProductListTagsComponent }
    ]
},
  {   
      path:'...'
	...
  } ] )

</sxh>

Dernieres remarques

Attention quand on indique des paths : Si la première chaine de caractères commence par /, cela signifie chemin absolu. Sinon, chemin relatif.

Autre notions et directive intéressantes

 <ng-content> 
 

@ViewChild

permet d'accéder aux composants enfants depuis le composant père. Ca peut permettre d'éviter d'avoir recours à @input

Exemple : Imaginons que le template de AppComponent contienne plusieurs balises <alert-component>, selector de AlertComponent

Dans ce cas, on peut mettre dans AppComponent le code suivant :

<sxh java>

@ViewChild('first') alert: AlertComponent; 							// permet de sélectionner le premier
@ViewChildren(AlertComponent) alerts: QueryList<AlertComponent>;   	// la, on les prend tous

</sxh>

@ContentChild

semble être un équivalent de @ViewChild mais pour le contenu

Les tests

TI : Remarque sur la syntaxe Gherkin (GWT)

Adaptée à la méthodo agile : User Story

 En tant que ...
 Je veux que ...
 Afin que ...   

Critère d'Acceptation

Une US est constituée de plusieurs CA (critères d'acceptation) 1 CA =

 Given ... (contexte)
 When ... (action ou évément)
 Then ... (assertions)

Tests unitaires

Angular >= 2 fournit plein d'options pour les TU. Ici, on ne va couvrir que les options par défaut : Jasmine et Karma

Presentation de jasmin et karma

Jasmine : framework qui utilise behaviour-driven notation

expect(actual).toBe(expected)

 expect(actual).toEqual(expected)
Voir ce qu'on peut mettre ici : https://github.com/JamieMason/Jasmine-Matchers
Karma

C'est un test-runner.

Il se charge de build l'application d'ouvrire le navigateur, de lui filer le code complet de l'appli à exécuter, et de lui demander de l'exécuter.

Il peut servir à mettre en place l'intégration continu

Presentation

Commande pour lancer les tests :

$ ng test
  1. build l'application
  2. ouvre le navigateur par défaut
  3. execute le testrunner karma

A la fin des test, le navigateur affiche les résultats.

Ces tests sont aussi en watch mode, donc modif d'un fichier relance les tests

Fichier de conf

karma.conf.js est le fichier de conf globale de l'application pour les TU. Dedans on retrouve :

Fichiers de tests unitaires

Tests e2e

Ce sont les tests d'intégration ou tests End-To-End. Fichier de conf : proactor.conf.js

$ ng e2e

 - ouvre le navigateur par défaut 
 - execute l'application
 - ferme le navigateur

Dans la console on voit 1 test passé en succès, décrit avec jasmine : Le test “shoud display welcome message” est en vert avec un symbole 'check' devant

travis : système d'intégration continue, il utilise phantomJS, qui est un navigateur sans écran (les tests ne seront donc pas affichés à l'écran)

Déployer son application

 $ ng build
 $ ng build --help

→ Construit, compile, et déploie l'application web dans le dossier dist. Les fichiers générés sont :

index.html

 inline.bundle.js
 polyfill.bundle.js
 style.bundle.js
 vendor.bundle.js
 main.bundle.js
 
 main.bundle.js.map
 inline.bundle.js.map
 polyfill.bundle.js.map
 style.bundle.js.map
 vendor.bundle.js.map
 $ ng build --prod
 

→ en plus, mimifie et enlève les commentaires et code mort

 iot

On voit la différence en sortie dans le dossier dist parce que le nom des fichiers contiennent un hashcode. Ce hash permet d'éviter au navigateur de ne pas recharger les pages si il les a déjà en cache.

ng build –help : fournit pleins d'options aussi : base url, environnement, target