运用Vue写一个datepicker

媒介

写插件是很有意义,也很磨炼人,由于这个历程当中能发明许多的细节题目。在前端生长的历程当中,jQuery无疑是一个主要的里程碑,围绕着这个优异项目也涌现了许多优异的插件能够直接运用,大大节省了开发者们的时候。jQuery最主要的作用是跨浏览器,而如今浏览器市场虽不圆满,但已远没有夙昔那末惨,数据驱动视图的头脑倍受欢迎,人人开始运用前端框架庖代jQuery,我个人比较喜好Vue.js,所以想试着用Vue.js写一个组件出来。

为了宣布到npm上,所以给项目地点改名字了,然则内部代码没有改,运用要领比之前轻易。
Demo演示: Here
GitHub地点: Here
愿望人人能给个star

功用&希冀

这个datepicker现在仅完成了一些经常使用的功用:

  • 挑选时候(这话说得有点过剩)

  • 最大/最小时候限定

  • 中/英文切换(实在也就礼拜和月份须要切换)

  • 能够以.vue情势运用,也可在浏览器环境中直接运用

  • 没了。。。

目次组织

万事的第一步依然是建立项目,只是单一组件,组织并不庞杂,Datepicker.vue是最主要的组件文件,dist是webpack的输出文件夹,index.js是webpack打包的进口文件,末了是webpack的配置文件,用来对我们的库文件举行打包用的。因而项目组织就是如许:

.
├── Datepicker.vue
├── LICENSE
├── README.md
├── dist
│   └── vue-datepicker.js
├── index.js
├── package.json
└── webpack.config.js

从Datepicker.vue入手

.vue的体式格局写Vue组件是一种特别写法,每一个Vue文件包含template, script, style三部份,template最好不要成为片断实例,所以最外层先套一层div,当作悉数组件的根元素。一个datepicker平常由两部份构成,一个用来显现日期的input框,一个用来挑选日期的panel,由于我发明input在挪动端会自动唤起键盘,所以没有运用input,直接用了div模仿,经由历程点击事宜决议panel的显隐。value是终究的效果,须要和父组件通讯,所以将value写成了prop,在父组件中运用value.sync="xxx",datepicker的value就和父组件的xxx双向绑定了。

<template>
    <div class="date-picker">
        <div class="input" v-text="value" @click="panelState = !panelState">
    </div>
    <div class="date-panel" v-show="panelState">
    </div>
</template>

<scrip>
    export default {
        data () {
            return {
                panelState: false //初始值,默许panel封闭
            }
        },
        props: {
            value: String
        }
    }
</script>

衬着日期列表

一个月起码是28天,假如把周日排在开首,那末起码(1号恰好是周日)须要4行,然则每月天数30,31占多数,而且1号又不一定是周日,我痛快痛快按最多的状况设想了,共6行,当月日期没填满的处所用上个月或下个月的日期补齐,如许就轻易盘算了,而且切换月份时候panel高度不会变化。日期列表的数组须要动态盘算,Vue供应了computed这个属性,所以直接将日期列表dateList写成盘算属性。我的要领是将日期列表固定为长度为42的数组,然后将本月,上个月,下个月的日期顺次添补。

computed: {
    dateList () {
        //猎取当月的天数
        let currentMonthLength = new Date(this.tmpMonth, this.tmpMonth + 1, 0).getDate()
        //先将当月的日期塞入dateList
        let dateList = Array.from({length: currentMonthLength}, (val, index) => {
            return {
                currentMonth: true,
                value: index + 1
            }
        })
        //猎取当月1号的礼拜是为了确定在1号前须要插若干天
        let startDay = new Date(this.year, this.tmpMonth, 1).getDay()
        //确认上个月一共若干天
        let previousMongthLength = new Date(this.year, this.tmpMonth, 0).getDate()
    }
    //在1号前插进去上个月日期
    for(let i = 0, len = startDay; i < len; i++){
        dateList = [{previousMonth: true, value: previousMongthLength - i}].concat(dateList)
    }
    //补全盈余位置
    for(let i = 0, item = 1; i < 42; i++, item++){
        dateList[dateList.length] = {nextMonth: true, value: i}
    }
    return dateList
}

