﻿# 認識 Angular Library 函式庫專案並學會自製 Angular 表單驗證器模組

當 Angular 越用越熟，你將會發現其開發效率極高無比，除了極佳的工具支援外，優異的模組化技術更是不在話下。上週在教 [Angular 7 開發實戰：進階開發篇](https://www.accupass.com/event/1904160911391132674338) 的時候，有學員許願，說希望我能分享一些 Angular 函式庫的作法，說著說著就這麼願望成真了！ 😆 當越來越多專案都用 Angular 開發時，你一定會發現有越來越多的共用元件出現。本篇文章我將設計出一款同時讓 Reactive Forms 與 Template-driven Forms 皆可使用的表單驗證器，並完整介紹如何用 Angular CLI 從頭到尾建立並發行一個自己的 Angular Library 函式庫！

![](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/7bccc2ffe3f5fcc9c607-56409206-8e20fe00-62aa-11e9-8c3f-21eca94dcc24.png)  
Photo by [Janko Ferlič](https://unsplash.com/photos/sfL_QOnmy00?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) on [Unsplash](https://unsplash.com/search/photos/library?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)

### 簡介 Angular CLI 建立專案過程

Angular CLI 從 v6 版本開始，使用 `ng new` 建立專案的時候，預設採用多專案架構進行管理，你可以從 `angular.json` 中看見一個 `projects` 節點，這裡就是定義多專案的設定。在這樣的架構下，我們可以很容易的採用 [monorepo](https://en.wikipedia.org/wiki/Monorepo) 來管理專案，讓多個專案在同一個 Git Repo 下進行開發。

不過，使用 `ng new` 建立專案時就會建立一個預設的 Angular 應用程式專案，目錄置於 `src/` 與 `e2e/` 目錄下。

如果我們只想要針對特定目的來撰寫一個 Angular Library (函式庫)，那麼你很有可能不想在建立專案時也建立預設 Angular 專案。此時，你可以使用 Angular CLI v7 新增的 `--create-application=false` 參數來建立新專案，這個建立的過程，就不會直接建立 Angular 應用程式，專案架構比較乾淨。

以下是建立 `ng-compare-equal-validator` 專案的命令：

```bash
ng new ng-compare-equal-validator --create-application=false --routing false --style css
```

建立完成後，你可以看到 `angular.json` 的內容非常乾淨：

```json
{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "version": 1,
  "newProjectRoot": "projects",
  "projects": {}
}
```

此時如果才想要加入專案進去，就可以用以下命令建立應用程式：

```bash
ng g application demo1 --routing
```

> ※ 建立應用程式時，專案預設會建立在 `./projects` 目錄下！

### 建立 Angular Library 函式庫專案

接下來我們就來一步步建立本次文章所需建立的 Angular 表單驗證器專案！

1.  建立專案骨架 (Monorepo)
    
    ```bash
    ng new ng-validators --create-application=false --routing false --style css
    cd ng-validators
    code .
    ```
    
2.  建立 Angular Library 函式庫專案
    
    對 Angular 應用程式來說，通常所有 Comeponts 與 Directives 都會以 `app-` 作為前置詞(prefix)，但開發給任意專案共用的函式庫，通常不會這樣命名。例如我公司的英文名稱為 Duotify，我就可以將前置詞改為 `dt-` 為主，那麼這個時候你可以這樣執行：
    
    ```bash
    ng g library dt-compare-equal-validator --prefix=dt
    ```
    
3.  建立 Angular 應用程式專案 (用來測試我們撰寫的函式庫元件之用)
    
    ```bash
    ng g application demo1 --routing --style css --skip-tests
    ```
    

### 使用函式庫與啟動 Angular 應用程式

目前為止，我們建立了三個專案：

1.  `dt-compare-equal-validator` 函式庫專案
2.  `demo1` 這個用來測試函式庫的 Angular 應用程式專案
3.  `demo1-e2e` 這個 Angular 應用程式附帶的 E2E 測試專案

此時你可以測試看看，使用 `npm run build` 或 `ng build` 就會自動建置 `dt-compare-equal-validator` 專案，那是因為在 `angular.json` 檔案中有個 `"defaultProject": "dt-compare-equal-validator"` 設定，因此預設會建置這個專案。

> 請注意：從 Angular CLI v6.1 開始，建置 Angular Library 預設就是採用 `--prod` 生產環境建置！

如果你想要建置 `demo1` 專案的話，可以試試 `ng build demo1` 或 `ng build --project demo1` 這個命令，就會自動對 `demo1` 專案進行建置！

這裡有個蠻值得一提的地方，就是專案根目錄下的 `tsconfig.json` 檔案，裡面有個 `"paths"` 區段設定，替 TypeScript 的編譯器指出 `import` 路徑的別名：

```json
"paths": {
  "dt-compare-equal-validator": [
    "dist/dt-compare-equal-validator"
  ],
  "dt-compare-equal-validator/*": [
    "dist/dt-compare-equal-validator/*"
  ]
}
```

也就是說，在這個 monorepo 專案中，任何一個子專案都可以透過 `'dt-compare-equal-validator'` 來 import 函式庫中的任何模組或服務元件：

```typescript
import { DtCompareEqualValidatorModule } from 'dt-compare-equal-validator';
```

所以為了讓我們的 Angular Library 專案可以被匯入到 Angular 應用程式之中，你可以先修改 `demo1` 專案內的 `app.module.ts` 檔案，並將 `DtCompareEqualValidatorModule` 模組匯入到預設的 `AppModule` 之中。

```typescript
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';

import { DtCompareEqualValidatorModule } from 'dt-compare-equal-validator';

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

如此一來，就完成了一個基本的 Monorepo 架構，並設定好模組匯入。

最後，我們需要確認專案可以正常建置，有幾件事需要提醒各位：

1.  所有的 Angular Library 都需要建置後才能被 Angular 應用程式開發時參考
    
    你可以用 `ng build dt-compare-equal-validator --watch` 命令自動建置專案，該命令會自動建置任意檔案變更，並自動重新建置此函式庫。
    
2.  執行 Angular 應用程式
    
    ```bash
    ng serve demo1
    ```
    
3.  如果要跑單元測試
    
    ```bash
    ng test dt-compare-equal-validator
    ng test demo1
    ```
    

> 由於專案根目錄下的 `tsconfig.json` 的 `"paths"` 屬性設定過 `dt-compare-equal-validator` 對應到 `dist/dt-compare-equal-validator` 目錄，因此所有函式庫都必須先建置過，才有可能讓其他專案參考到。但這個預設值設定，很容易造成問題，例如你可能會常遇到 `dt-compare-equal-validator` 建置失敗或 VS Code 運作異常等情況。此時建議你將 `"paths"` 調整如下，直接從 Angular 應用程式專案中參考 Library 目錄下的原始碼，如此一來就不用先建置 Library 專案就能跑了：
> 
> ```json
> "paths": {
>   "dt-compare-equal-validator": [
>     "projects/dt-compare-equal-validator/src/public-api.ts"
>   ],
>   "dt-compare-equal-validator/*": [
>     "projects/dt-compare-equal-validator/src/*"
>   ]
> }
> ```
> 
> 這樣的設定最大的缺點，就是當你真的想要建置 Library 專案時，必須手動將設定改回來，這樣才能建置出可以發行到 npm registry 的 npm 套件。

### 認識 Angular Library 函式庫專案

我們剛剛建立的 `dt-compare-equal-validator` 函式庫專案，有幾個重要的檔案，分別介紹如下：

-   `src/public-api.ts`
    
    這個檔案定義了本函式庫對外公開的所有類別或變數。
    
-   `src/lib/dt-compare-equal-validator.component.ts`
    
    這是個 Angular 共用 UI 元件範例 (Component)
    
-   `src/lib/dt-compare-equal-validator.service.ts`
    
    這是個 Angular 共用服務元件範例 (Service)
    
-   `src/lib/dt-compare-equal-validator.module.ts`
    
    這是個 Angular 共用模組範例，所有需要對外公開的元件都必須註冊在這裡。如果未來有新增元件，也都需要註冊在這個檔案內 `@NgModule` 的 `declarations` 與 `export` 屬性。
    

### 自製 Angular 表單驗證器 (Reactive Forms)

我先寫出 Reactive Forms 可用的表單驗證器，並透過以下步驟進行設計：

1.  修改 `src/lib/dt-compare-equal-validator.module.ts` 匯入 `ReactiveFormsModule`
    
    ```typescript
    import { NgModule } from '@angular/core';
    import { DtCompareEqualValidatorComponent } from './dt-compare-equal-validator.component';
    import { ReactiveFormsModule } from '@angular/forms';
    
    @NgModule({
      declarations: [DtCompareEqualValidatorComponent],
      imports: [ReactiveFormsModule],
      exports: [DtCompareEqualValidatorComponent]
    })
    export class DtCompareEqualValidatorModule {}
    ```
    
2.  寫出一個 Reactive Forms 使用的驗證器函式 (Function)
    
    建立 `src/lib/compareEqual.ts` 檔案，內容如下：
    
    ```typescript
    import {
      ValidatorFn,
      AbstractControl,
      ValidationErrors
    } from '@angular/forms';
    
    export function compareEqual(controlName: string): ValidatorFn {
      return (control: AbstractControl): ValidationErrors | null => {
        if (
          !control.parent ||
          control.parent.get(controlName).value === control.value
        ) {
          return null;
        } else {
          return { compareEqual: true };
        }
      };
    }
    ```
    
3.  修改 `src/public-api.ts` 公開這個 `compareEqual` 驗證器函式
    
    ```typescript
    /*
     * Public API Surface of dt-compare-equal-validator
     */
    
    export * from './lib/dt-compare-equal-validator.service';
    export * from './lib/dt-compare-equal-validator.component';
    export * from './lib/dt-compare-equal-validator.module';
    export * from './lib/compareEqual';
    ```
    
4.  我們來測試一下是否可以正常匯入 `compareEqual` 函式使用
    
    由於我們之前就先執行了 `ng build dt-compare-equal-validator --watch` 命令，修改程式碼的過程其實也一直在建置這個函式庫專案。
    
    > 有時候 `ng build dt-compare-equal-validator` 會執行失敗，只要關閉正在編輯的檔案，就可以順利進行建置。
    
    此時你可以直接開啟 `demo1` 專案的 `app.component.ts` 檔案，看能不能在 VSCode 中順利 `import` 這個 `compareEqual` 函式：
    
    ```typescript
    import { compareEqual } from 'dt-compare-equal-validator';
    ```
    
    ![](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/026ba646b255fd4528dd-image_3196.png)
    
5.  最後我們將 AppComponent 的表單功能完成
    
    -   `app.component.ts`
        
        ```typescript
        import { Component, OnInit } from '@angular/core';
        import { compareEqual } from 'dt-compare-equal-validator';
        import { FormBuilder, FormGroup, Validators } from '@angular/forms';
        
        @Component({
          selector: 'app-root',
          templateUrl: './app.component.html',
          styleUrls: ['./app.component.css']
        })
        export class AppComponent implements OnInit {
          form: FormGroup;
          constructor(private fb: FormBuilder) {}
          ngOnInit(): void {
            this.form = this.fb.group({
              pw: ['', [Validators.required]],
              pw2: ['', [Validators.required, compareEqual('pw')]]
            });
          }
        }
        ```
        
    -   `app.component.html`
        
        ```html
        <form [formGroup]="form">
          <div>輸入密碼: <input type="text" formControlName="pw" /></div>
          <pre>{{ this.form.get('pw').errors | json }}</pre>
          <div>確認密碼: <input type="text" formControlName="pw2" /></div>
          <pre>{{ this.form.get('pw2').errors | json }}</pre>
        </form>
        ```
        
    
    > 程式撰寫完成後，很有可能會遇到以下錯誤：
    > 
    > ![](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/e7ce946a636d9d3e66f5-image_3197.png)
    > 
    > 那是因為 Angular CLI 開發伺服器無法偵測到函式庫的新版本，這時只要重新啟動 Angular CLI 開發伺服器 (`npm start`) 即可解決。有時候 VSCode 也會發生問題，也需要重開才能解決。
    

### 自製 Angular 表單驗證器 (Template-driven Forms)

接著我們將 Reactive Forms 的表單驗證器進行 Directive 元件封裝，以便讓 Angular 元件的範本使用。

以下是設計的過程：

1.  先在 `dt-compare-equal-validator` 專案下建立一個名為 `CompareEqualDirective` 的元件
    
    ```bash
    ng g directive compare-equal --project dt-compare-equal-validator
    ```
    
    該元件預設的**選取器**將會是 `[dtCompareEqual]`
    
2.  然後將該元件類別註冊到 `src\lib\dt-compare-equal-validator.module.ts` 的 `exports`，同時也將沒用到的 `DtCompareEqualValidatorComponent` 預設元件移除。
    
    ```typescript
    import { NgModule } from '@angular/core';
    import { FormsModule } from '@angular/forms';
    import { CompareEqualDirective } from './compare-equal.directive';
    
    @NgModule({
      declarations: [CompareEqualDirective],
      imports: [FormsModule],
      exports: [CompareEqualDirective]
    })
    export class DtCompareEqualValidatorModule {}
    ```
    
3.  接著將驗證器元件完成
    
    這裡必須實作 `Validator` 介面與其 `validate(control: AbstractControl)` 方法。
    
    還有，我們必須傳入要比對的欄位名稱，因此必須傳入一個 `dtCompareEqual` 屬性，我們透過 `@Input()` 宣告這個屬性是個傳入參數即可。
    
    ```typescript
    import { Directive, forwardRef, Input } from '@angular/core';
    import {
      NG_VALIDATORS,
      Validator,
      AbstractControl,
      ValidationErrors
    } from '@angular/forms';
    import { compareEqual } from './compareEqual';
    
    const COMPARE_EQUAL_VALIDATOR: any = {
      provide: NG_VALIDATORS,
      useExisting: forwardRef(() => CompareEqualDirective),
      multi: true
    };
    
    @Directive({
      selector: '[dtCompareEqual]',
      providers: [COMPARE_EQUAL_VALIDATOR]
    })
    export class CompareEqualDirective implements Validator {
      @Input() dtCompareEqual: string;
      constructor() {}
      validate(control: AbstractControl): ValidationErrors {
        return compareEqual(this.dtCompareEqual)(control);
      }
    }
    ```
    
4.  然後將該 `CompareEqualDirective` 類別註冊到 `src/public-api.ts`
    
    ```typescript
    /*
     * Public API Surface of dt-compare-equal-validator
     */
    
    export * from './lib/dt-compare-equal-validator.service';
    export * from './lib/dt-compare-equal-validator.component';
    export * from './lib/dt-compare-equal-validator.module';
    export * from './lib/compareEqual';
    export * from './lib/compare-equal.directive';
    ```
    
5.  最後我們將 AppComponent 的表單功能完成
    
    -   `app.module.ts`
        
        ```typescript
        import { BrowserModule } from '@angular/platform-browser';
        import { NgModule } from '@angular/core';
        
        import { AppRoutingModule } from './app-routing.module';
        import { AppComponent } from './app.component';
        
        import { DtCompareEqualValidatorModule } from 'dt-compare-equal-validator';
        import { FormsModule } from '@angular/forms';
        
        @NgModule({
          declarations: [AppComponent],
          imports: [
            BrowserModule,
            FormsModule,
            AppRoutingModule,
            DtCompareEqualValidatorModule
          ],
          providers: [],
          bootstrap: [AppComponent]
        })
        export class AppModule {}
        ```
        
    -   `app.component.ts`
        
        ```typescript
        import { Component, OnInit } from '@angular/core';
        
        @Component({
          selector: 'app-root',
          templateUrl: './app.component.html',
          styleUrls: ['./app.component.css']
        })
        export class AppComponent implements OnInit {
          pw: string;
          pw2: string;
          constructor() {}
          ngOnInit(): void {}
        }
        ```
        
    -   `app.component.html`
        
        ```html
        <form>
          <div>
            輸入密碼:
            <input
              type="text"
              name="pw"
              #tPw="ngModel"
              [(ngModel)]="pw"
              required
            />
          </div>
          <pre>{{ tPw.errors | json }}</pre>
          <div>
            確認密碼:
            <input
              type="text"
              name="pw2"
              #tPw2="ngModel"
              [(ngModel)]="pw2"
              required
              dtCompareEqual="pw"
            />
          </div>
          <pre>{{ tPw2.errors | json }}</pre>
        </form>
        ```
        

### 將 Angular 函式庫封裝成 npm 套件

如果要將我們剛剛開發的 Angular 函式庫封裝成 npm 套件，這時就需要設定一下 npm scripts，方便我們日後可以方便的進行套件打包。

要將我們開發好的驗證器函式庫封裝，基本上需要進行以下兩個命令：

```bash
ng build dt-compare-equal-validator
cd dist/dt-compare-equal-validator && npm pack
```

這個過程會建立 `dt-compare-equal-validator-0.0.1.tgz` 壓縮檔，如果是在我們目前的 Monorepo 內進行開發，是完全不需要額外安裝的，原因是因為我們已經設定 `tsconfig.json` 裡面的路徑對應 ( `"paths"` )。但如果想到其他 Angular 專案使用這個函式庫，那就可以執行以下命令直接安裝起來：

```bash
npm install path/to/dt-compare-equal-validator-0.0.1.tgz
```

任何一個 Angular 專案，只要匯入 `dt-compare-equal-validator` 模組，就可以立即使用必要的函式與元件！

### 將 Angular 函式庫的 npm 套件發行至 npm registry

要發行 npm 套件到 [npm registry](https://www.npmjs.com/) 通常需要注意以下事項：

1.  決定套件名稱
    
    請開啟 `projects\dt-compare-equal-validator\package.json` 檔案，設定 `name` 與 `version` 屬性。這裡的 `name` 屬性必須是**全世界唯一**的名稱，不能跟現有的 npm 套件衝突。
    
    如果可以的話，請在 `projects\dt-compare-equal-validator\package.json` 額外填上 `license`、`repository`、`description`、`keywords` 與 `homepage` 資訊！詳細說明文件參見：[npm-package.json | npm Documentation](https://docs.npmjs.com/files/package.json)
    
2.  決定套件版本
    
    由於每次發佈版本時，版本號都不能重複，因次每次發行都應該要決定你想用的版本編號。
    
    這邊可以用 `npm version patch`、`npm version minor` 或 `npm version major` 來自動變更版號，詳情請見 [npm-version](https://docs.npmjs.com/cli/version.html) 說明。
    
3.  決定授權條款
    
    你可以在 `projects\dt-compare-equal-validator\` 目錄下建立一個 `LICENSE` 檔案，並將你想要的授權條款放進去。例如我這個函式庫，打算以 [MIT License](https://choosealicense.com/licenses/mit/) 進行授權，就可以將授權內容複製進去。
    
    > 你可以到 [Choose a License](https://choosealicense.com/) 網站選擇一個適合的授權條款。
    
4.  撰寫 README.md 說明文件
    
5.  封裝 npm 套件
    
    ```bash
    ng build dt-compare-equal-validator
    cd dist/dt-compare-equal-validator && npm pack
    ```
    
    > 注意：新版的 Angular CLI 已經會自動將 LICENSE 與 README.md 複製到發行目錄。
    
6.  發行 npm 套件
    
    其實任何人都可以發佈 npm 套件，只要到 [npm](https://www.npmjs.com/) 網站註冊會員即可。
    
    註冊好之後，可以用 `npm login` 命令進行登入，如果已經登入過，則可以用 `npm whoami` 查詢目前登入的身分。
    
    接著就只要進入 `dist/dt-compare-equal-validator` 目錄執行 `npm publish` 即可發佈套件！
    

### 線上測試

如果想在線上看看 [dt-compare-equal-validator](https://www.npmjs.com/package/dt-compare-equal-validator) 套件的使用方式，可以連到 [StackBlitz](https://stackblitz.com/) 網站，進入以下網址直接看測試結果即可：

-   [https://stackblitz.com/edit/dt-compare-equal-validator-sample](https://stackblitz.com/edit/dt-compare-equal-validator-sample)

### 開放原始碼

本文章的所有版控紀錄，也已經放上 [GitHub Repo](https://github.com/doggy8088/ng-validators) 給大家參考。

### 相關連結

-   [Angular Workspace: No Application for You! – Angular In Depth](https://blog.angularindepth.com/angular-workspace-no-application-for-you-4b451afcc2ba)
-   [The Angular Library Series — Creating a Library with the Angular CLI](https://blog.angularindepth.com/creating-a-library-in-angular-6-87799552e7e5)
-   [The Angular Library Series — Building and Packaging](https://blog.angularindepth.com/creating-a-library-in-angular-6-part-2-6e2bc1e14121)
-   [The Angular Library Series — Publishing](https://blog.angularindepth.com/the-angular-library-series-publishing-ce24bb673275)
-   [Choose an open source license | Choose a License](https://choosealicense.com/)
-   [dt-compare-equal-validator - npm](https://www.npmjs.com/package/dt-compare-equal-validator)
