从零实现Vue的组件库(十一)- Date-picker 实现

用于选择日期

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,通过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()
      }
    }
    复制代码
  • 下拉框的展开和收缩,与之前 Select 组件不同的是,由于 Date-picker 的下拉框存在着多种状态,而且后续提供输入功能,所以 tabIndexdiv 添加 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);
      }
    }
    复制代码
    当下拉框展开的时候,监听 documentclick 事件,同时定义事件传播模式为 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 内项比较多,所以利用事件委托技术,监听 tableclick 事件,

    如果触发的话,获取 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. 总结

这个组件原始的逻辑比较复杂,通过组件化的拆分以及数据的整合,使得整体的逻辑比较明了,也是我写这套组件库的原因。

往期文章:

原创声明: 该文章为原创文章,转载请注明出处。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值