Date-picer 元件的難點在於:用於選擇日期
- 獲取年月日,相關日期的邏輯以及
new Date
的 Api 使用; - 結合Functional Component,減少切換日期時
render
的代價; - 元件功能解耦。
1. 例項
程式碼
<!-- 基礎用法 -->
<fat-date-picker v-model="date" />
<!-- 語言為EN -->
<fat-date-picker lang="EN" v-model="secDate" />
複製程式碼
例項地址:DatePicker 例項
程式碼地址:Github UI-Library
2. 原理
基本結構如下
<template>
<div class="date-picker-wrapper" ref="date-picker">
<fat-input
type="text"
readonly
:class="['picker-data', 'not-select', {'disabled': disabled}]"
:value="selectValue | dateFormat('day', lang)"
:placeholder="placeholder"
@click="toggle"
/>
<transition name="fade">
<div class="picker-panel" v-show="UI.isOpen">
<!-- 顯示日期 -->
</div>
</transition>
</div>
</template>
<script>
export default {
props: {
value: { type: [Date, String, Number] },
...
},
filters: {
dateFormat(val, mode, lang) {
// 用於 format 對應日期
}
},
model: {
prop: "value",
event: "input"
},
data() {
return {
date: {
year: null,
month: null,
day: null
},
UI: {
isOpen: false
},
selectValue: null,
panelType: "day"
};
},
computed: {
...
},
watch: {
...
value: {
handler(newValue) {
this.date = dateToObj(newValue ? new Date(newValue) : new Date());
this.selectValue = newValue ? new Date(newValue) : "";
},
immediate: true
}
},
methods: {
...
toggle() {
this.UI.isOpen = !this.UI.isOpen;
if (this.UI.isOpen) {
const datePicker = this.$refs["date-picker"];
const handler = event => {
let dom = event.target;
let flag = false;
while (dom) {
if (dom === datePicker) {
flag = true;
break;
}
dom = dom.parentNode;
}
if (!flag) this.UI.isOpen = flag;
document.removeEventListener("click", handler, true);
};
document.addEventListener("click", handler, true);
}
}
}
};
</script>
複製程式碼
首先處理 Date-picker 的資料雙向繫結以及下拉框的展開與收縮:
- 資料繫結,與之前 Select 元件一直,需要定義
v-model
的相關prop
以及event
,通過watchprop
的變化,具體邏輯如下value: { handler(newValue) { // 從 new Date() 中分離出當前的年月日,方便生成對應的年Table、月Table、日Table this.date = dateToObj(newValue ? new Date(newValue) : new Date()); this.selectValue = newValue ? new Date(newValue) : ""; }, immediate: true } export const dateToObj = function (date) { return { year: date.getFullYear(), month: date.getMonth(), day: date.getDate() } } 複製程式碼
- 下拉框的展開和收縮,與之前 Select 元件不同的是,由於 Date-picker 的下拉框存在著多種狀態,而且後續提供輸入功能,所以
tabIndex
為div
新增blur
事件的方案實現起來較為複雜,所以採用比較常規的做法
當下拉框展開的時候,監聽toggle() { this.UI.isOpen = !this.UI.isOpen; if (this.UI.isOpen) { const datePicker = this.$refs["date-picker"]; const handler = event => { let dom = event.target; let flag = false; while (dom) { if (dom === datePicker) { flag = true; break; } dom = dom.parentNode; } if (!flag) this.UI.isOpen = flag; document.removeEventListener("click", handler, true); }; document.addEventListener("click", handler, true); } } 複製程式碼
document
的click
事件,同時定義事件傳播模式為 use capture,此時遍歷 Dom,判斷是否在event.target
是否為 Date-picker元件。
在處理資料的時候獲取到了當前的年、月、日,也就是 data
中的 date
物件
date: {
year: null,
month: null,
day: null
}
複製程式碼
利用該物件來生成相應的下拉框的資料:
-
年:
date.year
來生成年份的資料,也就是當前年份--到--當前年份+12;yearList() { const { date: { year } } = this; return Array.from({ length: 12 }, (v = year, i) => ({ type: "year", value: v + i }) ); } 複製程式碼
-
月:區分中英文,當前路徑下維護了一份
CONST.json
用於防止靜態的中英文月份;monthList() { const { lang } = this; return CONST[lang].month; } 複製程式碼
-
日:這一部分比較複雜,首先實現當前月份的總天數,之後依據本月一天的星期數以及下個月第一天的星期數來填充表格,如圖
dayList() { const { date: { year, month }, selectValue } = this; // 後去當前月份的天數 let curMonthDays = new Date(year, month + 1, 0).getDate(); // 第一天的星期數 let firstDay = new Date(year, month, 1).getDay(); // 下個月第一天的星期數 let preMonthDays = new Date(year, month, 0).getDate(); let days = Array.from( { length: curMonthDays }, (val, index) => { let value = index + 1; let date = { year, month, day: value }; // 選中日期高亮 let type = isEqualDay(date, new Date(selectValue)) ? "cur-month is-selected" : "cur-month"; return { type, value }; } ); // 標識上一月以及下一個月,對應做樣式處理 for (let index = 0; index < firstDay; index++) { days = [ { type: "pre-month", value: preMonthDays-- } ].concat(days); } for (let index = days.length, item = 1; index < 42; index++, item++) { days.push({ type: "next-month", value: item }); } return CONST[lang].day.concat(days); } 複製程式碼
下拉框主要分為兩部分:操作欄、日期選擇框
-
操作欄
<div class="picker-panel" v-show="UI.isOpen"> <div class="panel-header"> <div class="left-part"> <fat-icon class="panel-header-btn" name="chevron_left" :size="20" @click.stop="handleClick('decYear')" /> <fat-icon class="panel-header-btn" name="chevron_left" :size="20" @click.stop="handleClick('decMonth')" /> </div> ... <div> <fat-icon class="panel-header-btn" name="chevron_right" :size="20" @click.stop="handleClick('addMonth')" /> <fat-icon class="panel-header-btn" name="chevron_right" :size="20" @click.stop="handleClick('addYear')" /> </div> </div> </div> 複製程式碼
四個 icon 主要負責加減月份以及年份,由於四個都屬於點選事件,並且只修改了
data
,利用介面卡模式來處理handleClick(type) { const handlers = { addYear: () => ++this.date.year, decYear: () => --this.date.year, addMonth: () => ++this.date.month, decMonth: () => --this.date.month, year: () => (this.panelType = "year"), month: () => (this.panelType = "month") }; handlers[type](); } 複製程式碼
同時 watch 狀態
date
,完成相關年月的進位date: { handler(newValue) { let { month } = newValue; if (month > 11) { ++this.date.year; this.date.month = 0; } else if (month < 0) { --this.date.year; this.date.month = 11; } else { this.date.month = newValue.month; } }, deep: true }, 複製程式碼
-
日期選擇框:這部分要實時變動,為了省去模板解析的耗費,採用 Functional Component 來實現,也就是說這一部分是函式式元件。
<date-panel class="panel-content" :type="panelType" :data="list" @select="panelClick" /> 複製程式碼
其
props
包含上述年、月、日的資料,同樣也採用介面卡模式,依據panelType
來區分展示的是那一部分資料import GeneratorRows from './basic' export default Vue.component('panel', { functional: true, render: function (_h, context) { // 獲取panel元件的props,包含資料data以及型別type const { data: list, type } = context.props let result = null // 如果展示的日,一行的數量為7個,如果是年月則展示3個。 let num = type === 'day' ? 7 : 3 // 此處利用事件委託 const clickHandler = (e) => { if (e.target.attributes.index) { let value = e.target.attributes.index.value let params = { type, value } type === 'day' && Object.assign(params, { dateType: e.target.attributes.dateType.value }) context.listeners.select(params) } e.stopPropagation() } // GeneratorRows為自定義函式,用來生成對應的行 result = _h('table', { attrs: { class: context.data.staticClass, cellspacing: 0, cellpadding: 0 }, on: { click: clickHandler } }, GeneratorRows(_h, type, list, num)) return result } }) 複製程式碼
整體結構非常簡單,首先獲取該元件的
props
,從中得到資料和型別,然後利用
GeneratorRows
函式去生成對應的table
,由於table
內項比較多,所以利用事件委託技術,監聽table
的click
事件,如果觸發的話,獲取
e.target
對應的屬性值e.target.attributes.index.value
,結合之前的型別,構建引數params
,再觸發自定義事件context.listeners.select(params)
。export default function GeneratorRows(_h, type, list, itemNum) { let rows = [] let row = [] list.forEach((elem, index) => { let dom = index < itemNum ? 'th' : 'td' let className = index < itemNum && type === 'day' ? 'head-item' : `data-item ${elem.type}` let label = elem.label || elem.value row.push( _h( dom, { attrs: { class: className, // 用於事件委託 dateType: elem.type, index: elem.value, } }, label ) ) if (row.length % itemNum === 0 && row.length) { // 換行 rows.push( _h( 'tr', { attrs: { class: "panel-content-row" } }, row ) ) row = [] } }) return rows } 複製程式碼
GeneratorRows
函式就是遍歷上述list
,然後依據規則生成對應table
。
3. 總結
這個元件原始的邏輯比較複雜,通過元件化的拆分以及資料的整合,使得整體的邏輯比較明瞭,也是我寫這套元件庫的原因。
往期文章:
- 從零實現Vue的元件庫(零)- 基本結構以及構建工具
- 從零實現Vue的元件庫(一)- Toast 實現
- 從零實現Vue的元件庫(二)- Slider 實現
- 從零實現Vue的元件庫(三)- Tabs 實現
- 從零實現Vue的元件庫(四)- File-Reader 實現
- 從零實現Vue的元件庫(五)- Breadcrumb 實現
- 從零實現Vue的元件庫(六)- Hover-Tip 實現
- 從零實現Vue的元件庫(七)- Message-Box 實現
- 從零實現Vue的元件庫(八)- Input 實現
- 從零實現Vue的元件庫(九)- InputNumber 實現
- 從零實現Vue的元件庫(十)- Select 實現
- 從零實現Vue的元件庫(十一)- Date-picker 實現
- 從零實現Vue的元件庫(十二)- Table 實現
- 從零實現Vue的元件庫(十三)- Pagination 實現
- 從零實現Vue的元件庫(十四)- RadioGroup 實現
- 從零實現Vue的元件庫(十五)- CheckboxGroup 實現
原創宣告: 該文章為原創文章,轉載請註明出處。