这里用Array.from来初始化了一个数组,传入一个Array Like,转化成数组,在拼接字符串时候采用了arr[arr.length][{}].concat(arr)这类体式格局,由于在JsTips上学到如许做机能更好,文章的末了会贴出相干链接。
如许,日期列表就构建好了,在template中运用v-for轮回衬着出来

<ul class="date-list">
    <li v-for="item in dateList"
        v-text="item.value" 
        :class="{preMonth: item.previousMonth, nextMonth: item.nextMonth,
            selected: date === item.value && month === tmpMonth && item.currentMonth, invalid: validateDate(item)}"
        @click="selectDate(item)">
    </li>
</ul>
            

款式上就能够自身发挥了,怎样喜好怎样写。须要注重的是轮回日期可能会涌现上个月或这个月的日期,我经由历程previuosMonth,currentMonthnextMonth离别做了标记,对其他功用供应推断前提。
年份和月份的列表都是差不多的原理,年份列表的初始值我直接写在了data里,以当前年份为第一个,为了和月份保持一致,每次显现12个,都经由历程v-for衬着。


data () {
    return {
        yearList: Array.from({length: 12}, (value, index) => new Date().getFullYear() + index)
    }
}

挑选日期功用

挑选递次是:年 -> 月 -> 日,所以我们能够经由历程一个状况变量来掌握panel中显现的内容,绑定合适的函数切换显现状况。

<div>
    <div class="type-year" v-show="panelType === 'year'">
        <ul class="year-list">
          <li v-for="item in yearList"
              v-text="item"
              :class="{selected: item === tmpYear, invalid: validateYear(item)}" 
              @click="selectYear(item)"
          >
          </li>
      </ul>
    </div>
    <div class="type-month" v-show="panelType === 'month'">
        <ul class="month-list">
          <li v-for="item in monthList"
              v-text="item | month language"
              :class="{selected: $index === tmpMonth && year === tmpYear, invalid: validateMonth($index)}" 
              @click="selectMonth($index)"
          >
          </li>
      </ul>
    </div>
    <div class="type-date" v-show="panelType === 'date'">
        <ul class="date-list">
          <li v-for="item in dateList"
              v-text="item.value" 
              track-by="$index" 
              :class="{preMonth: item.previousMonth, nextMonth: item.nextMonth,
                  selected: date === item.value && month === tmpMonth && item.currentMonth, invalid: validateDate(item)}"
              @click="selectDate(item)">
          </li>
      </ul>
    </div>
</div>

挑选日期的要领就不细说了,在selectYear,selectMonth中对年份,月份变量赋值,再离别将panelType推向下一步就完成了日期挑选功用。
不过在未挑选完日期之前,你可能不愿望当前年月的实在值发生变化,所以在这些要领中可先将挑选的值赋给一个暂时变量,比及seletDate的时候再一次性悉数赋值。

selectMonth (month) {
    if(this.validateMonth(month)){
        return
    }else{
        //暂时变量
        this.tmpMonth = month
        //切换panel状况
        this.panelType = 'date'
    }
},
selectDate (date) {
    //validate logic above...
    //一次性悉数赋值
    this.year = tmpYear
    this.month = tmpMonth
    this.date = date.value
    this.value = `${this.tmpYear}-${('0' + (this.month + 1)).slice(-2)}-${('0' + this.date).slice(-2)}`
    //挑选完日期后,panel自动隐藏
    this.panelState = false
}

最大/小时候限定

最大/小值是须要从父组件通报下来的,因而应当运用props,别的,这个值能够是字符串,也应当能够是变量(比方同时存在两个datepicker,第二个的日期不能比第一个大这类逻辑),所以应当运用Dynamically bind的体式格局传值。

