我們在構(gòu)建企業(yè)級應(yīng)用時(shí),通常會遇到各種各樣的定制化功能,因?yàn)槊總€(gè)企業(yè)都有自己獨(dú)特的流程、思維方式和行為習(xí)慣。有很多時(shí)候,軟件企業(yè)是不太理解這種情況,習(xí)慣性的會給出一個(gè)診斷,『你這么做不對,按邏輯應(yīng)該這樣這樣』。但企業(yè)往往不會接受這種說法,習(xí)慣的力量是強(qiáng)大的,我們一定要尊重這種事實(shí)。所以在構(gòu)建企業(yè)應(yīng)用的時(shí)候,我們不僅僅要了解對方的基本需求,也要了解他們習(xí)慣于怎么處理流程,在設(shè)計(jì)的時(shí)候需要予以充分重視。當(dāng)然這也不是說客戶說怎么改我們就怎么改,而是要了解到對方真正的訴求和背后的原因,在產(chǎn)品規(guī)劃設(shè)計(jì)的時(shí)候,將這種因素考慮進(jìn)去,才能在維持產(chǎn)品統(tǒng)一的框架下滿足不同用戶的需求。
那么這里我們舉一個(gè)例子,比如我們正在開發(fā)一個(gè)醫(yī)療衛(wèi)生領(lǐng)域的企業(yè)軟件,客戶要求提供一個(gè)出生日期的控件,但這個(gè)控件不光可以輸入年月日,而且可以輸入年齡數(shù)值以及選擇年齡單位??蛻舻南M牵?/p>
- 填寫日期時(shí),年齡和年齡單位隨之變化
- 填寫年齡和選擇年齡單位時(shí)出生日期也隨之變化
看起來好像很無用的一個(gè)需求,這個(gè)在面向互聯(lián)網(wǎng)的應(yīng)用中確實(shí)如此。但在特定領(lǐng)域,其實(shí)有其背景原因,比如客戶提出這個(gè)需求是由于很多人,尤其是小城鎮(zhèn)的,是不記公歷生日的,這樣會導(dǎo)致出生日期不是很準(zhǔn)確,另外還會有一些人的身份證日期和真實(shí)年齡是不一致的。這種情況對于成人來說還好,但對于兒童來說就偏差很大,但一般人會記得孩子現(xiàn)在是多少天或多少個(gè)月大。這樣的話是不是覺得這個(gè)需求還有些道理?
那么我們就接著來看一下這個(gè)需求應(yīng)該怎樣實(shí)現(xiàn),首先分析一下:
- 無論是輸入出生日期還是年齡,其實(shí)最終要得到一個(gè)日期,也就是說年齡只是得到日期的一個(gè)輔助手段。
- 年齡單位的轉(zhuǎn)換我們需要有一個(gè)界定,否則切換起來沒有規(guī)則的話會導(dǎo)致邏輯的混亂。那這里我們定義一下:以天為單位時(shí)的上限為:90,下限為 0,也就是只有小于等于 90 天的嬰兒我們會使用天作為年齡單位。類似的,以月為單位的上限為 24,下限為 1;以年為單位的上限為 150,下限為 1。
- 同樣的出生日期的驗(yàn)證規(guī)則為:這個(gè)日期不能是未來的時(shí)間,一定是小于等于當(dāng)前時(shí)間的,再有就是年齡的上限既然是 150,那么出生日期也不能比當(dāng)前日期減去 150 年更早,對嗎?
- 聯(lián)動的規(guī)則應(yīng)該是調(diào)整出生日期時(shí),會將日期按上面規(guī)則轉(zhuǎn)換成年齡和單位,改變控件中的值;而調(diào)整年齡或者單位的時(shí)候,我們會根據(jù)年齡推算出出生日期,當(dāng)然這里是估算,以當(dāng)前日期減去年齡得出,然后更新出生日期輸入框中的值。

