Support Bonnes Pratiques de Angular Avec NGRX [PDF]

  • 0 0 0
  • Gefällt Ihnen dieses papier und der download? Sie können Ihre eigene PDF-Datei in wenigen Minuten kostenlos online veröffentlichen! Anmelden
Datei wird geladen, bitte warten...
Zitiervorschau

Angular 11 : La Synthèse

Mohamed Youssfi Laboratoire Signaux Systèmes Distribués et Intelligence Artificielle (SSDIA) ENSET, Université Hassan II Casablanca, Maroc Email : [email protected] Supports de cours : http://fr.slideshare.net/mohamedyoussfi9 Chaîne vidéo : http://youtube.com/mohamedYoussfi Recherche : http://www.researchgate.net/profile/Youssfi_Mohamed/publications



Créer une application qui permet de gérer des produits :       

Partie Backend de Test : Json-server Bases fondamentales de Angular Ractive Forms Comment décomposer un Component Différentes façon de Communication entre les components Quelques bonnes pratiques : Transition vers NGRX pour le State Management

db.json {

"bootstrap": "^4.5.3", "concurrently": "^5.3.0", "font-awesome": "^4.7.0", "jquery": "^3.5.1", "json-server": "^0.16.3", "scripts": { "ng": "ng", "start": "concurrently \"ng serve\" "build": "ng build", "test": "ng test", "lint": "ng lint", "e2e": "ng e2e" },

"products": [ { "id": 1, "name": "Computer", "price": 21000, "quantity": 89, "available": true, "selected": false }, { "id": 2, "name": "Printer", "price": 6500, "quantity": 8, "selected": true, "available": true }, { "id": 3, "name": "Smart Phone", "price": 12000, "quantity": 21, "selected": false, "available": true } ]

\"json-server --watch db.json\"",

}

ProductsComponent Single Component

ProductNavBarComponent

Products Component ProductNavBar Component

ProductList Component ProductItem Component

ProductItem Component

ProductListComponent

ProductItemComponent

ProductsComponent Single Component

Products Component ProductNavBar Component

ProductNavBarComponent

Data ProductList Component

ProductItem Component

ProductItem Component

ProductListComponent

ProductItemComponent

ProductsComponent ProductNavBarComponent Single Component

Products Component ProductNavBar Component

Subscribe Data

ProductListComponent

ProductList Component

Publish

EventSubjectService Subject Observable

ProductItem Component

ProductItem Component

ProductItemComponent

product.model.ts

product.actions.ts

export interface Product { id:number; name:string; price:number; quantity:number; selected:boolean; available:boolean; }

export enum ProductQueryActions { GET_ALL_PRODUCTS="GET_ALL_PRODUCTS", GET_SELECTED_PRODUCTS="GET_SELECTED_PRODUCTS", GET_AVAILABLE_PRODUCTS="GET_AVAILABLE_PRODUCTS", EDIT_PRODUCT="EDIT_PRODUCT", SEARCH_PRODUCT="SEARCH_PRODUCT", NEW_PRODUCT="NEW_PRODUCT" } export enum ProductCommandActions { ADD_PRODUCT="ADD_PRODUCT", DELETE_PRODUCT="DELETE_PRODUCT", UPDATE_PRODUCT="UPDATE_PRODUCT", SELECT_PRODUCT="SELECT_PRODUCT", EDIT_PRODUCT="EDIT_PRODUCT", }

product.state.ts export enum DataStateEnum { LOADING, LOADED, ERROR, } export interface AppDataState { dataState: DataStateEnum; data?: T, errorMessage?:string }

export interface ActionEvent{ type:A; payload?:T; }

environement.ts product.service.ts

@Injectable({providedIn:"root"}) export class ProductService {

export const environment = { production: false, host:"http://localhost:3000", unreachableHost:"http://localhost:3008" };

constructor(private http:HttpClient) { } public getProducts():Observable{ let host=Math.random()>0.2?environment.host:environment.unreachableHost; return this.http.get(host+"/products"); //return throwError("Not Implemented yet"); } public getSelectedProducts():Observable{ //if(Math.random()>0.1) return throwError({message:"Internal Error"}); //else return this.http.get(environment.host+"/products?selected=true"); } public getAvailableProducts():Observable{ return this.http.get(environment.host+"/products?available=true"); }

product.service.ts

public searchProducts(name:string):Observable{ return this.http.get(environment.host+"/products?name_like="+name); } public setSelected(product:Product):Observable{ product.selected=!product.selected; return this.http.put(environment.host+"/products/"+product.id,product); } public delete(id:number):Observable{ return this.http.delete(environment.host+"/products/"+id); } public save(product:Product):Observable{ return this.http.post(environment.host+"/products/",product); } public update(product:Product):Observable{ return this.http.put(environment.host+"/products/"+product.id,product); } public getProductById(id:number):Observable{ return this.http.get(environment.host+"/products/"+id); } }

Event.driven.service.ts

import {Injectable} from '@angular/core'; import {Subject} from 'rxjs'; import {ActionEvent, ProductCommandActions, ProductQueryActions} from '../state/state'; @Injectable({providedIn:"root"}) export class EventDrivenService { private queryEventSource=new Subject(); queryEventSourceObservable=this.queryEventSource.asObservable(); private commandEventSource=new Subject(); commandEventSourceObservable=this.commandEventSource.asObservable(); public publishQueryAction(action :ActionEvent){ this.queryEventSource.next(action); } public publishCommandAction(action :ActionEvent){ this.commandEventSource.next(action); } }



NgRx est une librairie implémentant le pattern Redux en utilisant la programmation réactive basée sur RxJS.



NgRX permet à une application Angular de centraliser l’état de l’application dans un unique Objet.



Tous les composants de l’application peuvent accéder facilement à l’état de l’application d’une manière réactive.



Chaque composant peut faire une souscription aux données du State dont il a besoin en utilisant des sélectors.



À chaque fois que ces données du state changent le composant est mis à jour d’une manière réactive (Real Time)



Ce mécanisme de NgRx propose des mécanisme puissant alternatifs aux solutions classiques fournies par Angular tels que 

@Input et @Output permettant de faire communiquer d’une manière simple les web composants à travers l’arbre des composants, mais qui s’avère parfois complexe pour une grande application



Les services qui peuvent être utilisés pour partager les données de l’applications et aussi les traitements pour l’ensembles de composants web de l’application



NgRX est une implémentation du Pattern Redux en utilisant RxJS.



L’architecture de NgRX repose sue les composants suivants: 

Store : Un objet JavaScript contenant l’état (State) de votre application. Le State est un Objet Java Script immutable.



Web Components : Tout composant web de l’application qui souhaite utiliser une partie de l’état de l’application doit souscrire un abonnement dans le Store.



Selectors : pour permettre à un composant d’observer des parties précises du state au lieu d’observer tout le state, NgRx fournit le mécanisme des sélecteurs.



Actions : Ce sont les évènements émis par votre application au niveau des composants suite aux interactions IHM ou dans les services suite des interactions avec le Backend. Pour dispatcher une action ont utiliser l’objet store. Une action est un objet Java Script définit par deux attributs:





type : qui représente le type de l’événement de type string



payload : un objet java script contenant les paramètres de l’action.

Reduces : Un Reducer est une fonction java scripts pures représentant des écouteurs d’événements (Actions) qui reçoivent le state actuel et l’action dispatchée par le store. En fonction du type de l’action et son payload, le Reducer returne un nouveau State du Store, en permettant à l’application de passer d’un état courant vers un nouvel état. Les Reducers doivent préserver l’émmutabilité du state. Ce qui ouvre la possibilité de préserver l’historique des différents changements dans le state de l’application.



Effects : Un Effect est un observateur d’un certains types d’actions particulières qu’il faudrait intercepter pour faire appel aux services de l’applications qui sont souvent utilisés pour interagir avec le backend. Chaque interaction avec le backend entraine un événement (Success ou Error) qui est traduit par l’effect dispatchant une autre action destiné à être interceptée par un Reducer en vu de mettre à jour le state du store.



Entities : pour faciliter la gestion des collections d’entités du State, NgRX fournit un mécanisme qui permet d’ajouter, rechercher, mettre à jour et supprimer des entités du State de l’application en utilisant EntityFactory.

Component

Store

Reducer

State Observable

Effect

Services

Backend

Subscribe Subscribe

Observable

dispatch (action)

action

:Action

State View Date State

action

New State

State View State Status

State

dispatch (action) action

State New State

action

Data

REST



Créer une application qui permet de gérer des produits : 

Partie Backend de Test : Json-server



State Management avec NGRX

https://github.com/mohamedYoussfi/angular-ngrx-products-app.git    

npm install --save bootstrap jquery font-awesome npm install --save json-server npm i --save concurrently

 { "products": [ {"id": 1,"name": "Computer","price": 60000,"quantity": 12,"selected": true,"available": true}, {"id": 2,"name": "Printer","price": 1200,"quantity": 10,"selected": true,"available": false}, {"id": 3,"name": "Smartphone","price": 2000,"quantity": 32,"selected": false,"available": true} ] }

 "scripts": { "ng": "ng", "start": "concurrently \"ng serve\" \"json-server --watch db.json\"",

https://github.com/mohamedYoussfi/angular-ngrx-products-app.git  "styles": [ "src/styles.css", "node_modules/bootstrap/dist/css/bootstrap.min.css" ], "scripts": [ "node_modules/jquery/dist/jquery.min.js", "node_modules/bootstrap/dist/js/bootstrap.min.js" ]

 @import "~font-awesome/css/font-awesome.min.css";

 export interface Product { id:number; name:string; price:number; quantity:number; selected:boolean; available:boolean; }

 imports: [ BrowserModule, AppRoutingModule, HttpClientModule ],

https://github.com/mohamedYoussfi/angular-ngrx-products-app.git  import import import import import

{Injectable} from '@angular/core'; {HttpClient} from '@angular/common/http'; {Observable} from 'rxjs'; {environment} from '../../environments/environment'; {Product} from '../model/product.model';

@Injectable({providedIn:"root"}) export class ProductService { constructor(private http:HttpClient) { } public getProducts():Observable{ let host=Math.random()>0.2?environment.host:environment.unreachableHost; //let host=environment.host; return this.http.get(host+"/products"); //return throwError("Not Implemented yet"); } public getSelectedProducts():Observable{ return this.http.get(environment.host+"/products?selected=true"); } public getAvailableProducts():Observable{ return this.http.get(environment.host+"/products?available=true"); }

public searchProducts(name:string):Observable{ return this.http.get(environment.host+"/products?name_like="+name); } public setSelected(product:Product):Observable{ return this.http.put(environment.host+"/products/"+product.id,{...pro duct,selected:!product.selected}); } public delete(id:number):Observable{ return this.http.delete(environment.host+"/products/"+id); } public save(product:Product):Observable{ return this.http.post(environment.host+"/products/",product); } public update(product:Product):Observable{ return this.http.put(environment.host+"/products/"+product.id,product ); } public getProductById(id:number):Observable{ return this.http.get(environment.host+"/products/"+id); } }

https://github.com/mohamedYoussfi/angular-ngrx-products-app.git 



$ npm start

https://github.com/mohamedYoussfi/angular-ngrx-products-app.git $ npm start > [email protected] start D:\Docs3\apps\ang11\ngrx-app > concurrently "ng serve" "json-server --watch db.json" [1] [1] \{^_^}/ hi! [1] Loading db.json [1] Done [1] Resources [1] http://localhost:3000/products [1] Home [1] http://localhost:3000 [1] Type s + enter at any time to create a snapshot of the database [1] Watching... [0] - Generating browser application bundles... [0] √ Browser application bundle generation complete. [0] Initial Chunk Files | Names | Size [0] vendor.js | vendor | 2.76 MB [0] styles.css, styles.js | styles | 519.03 kB [0] polyfills.js | polyfills | 485.30 kB [0] scripts.js | scripts | 149.40 kB [0] main.js | main | 13.83 kB [0] runtime.js | runtime | 6.15 kB [0] | Initial Total | 3.90 MB [0] Build at: 2021-02-28T09:17:55.052Z - Hash: 7be137dc3c31e73fa8c7 - Time: 6032ms [0] ** Angular Live Development Server is listening on localhost:4200, open your browser on http://localh ost:4200/ ** [0] [0] √ Compiled successfully.



Dépendances à installer : npm install --save bootstrap jquery  npm install --save json-server  npm i --save @ngrx/store  npm i --save @ngrx/effects  npm i --save @ngrx/entity  npm i --save @ngrx/store-devtools  npm i --save concurrently 



db.json :

{

"products": [ {"id": 1,"name": {"id": 2,"name": {"id": 3,"name": ], "categories": [ {"id": 1,"name": {"id": 1,"name": ]

"Computer Mac Book","price": 20000,"categotyID":1}, "Computer HP 45","price": 10000,"categotyID":1}, "Printer Epson LX","price": 3000,"categotyID":2} "Computer"}, "Printer"}

} 

package.json :

"scripts": { "ng": "ng", "start": "concurrently \"ng serve\" \"json-server --watch db.json\"",

$ npm run start

angular.json :  "styles": [ "src/styles.css", "node_modules/bootstrap/dist/css/bootstrap.min.css" ], "scripts": [ "node_modules/jquery/dist/jquery.min.js", "node_modules/bootstrap/dist/js/bootstrap.min.js" ] Dépendances à installer : ng g c home ng g c navbar ng g m products ng g c products/products -m products ng g c products/products-create -m products ng g c products/products-edit -m products ng g c products/products-list -m products ng g m categories ng g c categories/categories -m categories ng g c categories/categories-create -m categories ng g c categories/categories-edit -m categories ng g c categories/categories-list -m categories

           

import import import import import import

{ { { { { {

BrowserModule } from '@angular/platform-browser'; NgModule } from '@angular/core'; AppRoutingModule } from './app-routing.module'; AppComponent } from './app.component'; HomeComponent } from './home/home.component'; NavbarComponent } from './navbar/navbar.component';

@NgModule({ declarations: [ AppComponent, HomeComponent, NavbarComponent ], imports: [ BrowserModule, AppRoutingModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }

import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; const routes: Routes = [ {path: 'products', loadChildren:()=>import("./products/products.module").then((m)=>m.ProductsModule) }, {path: 'categories', loadChildren:()=>import("./categories/categories.module").then((m)=>m.CategoriesModule) } ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { }

import import import import import import import

{ NgModule } from '@angular/core'; { CommonModule } from '@angular/common'; { ProductsComponent } from './products/products.component'; { ProductsCreateComponent } from './products-create/products-create.component'; { ProductsEditComponent } from './products-edit/products-edit.component'; {RouterModule, Routes} from '@angular/router'; { ProductsListComponent } from './products-list/products-list.component';

const productsRoutes:Routes=[ {path:"",component:ProductsComponent} ] @NgModule({ declarations: [ProductsComponent, ProductsCreateComponent, ProductsEditComponent, ProductsListComponent], imports: [ CommonModule, RouterModule.forChild(productsRoutes) ] }) export class ProductsModule { }

import import import import import import import import

{ NgModule } from '@angular/core'; { CommonModule } from '@angular/common'; { CategoriesComponent } from './categories/categories.component'; {RouterModule, Routes} from '@angular/router'; {ProductsComponent} from '../products/products/products.component'; { CategoriesCreateComponent } from './categories-create/categories-create.component'; { CategoriesEditComponent } from './categories-edit/categories-edit.component'; { CategoriesListComponent } from './categories-list/categories-list.component';

const categoriesRoutes:Routes=[ {path:"",component:CategoriesComponent} ] @NgModule({ declarations: [CategoriesComponent, CategoriesCreateComponent, CategoriesEditComponent, CategoriesListComponent], imports: [ CommonModule, RouterModule.forChild(categoriesRoutes) ] }) export class CategoriesModule { }













product.module.ts export interface Product { id?:number; name:string; price:number; categoryID:number; } app.state.ts export interface AppState { } app.module.ts imports: [ BrowserModule, AppRoutingModule, HttpClientModule, StoreModule.forRoot({}), EffectsModule.forRoot([]), StoreDevtoolsModule.instrument() ],

products.module.ts imports: [ CommonModule, RouterModule.forChild(productsRoutes), StoreModule.forFeature("catalog",productReducer), EffectsModule.forFeature([ProductEffects]), FormsModule, ReactiveFormsModule ]

product.module.ts import { Injectable } from '@angular/core'; import {HttpClient} from '@angular/common/http'; import {Observable} from 'rxjs'; import {Product} from '../model/products.model'; import {environment} from '../../environments/environment'; @Injectable({ providedIn: 'root' }) export class ProductService { constructor(private http:HttpClient) { } public getProducts():Observable{ return this.http.get(environment.backendHost+"/products"); } public getProductByID(id:number):Observable{ return this.http.get(environment.backendHost+`/products/${id}`); } public addProduct(product:Product):Observable{ return this.http.post(environment.backendHost+`/products`,product); } public updateProduct(product:Product):Observable{ return this.http.patch(environment.backendHost+`/products/${product.id}`,product); } public deleteProduct(id:number){ return this.http.delete(environment.backendHost+`/products/${id}`); } }

products.actions.ts import {Action} from '@ngrx/store'; import {Product} from '../../model/products.model'; export enum ProductActionsTypes { LOAD_PRODUCTS="[Product] Load Products", LOAD_PRODUCTS_SUCCESS="[Product] Load Products Success", LOAD_PRODUCTS_ERROR="[Product] Load Products Error" } export class LoadProductsAction implements Action{ readonly type:string=ProductActionsTypes.LOAD_PRODUCTS; constructor(public payload:any) { } } export class LoadProductsActionSuccess implements Action{ readonly type:string=ProductActionsTypes.LOAD_PRODUCTS_SUCCESS; constructor(public payload:Product[]) { } } export class LoadProductsActionError implements Action{ readonly type:string=ProductActionsTypes.LOAD_PRODUCTS_ERROR; constructor(public payload:string) { } } export type ProductActions= LoadProductsAction|LoadProductsActionSuccess|LoadProductsActionError;

products.affects.ts import {Injectable} from '@angular/core'; import {Actions, Effect, ofType} from '@ngrx/effects'; import {ProductService} from '../product.service'; import {Observable, of} from 'rxjs'; import {Action} from '@ngrx/store'; import {LoadProductsActionError, LoadProductsActionSuccess, ProductActions, ProductActionsTypes} from './product.actions'; import {catchError, map, mergeMap} from 'rxjs/operators'; import {Product} from '../../model/products.model'; @Injectable() export class ProductEffects { constructor(private effectActions:Actions, private productService:ProductService) { } @Effect() loadProducts:Observable=this.effectActions.pipe( ofType(ProductActionsTypes.LOAD_PRODUCTS), mergeMap((action:ProductActions)=> this.productService.getProducts() .pipe( map((products:Product[])=>new LoadProductsActionSuccess(products)), catchError(err=>of(new LoadProductsActionError(err))) ) ) ); }

products.affects.ts import {Product} from '../../model/products.model'; import {ProductActions, ProductActionsTypes} from './product.actions'; import {AppState} from '../../state/app.state'; export interface ProductState extends AppState{ products:Product[]; loading:boolean; loaded:boolean; error:string; } const initialState:ProductState={ products:[],loaded:false,loading:false,error:null } export function productReducer(state:ProductState=initialState,action:ProductActions):ProductState { switch (action.type) { case ProductActionsTypes.LOAD_PRODUCTS: return {...state,loading:true} case ProductActionsTypes.LOAD_PRODUCTS_SUCCESS: return {...state,loaded:true,loading:false,products:action.payload} case ProductActionsTypes.LOAD_PRODUCTS_ERROR: return {...state,error:action.payload} default: return {...state}; } }

products-list.component.ts import import import import import import import import

{ Component, OnInit } from '@angular/core'; {State, Store} from '@ngrx/store'; {LoadProductsAction} from '../ngrxFeatures/product.actions'; {ProductState} from '../ngrxFeatures/product.reducer'; {Observable} from 'rxjs'; {Product} from '../../model/products.model'; {AppState} from '../../state/app.state'; {map} from 'rxjs/operators';

@Component({ selector: 'app-products-list', templateUrl: './products-list.component.html', styleUrls: ['./products-list.component.css'] }) export class ProductsListComponent implements OnInit { products:Observable; constructor(private store:Store) { } ngOnInit(): void { this.store.dispatch(new LoadProductsAction({})); this.products=this.store.pipe( map((state)=>state.catalog.products) ); } deleteProduct(p: Product) { } editProduct(p: Product) { } }

products-list.component.html





IDNamePriceCategoryID
{{p.id}} {{p.name}} {{p.price}} {{p.categoryID}} Edit Delete


products-list.component.ts import import import import import import import import

{ Component, OnInit } from '@angular/core'; {State, Store} from '@ngrx/store'; {LoadProductsAction} from '../ngrxFeatures/product.actions'; {ProductState} from '../ngrxFeatures/product.reducer'; {Observable} from 'rxjs'; {Product} from '../../model/products.model'; {AppState} from '../../state/app.state'; {map} from 'rxjs/operators';

@Component({ selector: 'app-products-list', templateUrl: './products-list.component.html', styleUrls: ['./products-list.component.css'] }) export class ProductsListComponent implements OnInit { products:Observable; constructor(private store:Store) { } ngOnInit(): void { this.store.dispatch(new LoadProductsAction({})); this.products=this.store.pipe( map((state)=>state.catalog.products) ); } deleteProduct(p: Product) { } editProduct(p: Product) { } }

products-list.component.html





IDNamePriceCategoryID
{{p.id}} {{p.name}} {{p.price}} {{p.categoryID}} Edit Delete


Angular Fundamentals STEP 0 Understand Angular Architecture

Mohamed Youssfi Laboratoire Signaux Systèmes Distribués et Intelligence Artificielle (SSDIA) ENSET, Université Hassan II Casablanca, Maroc Email : [email protected] Supports de cours : http://fr.slideshare.net/mohamedyoussfi9 Chaîne vidéo : http://youtube.com/mohamedYoussfi Recherche : http://www.researchgate.net/profile/Youssfi_Mohamed/publications

Angular Fundamentals STEP 0 Understand Angular Architecture