<datepicker :value.sync="start"></datepicker>
<!-- 如今min的值会跟着start的变化而变化 -->
<datepicker :value.sync="end" :min="start" ></datepicker>

增加了限定前提,关于不正当的日期,其按钮应当变成置灰状况,我用了比较时候戳的体式格局来推断日期是不是正当,由于就算当前panel中的日期是跨年或是跨月的,经由历程日期组织函数建立时都邑帮你转换成对应的正当值,省去许多推断的贫苦:


new Date(2015, 0, 0).getTime() === new Date(2014, 11, 31).getTime() //true
new Date(2015, 12, 0).getTime() === new Date(2016, 0, 0).getTime() //true

因而考证日期是不是正当的函数是如许的:


validateDate (date) {
  let mon = this.tmpMonth
  if(date.previousMonth){
      mon -= 1
  }else if(date.nextMonth){
      mon += 1
  }
  if(new Date(this.tmpYear, mon, date.value).getTime() >= new Date(this.minYear, this.minMonth - 1, this.minDate).getTime()
      && new Date(this.tmpYear, mon, date.value).getTime() <= new Date(this.maxYear, this.maxMonth - 1, this.maxDate).getTime()){
      return false
  }
  return true

}

动态盘算位置

当页面右边有充足的空间显现时,datepicker的panel会定位为相关于父元素left: 0的位置,假如没有充足的空间,则应当置于right: 0的位置,这一点能够经由历程Vue供应的动态款式和款式对象来完成(动态class和动态style实在只是动态props的惯例),而盘算位置的时候,我放在了组件声明周期的ready周期中,由于这时候组件已插进去到DOM树中,能够猎取style举行动态盘算:

ready () {
    if(this.$el.parentNode.offsetWidth + this.$el.parentNode.offsetLeft - this.$el.offsetLeft <= 300){
        this.coordinates = {right: '0', top: `${window.getComputedStyle(this.$el.children[0]).offsetHeight + 4}px`}
    }else{
        this.coordinates = {left: '0', top: `${window.getComputedStyle(this.$el.children[0]).offsetHeight + 4}px`}
    }
}
<!-- template中对应的动态style -->
<div :style="coordinates"></div>

为了panel的显隐能够腻滑过渡,能够运用transition做过渡动画,这里我简朴地经由历程一个0.2秒的透明度过渡让显隐更腻滑。

<div :style="this.coordinates" v-show="panelState" transition="toggle"></div>

//less syntax
.toggle{
    &-transition{
        transition: all ease .2s;
    }
    &-enter, &-leave{
        opacity: 0;
    }
}

中英文切换

这里实在也很简朴,这类多言语切换本质就是一个key依据差别的type而输出差别的value,所以运用filter能够很轻易的完成它!比方衬着礼拜的列表:

<ul class="weeks">
     <li v-for="item in weekList" v-text="item | week language"></li>
 </ul>
 
filters : {
    week (item, lang){
        switch (lang) {
          case 'en':
              return {0: 'Su', 1: 'Mo', 2: 'Tu', 3: 'We', 4: 'Th', 5: 'Fr', 6: 'Sa'}[item]
          case 'ch':
              return {0: '日', 1: '一', 2: '二', 3: '三', 4: '四', 5: '五', 6: '六'}[item]
          default:
              return item
      }
    }
}

多种运用体式格局

关于一个Vue组件,假如是运用webpack + vue-loader.vue单文件写法,我愿望如许运用:

//App.vue
<script>
    import datepicker from 'path/to/datepicker.vue'
    export default {
        components: { datepicker}
    }
</script>

假如是直接在浏览器中运用,那末我愿望datepicker这个组件是暴露在全局下的,能够这么运用:


//index.html
<html>
    <script src="path/to/vue.js"></script>
    <script src="path/to/datepicker.js"></script>
    <body>
        <div id="app"></div>
        <script>
            new Vue({
                el: '#app',
                components: { datepicker }
            })
        </script>
    </body>
