構建一個自定義 angular2 輸入組件
構建一個自定義 angular2 輸入組件
今天我們來學習如何正確的構建和一個具有和 <input type="text"> 同樣作用,但同時也具有自己的邏輯的輸入組件。
在讀這篇文章之前,希望你已經把官方的文檔和案例都看過至少一遍了,具體的一些概念和細節不會在文章中講解。
我們先來看一下我們這篇文章里面所介紹的組件的表現形式是怎么樣的:
OK,上圖就是我們所要達到的效果了。那么,我們來分析下我們這個組件該具備哪些功能。
-
聚焦的時候,底部邊框為綠色
-
具有自己的部分邏輯,比如在有輸入值的情況下,會出現一個刪除圖標
-
當輸入值為空的時候,提示錯誤文案
-
可以插入其它的 DOM,比如最下面的發送驗證碼按鈕
-
支持 input 的必要屬性,比如 maxlength、placeholder 等
-
支持表單 angular2 form-control 表單綁定,如上圖中的值都是從 FormBuilder 中構建的
我們將在后面一步步的來講解如何實現這樣一個自定義組件的功能;
創建一個 angular2 組件
我們先來構建一個基礎的 angular2 組件,這里我們先新建一個叫做 input-control 的組件。
首先是 input-control.component.ts 文件:
@Component({
selector: 'input-control',
templateUrl: 'input-control.component.html',
styleUrls: ['input-control.component.scss'],
encapsulation: ViewEncapsulation.None,
})
然后是 input-control.component.html 文件:
<input #input
[type]="type"
[name]="name"
(focus)="_handleFocus($event)"
(blur)="_handleBlur($event)"
[placeholder]="placeholder"
[(ngModel)]="value"
[minlength]="minlength"
[maxlength]="maxlength"
[readonly]="readonly"
[disabled]="disabled">
<i #iconDelete *ngIf="focused && !readonly" class="icon icon-delete" (click)="_handleClear($event)"></i>
剩下就是 input-control.component.scss 文件了,這里我就不貼出代碼了,各位可以根據自己的項目來設置對應的樣式
最后,就是我們調用的時候的方式:
<input-control class="input-control"
[class.error]="!mobile.valid && mobile.touched"
type="tel"
name="mobile"
placeholder="手機號"
maxlength="11"
[formControl]="mobile">
<p *ngIf="mobile.touched && mobile.hasError('mobile')" class="error-tips">請輸入正確的手機號碼</p>
</input-control>
是否對于上面的一些屬性和變量感到困惑,別急,讓我一步步道來!
功能細分
輸入屬性 @Input()
有一點要謹記: 我們是在用 DIV 來模擬一個 input 的表現,同時具備自己的邏輯 ; 所以,當我們需要 input 的對應屬性值的時候,我們都需要從父容器傳遞到組件內部的 input 上面,所以在這里我們需要用到 @Input 特性了
我們在 input-control.component.ts 定義我們所需的一些屬性:
@Component({
selector: 'input-control',
templateUrl: 'input-control.component.html',
styleUrls: ['input-control.component.scss'],
host: {
// 宿主元素 click 事件,觸發 focus() 事件
'(click)': 'focus()',
// 切換宿主元素 focus 樣式
'[class.focus]': 'focused'
}
})
export class InputControlComponent {
private _focused: boolean = false;
private _value: any = '';
private _disabled: boolean = false;
private _readonly: boolean = false;
private _required: boolean = false;
// 外部傳入屬性
@Input() type: string = 'text';
@Input() name: string = null;
@Input() placeholder: string = null;
@Input() minlength: number;
@Input() maxlength: number;
// value 屬性,以 get 方式攔截
get value(): any {
return this._value;
};
@Input() set value(v: any) {
v = this._convertValueForInputType(v);
if (v !== this._value) {
this._value = v;
// 觸發值改變事件,冒泡給父級
this._onChangeCallback(v);
}
}
// 只讀屬性
get focused() {
return this._focused;
}
@Input()
get disabled(): boolean {
return this._disabled;
}
set disabled(value) {
this._disabled = this._coerceBooleanProperty(value);
}
@Input()
get readonly(): boolean {
return this._readonly;
}
set readonly(value) {
this._readonly = this._coerceBooleanProperty(value);
}
@Input()
get required(): boolean {
return this._required;
}
set required(value) {
this._required = this._coerceBooleanProperty(value);
}
}</code></pre>
回顧的我們前面的 input-control.component.html 文件,我們定義了 type 、 name 、 placeholder 、 minlength 、 maxlength 可讀寫的屬性,同時還有 value 、 readonly 、 disabled 、 required 等只讀屬性。通過 [屬性]="源" 方式,接收父級傳入的數據。
OK,屬性我們都知道如何從父級去接收了,那么接下來我們來實現 點擊 操作:
我們先修改 input-control.component.ts 文件
@Component({
……
host: {
// 宿主元素 click 事件,觸發 focus() 事件
'(click)': 'focus()',
// 切換宿主元素 focus 樣式
'[class.focus]': 'focused'
}
})
我們利用了 host 這個屬性,用來給宿主元素對應操作,傳送門 @Component 相關屬性 ;
我們給宿主元素也就是 <input-control></input-control> 綁定了一個 click 事件,同時根據自身屬性 focused 來切換一個 .focus 類。在我們組件的 focus() 事件中,我們需要讓組件內部的 input 聚焦,同時切換自身的 focused 值。為了拿到我們組件內部的 input 元素,這里我們需要使用 @ViewChild() 。
修改 input-control.component.ts 文件如下:
@Component({
……
host: {
// 宿主元素 click 事件,觸發 focus() 事件
'(click)': 'focus()',
// 切換宿主元素 focus 樣式
'[class.focus]': 'focused'
}
})
export class InputControlComponent {
……
……
private _focusEmitter: EventEmitter<FocusEvent> = new EventEmitter<FocusEvent>();
@ViewChild('input') _inputElement: ElementRef; // 組件內部 input 元素
@ViewChild('iconDelete') iconDelete: ElementRef; // 刪除圖標元素
constructor(private hostRef: ElementRef) {
}
// 監聽全局的點擊事件,如果不是當前 input-control 組,則視為失去焦點操作
@HostListener('window:click', ['$event'])
inputControlBlurHandler(event) {
var parent = event.target;
// 如何當前節點不是宿主節點,并且不等于 document 節點
while (parent && parent != this.hostRef.nativeElement && parent != document) {
// 取當前節點的父節點繼續尋找
parent = parent.parentNode;
}
// 找到最頂層,則表示已經不在宿主元素內部了,觸發失去焦點 fn
if (parent == document) {
this._focused = false;
}
}
// 宿主聚焦
focus() {
// 觸發下面的 _handleFocus() 事件
this._inputElement.nativeElement.focus();
}
// 輸入框聚焦
_handleFocus(event: FocusEvent) {
this._focused = true;
this._focusEmitter.emit(event);
}
// 清空輸入值
_handleClear() {
this.value = '';
return false;
}
// 這里觸發 blur 操作,但是不改變 this._focused 的值,
// 不然刪除圖標無法實現它的功能,
//設置 this._focused 的值將由上面的 @HostListener('window:click', ['$event']) 來處理
// 觸發父級的 blur 事件
_handleBlur(event: any) {
this._onTouchedCallback();
this._blurEmitter.emit(event);
}
// 對外暴露 focus 事件
@Output('focus') onFocus = this._focusEmitter.asObservable();
……
……
}</code></pre>
在上面的代碼中,我們通過宿主的 focus() 事件,讓 input 元素 focus , 同時 input 元素聚焦之后,會觸發下面的 _handleFocus() 方法,在這個方法里面,我們修改組件自身的 focused 屬性,并對外發射一個 focus 事件,用來向父級傳遞使用。同時,我們的刪除圖標也是根據組件的 focused 屬性切換顯示:
<input #input
[type]="type"
[name]="name"
(focus)="_handleFocus($event)"
(blur)="_handleBlur($event)"
[placeholder]="placeholder"
[(ngModel)]="value">
<i #iconDelete
*ngIf="focused && !readonly"
class="icon icon-delete"
(click)="_handleClear($event)"></i>
我們的 input 和組件內部的 value 屬性進行了雙向綁定,所以在 _handleClear 之后,我們的輸入框的值自然也就被清空了。
值訪問器 ControlValueAccessor
在完成上面的一些步驟之后,我們的組件基本功能完成了,但是接下來還有最重要的一部分內容,那就是讓我們的自定義組件獲得 值訪問 權限。
在官方的文檔中有提到一點 https://github.com/angular/material2/blob/master/src/lib/input/input.ts
在查看官方的文檔之后,我們發現要實現自定義組件的值訪問權限,我們需要繼承 ControlValueAccessor 接口,同時實現它內部的對應的接口
// 要實現雙向數據綁定,這個不可少
export const INPUT_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => InputControlComponent),
multi: true
};
const noop = () => {
};
@Component({
selector: 'input-control',
templateUrl: 'input-control.component.html',
styleUrls: ['input-control.component.scss'],
host: {
// 宿主元素 click 事件,觸發 focus() 事件
'(click)': 'focus()',
// 切換宿主元素 focus 樣式
'[class.focus]': 'focused'
},
//
encapsulation: ViewEncapsulation.None,
providers: [INPUT_CONTROL_VALUE_ACCESSOR]
})
export class InputControlComponent implements ControlValueAccessor {
……
……
/** Callback registered via registerOnTouched (ControlValueAccessor)
- 此屬性在做表單校驗的時候,不可少,
如果缺少了這個屬性,FormControl.touched 屬性將監測不到,切記!!
/
private _onTouchedCallback: () => void = noop;
/** Callback registered via registerOnChange (ControlValueAccessor) /
private onChangeCallback: (: any) => void = noop;
/**
Write a new value to the element.
*/
writeValue(value: any) {
this._value = value;
}
/**
Set the function to be called when the control receives a change event.
*/
registerOnChange(fn: any) {
this._onChangeCallback = fn;
};
/**
- Set the function to be called when the control receives a touch event.
*/
registerOnTouched(fn: any) {
this._onTouchedCallback = fn;
}
……
……
}</code></pre>
正如上面代碼中所示的一樣,實現了這些對應的接口之后,我們就能像使用普通的 input 元素一樣使用我們的自定義組件了。
允許組件加載內部其它的 DOM 元素
回顧我們前面文章開頭的 GIF 圖片,我們還有一個獲取驗證碼的按鈕,同時,我們的錯誤提示也是放在組件內部的。要支持這種形式的,我們需要在組件內部加上 <ng-content></ng-content> 標簽
有了這個之后,所有包裹在 <input-control></input-control> 組件內部的元素都將被渲染到組件內部
父組件調用 input-control :
<input-control class="input-control sms-control"
[class.error]="!captcha.valid && captcha.touched"
type="tel"
name="captcha"
placeholder="請輸入驗證碼"
[formControl]="captcha"
maxlength="5">
<count-down class="btn-send-sms" counter="50" title="獲取驗證碼" countText="秒后重新獲取"></count-down>
<p *ngIf="!captcha.valid && captcha.touched" class="error-tips">請輸入驗證碼</p>
</input-control>
瀏覽器渲染之后的的 DOM 結構:
<input-control class="input-control sms-control ng-untouched ng-pristine ng-invalid" maxlength="5" name="captcha" placeholder="請輸入驗證碼" type="tel" ng-reflect-maxlength="5" ng-reflect-type="tel" ng-reflect-name="captcha" ng-reflect-placeholder="請輸入驗證碼" ng-reflect-form="[object Object]">
<input ng-reflect-maxlength="5" ng-reflect-name="captcha" ng-reflect-type="tel" type="tel" ng-reflect-placeholder="請輸入驗證碼" placeholder="請輸入驗證碼" maxlength="5" class="ng-untouched ng-pristine ng-valid">
<!--template bindings={
"ng-reflect-ng-if": null
}-->
<count-down class="btn-send-sms" counttext="秒后重新獲取" counter="50" title="獲取驗證碼" ng-reflect-counter="50" ng-reflect-title="獲取驗證碼" ng-reflect-count-text="秒后重新獲取"><button>獲取驗證碼</button></count-down>
<!--template bindings={
"ng-reflect-ng-if": null
}-->
</input-control>
與 FormControl 結合使用注意事項
在后期的時候,我整合了自定輸入組件與 FormControl 一起使用,在使用過程中,發現在需要使用 .touched 特性的時候,發現無法生效,通過查資料發現,如果需要讓這個特性生性,我們的輸入組件必須監聽 blur 事件并且在處理事件中調用觸發對外的 blur 事件,具體代碼見前面的 _handleBlur() 內容。
完整 Demo 地址: mcare-app
這個 Demo 里面整合了路由、子模塊、服務、動態表單等特性的使用方法,有興趣的可以參考下,還在持續完善中。這個 Demo 是參照自己做過的項目部分UI,當然不會涉及核心的業務代碼:)。
參考資料
CUSTOM FORM CONTROLS IN ANGULAR 2
來自:https://segmentfault.com/a/1190000007603861