iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 11
0
Modern Web

Angular新手村學習筆記(2019)系列 第 11

Day11_土炮 Tab 元件 - 使用 ControlValueAccessor

  • 分享至 

  • xImage
  •  

因為參賽的進度壓力,後面有時間再回來補程式碼註解

[S06E10] 土炮 Tab 元件 - 使用 ControlValueAccessor

https://www.youtube.com/watch?v=282ITUdC12Y&list=PL9LUW6O9WZqgUMHwDsKQf3prtqVvjGZ6S&index=3

這一集是由 Ryan Tseng 老師來教學
真的蠻精彩、實用的,用有順序的方式分3階段帶我們實作「客製化元件」
最後ngx-bootstrap的tabs、rating也用類似做法,可以欣賞

本集順序如下:

  1. Ryan老師的投影片說明 ControlValueAccessor Interface
    Angular forms API 跟 DOM native element 的溝通介面
    https://angular.io/api/forms/ControlValueAccessor

interface ControlValueAccessor {
writeValue(obj: any): void 寫值到element
registerOnChange(fn: any): void 註冊callback function,當UI的control的值變更時觸發
registerOnTouched(fn: any): void 註冊callback function,當onTouched時觸發
setDisabledState(isDisabled: boolean)?: void 設成Disabled
}

  1. 用一個HTML+SCSS的樣板用Angular的語法改寫
  • Attribute Directives
  • Structural Directives

Ryan Tseng 老師將練習用程式碼放在他的github了
https://gist.github.com/ryan10328/24d213236bf19e1b357e7def76baf9d9
https://codepen.io/renatorib/pen/rlpfj

  1. 用 Reactive Form 做 binding
    精華

  2. 類似作法如 ngx-bootstrap 源始碼裡的 tabs、rating
    偷懶想速成的同學,直接看tabs、rating,如果看得懂,你就已學會「客製化元件了」
    所以ngx-bootstrap源始碼,弱者也是很看懂的,能挖出一點東西的
    真希望能一季是大神帶我們看懂Angaular一些知名套件的原始碼

開始實作

  1. ng g c tab
    https://gist.github.com/ryan10328/24d213236bf19e1b357e7def76baf9d9
    html、scss貼一貼

  2. 用Angular的語法改寫
    tab.component.html

<div class="pc-tab">
    <ng-container *ngFor="let item of data; let i =index;>
        <input 
            [checked]="item === itemSelected"
            [id]="'tab' + (i+1)"
            type="radio" name="pct" 
            (click)="selectItem(item)"
            />
    </ng-container>

    <nav>
        <ul>
            <li [class]="'tab' + (i+1)" 
            *ngFor="let item of data; let i = index;>
                <label [for]="'tab' + (i+1)">{{ item?.title }}</label>
            </li>
        </ul>
    </nav>
    <section>
        <div [class]="'tab' + (i+1) 
        *ngFor="let item of data; let i =index;>
            <h2>{{ item?.subTitle }}</h2>
            <p>{{ item?.content }}</p>
        </div>
    </section>
</div>
  1. tab.component.ts
export class TabComponent implements OnInit{   
    data: any[] = [];
    itemSelected: any;
    ngOnInit(){
        this.data = [
            { title: 'a1', subTitle: 'aa', content: 'aaa' },
            { title: 'a2', subTitle: 'aa', content: 'aaa' },
            { title: 'a3', subTitle: 'aa', content: 'aaa' }
        ];

        if (this.data.length > 0){
            this.itemSelected = this.data[0];
        }

    }

    selectItem(item){
        this.itemSelected = item;
    }
}
  1. app.component.html
<app-tab></app-tab>
  1. app.module.ts
@NgModule({
    imports: [ FormsModule ]
})
  1. 使用ControlValueAccessor
    app.module.ts
@NgModule({
    imports: [ FormsModule ]
})

tab.component.html

