選單
GSS 技術部落格
在這個園地裡我們將從技術、專案管理、客戶對談面和大家分享我們多年的經驗,希望大家不管是喜歡或是有意見,都可以回饋給我們,讓我們有機會和大家對話並一起成長!
若有任何問題請來信:gss_crm@gss.com.tw
8 分鐘閱讀時間 (1552 個字)

Angular #17 - 動手建個 dashboard [2]

shutterstock_198004562
  • 目前為止我們完全沒有針對傳入 @Input 屬性的資料作任何的消毒(Sanitization),那麼實際上該怎麼做呢?

    • Getter/Setter(484 很眼熟)

      • 我們可以攔截要消毒的欄位

      • 攔截指的是把 @Input 的 binding 對應到一個 setter 方法, 其實作是將真正的值 存放在一個 private property, 換言之,也是封裝的一種

      • 另一方面 getter 方法, 回傳的也是上方提及的 private property 的值

      • 事不宜遲,趕快來改造一下 MetricComponent 的 Controller:

          import { Component, Input } from '@angular/core';
        
          @Component({
            selector: 'app-metric',
            templateUrl: './metric.component.html',
            styleUrls: ['./metric.component.css']
          })
          export class MetricComponent {
            private _value: number = 0;
            private _max: number = 100;
        
            @Input() title: string = '';
            @Input() description: string = '';
        
            @Input('used')
            set value(value: number) {
              if(isNaN(value)) value = 0;
              this._value = value;
            }
            get value() { return this._value; }
        
            @Input('available')
            set max(max: number) {
              if(isNaN(max)) max = 100;
              this._max = max;
            }
            get max(){ return this._max; }
        
            isDanger() {
              return this.value / this.max > 0.7;
            }
          }
        • 順帶一提,Setter 跟 Getter 不是 Typescript 的東西,而是 ES2015 開始就有的要素,可以參考 MDN
        • Setter 或 Getter 只要在方法名稱前面加上 setget 就好
        • 此外,方法的名稱必須跟 property 是一致的,綜合上面的設定,只要有任何地方用到 this.value 就會使得 get value(){} 這個方法被呼叫
        • 調整後的 Controller 最上方定義了兩個 private 的屬性,分別是 _value_max(加底線是一種不成文的規定),Typescript 會確保這兩個屬性的值不會被公開(外部無法直接存取)
        • 這個方法只有一點要注意的,如果你的 setter/getter 過於複雜,那可能會對效能造成一些衝擊
  • Content Projection(※很重要)

    • 假設你有一個 CardComponent,而且想讓它的內容是能海納百川,放入任何內容都行的話,該怎麼辦?

      • 這就是 Content Projection 派上用場的時候了(還記得前面提到的 ViewChild/Content Child 嗎?)
      • Tabs/Tab, Table/TableRows, Modals, Dialogs 都適用同樣的情境
    • 我們既將在這個 Dashboard 新增兩個 Component,分別是 NodesNodes-Row

      • 如同往常,透過 Angular-CLI 建立吧!
          ng g c nodes
          ng g c nodes-row
      • 接著替 Nodes 的 Template 填入以下的內容
          <thead>
            <tr>
              <th>Node</th>
              <th [colSpan]="2">CPU</th>
              <th [colSpan]="2">Memory</th>
              <th>Details</th>
            </tr>
          </thead>
          <ng-content></ng-content>
        • 特別留意這裡有一個 property 綁定,colSpan,但其實 Table 的 attribute 是 colspan(全小寫),而這種 property 綁定的使用時機通常是其對應到的值是動態的(e.g. 方法回傳的值),而非像範例的靜態值(這邊只是舉例才故意這樣用)
        • Node 的 Template 中可以看到 NgContent 這個 Directive 被拿出來用了,目的就是建立一個插入點
        • 定義了插入點,然後呢?其使用方法就是在使用 Node 的 selector 時,將要插入的內容以巢狀的方式在 Node 內
      • 在開始插入前,先作一個小調整: 把
          selector: 'app-nodes'
        改成
          selector: '[app-nodes]'
        • 這個小調整會在渲染時以 attribute 當 selector 而非平常的以 element 當作 selector
    • Nodes 已經完成了 table header 的部份,接著要處理 row 的部份

      • 加入以下的內容到 nodes-row 的 template

          <th scope="row">{{ node.name }}</th>
          <td [class.table-danger]="isDanger('cpu')">
            {{ node.cpu.used }}/{{ node.cpu.available }}
          </td>
          <td [class.table-danger]="isDanger('cpu')">
            ({{ node.cpu.used / node.cpu.available | percent }})
          </td>
          <td [class.table-danger]="isDanger('mem')">
            {{ node.mem.used }}/{{ node.mem.available }}
          </td>
          <td [class.table-danger]="isDanger('mem')">
            ({{ node.mem.used / node.mem.available | percent }})
          </td>
          <td><button class="btn btn-secondary">View</button></td>
        • 有一個特殊的 CSS 綁定(還記得嗎?)
        • [class.table-danger] 指的是 table-danger 這個 class 會依 isDanger 這個方法的結果決定要不要 enable
      • 有了 Nodes 與 Nodes-row,接著就要塞資料了,資料從哪來? 當然是 DashboardComponent(資料型),所以要定義 nodes-row.component.ts 如下:

          import { Component, Input } from '@angular/core';
        
          @Component({
            selector: '[app-nodes-row]',
            templateUrl: './nodes-row.component.html',
            styleUrls: ['./nodes-row.component.css']
          })
          export class NodesRowComponent {
        
            constructor() { }
        
            @Input() node: any;
        
            isDanger(prop) {
              return this.node[prop].used / this.node[prop].available > 0.7;
            }
        
          }
        • 留意我們再一次使用 attribute selector
        • 有了 node 這個 @Input 屬性,這個 component 就可以接收資料了
    • 快要大功告成了,只差 Dashboard

      • 調整其Template 加上以下的內容
          <div class="container mt-2">
            <div class="card card-block">
              <div class="card-body">
                <nav class="navbar navbar-dark bg-inverse mb-1">
                  <h1 class="navbar-brand mb-0">Cluster 1</h1>
                </nav>
                <table app-nodes class="table table-hover">
                  <tr app-nodes-row *ngFor="let node of cluster1" [node]="node"></tr>
                </table>
                <nav class="navbar navbar-dark bg-inverse mb-1">
                  <h1 class="navbar-brand mb-0">Cluster 2</h1>
                </nav>
                <table app-nodes class="table table-hover">
                  <tr app-nodes-row *ngFor="let node of cluster2" [node]="node"></tr>
                </table>
              </div>
            </div>
          </div>
        • app-nodes 是以 attribute 的型式用在 table 這個元素當作屬性的,這在渲染的時候會長出 header 並帶上一個插入點
        • 此外,app-nodes-row 被設定為 tr 元素的 attribute, 而且被巢狀放在 app-nodes 的標籤內。也就是說 app-nodes-row 填滿了插入點,會在 header 底下長出 tr(看到那華麗的 *ngFor 了嗎?)
        • 資料是從 Dashboardcomponent 傳下來的(cluster1, cluster2) 並設定到 Nodes-row Component 的 node 屬性
    • 如果我們需要一個以上的插入點呢?

      • 這還不簡單,插入點都命個名就好了!
      • 當作示範,我們來重構一下 MetricComponent 的 Template,將 NgContent 用在其 title 與 description 上
          <div class="card card-block">
            <div class="card-body">
              <nav
                class="navbar navbar-dark bg-primary mb-1"
                [ngClass]="{ 'bg-danger': isDanger(), 'bg-success': !isDanger() }"
              >
                <h1 class="navbar-brand mb-0">
                    <ng-content select="metric-title"></ng-content>
                </h1>
              </nav>
              <h4 class="card-title">
                {{ value }}/{{ max }} ({{ value / max | percent: "1.0-2" }})
              </h4>
              <p class="card-text">
                <ng-content select="metric-description"></ng-content>>
              </p>
              <ngb-progressbar
                [value]="value"
                [max]="max"
                [type]="isDanger() ? 'danger' : 'success'"
              ></ngb-progressbar>
            </div>
          </div>
      • 可以看到 NgContent 的區塊有兩個,並透過 select 給定名稱
      • 因為 Template 調整了,所以記得移除多餘的屬性,不然編譯不會過喔
          // Remove these two declarations
          @Input() title: string = '';
          @Input() description: string = '';
      • 再來就是再多加東西到插入點內
          ...(略)
          <app-metric
              class="col-sm-6"
              [used]="cpu.used"
              [available]="cpu.available"
              [title]="'CPU'"
              [description]="'utilization of CPU cores'"
          >
              <metric-title>CPU</metric-title>
              <metric-description>utilization of CPU cores</metric-description>
          </app-metric>
          <app-metric
              class="col-sm-6"
              [used]="mem.used"
              [available]="mem.available"
              [title]="'Memory'"
              [description]="'utilization of memory in GB'"
          >
              <metric-title>Memory</metric-title>
              <metric-description>utilization of memory in GB</metric-description>
          </app-metric>
          ...(ellipsised)
        • 看看這神奇的一幕,metric-titlemetric-description 被巢狀用在 app-metric
        • 就前面的說明,你應該能明白那兩個元素會被插入 MetricComponent 所定義的插入點
      • 然而,如果你執行測試 server,會發現畫面一片空白,透過開發者工具還會看到錯誤訊息
      • 這是因為並沒有 metric-titlemetric-description 這兩個 Component。解決的方式就是透過修改 app.module.ts 的內容如下:
          import { NgModule, NO_ERRORS_SCHEMA } from '@angular/core';
          // ...(ellipsised)
          bootstrap: [AppComponent],
          schemas: [NO_ERRORS_SCHEMA]
        • 在 NgModule 的 metadata 中加入一個 schemas 的陣列,並加入一個元素 NO_ERRORS_SCHEMA
        • 承上,這個的作用在於讓 Angular 不再針對沒有定義的 Component 發出錯誤訊息,並且允許依命名建立插入點
        • 重新整理就可以看到畫面正常運作如下了:
  • 以上,就是一個真真假假,假假真真的 Dashboard

    • 看似簡單,其實內容學問很大,不過先做個小結吧!
      • 如果要對 @Input 作消毒,getter/setter 是一個方法
      • 當要動態在一個 Component 內塞入內容時,可以透過 Content Projection
Angular #18 - 深入 Components [1]
Angular # 16 - 動手建個 dashboard [1]

相關文章

 

評論

尚無評論
已經注冊了? 這裡登入
Guest
2024/05/18, 週六

Captcha 圖像