但這里面有幾個(gè)值得注意的地方:
- 可能存在反復(fù)聯(lián)動的問題,比如改變出生日期后,年齡和單位隨之改變,這又引發(fā)了由年齡和單位的變化而導(dǎo)致的出生日期的重算。
- 如果輸入非法的值,可能導(dǎo)致計(jì)算出現(xiàn)異常,因而控件狀態(tài)出現(xiàn)不正確的狀態(tài)值,進(jìn)一步影響未來的計(jì)算。
- 如果每次輸入改動都會引發(fā)重新計(jì)算,會帶來大量的過程中無用計(jì)算,耗費(fèi)資源,因此需要進(jìn)行對輸入事件的『整流』控制。
搭建自定義表單控件的框架
首先為什么要實(shí)現(xiàn)一個(gè)自定義表單控件?我們當(dāng)然可以直接把這個(gè)邏輯放在表單中,但問題是表單真的需要關(guān)心這幾個(gè)框的聯(lián)動嗎?
其實(shí)從表單的角度看,它只要一個(gè)值:那就是經(jīng)過計(jì)算的出生日期。至于你是手動輸入的還是按年齡和單位計(jì)算的,表單根本就不應(yīng)該關(guān)心。另外一點(diǎn)是隨著表單的復(fù)雜化,如果我們不把這些邏輯剝離出去的話,我們的表單本身的邏輯就會越來越復(fù)雜。最后是,封裝成表單控件意味著我們以后可以復(fù)用這個(gè)控件了。
知道了 why,我們看看 how。在 Angular 中實(shí)現(xiàn)一個(gè)自定義的表單控件還是比較簡單的,下面是一個(gè)表單控件的骨架。
import {ChangeDetectionStrategy, Component, forwardRef, OnInit, OnDestroy, Input} from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormControl, FormGroup, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
@Component({
selector: 'app-age-input',
template: `
// 省略
`,
styles: [`
// 省略
`],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => AgeInputComponent),
multi: true,
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => AgeInputComponent),
multi: true,
}
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AgeInputComponent implements ControlValueAccessor {
private propagateChange = (_: any) => {};
constructor() { }
// 提供值的寫入方法
public writeValue(obj: Date)
}
// 當(dāng)表單控件值改變時(shí),函數(shù) fn 會被調(diào)用
// 這也是我們把變化 emit 回表單的機(jī)制
public registerOnChange(fn: any) {
this.propagateChange = fn;
}
// 這里沒有使用,用于注冊 touched 時(shí)的回調(diào)函數(shù)
public registerOnTouched() {
}
// 驗(yàn)證表單,驗(yàn)證結(jié)果正確返回 null 否則返回一個(gè)驗(yàn)證結(jié)果對象
validate(c: FormControl): {[key: string]: any} {
// 省略
}
}
我們可以看到要實(shí)現(xiàn)一個(gè)表單控件的話,要實(shí)現(xiàn) ControlValueAccessor
這樣一個(gè)接口。這個(gè)接口顧名思義是用于寫入控件值的,它是一個(gè)控件和原生 DOM 元素之間的橋梁,通過實(shí)現(xiàn)這個(gè)接口,我們可以對原生 DOM 元素寫入值。而這個(gè)接口需要實(shí)現(xiàn)三個(gè)必選方法: writeValue(obj: any)
、 registerOnChange(fn: any)
和 registerOnTouched(fn: any)
。
-
writeValue(obj: any)
:用于向元素中寫入值 -
registerOnChange(fn: any)
:設(shè)置一個(gè)當(dāng)控件接受到改變的事件時(shí)所要調(diào)用的函數(shù)。 -
registerOnTouched(fn: any)
:設(shè)置一個(gè)當(dāng)控件接受到 touch 事件時(shí)所要調(diào)用的函數(shù)。
另外的一個(gè) validate(c: FormControl): {[key: string]: any}
是控件的驗(yàn)證器函數(shù)。除了這些函數(shù),你應(yīng)該也注意到,我們注冊了兩個(gè) provider,一個(gè)的 token 是 NG_VALUE_ACCESSOR
這是將控件本身注冊到 DI 框架成為一個(gè)可以讓表單訪問其值的控件。但問題來了,如果在元數(shù)據(jù)中注冊了控件本身,而此時(shí)控件仍為創(chuàng)建,這怎么破?這就得用到 forwardRef
了,這個(gè)函數(shù)允許我們引用一個(gè)尚未定義的對象。另外一個(gè) NG_VALIDATORS
是讓控件注冊成為一個(gè)可以讓表單得到其驗(yàn)證狀態(tài)的控件
。當(dāng)然這里還有一個(gè)奇怪的東西,就是那個(gè) multi: true,
,這是聲明這個(gè) token 對應(yīng)的類很多,分散在各處。
控件的界面
我們這里使用了 @angular/material
的 input
、 datepicker
和 button-toggle
控件來分別實(shí)現(xiàn)日期輸入、年齡輸入和年齡單位的選擇。注意到我們在里面使用了響應(yīng)式表單,這感覺好像有點(diǎn)怪,我們本身不是一個(gè)表單控件嗎?怎么自己的模板還是一個(gè)表單?這個(gè)其實(shí)沒啥問題,因?yàn)?Angular 中的組件是和外界隔離的,所以組件自身的模板其實(shí)想怎么使用都可以。
<div [formGroup]="form" class="age-input">
<div>
<md-input-container>
<input mdInput [mdDatepicker]="birthPicker" type="text" placeholder="出生日期" formControlName="birthday" >
<button mdSuffix [mdDatepickerToggle]="birthPicker" type="button"></button>
<md-error>日期不正確</md-error>
</md-input-container>
<md-datepicker touchUi="true" #birthPicker></md-datepicker>
</div>
<ng-container formGroupName="age">
<div class="age-num">
<md-input-container>
<input mdInput type="number" placeholder="年齡" formControlName="ageNum">
</md-input-container>
</div>
<div>
<md-button-toggle-group formControlName="ageUnit" [(ngModel)]="selectedUnit">
<md-button-toggle *ngFor="let unit of ageUnits" [value]="unit.value">
{{ unit.label }}
</md-button-toggle>
</md-button-toggle-group>
</div>
<md-error class="mat-body-2" *ngIf="form.get('age').hasError('ageInvalid')">年齡或單位不正確</md-error>
</ng-container>
</div>
上面這個(gè)模板中值得注意的一點(diǎn)是,我們把年齡的數(shù)值和單位放在了一個(gè) FormGroup
里面,這是由于這兩個(gè)值組合在一起才有意義,而且后面的表單驗(yàn)證也是這兩個(gè)值在一起組合后驗(yàn)證。
使用 Rx 的事件流來重新梳理邏輯
私以為 Rx 的兩大優(yōu)點(diǎn):
- 由于在 Rx 世界里,一切都是事件流,所以這『逼迫』開發(fā)者將時(shí)間維度納入設(shè)計(jì)的考量
- 提供的各種強(qiáng)大的操作符可以將邏輯非常輕松的組合
那么從 Rx 的角度看的話,這個(gè)控件會產(chǎn)生三個(gè)事件流:出生日期、年齡數(shù)值和年齡單位:
出生日期:-------d----------d---------------d--------------
年齡數(shù)值:----------num----------num----------------num----
年齡單位:----unit-------------unit-------------unit-------
寫成代碼的話就是下面的樣子,Angular 的響應(yīng)式表單為我們提供了非常便利的方法可以得到這些變化的事件流,FormControl
的 valueChanges
屬性就是一個(gè) Observable
。
// 得到出生日期的值的變化流
const birthday$ = this.form.get('birthday').valueChanges;
// 得到年齡數(shù)值的變化流
const ageNum$ = this.form.get('age').get('ageNum').valueChanges;
// 得到年齡單位的變化流
const ageUnit$ = this.form.get('age').get('ageUnit').valueChanges;
由于年齡數(shù)值和年齡單位需要合并在一起才有意義,所以這兩個(gè)流需要做一個(gè)合并操作,而且不管是數(shù)值變化還是單位變化,我們都要在新的合并流中有一個(gè)反映:
年齡數(shù)值:----------n1----------------n2------------------n3-------
年齡單位:----u1-------------u2------------------u3----------------
合并后: ------(n1,u1)--(n1,u2)--(n2,u2)----(n2,u3)---(n3,u3)---
仔細(xì)觀察一下,你可能會發(fā)現(xiàn)這個(gè)合并流還有一個(gè)特點(diǎn)就是只有在參與合并的兩個(gè)流都有事件產(chǎn)生后才會有合并的事件發(fā)生,在這之后就是任何一個(gè)參與合并的流有新的事件,合并流就會產(chǎn)生一個(gè)事件,這個(gè)合并的值會取剛剛發(fā)生的那個(gè)事件和另一個(gè)參與合并的流中的『最新』事件。這種合并方法在 Rx 中叫做 combineLatest
:
const age$ = Observable
.combineLatest(ageNum$, ageUnit$, (_num, _unit) => this.toDate({age: _num, unit: _unit}));
上面的代碼中,我們將年齡數(shù)值的事件流(ageNum$
)以及年齡單位的事件流(ageUnit$
)做了合并,而且通過一個(gè) this.toDate
的工具函數(shù)將年齡和單位計(jì)算出了一個(gè)估算的出生日期。
出生日期:-------d----------d---------------d--------------
年齡合并:---d^----d^----d^---d^--------d^------d^---------
// 年齡合并后產(chǎn)生的出生日期用 d^ 來標(biāo)識
現(xiàn)在看起來這兩個(gè)流都產(chǎn)生日期,只不過是不同的控件變化引起的。那么我們應(yīng)該可以把它們也做一個(gè)合并,這個(gè)合并就比較簡單,可以想象成按照各自流中的位置把兩個(gè)流做投影。
最終合并:---d^--d--d^----d^--d-d^-------d^--d----d^-------
而這種合并在 Rx 中叫做 merge
const merge$ = Observable.merge(birthday$, age$);
但為了要能區(qū)分這個(gè)日期是來自于出生日期那個(gè)輸入框還是來自于年齡和單位的輸入變化,我們得標(biāo)識出這個(gè)日期的來源。所以我們需要對 birthday$
和 age$
做一個(gè)變換處理,不在單純的發(fā)射日期,而是將日期和來源組合成一個(gè)新的對象 {date: string; from: string}
發(fā)射。
const birthday$ = this.form.get('birthday').valueChanges
.map(d => ({date: d, from: 'birthday'}));
const age$ = Observable
.combineLatest(ageNum$, ageUnit$, (_num, _unit) => this.toDate({age: _num, unit: _unit}))
.map(d => ({date: d, from: 'age'}));
這樣處理之后,我們就可以根據(jù)不同情況,根據(jù)日期設(shè)置年齡和單位,或者反之,由年齡和單位的變化設(shè)置出生日期。
this.subBirth = merged$.subscribe(date => {
const age = this.toAge(date.date);
const ageNum = this.form.get('age').get('ageNum');
const ageUnit = this.form.get('age').get('ageUnit');
if(date.from === 'birthday') {
if(age.age === ageNum.value && age.unit === ageUnit.value) {
return;
}
ageUnit.patchValue(age.unit, {emitEvent: false, emitModelToViewChange: true, emitViewToModelChange: true});
ageNum.patchValue(age.age, {emitEvent: false});
this.selectedUnit = age.unit;
this.propagateChange(date.date);
} else {
const ageToCompare = this.toAge(this.form.get('birthday').value);
// 如果要設(shè)置的日期換算成年齡和單位,如果這兩個(gè)值和現(xiàn)有控件的值是一樣的,那就沒有必要更新日期的值了
if(age.age !== ageToCompare.age || age.unit !== ageToCompare.unit) {
this.form.get('birthday').patchValue(date.date, {emitEvent: false});
this.propagateChange(date.date);
}
}
});
大致的邏輯就是這樣了,但我們還有幾個(gè)問題需要解決
- 現(xiàn)在的情況是不管你以多快的速度輸入日期,或者輸錯(cuò)了按
backspace
都會產(chǎn)生新的事件,也因此會有計(jì)算。但顯然這樣做一方面浪費(fèi)了性能,另一方面會導(dǎo)致一些不合法的值大量出現(xiàn)(比如本來要輸入2000-12-11
, 但事實(shí)上現(xiàn)在當(dāng)你剛剛敲了 2 ,事件就已經(jīng)產(chǎn)生了,但顯然年份 2 不是一個(gè)合理的出生年份,我們畢竟不是在做一個(gè)考古信息系統(tǒng))。 - 當(dāng)你和上一次輸入相同的值時(shí),現(xiàn)在的系統(tǒng)仍然會發(fā)射事件,但這其實(shí)是在做無用功。
- 我們現(xiàn)在的事件流沒有經(jīng)過一個(gè)驗(yàn)證就會把數(shù)據(jù)發(fā)射出來,但一個(gè)沒有驗(yàn)證成功的值其實(shí)對我們來說是沒有意義的。
- 年齡和單位的合并流只有在年齡和單位都產(chǎn)生變化的時(shí)候才開始發(fā)射,但一開始的初始狀態(tài),這兩個(gè)控件并沒有值,這顯然不是我們希望的(比如你可能不想填完年齡,例如 30,然后還得點(diǎn)一下『天』,再點(diǎn)回『歲』來得到合并計(jì)算的值)。
const birthday$ = this.form.get('birthday').valueChanges
.map(d => ({date: d, from: 'birthday'}))
.debounceTime(300)
.distinctUntilChanged()
.filter(date => this.form.get('birthday').valid);
const ageNum$ = this.form.get('age').get('ageNum').valueChanges
.startWith(this.form.get('age').get('ageNum').value)
.debounceTime(300)
.distinctUntilChanged();
const ageUnit$ = this.form.get('age').get('ageUnit').valueChanges
.startWith(this.form.get('age').get('ageUnit').value)
.debounceTime(300)
.distinctUntilChanged();
const age$ = Observable
.combineLatest(ageNum$, ageUnit$, (_num, _unit) => this.toDate({age: _num, unit: _unit}))
.map(d => ({date: d, from: 'age'}))
.filter(_ => this.form.get('age').valid);
const merged$ = Observable
.merge(birthday$, age$)
.filter(_ => this.form.valid);
上面的代碼中,我們使用 debounceTime
過濾掉了短時(shí)間內(nèi)的輸入,等待用戶略有停頓或輸入完成時(shí)才發(fā)射新的事件。我們還使用了 distinctUntilChanged
來過濾掉和之前一樣的輸入。而 startWith
其實(shí)是在幫事件流拼接一個(gè)初始值,使得合并流按我們想像中那樣運(yùn)行。那么 filter
則是屏蔽掉驗(yàn)證未通過的數(shù)據(jù)。
這樣簡單的通過幾個(gè) Rx 的操作符我們就完成了核心邏輯,而且在核心邏輯不變的前提下對數(shù)據(jù)驗(yàn)證、事件的『整流』、篩選等進(jìn)行了調(diào)整。
總結(jié)和思考
針對復(fù)雜的表單,我們通常應(yīng)該使用『復(fù)雜問題簡單化』的方法將一個(gè)復(fù)雜問題拆分成多個(gè)簡單問題。對于較復(fù)雜的表單來講,自定義表單控件是一個(gè)很有用的可以簡單化表單邏輯,封裝局部邏輯的一種方法。
而使用 Rx 進(jìn)行邏輯的組裝、轉(zhuǎn)換、拼接以及合并是非常容易的事情,而且 Rx 的事件流特點(diǎn)會讓你把邏輯梳理的非常清晰,以時(shí)間維度把業(yè)務(wù)邏輯的先后和組裝的次序考慮周全。
源碼
import {ChangeDetectionStrategy, Component, forwardRef, OnInit, OnDestroy, Input} from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormControl, FormGroup, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
import {
subYears,
subMonths,
subDays,
isBefore,
differenceInDays,
differenceInMonths,
differenceInYears,
parse
} from 'date-fns';
import {Observable} from 'rxjs/Observable';
import { Subscription } from 'rxjs/Subscription';
import { toDate, isValidDate } from '../../utils/date.util';
export enum AgeUnit {
Year = 0,
Month,
Day
}
export interface Age {
age: number;
unit: AgeUnit;
}
@Component({
selector: 'app-age-input',
template: `
<div [formGroup]="form" class="age-input">
<div>
<md-input-container>
<input mdInput [mdDatepicker]="birthPicker" type="text" placeholder="出生日期" formControlName="birthday" >
<button mdSuffix [mdDatepickerToggle]="birthPicker" type="button"></button>
<md-error>日期不正確</md-error>
</md-input-container>
<md-datepicker touchUi="true" #birthPicker></md-datepicker>
</div>
<ng-container formGroupName="age">
<div class="age-num">
<md-input-container>
<input mdInput type="number" placeholder="年齡" formControlName="ageNum">
</md-input-container>
</div>
<div>
<md-button-toggle-group formControlName="ageUnit" [(ngModel)]="selectedUnit">
<md-button-toggle *ngFor="let unit of ageUnits" [value]="unit.value">
{{ unit.label }}
</md-button-toggle>
</md-button-toggle-group>
</div>
<md-error class="mat-body-2" *ngIf="form.get('age').hasError('ageInvalid')">年齡或單位不正確</md-error>
</ng-container>
</div>
`,
styles: [`
.age-num{
width: 50px;
}
.age-input{
display: flex;
flex-wrap: nowrap;
flex-direction: row;
align-items: baseline;
}
`],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => AgeInputComponent),
multi: true,
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => AgeInputComponent),
multi: true,
}
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AgeInputComponent implements ControlValueAccessor, OnInit, OnDestroy {
selectedUnit = AgeUnit.Year;
form: FormGroup;
ageUnits = [
{value: AgeUnit.Year, label: '歲'},
{value: AgeUnit.Month, label: '月'},
{value: AgeUnit.Day, label: '天'}
];
dateOfBirth;
@Input() daysTop = 90;
@Input() daysBottom = 0;
@Input() monthsTop = 24;
@Input() monthsBottom = 1;
@Input() yearsBottom = 1;
@Input() yearsTop = 150;
@Input() debounceTime = 300;
private subBirth: Subscription;
private propagateChange = (_: any) => {};
constructor(private fb: FormBuilder) { }
ngOnInit() {
const initDate = this.dateOfBirth ? this.dateOfBirth : toDate(subYears(Date.now(), 30));
const initAge = this.toAge(initDate);
this.form = this.fb.group({
birthday: [initDate, this.validateDate],
age: this.fb.group({
ageNum: [initAge.age],
ageUnit: [initAge.unit]
}, {validator: this.validateAge('ageNum', 'ageUnit')})
});
const birthday = this.form.get('birthday');
const ageNum = this.form.get('age').get('ageNum');
const ageUnit = this.form.get('age').get('ageUnit');
const birthday$ = birthday.valueChanges
.map(d => ({date: d, from: 'birthday'}))
.debounceTime(this.debounceTime)
.distinctUntilChanged()
.filter(date => birthday.valid);
const ageNum$ = ageNum.valueChanges
.startWith(ageNum.value)
.debounceTime(this.debounceTime)
.distinctUntilChanged();
const ageUnit$ = ageUnit.valueChanges
.startWith(ageUnit.value)
.debounceTime(this.debounceTime)
.distinctUntilChanged();
const age$ = Observable
.combineLatest(ageNum$, ageUnit$, (_num, _unit) => this.toDate({age: _num, unit: _unit}))
.map(d => ({date: d, from: 'age'}))
.filter(_ => this.form.get('age').valid);
const merged$ = Observable
.merge(birthday$, age$)
.filter(_ => this.form.valid)
.debug('[Age-Input][Merged]:');
this.subBirth = merged$.subscribe(date => {
const age = this.toAge(date.date);
if(date.from === 'birthday') {
if(age.age === ageNum.value && age.unit === ageUnit.value) {
return;
}
ageUnit.patchValue(age.unit, {emitEvent: false, emitModelToViewChange: true, emitViewToModelChange: true});
ageNum.patchValue(age.age, {emitEvent: false});
this.selectedUnit = age.unit;
this.propagateChange(date.date);
} else {
const ageToCompare = this.toAge(this.form.get('birthday').value);
if(age.age !== ageToCompare.age || age.unit !== ageToCompare.unit) {
this.form.get('birthday').patchValue(date.date, {emitEvent: false});
this.propagateChange(date.date);
}
}
});
}
ngOnDestroy() {
if(this.subBirth) {
this.subBirth.unsubscribe();
}
}
public writeValue(obj: Date) {
if (obj) {
const date = toDate(obj);
this.form.get('birthday').patchValue(date, {emitEvent: false});
}
}
public registerOnChange(fn: any) {
this.propagateChange = fn;
}
public registerOnTouched() {
}
validate(c: FormControl): {[key: string]: any} {
const val = c.value;
if (!val) {
return null;
}
if (isValidDate(val)) {
return null;
}
return {
ageInvalid: true
};
}
validateDate(c: FormControl): {[key: string]: any} {
const val = c.value;
return isValidDate(val) ? null : {
birthdayInvalid: true
}
}
validateAge(ageNumKey: string, ageUnitKey:string): {[key: string]: any} {
return (group: FormGroup): {[key: string]: any} => {
const ageNum = group.controls[ageNumKey];
const ageUnit = group.controls[ageUnitKey];
let result = false;
const ageNumVal = ageNum.value;
switch (ageUnit.value) {
case AgeUnit.Year: {
result = ageNumVal >= this.yearsBottom && ageNumVal <= this.yearsTop
break;
}
case AgeUnit.Month: {
result = ageNumVal >= this.monthsBottom && ageNumVal <= this.monthsTop
break;
}
case AgeUnit.Day: {
result = ageNumVal >= this.daysBottom && ageNumVal <= this.daysTop
break;
}
default:
result = false;
}
return result ? null : {
ageInvalid: true
}
}
}
private toAge(dateStr: string): Age {
const date = parse(dateStr);
const now = new Date();
if (isBefore(subDays(now, this.daysTop), date)) {
return {
age: differenceInDays(now, date),
unit: AgeUnit.Day
};
} else if (isBefore(subMonths(now, this.monthsTop), date)) {
return {
age: differenceInMonths(now, date),
unit: AgeUnit.Month
};
} else {
return {
age: differenceInYears(now, date),
unit: AgeUnit.Year
};
}
}
private toDate(age: Age): string {
const now = new Date();
switch (age.unit) {
case AgeUnit.Year: {
return toDate(subYears(now, age.age));
}
case AgeUnit.Month: {
return toDate(subMonths(now, age.age));
}
case AgeUnit.Day: {
return toDate(subDays(now, age.age));
}
default: {
return this.dateOfBirth;
}
}
}
}
慕課網(wǎng) Angular 視頻課上線: http://coding.imooc.com/class/123.html?mc_marking=1fdb7649e8a8143e8b81e221f9621c4a&mc_channel=banner