<div class="pc-tab">
    <ng-container *ngFor="let item of data; let i =index;>
        <input 
            [checked]="item === itemSelected"
            [id]="'tab' + (i+1)"
            type="radio" name="pct" 
            (click)="selectItem(item)
            />
    </ng-container>

    <nav>
        <ul>
            <li [class]="'tab' + (i+1)" 
            *ngFor="let item of data; let i = index;>
                <label [for]="'tab' + (i+1)">{{ item?.title }}</label>
            </li>
        </ul>
    </nav>
    <section>
        <div [class]="'tab' + (i+1) 
        *ngFor="let item of data; let i =index;>
            <h2>{{ item?.subTitle }}</h2>
            <p>{{ item?.content }}</p>
        </div>
    </section>
</div>

tab.component.ts

import { Component, OnInit, Input, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
@Component({
    ...
    providers:[
      {
        provide: NG_VALUE_ACESSOR,
        multi: true,
        useExisting: forwardRef(() => TabComponent)
      }
    ]
})

export class TabComponent implements OnInit, ControlValueAccessor {
    // 要實作ControlValueAccessor的4個methods
    writeValue(obj:any):void{
        if(obj){
            this.itemSelected = obj;
        }
    }
    registerOnChange(fn:any):void{
        this.propagateChange = fn;
    }
    registerOnTouched(fn:any):void{
        // throw new Error('沒用到');
    }
    
    // 假設上層 <app-tab disable [data]="data" [(ngModel)]="obj"></app-tab>
                        ^^^^^^^
    setDisabledState?(isDisabled:boolean):void{
                        ^^^^^ 傳入true
        console.log(isDisabled); // 只觀察isDisabled
        this.isDisabled = isDisabled;
    }
    // callback function
    propagateChange: Function;
    
    // 改寫 data: any[] = [];
    @Input() data: any[] = []; // data改由app.component.html輸入
    @Output() tabChange = new EventEmitter(); // emit寫在selectItem(item)
    
    private _itemSelected: any;
    // 透過 get、set 存取 private _itemSelected
    get itemSelected(){
        return this._itemSelected;
    }
    
    set itemSelected(value){
        this._itemSelected = value;
        this.propagateChange(this._itemSelected);
    }
    
    ngOnInit(){
        // 利用setTimeout來觸發執行?
        setTimeout(() => {
            if (this.data.length > 0){
                this.itemSelected = this.data[0];
            }
        },0);
    }

    // 點選tab的時候
    selectItem(item){
        this.itemSelected = item;
        // emit event
        this.tabChange.emit(this.itemSelected); // 再送出一次
    }

}

app.component.html

<app-tab 
    [data]="data" 
    [(ngModel)]="obj"
    [disabled]="isDisabled"
      ^^^^^^
                對應 setDisabledState?(isDisabled:boolean):void{
                        this.isDisabled = isDisabled;
                }
    (tabChange)="hello($event)"      
                 ^^^^^^^^^^^ 當點Tab時TabComponent emit (tabChange)到上層
                 監聽到,執行hello($event)
></app-tab>
<footer>
    {{ obj | json }}
</footer>

寫法補充

@Component({
    ...
    providers:[
      {
        provide: NG_VALUE_ACESSOR,
        multi: true,
        useExisting: forwardRef(() => TabComponent)
      }
    ]
})
可以寫成
const TAB_VALUE_ACCESSOR = {
        provide: NG_VALUE_ACESSOR,
        multi: true,
        useExisting: forwardRef(() => TabComponent)
}

@Component({
    ...
    providers:[
        TAB_VALUE_ACCESSOR
    ]
})
  1. 用 Reactive Form 做 binding
    使用情境:達到土砲級的 客製元件 需求

app.module.ts

@NgModule({
    imports: [ FormsModule, ReactiveFormsModule ]
})

app.component.ts

data:any[]=[];
obj:any;

frmTab: FormGroup;

ngOnInit(): void{
    this.data = [
        {title:'a1', subTtile: 'aa1', content: 'aaa1'},
        {title:'a2', subTtile: 'aa2', content: 'aaa2'},
        {title:'a3', subTtile: 'aa3', content: 'aaa3'}
    ];
    this.frmTab = new FormGroup({
        tabControl: new FormControl()
    });
    
    hello(evt){
        console.log(evt);
    }

tab.component.html

<div class="pc-tab">
    <ng-container *ngFor="let item of data; let i =index;>
        <input 
            [checked]="item === itemSelected"
            [id]="'tab' + (i+1)"
            type="radio" name="pct" 
            (click)="selectItem(item)
            />
    </ng-container>

    <nav>
        <ul>
            <li [class]="'tab' + (i+1)" 
            *ngFor="let item of data; let i = index;>
                <label [for]="'tab' + (i+1)">{{ item?.title }}</label>
            </li>
        </ul>
    </nav>
    <section>
        <div [class]="'tab' + (i+1) 
        *ngFor="let item of data; let i =index;>
            <h2>{{ item?.subTitle }}</h2>
            <p>{{ item?.content }}</p>
        </div>
    </section>
</div>

tab.component.ts

import { Component, OnInit, Input, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
@Component({
    ...
    providers:[
      {
        provide: NG_VALUE_ACESSOR,
        multi: true,
        useExisting: forwardRef(() => TabComponent)
      }
    ]
})

export class TabComponent implements OnInit, ControlValueAccessor {
    // 要實作ControlValueAccessor的4個methods
    writeValue(obj:any):void{
        if(obj){
            this.itemSelected = obj;
        }
    }
    registerOnChange(fn:any):void{
        this.propagateChange = fn;
    }
    registerOnTouched(fn:any):void{
        // throw new Error('沒用到');
    }
    
    // 假設上層 <app-tab disable [data]="data" [(ngModel)]="obj"></app-tab>
                        ^^^^^^^
    setDisabledState?(isDisabled:boolean):void{
                        ^^^^^ 傳入true
        console.log(isDisabled); // 只觀察isDisabled
        this.isDisabled = isDisabled;
    }
    // callback function
    propagateChange: Function;
    
    // 改寫 data: any[] = [];
    @Input() data: any[] = []; // data改由app.component.html輸入
    @Output() tabChange = new EventEmitter(); // emit寫在selectItem(item)
    
    private _itemSelected: any;
    // 透過 get、set 存取 private _itemSelected
    get itemSelected(){
        return this._itemSelected;
    }
    
    set itemSelected(value){
        this._itemSelected = value;
        this.propagateChange(this._itemSelected);
    }
    
    ngOnInit(){
        // 利用setTimeout來觸發執行?
        setTimeout(() => {
            if (this.data.length > 0){
                this.itemSelected = this.data[0];
            }
        },0);
    }

    // 點選tab的時候
    selectItem(item){
        this.itemSelected = item;
        // emit event
        this.tabChange.emit(this.itemSelected); // 再送出一次
    }

}

app.component.html

<form [formGroup]="frmTab">
    <app-tab 
        formControlName="tabControl"
        [data]="data" 
        <!-- [(ngModel)]="obj" -->
        [disabled]="isDisabled"
          ^^^^^^
                    對應 setDisabledState?(isDisabled:boolean):void{
                            this.isDisabled = isDisabled;
                    }
        (tabChange)="hello($event)"      
                     ^^^^^^^^^^^ 當點Tab時TabComponent emit (tabChange)到上層
                     監聽到,執行hello($event)
    ></app-tab>
</form>


<footer>
    {{ obj | json }}
    試一下Reactive Form有沒有綁成功
    {{ frmTab.get('tabControl')?.value | json }}
</footer>

課程結束前提到的資源

上面 用上層的 Reactive Form 用 <app-tab> </app-tab> 當樣版元件,
可以拿來做 客製元件 需求

類似作法如 ngx-bootstrap 源始碼裡的 tabs、rating

https://valor-software.com/ngx-bootstrap/#/


上一篇
Day10_動態產生元件及應用
下一篇
Day12_Pipe
系列文
Angular新手村學習筆記(2019)33
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言