---
# System prepended metadata

title: "\U0001F954基於工廠模式實現的應用 (二) 應用篇"
tags: [Angular]

---

###### 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) -->
