因為參賽的進度壓力,後面有時間再回來補程式碼註解
https://www.youtube.com/watch?v=282ITUdC12Y&list=PL9LUW6O9WZqgUMHwDsKQf3prtqVvjGZ6S&index=3
這一集是由 Ryan Tseng 老師來教學
真的蠻精彩、實用的,用有順序的方式分3階段帶我們實作「客製化元件」
最後ngx-bootstrap的tabs、rating也用類似做法,可以欣賞
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
}
Ryan Tseng 老師將練習用程式碼放在他的github了
https://gist.github.com/ryan10328/24d213236bf19e1b357e7def76baf9d9
https://codepen.io/renatorib/pen/rlpfj
用 Reactive Form 做 binding精華
類似作法如 ngx-bootstrap 源始碼裡的 tabs、rating
偷懶想速成的同學,直接看tabs、rating,如果看得懂,你就已學會「客製化元件了」
所以ngx-bootstrap源始碼,弱者也是很看懂的,能挖出一點東西的真希望能一季是大神帶我們看懂Angaular一些知名套件的原始碼
ng g c tab
https://gist.github.com/ryan10328/24d213236bf19e1b357e7def76baf9d9
html、scss貼一貼
用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>
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;
}
}
<app-tab></app-tab>
@NgModule({
imports: [ FormsModule ]
})
@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
]
})
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>
當樣版元件,
可以拿來做 客製元件 需求
https://valor-software.com/ngx-bootstrap/#/