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 实现
原创声明: 该文章为原创文章,转载请注明出处。