###### tags: `Angular` # 🥔基於工廠模式實現的應用 (二) 應用篇 本篇將說明如何應用下列`元件`、`工廠`實現需求。 ![](https://i.imgur.com/5ARj0Bx.png ) ### route-menu.factory.ts ==**src/app/menu/factory/route.factory.ts**== #### 🌱說明 - 透過`路由設定`實現`選單`及`黑白名單`的應用。 #### 🌱原始碼 ```typescript= import { Injectable } from "@angular/core"; import { Router } from '@angular/router'; import { AuthNameRoute } from "src/app/route/route.type"; import { IFactory } from '../../dynamic/factory.interface'; import { IMenu } from "../menu.interface"; import { IAuth } from './../../auth/auth.interface'; import { CaService } from './../../service/ca.service'; export interface IRouteMenuPara {} @Injectable({ providedIn: 'root' }) export class RouteMenuFactory<CI extends IMenu<any> = any> implements IFactory<CI, IRouteMenuPara> { constructor( private router: Router, private ca: CaService ) { } run(component: CI) { component.setMenuList(this.getMenuListByRoute()); component.currentMenu = component.menuList.find(menu => `/${menu.route}` === `${this.router.url}`) component.clickMenu.subscribe(this.clickMenu) } checkPara(para: IRouteMenuPara): IRouteMenuPara { throw new Error('Method not implemented.'); } getMenuListByRoute() { const routes = this.router.config; const menuList = routes .filter(r => r?.data?.displayName) .map(r => { return { displayName: r.data.displayName, route: r.path, isHide: !this.isValidAuth(r as AuthNameRoute) } }) return menuList; } clickMenu = (menu) => { this.router.navigate([menu.route]); } isValidAuth(data: AuthNameRoute) { if (!data.data) { console.warn('請設定權限!'); return false; } const country = this.ca.getCountry(); const main = this.ca.getMain(); let isValid = true; isValid = isValid ? this.check(country, data.data.country) : isValid; isValid = isValid ? this.check(main, data.data.main) : isValid; return isValid; } check<AE>(e: AE, auth: IAuth<AE>) { if (auth.type === 'allow') { return auth.list.filter(x => x === e).length > 0; } else if (auth.type === 'block') { return auth.list.filter(x => x === e).length < 1; } } } ``` #### 🌱應用說明 **app-routing.module.ts** ```typescript= import { EAuthCountry } from './auth/auth.interface'; import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; import { Page01Component } from './page/page01/page01.component'; import { Page02Component } from './page/page02/page02.component'; import { AuthNameRoutes } from './route/route.type'; const routes: AuthNameRoutes = [ { path: '', redirectTo: 'page01', pathMatch: 'full', }, { path: 'page01', component: Page01Component, data: { country: { type: 'block', list: [] }, main: { type: 'block', list: [] }, displayName: '第一頁', } }, { path: 'page02', component: Page02Component, data: { country: { type: 'allow', list: [EAuthCountry.新北] }, main: { type: 'block', list: [] }, displayName: '第二頁', } } ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { } ``` 一般在我們產生 `routing.module` 時,系統會預設 `routes` 的型別為 `Routes`。 那由於希望大家在設定 `routes` 時可以加上統一的參數來設定權限,讓特定的工廠可以實現權限功能,所以就擴充了 `Routes` 的型別,讓 Angular 來幫我們檢查權限參數是否正確。 **scr/app/route/route.type.ts** ```typescript= import { IAuthRoute } from "./auth-route.interface"; import { INameRoute } from "./name-route.interface"; export type NameRoutes = INameRoute[]; export type AuthRoutes = IAuthRoute[]; export type AuthNameRoute = (INameRoute & IAuthRoute); export type AuthNameRoutes = AuthNameRoute[] ``` 從上面可以看到,`AuthNameRoute` 是由 `INameRoute` 及 `IAuthRoute` 組合而成的。 * `INameRoute` 提供顯示名稱 **scr/app/route/name-route.interface.ts** ```typescript= import { Route } from '@angular/router'; export interface INameRoute extends Route { data?: { displayName: string; } } ``` **參數說明 :** `displayName`: 顯示名稱 * `IAuthRoute` 提供權限設定 **scr/app/route/auth-route.interface.ts** ```typescript= import { Route } from '@angular/router'; import { EAuthCountry, EAuthMain, IAuth } from './../auth/auth.interface'; export interface IAuthRoute extends Route { data?: { country: IAuth<EAuthCountry>; main: IAuth<EAuthMain>; } } ``` **參數說明 :** `country`: 縣市黑白名單設定 `main`: 權限黑白名單設定 :::info 💡小補充 `IAuth<AE>` : 黑/白名單設定型別。 ```typescript export interface IAuth<AE> { type: 'allow' | 'block'; list: AE[]; } ``` * type, 'allow': 白名單 | 'block': 黑名單 * list, 名單列表 📝小備註 預設不阻擋的寫法: `{ type: 'block', list: [] }` ::: ### api-table-factory.ts #### 🌱說明 * 提供表單有關 API 的處理方法 #### 🌱原始碼 ```typescript= import { Injectable } from "@angular/core"; import { concat, Observable, of } from "rxjs"; import { concatMap, tap } from "rxjs/operators"; import { IFactory } from '../../dynamic/factory.interface'; import { IField } from "../table-a/table-a.component"; import { ITable } from "../table.interface"; export interface IApiTablePara { read$: Observable<any>[]; fieldList: IField[]; } @Injectable({ providedIn: 'root' }) export class ApiTableFactory<CI extends ITable<IField> = any> implements IFactory<CI, IApiTablePara> { constructor( ) { } run(component: CI, para?: IApiTablePara) { if (!para?.read$?.length) { return } this.getDataListByRead$(para.read$).subscribe( (dataList) => { component.fieldList = para.fieldList; component.dataList = this.combineData(component.dataList, dataList) } ); } checkPara(obj: IApiTablePara): IApiTablePara { return obj; } getDataListByRead$(read$: Observable<any>[]): Observable<any> { return concat(read$).pipe( concatMap((d$) => d$), ) } combineData<D>(data: D[], concatData: D[]) { data = data ? data : []; return data.concat(concatData); } // CRUD onCreate(component: CI, data: any): void { // this.dataList.push(data); component.dataList.push(data); } onRead(component: CI, read$: Observable<any>[]): void { component.dataList = []; concat(read$).pipe( concatMap((d$) => d$), ).subscribe((data) => { component.dataList = component.dataList.concat(data) }) } onUpdate(component: CI, uuid: string, data: any): void { const dataIndex = component.dataList.findIndex(d => d.uuid === uuid); component.dataList[dataIndex] = data; } onDelete(component: CI, uuid: string): void { const dataIndex = component.dataList.findIndex(d => d.uuid === uuid); component.dataList.splice(dataIndex, 1); } } ``` #### 🌱應用說明 大家在使用表單時,通常都會需要實現 CRUD 的功能,為了要減少撰寫重複的 CRUD 功能,希望可以透過 `ApiTableFactory` 提供基本的方法達到此目的。 :::warning ⭐小提醒 `Factory` 主要還是只提供`共通`、`常用`的功能,真正實現完整的需求還是需要各自在子元件上實現。 ::: **page01.component.ts** ```typescript= import { IMenu } from './../../menu/menu.interface'; import { WebapiService } from './../../service/webapi.service'; import { ApiTableFactory } from '../../table/factory/api-table.factory'; import { TableAComponent } from './../../table/table-a/table-a.component'; import { HomeMenuFactory } from '../../menu/factory/home-menu.factory'; import { MenuAComponent, IMenuA } from './../../menu/menu-a/menu-a.component'; import { DynamicFactory } from './../../dynamic/dynamic-factory'; import { DynamicDirective } from './../../dynamic/dynamic.directive'; import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core'; import { RouteMenuFactory } from 'src/app/menu/factory/route-menu.factory'; import { MenuBComponent } from 'src/app/menu/menu-b/menu-b.component'; import { of } from 'rxjs'; import { areAllEquivalent } from '@angular/compiler/src/output/output_ast'; import { conditionallyCreateMapObjectLiteral } from '@angular/compiler/src/render3/view/util'; @Component({ selector: 'app-page01', templateUrl: './page01.component.html', styleUrls: ['./page01.component.scss'] }) export class Page01Component implements OnInit, AfterViewInit { @ViewChild('menu', { read: DynamicDirective }) menu: DynamicDirective<MenuAComponent>; @ViewChild('table', { read: DynamicDirective }) table: DynamicDirective<TableAComponent>; constructor( private dynamicF: DynamicFactory, private routeMenuF: RouteMenuFactory, private homeMenuF: HomeMenuFactory, private apiTableF: ApiTableFactory, private webApiS: WebapiService, ) { } ngOnInit(): void { } ngAfterViewInit(): void { this.createMenu(); this.createTable(); } createMenu() { this.menu.component = this.dynamicF.create(this.menu, MenuBComponent, this.routeMenuF); } createTable() { const apiTablePara = { read$: [this.webApiS.getData(), this.webApiS.getData()], fieldList: [ { key: 'uuid', displayName: '序', isHide: false, }, { key: 'name', displayName: '名稱', isHide: false, }, { key: 'money', displayName: '金額', isHide: false, } ], } // this.table.component = this.dynamicF.create(this.table, TableAComponent, this.apiTableF, this.apiTableF.checkPara(apiTablePara)) this.table.component = this.dynamicF.create(this.table, TableAComponent, this.apiTableF, this.apiTableF.checkPara(apiTablePara)) } onClickC() { this.apiTableF.onCreate(this.table.component, {uuid: '3', name: '莫菲斯', money: '450'}); } onClickR() { this.apiTableF.onRead(this.table.component, [this.webApiS.getData(), this.webApiS.getData()]); } onClickU() { const newData = {uuid: '4', name: '貝拉', money: '480'} const data = this.table.component.dataList[0]; this.apiTableF.onUpdate(this.table.component, data.uuid, newData); } onClickD() { const data = this.table.component.dataList[0]; this.apiTableF.onDelete(this.table.component, data.uuid); } } ``` **page01.component.html** ```htmlembedded= <alle-dynamic #menu></alle-dynamic> <button (click)="onClickC()">新增</button> <button (click)="onClickD()">刪除</button> <button (click)="onClickU()">修改</button> <button (click)="onClickR()">查詢</button> <alle-dynamic #table></alle-dynamic> ``` ### 小試身手 1. 請在 page01.component.ts 使用 TableAComponent 來實作表單功能。 2. 請在 page02.component.ts 自行實現下方需求 - 產生 MenuBComponent , 並將page02 選單名稱改成「Hello World!」 - 建立四個按鈕來實現 CRUD 功能 - 產生 TableAComponent **(示意圖) 非實際畫面** ![](https://i.imgur.com/JVVn6Is.png) <!-- ![](https://i.imgur.com/GpUjH6F.png) -->