</html>

这里我挑选了webpack作为打包东西,运用webpack的output.libraryoutput.linraryTarget这两个属性就能够把你的bundle文件作为库文件打包。library定义了库的名字,libraryTarget定义了你想要打包的花样,详细能够看文档。我愿望自身的库能够经由历程datepicker加载到,而且打包成umd花样,因而我的webpack.config.js是如许的:

module.exports = {
    entry: './index.js',
    output: {
        path: './dist',
        library: 'datepicker',
        filename: 'vue-datepicker.js',
        libraryTarget: 'umd'
    },
    module: {
        loaders: [
            {test: /\.vue$/, loaders: ['vue']},
            {test: /\.js$/, exclude: /node_modules/, loaders: ['babel']}
        ]
    }
}

打包完成的模块就是一个umd花样的模块啦,能够在浏览器中直接运用,也能够合营require.js等模块加载器运用!

适配 Vue 2.x

Vue 2.0已宣布有段时候了,如今把之前的组件适配到Vue 2.0。迁徙历程照样很顺遂的,中心API修正不大,能够借助vue-migration-helper来找出烧毁的API再逐渐修正。这里只枚举一些我须要修正的API。

filter

2.0中的filter只能在mustache绑定中运用,假如想在指令式绑定中绑定过滤后的值,能够挑选盘算属性。我在月份和礼拜的显现中运用到了过滤器来过滤言语范例,但我之前是在指令式绑定中运用的filter,所以须要以下修正,:

//修正前
<div class="month-box" @click="chType('month')" v-text="tmpMonth + 1 | month language"></div>
//修正后,filter传参的体式格局也变了,变成了函数挪用的作风
<div class="month-box" @click="chType('month')">{{tmpMonth + 1 | month(language)}}</div>

移除$index$key

这两个属性不会在v-for中被自动建立了,如需运用,要在v-for中自行声明:

<li v-for="item in monthList" @click="selectMonth($index)"></li>
//
<li v-for="(item, index) in monthList" @click="selectMonth(index)"></li>

ready 生命周期移除

ready从生命周期钩子中移除了,迁徙要领很简朴,运用mountedthis.$nextTick来替代。

prop.sync弃用

propsync弃用了,迁徙计划是运用自定义事宜,而且Datepicker这类input范例组件,能够运用表单输入组件的自定义事宜作为替代计划。自定义组件也能够运用v-model指令了,然则必需满足两个前提:

  1. 吸收一个valueprop

  2. 值发生变化时,触发一个input事宜,传入新值。

所以Datepicker的运用体式格局也不是<datepicker value.sync="now"></datepicker>了,而是<datepicker v-model="now"></datepicker>。组件自身向父级传值的体式格局也不一样了:

//1.x版本,设置了value的值会同步到父级
this.value = `${this.tmpYear}-${('0' + (this.month + 1)).slice(-2)}-${('0' + this.date).slice(-2)}`

//2.x版本,须要自身触发input事宜,将新值作为参数通报归去
let value = `${this.tmpYear}-${('0' + (this.month + 1)).slice(-2)}-${('0' + this.date).slice(-2)}`
this.$emit('input', value)

总结

以上就是我在写这个datepicker时的大抵思绪,自身也是很简朴的事变,没有到处睁开来讲,写在这里作为自身的一个总结,假如有刚开始运用Vue的同砚也愿望这篇文章能够在思绪上协助到你们:P,关于列位老鸟假如有什么指导的处所我也很谢谢:D,那末差不多就如许,背面贴一些相干引荐浏览。

引荐浏览

高效地向数组中插值
Vue.js-片断实例
Vue.js-动态绑定
Js日期对象基本
Webpack: export bundle as library
UMD(universial Module Defination)
Migration from Vue 1.x

    原文作者:Showonne
    原文地址: https://segmentfault.com/a/1190000006194285
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