Date-picer 組件的難點在於:用於選擇日期javascript
new Date
的 Api 使用;render
的代價;代碼html
<!-- 基礎用法 -->
<fat-date-picker v-model="date" />
<!-- 語言爲EN -->
<fat-date-picker lang="EN" v-model="secDate" />
複製代碼
實例地址:DatePicker 實例java
代碼地址:Github UI-Librarygit
基本結構以下github
<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 的數據雙向綁定以及下拉框的展開與收縮:json
v-model
的相關 prop
以及 event
,經過watch prop
的變化,具體邏輯以下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()
}
}
複製代碼
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
對象bash
date: {
year: null,
month: null,
day: null
}
複製代碼
利用該對象來生成相應的下拉框的數據:app
年:date.year
來生成年份的數據,也就是當前年份--到--當前年份+12;dom
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
。
這個組件原始的邏輯比較複雜,經過組件化的拆分以及數據的整合,使得總體的邏輯比較明瞭,也是我寫這套組件庫的緣由。
原創聲明: 該文章爲原創文章,轉載請註明出處。