资讯专栏INFORMATION COLUMN

使用Vue写一个datepicker

luckyyulin / 2768人阅读

摘要:演示地址希望大家能给个功能期望这个目前仅实现了一些常用的功能选择时间这话说得有点多余最大最小时间限制中英文切换其实也就星期和月份需要切换可以以形式使用,也可在浏览器环境中直接使用没了。。。

前言

写插件是很有意思,也很锻炼人,因为这个过程中能发现许多的细节问题。在前端发展的过程中,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双向绑定了。




    export default {
        data () {
            return {
                panelState: false //初始值,默认panel关闭
            }
        },
        props: {
            value: String
        }
    }

渲染日期列表

一个月最少是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循环渲染出来

样式上就可以自己发挥了,怎么喜欢怎么写。需要注意的是循环日期可能会出现上个月或这个月的日期,我通过previuosMonth,currentMonthnextMonth分别做了标记,对其他功能提供判断条件。
年份和月份的列表都是差不多的道理,年份列表的初始值我直接写在了data里,以当前年份为第一个,为了和月份保持一致,每次显示12个,都通过v-for渲染。

data () {
    return {
        yearList: Array.from({length: 12}, (value, index) => new Date().getFullYear() + index)
    }
}
选择日期功能

选择顺序是:年 -> 月 -> 日,所以我们可以通过一个状态变量来控制panel中显示的内容,绑定适合的函数切换显示状态。

选择日期的方法就不细说了,在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的方式传值。




增加了限制条件,对于不合法的日期,其按钮应该变为置灰状态,我用了比较时间戳的方式来判断日期是否合法,因为就算当前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`}
    }
}

为了panel的显隐可以平滑过渡,可以使用transition做过渡动画,这里我简单地通过一个0.2秒的透明度过渡让显隐更平滑。

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

这里其实也很简单,这种多语言切换实质就是一个key根据不同的type而输出不同的value,所以使用filter可以很容易的实现它!比如渲染星期的列表:

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

如果是直接在浏览器中使用,那么我希望datepicker这个组件是暴露在全局下的,可以这么使用:

//index.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,所以需要如下修改,:

//修改前
//修改后,filter传参的方式也变了,变成了函数调用的风格
{{tmpMonth + 1 | month(language)}}
移除$index$key

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

  • //
  • ready 生命周期移除

    ready从生命周期钩子中移除了,迁移方法很简单,使用mountedthis.$nextTick来替换。

    prop.sync弃用

    propsync弃用了,迁移方案是使用自定义事件,而且Datepicker这种input类型组件,可以使用表单输入组件的自定义事件作为替换方案。自定义组件也可以使用v-model指令了,但是必须满足两个条件:

    接收一个valueprop

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

    所以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

    文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

    转载请注明本文地址:https://www.ucloud.cn/yun/90895.html

    相关文章

    • 基于Vue2.x日期选择组件

      摘要:基于日期选择组件是一款稍微复杂的组件,其中涉及的日历计算与显示需要比较清晰的逻辑。 vue-datepicker - 基于Vue2.x日期选择组件 Datepicker是一款稍微复杂的组件,其中涉及的日历计算与显示需要比较清晰的逻辑。 项目地址 https://github.com/watson-yan... 预览图 showImg(https://segmentfault.com/i...

      NusterCache 评论0 收藏0
    • vue一个时间选择器

      摘要:实现代码于文章末尾处构思页面结构组件由输入框和日历面板组成,写好页面主体结构。输入框点击显示或隐藏日历面板方法改变布尔值控制日历面板的显示隐藏。同时,当组件销毁时,也要及时清除该监听器。 最近研究了 DatePicker 的实现原理后做了一个 vue 的 DatePicker 组件,今天带大家一步一步实现 DatePicker 的 vue 组件。 原理 DatePicker 的原理是—...

      sf_wangchong 评论0 收藏0
    • Nuxt.js按需引入 emement-ui

      摘要:全面配置文章系列安装依赖修改修改 ☞☞☞nuxt.js全面配置☜☜☜ ☞☞☞nuxt文章系列☜☜☜ babel-plugin-component 安装依赖 npm i -D babel-plugin-component // or yarn add -D babel-plugin-component 修改nuxt.config.js module.exports = { bui...

      microcosm1994 评论0 收藏0
    • 移动端material风格日期时间选择器

      摘要:好多时候在移动端需要一个的日期选择器,由于在应用上有可能应用各种框架库等所以说一个无依赖的,这样易于上层进行封装。主要包含两种选择器日期和时间。 好多时候在移动端需要一个的日期选择器,由于在应用上有可能应用各种框架库(Vue.js, React.js, zepto.js等);所以说一个无依赖的,这样易于上层进行封装。直接开门见山,先来张动图看看效果: showImg(https://s...

      philadelphia 评论0 收藏0
    • 使用 Webpack 时需避免循环引用

      摘要:然则明明是定义了的,只是验证两个类文件的话,均未出现任何语法错误。打开开发者工具,勾上,观察发生异常时的状况,一遍又一遍,我渐渐意识到,发生这个错误的时候,还未能在的环境中完成注册。 开发日历控件的时候,对方变更了一下需求,基本上将最终产品分成两个: 选择连续时间段 选择多个不连续时间 那么我们知道,对于这种大部分功能一致,只有若干函数逻辑不同的产品,最合适的就是状态模式。于是很自...

      paulquei 评论0 收藏0

    发表评论

    0条评论

    luckyyulin

    |高级讲师

    TA的文章

    阅读更多
    最新活动
    阅读需要支付1元查看
    <