===== Prerequis ===== * Node.js > 4.7 * NPM version > 4 Ma démo se trouve ici : [[this>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 * Pluralsight : https://github.com/DeborahK/Angular-GettingStarted/tree/master/APM-Start ===== Mise en place ===== __E1: Creation manuelle de l'application __ + Create application folder + 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 $ npm install -> Ca installe toutes les dépendances dans le dossier /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 $ npm start - ca lance le serveur web sur le port TCP 4200 par défaut - ca ouvre un navigateur web sur le port 4200, qui affichera la page index.html - 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 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 : * @angular/common * @angular/core * @angular/forms * @angular/... * bootstrap --> pour démarrer l'application angular * rxjs --> met en oeuvre le concept de Reactive Programming (Observable (asynchrone)) == devDependencies == on y retrouve notamment : * angular-cli : * TypeScript : il doit s'agit du transpiler * karma : lanceur de test unitaire et intégration * jasmine, qui est une méthodologie pour écrire des tests avec des phrases ; Des annotations, portant des regex sont notées à des méthodes de test. --> 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 === - le dossier app, qui contient des fichier TypeScript (*.ts) et des fichiers html - le fichier index.html : à savoir qu'angular4 est de type SPA (Single Page Application) - 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 * assets : permet de définir des fichiers embarqués en plus dans notre application * prefix : default application selector prefix. C'est pour pour la génération de fichier avec ng new ... * styles : styles globaux de l'application Prerequis : Installer/Réinstaller correctement angular-cli : npm install -g @angular/cli angular-cli, c'est un package node.js qui permet de : - 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 - builder le projet complet, i.e transpiler et mettre tout ce qu'il faut dans le dossier de sortie - lancer les tests "End to End" ou unitaire - 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 == * E1 : compile (transpile) et build le projet --> sortie console = webpack: Compiled successfully * E2 : watchdog sur les fichiers de l'appli, recompile et redéploie à la volée et lance une commande au sessions 4200 du navigateur pour qu'elles se raffraichissent $ 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 à 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 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 : - index:index.html - 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 ===== * ES 3 : supported by all browsers * ES 5 : supported by recent browsers * ES 6 = ES 2015 : must be transpiled Dernière version de javascript ou * TypeScript : must be transpiled 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. * Dart : must be transpiled (it contains no javascript) ===== 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. export interface IProduct { ... } Une interface peut contenir : * des propriétés * des déclarations de méthodes ===== 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 : et _filteredByName: string; set filteredByName(value: string) { this._filteredByName = value; this.performProductFilter(); } get filteredByName():string { return this._filteredByName; } ===== Méthode constructor() et injection de dépendance ===== Méthode constructor(MonService) --> injection de dépendance : retrouve le service MonService nécessaire pour instancier le composant ===== Créer un service ===== E1 : Creer la classe service @Injectable() export Service ProductService { getProducts() : IProduct[] { } } 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(). export class PanelProduct { private _productService : ProductService; constructor(productService : ProductService) { this._productService = productService; } } 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 return this._http.get(this.productUrl); // ici, retourne un json (synchrone) ou encore getProducts() : Observable { return this._http.get(this.productUrl) .do(data => console.log(JSON.stringify(data))) .catch(handlerFunction(err: HttpErrorResponse)) ; } handlerFunction(...) { console.log(); Observable.throw(somehing); } == E4 : Souscrire à l'Observable dans l'appel du service == ProduitService.getProducts().suscribe(result => succesFn(result), error => errorFn(), completeFn); 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 : * définir dans index.html ** * importer le module de routing : __RouterModule__ * mettre **** à la racine de la où l'on veut afficher les différents composants à afficher en fonction du path. * _enlever le selector_ que l'on avait éventuellement mis dans les composants à afficher conditionnellement * définir dans le module de l'application le mapping des routes --> composant à affichier RouterModule.forRoot([ { path: 'products', component: PanelProductsComponent }, { path: 'productDetail/:id', canActivate : [ProductGardService], component: PanelProductDetailComponent } ... * enfin mettre des boutons ou comme ceci : Home ou dans un handler de fonction this._router.navigate(['productDetail','' + p.productId]); **_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() === * Declare le mapping * Register the router service * Ne doit etre utilisé qu'une fois dans l'application === RouterModule.forChild() === * Declare le mapping * Does NOT Register the router service * Doit etre utilisé dans les features modules __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 ==== === RouterLink === 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 === * On ne les retrouve pas dans les paramètres de configuration des mappings path-->composants (forRoot, forChild) => il n'affectent pas la stratégie de routing * Ils sont définit comme un objet {} de l'array que l'on affecte à [routerLink]= Exemple pour un path du genre products/:id : DetailProduit On les lit ensuite de la même manière : this.route.snapshot.params['option1'] === 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. DetailProduit ou DetailProduit ou onclickOnButton:void() { this.router.navigate(['products'], {preserveQueryParams : true}); } **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 : this.monParamId = this.route.snapshot.queryParams['paramsId']; ==== 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 === RouterModule.forChild([ path:'products', component: ProductComponent, data: { pageTitle: 'Product List'} ]) Pour les lire : this.route.snapshot.data['pageTitle'] === route resolver service === Il est chargé de faire des traitements avant de passer à la page demandée E1 : Register a route resolver service @Injectable() export class ProductResolver implements Resolve { resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) : Observable { ... } } Dans le module qui va l'utiliser providers [ ... , ProductResolver ] E2 : Add **resolve** to the route configuration RouterModule.forChild([ path:'products/:id', component: ProductDetailComponent, resolve: { product : ProductResolver, autresData : autresDataResolver } ]) __product__ sera la clé qui permettra au composant de récupérer la valeur E3 : Read data from ActivatedRoute this.route.snapshot.data['product'] ou mieux : this.route.data.suscribe(); ===== Routing et aiguillage (router-oulet) ===== Au début on a utilisé un 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 . On parle de child routing Dans la config des routes : RouterModule.forChild([ { path:'products', component:ProductListComponent, children: [ { path:'', redirectTo : 'info', pathMatch : 'full' } { path:'info',component : ProductListInfoComponent }, { path:'tags',component : ProductListTagsComponent } ] }, { path:'...' ... } ] ) ==== 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 ===== ==== @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 , selector de AlertComponent Dans ce cas, on peut mettre dans AppComponent le code suivant : @ViewChild('first') alert: AlertComponent; // permet de sélectionner le premier @ViewChildren(AlertComponent) alerts: QueryList; // la, on les prend tous ==== @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 == * Suites : describe(string, function) : la fonction contient des specs * Specs : it(string, function) : ce sont les TU proprement dits. fonction will contain one or more expectations * Expectation = assertion : 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 - build l'application - ouvre le navigateur par défaut - 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 : * des plugins qu'il tire pour s'exécuter * l'environnement à builder pour les tests : dev ou prod * le port tcp du navigateur * le niveau de log à afficher * le navigateur à utiliser === Fichiers de tests unitaires === * Ce sont des fichiers d'extension .spec.ts * Ils doivent être déposés dans le dossier app * Chaque fichier .spec.ts contient des Suites, des Specs (voir au dessus), ... ==== 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