使用Vue寫一個datepicker的樣本,vuedatepicker樣本

來源:互聯網
上載者:User

使用Vue寫一個datepicker的樣本,vuedatepicker樣本
前言

寫外掛程式是很有意思,也很鍛煉人,因為這個過程中能發現許多的細節問題。在前端發展的過程中,jQuery無疑是一個重要的裡程碑,圍繞著這個優秀項目也出現了很多優秀的外掛程式可以直接使用,大大節省了開發人員們的時間。jQuery最重要的作用是跨瀏覽器,而現在瀏覽器市場雖不完美,但已遠沒有從前那麼慘,資料驅動視圖的思想倍受歡迎,大家開始使用前端架構取代jQuery,我個人比較喜歡Vue.js,所以想試著用Vue.js寫一個組件出來。

為了發布到npm上,所以給項目地址改名字了,但是內部代碼沒有改,使用方法比之前方便。

GitHub地址: Here

功能&期望

這個datepicker目前僅實現了一些常用的功能:

  1. 選擇時間(這話說得有點多餘)
  2. 最大/最小時間限制
  3. 中/英文切換(其實也就星期和月份需要切換)
  4. 可以以.vue形式使用,也可在瀏覽器環境中直接使用
  5. 沒了。。。
目錄結構

萬事的第一步依然是建立項目,只是單一組件,結構並不複雜,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,currentMonth和nextMonth分別做了標記,對其他功能提供判斷條件。
年份和月份的列表都是差不多的道理,年份列表的初始值我直接寫在了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() //truenew 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.library和output.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從生命週期鉤子中移除了,遷移方法很簡單,使用mounted和this.$nextTick來替換。

prop.sync棄用

prop的sync棄用了,遷移方案是使用自訂事件,而且Datepicker這種input類型組件,可以使用表單輸入組件的自訂事件作為替換方案。自訂群組件也可以使用v-model指令了,但是必須滿足兩個條件:

  1. 接收一個value的prop
  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,那麼差不多就這樣。也希望大家多多支援幫客之家。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.