Support Bonnes Pratiques de Angular Avec NGRX
Support Bonnes Pratiques de Angular Avec NGRX
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", "products": [
{
"font-awesome": "^4.7.0",
"id": 1,
"jquery": "^3.5.1", "name": "Computer",
"json-server": "^0.16.3", "price": 21000,
"quantity": 89,
"available": true,
"scripts": {
"selected": false
"ng": "ng", },
"start": "concurrently \"ng serve\" \"json-server --watch db.json\"", {
"build": "ng build", "id": 2,
"test": "ng test", "name": "Printer",
"lint": "ng lint", "price": 6500,
"quantity": 8,
"e2e": "ng e2e"
"selected": true,
}, "available": true
},
{
"id": 3,
"name": "Smart Phone",
"price": 12000,
"quantity": 21,
"selected": false,
"available": true
}
]
}
ProductsComponent
Products
Component
ProductItem ProductItem
Component Component
ProductsComponent
Products
Data
Component
ProductItem ProductItem
Component Component
ProductsComponent
ProductNavBarComponent
Single Component
Subscribe
Products
Component Data ProductListComponent ProductItemComponent
Publish EventSubjectService
ProductNavBar ProductList
Component Component Subject
Observable
ProductItem ProductItem
Component Component
product.model.ts product.actions.ts
export interface Product { export enum ProductQueryActions {
id:number; GET_ALL_PRODUCTS="GET_ALL_PRODUCTS",
name:string; GET_SELECTED_PRODUCTS="GET_SELECTED_PRODUCTS",
GET_AVAILABLE_PRODUCTS="GET_AVAILABLE_PRODUCTS",
price:number;
EDIT_PRODUCT="EDIT_PRODUCT",
quantity:number; SEARCH_PRODUCT="SEARCH_PRODUCT",
selected:boolean; NEW_PRODUCT="NEW_PRODUCT"
available:boolean; }
} export enum ProductCommandActions {
ADD_PRODUCT="ADD_PRODUCT",
product.state.ts DELETE_PRODUCT="DELETE_PRODUCT",
UPDATE_PRODUCT="UPDATE_PRODUCT",
export enum DataStateEnum { SELECT_PRODUCT="SELECT_PRODUCT",
LOADING, EDIT_PRODUCT="EDIT_PRODUCT",
LOADED, }
ERROR,
} export interface ActionEvent<A,T>{
type:A;
export interface AppDataState<T> { payload?:T;
dataState: DataStateEnum; }
data?: T,
errorMessage?:string
}
environement.ts
export const environment = {
product.service.ts production: false,
host:"http://localhost:3000",
@Injectable({providedIn:"root"})
unreachableHost:"http://localhost:3008"
export class ProductService {
};
constructor(private http:HttpClient) {
}
public getProducts():Observable<Product[]>{
let host=Math.random()>0.2?environment.host:environment.unreachableHost;
return this.http.get<Product[]>(host+"/products");
//return throwError("Not Implemented yet");
}
public getSelectedProducts():Observable<Product[]>{
//if(Math.random()>0.1) return throwError({message:"Internal Error"});
//else
return this.http.get<Product[]>(environment.host+"/products?selected=true");
}
public getAvailableProducts():Observable<Product[]>{
return this.http.get<Product[]>(environment.host+"/products?available=true");
}
product.service.ts
public searchProducts(name:string):Observable<Product[]>{
return this.http.get<Product[]>(environment.host+"/products?name_like="+name);
}
public setSelected(product:Product):Observable<Product>{
product.selected=!product.selected;
return this.http.put<Product>(environment.host+"/products/"+product.id,product);
}
public delete(id:number):Observable<void>{
return this.http.delete<void>(environment.host+"/products/"+id);
}
public save(product:Product):Observable<Product>{
return this.http.post<Product>(environment.host+"/products/",product);
}
public update(product:Product):Observable<Product>{
return this.http.put<Product>(environment.host+"/products/"+product.id,product);
}
public getProductById(id:number):Observable<Product>{
return this.http.get<Product>(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<ActionEvent<ProductQueryActions,any>>();
queryEventSourceObservable=this.queryEventSource.asObservable();
private commandEventSource=new Subject<ActionEvent<ProductCommandActions,any>>();
commandEventSourceObservable=this.commandEventSource.asObservable();
Observable
<State>
New State
State
View
Date State
Créer une application qui permet de gérer des produits :
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", export interface Product {
"node_modules/bootstrap/dist/css/bootstrap.min.css" id:number;
], name:string;
"scripts": [ price:number;
"node_modules/jquery/dist/jquery.min.js", quantity:number;
"node_modules/bootstrap/dist/js/bootstrap.min.js" selected:boolean;
] available:boolean;
}
@import "~font-awesome/css/font-awesome.min.css";
imports: [
BrowserModule,
AppRoutingModule,
HttpClientModule
],
https://github.com/mohamedYoussfi/angular-ngrx-products-app.git
public searchProducts(name:string):Observable<Product[]>{
return
this.http.get<Product[]>(environment.host+"/products?name_like="+name);
}
import {Injectable} from '@angular/core'; public setSelected(product:Product):Observable<Product>{
import {HttpClient} from '@angular/common/http'; return
import {Observable} from 'rxjs'; this.http.put<Product>(environment.host+"/products/"+product.id,{...pro
import {environment} from '../../environments/environment'; duct,selected:!product.selected});
import {Product} from '../model/product.model'; }
public delete(id:number):Observable<void>{
@Injectable({providedIn:"root"}) return this.http.delete<void>(environment.host+"/products/"+id);
export class ProductService { }
public save(product:Product):Observable<Product>{
constructor(private http:HttpClient) { return
}
this.http.post<Product>(environment.host+"/products/",product);
}
public getProducts():Observable<Product[]>{
public update(product:Product):Observable<Product>{
let host=Math.random()>0.2?environment.host:environment.unreachableHost;
//let host=environment.host; return
return this.http.get<Product[]>(host+"/products"); this.http.put<Product>(environment.host+"/products/"+product.id,product
//return throwError("Not Implemented yet"); );
} }
public getSelectedProducts():Observable<Product[]>{ public getProductById(id:number):Observable<Product>{
return this.http.get<Product[]>(environment.host+"/products?selected=true"); return this.http.get<Product>(environment.host+"/products/"+id);
} }
public getAvailableProducts():Observable<Product[]>{
return this.http.get<Product[]>(environment.host+"/products?available=true"); }
}
https://github.com/mohamedYoussfi/angular-ngrx-products-app.git
<nav class="navbar navbar-expand-sm bg-dark navbar-dark">
<!-- Brand -->
<a class="navbar-brand" href="#">Logo</a>
[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": "Computer Mac Book","price": 20000,"categotyID":1},
{"id": 2,"name": "Computer HP 45","price": 10000,"categotyID":1},
{"id": 3,"name": "Printer Epson LX","price": 3000,"categotyID":2}
],
"categories": [
{"id": 1,"name": "Computer"},
{"id": 1,"name": "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 { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HomeComponent } from './home/home.component';
import { 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';
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ProductsComponent } from './products/products.component';
import { ProductsCreateComponent } from './products-create/products-create.component';
import { ProductsEditComponent } from './products-edit/products-edit.component';
import {RouterModule, Routes} from '@angular/router';
import { 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 { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CategoriesComponent } from './categories/categories.component';
import {RouterModule, Routes} from '@angular/router';
import {ProductsComponent} from '../products/products/products.component';
import { CategoriesCreateComponent } from './categories-create/categories-create.component';
import { CategoriesEditComponent } from './categories-edit/categories-edit.component';
import { 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 { }
<div class="container mt-3">
<app-products-create></app-products-create>
<hr>
<app-products-list></app-products-list>
<hr>
<app-products-edit></app-products-edit>
</div>
<div class="container mt-3">
<app-categories-create></app-categories-create>
<hr>
<app-categories-list></app-categories-list>
<hr>
<app-categories-edit></app-categories-edit>
</div>
product.module.ts
app.module.ts
imports: [
BrowserModule,
AppRoutingModule,
HttpClientModule,
StoreModule.forRoot({}), products.module.ts
EffectsModule.forRoot([]),
imports: [
StoreDevtoolsModule.instrument()
CommonModule, RouterModule.forChild(productsRoutes),
],
StoreModule.forFeature("catalog",productReducer),
EffectsModule.forFeature([ProductEffects]),
FormsModule, ReactiveFormsModule
]
product.module.ts
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<Action>=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';
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 { Component, OnInit } from '@angular/core'; products-list.component.html
import {State, Store} from '@ngrx/store';
import {LoadProductsAction} from '../ngrxFeatures/product.actions'; <table class="table table-striped" *ngIf="products">
import {ProductState} from '../ngrxFeatures/product.reducer'; <thead>
import {Observable} from 'rxjs';
import {Product} from '../../model/products.model';
<tr>
import {AppState} from '../../state/app.state'; <th>ID</th><th>Name</th><th>Price</th><th>CategoryID</th>
import {map} from 'rxjs/operators'; </tr>
</thead>
@Component({ <tbody>
selector: 'app-products-list', <tr *ngFor="let p of products|async">
templateUrl: './products-list.component.html', <td>{{p.id}}</td>
styleUrls: ['./products-list.component.css'] <td>{{p.name}}</td>
}) <td>{{p.price}}</td>
export class ProductsListComponent implements OnInit { <td>{{p.categoryID}}</td>
products:Observable<Product[]>; <td><a (click)="editProduct(p)" class="btn btn-
constructor(private store:Store<any>) { } success">Edit</a></td>
<td><a (click)="deleteProduct(p)" class="btn btn-
ngOnInit(): void { danger">Delete</a></td>
this.store.dispatch(new LoadProductsAction({})); </tr>
this.products=this.store.pipe( </tbody>
map((state)=>state.catalog.products) </table>
);
}
deleteProduct(p: Product) { }
editProduct(p: Product) { }
}
products-list.component.ts
import { Component, OnInit } from '@angular/core'; products-list.component.html
import {State, Store} from '@ngrx/store';
import {LoadProductsAction} from '../ngrxFeatures/product.actions'; <table class="table table-striped" *ngIf="products">
import {ProductState} from '../ngrxFeatures/product.reducer'; <thead>
import {Observable} from 'rxjs';
import {Product} from '../../model/products.model';
<tr>
import {AppState} from '../../state/app.state'; <th>ID</th><th>Name</th><th>Price</th><th>CategoryID</th>
import {map} from 'rxjs/operators'; </tr>
</thead>
@Component({ <tbody>
selector: 'app-products-list', <tr *ngFor="let p of products|async">
templateUrl: './products-list.component.html', <td>{{p.id}}</td>
styleUrls: ['./products-list.component.css'] <td>{{p.name}}</td>
}) <td>{{p.price}}</td>
export class ProductsListComponent implements OnInit { <td>{{p.categoryID}}</td>
products:Observable<Product[]>; <td><a (click)="editProduct(p)" class="btn btn-
constructor(private store:Store<any>) { } success">Edit</a></td>
<td><a (click)="deleteProduct(p)" class="btn btn-
ngOnInit(): void { danger">Delete</a></td>
this.store.dispatch(new LoadProductsAction({})); </tr>
this.products=this.store.pipe( </tbody>
map((state)=>state.catalog.products) </table>
);
}
deleteProduct(p: Product) { }
editProduct(p: Product) { }
}
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