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
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
E4: Create the app's angular module E5: Create main.ts files E6 Create the host web page
Le projet <AngularApplicationRootDirPath> contient notamment :
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
Ce sont ces dépendances que l'on va installer à l'étape suivante On retrouve des lignes comme ceci :
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"
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
fichier de détinition de type TypeScript
Définit les règles de codages (transpiler). Il permet aux IDE de vérifier le code.
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.
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 :
$ ng g component MonComposant
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
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 -o : fait la meme chose, en ouvrant un navigateur (chrome par défaut)
$ ng serve --help
Permet de générer : directive, enum, guard, interface, module, pipe, service
$ ng g --help
Exemple :
$ ng g c welcome
Pour les tests, voir plus bas
Pour constuire et déployer l'application web
Fichier de conf pour les test avec le moteur de test karma
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.
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 :
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é
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.
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 pour définir un module et on importe le module dans un autre module avec le mot clé import
Tapper F12, aller dans Sources > webpack > leDossierContenantVotreApp
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 :
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.
→ 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(MonService)
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()
* 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)
Injecter le service HttpClient dans ProductService : constructor(private _http : HttpClient)
Remarque : les services d'angular sont en singleton.
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>
<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 }
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é.
RouterModule
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.
permet d'indiquer qu'un clic doit invoquer le service de routing ou fournissant le path demandé
Directive optionnel pour spécifier des classes css à positionner si le path visé est bien celle qui est actif
Permet de définir une base pour placer les composants
Permet de lire les paramètres de l'url passée au routing
Ils sont de la forme :IdentifiantVariable
Exemple : products/:id/edit/subcomponent/:idSubComponent
⇒ 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>
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>
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 :
C'est ceux qu'on a vu au dessus
<sxh javascript>
RouterModule.forChild([
path:'products',
component: ProductComponent,
data: { pageTitle: 'Product List'}
])
</sxh>
Pour les lire : <sxh javascript>
this.route.snapshot.data['pageTitle']
</sxh>
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>
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>
Attention quand on indique des paths : Si la première chaine de caractères commence par /, cela signifie chemin absolu. Sinon, chemin relatif.
<ng-content>
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>
semble être un équivalent de @ViewChild mais pour le contenu
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)
Angular >= 2 fournit plein d'options pour les TU. Ici, on ne va couvrir que les options par défaut : Jasmine et Karma
expect(actual).toBe(expected)
expect(actual).toEqual(expected) Voir ce qu'on peut mettre ici : https://github.com/JamieMason/Jasmine-Matchers
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
Commande pour lancer les tests :
$ ng test
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
karma.conf.js est le fichier de conf globale de l'application pour les TU. Dedans on retrouve :
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)
$ 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