Recently studied the realization principle of DatePicker made a Vue DatePicker component, today with you step by step to realize the VUE component of DatePicker.

The principle of

The DatePicker works by calculating the total number of days in the calendar panel for the current or selected month and the days that are close to the calendar panel, calculating the calendar panel display based on click events, and assigning selected values to the TAB.

implementation

  • The CSS code is at the end of the article

1. Structure your page

The DatePicker component consists of an input box and a calendar panel that writes the page body structure.

<div class="date-picker">
  <input class="input" v-model="dateValue" @click="openPanel"/>
  <transition name="fadeDownBig">
    <div class="date-panel" v-show="panelState"></div>
  </transiton>
</div>
Copy the code

< INPUT > Click to show or hide the calendar panel. The openPanel() method changes the Boolean value of panelState to control the display and hide of the calendar panel.

The calendar panel is composed of the top bar and the panel, while the panel is composed of the year selection panel, the month selection panel and the date selection panel. The structure is as follows:

<div class="date-panel" v-show="panelState">
  <! -- Top button and date display bar -->
  <div class="topbar">
    <span @click="leftBig">&lt; &lt;</span>
    <span @click="left">&lt;</span>
    <span class="year" @click="panelType = 'year'">{{tmpYear}}</span>
    <span class="month" @click="panelType = 'month'">{{changeTmpMonth}}</span>
    <span @click="right">&gt;</span>
    <span @click="rightBig">&gt; &gt;</span>
  </div>
  <! -- Year panel -->
  <div class="type-year" v-show="panelType === 'year'">
    <ul class="year-list">
      <li v-for="(item, index) in yearList"
          :key="index"
          @click="selectYear(item)"
      >
        <span :class="{selected: item === tmpYear}" >{{item}}</span>
      </li>
    </ul>
  </div>
  <! -- Moon Panel -->
  <div class="type-year" v-show="panelType === 'month'">
    <ul class="year-list">
      <li v-for="(item, index) in monthList"
          :key="index"
          @click="selectMonth(item)"
      >
        <span :class="{selected: item.value === tmpMonth}" >{{item.label}}</span>
      </li>
    </ul>
  </div>
  <! -- Date panel -->
  <div class="date-group" v-show="panelType === 'date'">
    <span v-for="(item, index) in weekList" :key="index" class="weekday">{{item.label}}</span>
    <ul class="date-list">
      <li v-for="(item, index) in dateList"
          v-text="item.value"
          :class="{preMonth: item.previousMonth, nextMonth: item.nextMonth, selected: date === item.value && month === tmpMonth && item.currentMonth, invalid: validateDate(item)}"
          :key="index" 
          @click="selectDate(item)">
      </li>
    </ul>
  </div>
</div>
Copy the code

2. Page data implementation

DatePicker corresponds to the data code

data() {
  return {
    dateValue: "".// The input box displays the date
    date: new Date().getDate(), // The current date
    panelState: false.// Initial value, default panel off
    tmpMonth: new Date().getMonth(), // Temporary month, modifiable
    month: new Date().getMonth(),
    tmpYear: new Date().getFullYear(), // Temporary year, modifiable
    weekList: [
      { label: "Sun".value: 0 },
      { label: "Mon".value: 1 },
      { label: "Tue".value: 2 },
      { label: "Wed".value: 3 },
      { label: "Thu".value: 4 },
      { label: "Fri".value: 5 },
      { label: "Sat".value: 6}]./ / week
    monthList: [
      { label: "Jan".value: 0 },
      { label: "Feb".value: 1 },
      { label: "Mar".value: 2 },
      { label: "Apr".value: 3 },
      { label: "May".value: 4 },
      { label: "Jun".value: 5 },
      { label: "Jul".value: 6 },
      { label: "Aug".value: 7 },
      { label: "Sept".value: 8 },
      { label: "Oct".value: 9 },
      { label: "Nov".value: 10 },
      { label: "Dec".value: 11}]./ / month
    nowValue: 0.// Currently selected date value
    panelType: "date" // Panel status
  };
},
Copy the code

The core of DatePicker is the data in the date panel. We know that the month has 31 days at most and 28 days at least. Panels are designed from Sunday to Saturday, with the most extreme cases as follows:

The most extreme cases:

day one two three four five six
* * * * * * 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5

The least extreme:

day one two three four five six
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
1 2 3 4 5 6 7
8 9 10 11 12 13 14

According to the above table, we can know that a month takes up six weeks at most and four weeks at least, so the calendar panel must be designed with six lines, and the rest should be filled with the date of the next month, with 14 days at most. So the date array can be designed like this:

computed: {
  dateList() {
    // Get the days of the month
    let currentMonthLength = new Date(
      this.tmpYear,
      this.tmpMonth + 1.0
    ).getDate();
    // Fill a dateList with the date of the current month
    let dateList = Array.from(
      { length: currentMonthLength },
      (val, index) => {
        return {
          currentMonth: true.value: index + 1}; });// Get the week of the first day of the month to determine how many days to insert before the first day
    let startDay = new Date(this.tmpYear, this.tmpMonth, 1).getDay();
    // Confirm the number of days in the last month
    let previousMongthLength = new Date(
      this.tmpYear,
      this.tmpMonth,
      0
    ).getDate();
    // Insert last month's date before 1
    for (let i = 0, len = startDay; i < len; i++) {
      dateList = [
        { previousMonth: true.value: previousMongthLength - i }
      ].concat(dateList);
    }
    // Complete the remaining positions, at least 14 days, then I < 15
    for (let i = 1, item = 1; i < 15; i++, item++) {
      dateList[dateList.length] = { nextMonth: true.value: i };
    }
    returndateList; }},Copy the code

ChangeTmpMonth is the copy displayed after the month is selected, and yearList is the list of years. In order to keep consistent with the number of months, we also set the length to 12.

computed: {
  changeTmpMonth() {
    return this.monthList[this.tmpMonth].label;
  },
  // Change the year array by changing this.tmpYear
  yearList() {
    return Array.from({ length: 12 }, (value, index) => this.tmpYear + index); }}Copy the code

3. Implement page functions

(1) Panel switching function

  • Click the input box to open the calendar panel as well as the date panel by default
openPanel() {
  this.panelState = !this.panelState;
  this.panelType = "date";
},
Copy the code
  • Click the year 2018 to enter the year panel, click the corresponding year to display the year and enter the month selection panel
<span class="year" @click="panelType = 'year'">{{tmpYear}}</span>
Copy the code
selectYear(item) {
  this.tmpYear = item;
  this.panelType = "month";
},
Copy the code
  • Click Aug to enter the month panel, click corresponding month to display the month and enter the date selection panel
<span class="month" @click="panelType = 'month'">{{changeTmpMonth}}</span>
Copy the code
selectMonth(item) {
  this.tmpMonth = item.value;
  this.panelType = "date";
},
Copy the code

Click date to select the date, close the panel and assign to the input box

// methods
selectDate(item) {
  // Assign the current nowValue to control the style to highlight the date of the current month
  this.nowValue = item.value;
  // Select last month
  if (item.previousMonth) this.tmpMonth--;
  // Next month is selected
  if (item.nextMonth) this.tmpMonth++;
  // Get the date of the selected date
  let selectDay = new Date(this.tmpYear, this.tmpMonth, this.nowValue);
  // Format the date as a string and assign it to input
  this.dateValue = this.formatDate(selectDay.getTime());
  // Close the panel
  this.panelState = !this.panelState;
},
// Date format method
formatDate(date, fmt = this.format) {
  if (date === null || date === "null") {
    return "--";
  }
  date = new Date(Number(date));
  var o = {
    "M+": date.getMonth() + 1./ / in
    "d+": date.getDate(), / /,
    "h+": date.getHours(), / / hour
    "m+": date.getMinutes(), / / points
    "s+": date.getSeconds(), / / SEC.
    "q+": Math.floor((date.getMonth() + 3) / 3), / / quarter
    S: date.getMilliseconds() / / ms
  };
  if (/(y+)/.test(fmt))
    fmt = fmt.replace(
      RegExp. $1,
      (date.getFullYear() + "").substr(4 - RegExp. $1.length)
    );
  for (var k in o) {
    if (new RegExp("(" + k + ")").test(fmt))
      fmt = fmt.replace(
        RegExp. $1.RegExp. $1.length === 1
          ? o[k]
          : ("00" + o[k]).substr(("" + o[k]).length)
      );
  }
  return fmt;
},
// Check whether it is the current month
validateDate(item) {
  if (this.nowValue === item.value && item.currentMonth) return true;
},
Copy the code

(2) The left and right arrows function in Topbar. See the following method for details

// <
left() {
  if (this.panelType === "year") this.tmpYear--;
  else {
    if (this.tmpMonth === 0) {
      this.tmpYear--;
      this.tmpMonth = 11;
    } else this.tmpMonth--; }},// <<
leftBig() {
  if (this.panelType === "year") this.tmpYear -= 12;
  else this.tmpYear--;
},
// >
right() {
  if (this.panelType === "year") this.tmpYear++;
  else {
    if (this.tmpMonth === 11) {
      this.tmpYear++;
      this.tmpMonth = 0;
    } else this.tmpMonth++; }},// >>
rightBig() {
  if (this.panelType === "year") this.tmpYear += 12;
  else this.tmpYear++;
},
Copy the code

(3) To achieve bidirectional binding and format provision of input box

props

props: {
  value: {
    type: [Date.String].default: ""
  },
  format: {
    type: String.default: "yyyy-MM-dd"}},Copy the code

Value supports date formats and strings. When props is set, initialize the input value in the Monted hook function. Format The default value is “YYYY-MM-DD “, but you can also set it to” DD-MM-YYYY “.

mounted() {
  if (this.value) {
    this.dateValue = this.formatDate(new Date(this.value).getTime()); }},Copy the code

The bidirectionally bound parent component assigns props to value and the child component passes an event to input, so emit events and data to the parent component in the selectDate method

selectDate(item) {
  ...
  this.$emit("input", selectDay);
},
Copy the code

In this way, the parent component can be bidirectionally bound

<Datepicker v-model="time" format="dd-MM-yyyy"/>
Copy the code

(4) Click other places on the page to fold up the calendar panel

The principle of

Listen for a click event on a page and close the panel when it detects a click event, but clicking on component content also triggers a click event, so you need to prevent bubbling inside the component. At the same time, when the component is destroyed, the listener should be cleared promptly.

Outermost layer of component prevents bubbling

<div class="date-picker" @click.stop></div>
Copy the code

Page creation setup listening

mounted() {
  ...
  window.addEventListener("click".this.eventListener);
}
Copy the code

Page destruction cleanup listener

destroyed() {
  window.removeEventListener("click".this.eventListener);
}
Copy the code

Public methods

eventListener() {
  this.panelState = false;
},
Copy the code

The Demo project

Program source code

Give it a thumbs up if it works

Finally, paste the CSS code…

  • The style after fadeDownBig is vUE<transiton>Animation effects.
.topbar {
  padding-top: 8px;
}
.topbar span {
  display: inline-block;
  width: 20px;
  height: 30px;
  line-height: 30px;
  color: #515a6e;
  cursor: pointer;
}
.topbar span:hover {
  color: #2d8cf0;
}
.topbar .year..topbar .month {
  width: 60px;
}
.year-list {
  height: 200px;
  width: 210px;
}
.year-list .selected {
  background: #2d8cf0;
  border-radius: 4px;
  color: #fff;
}
.year-list li {
  display: inline-block;
  width: 70px;
  height: 50px;
  line-height: 50px;
  border-radius: 10px;
  cursor: pointer;
}
.year-list span {
  display: inline-block;
  line-height: 16px;
  padding: 8px;
}
.year-list span:hover {
  background: #e1f0fe;
}
.weekday {
  display: inline-block;
  font-size: 13px;
  width: 30px;
  color: #c5c8ce;
  text-align: center;
}
.date-picker {
  width: 210px;
  text-align: center;
  font-family: "Avenir", Helvetica, Arial, sans-serif;
}
.date-panel {
  width: 210px;
  box-shadow: 0 0 8px #ccc;
  background: #fff;
}
ul {
  list-style: none;
  padding: 0;
  margin: 0;
}
.date-list {
  width: 210px;
  text-align: left;
  height: 180px;
  overflow: hidden;
  margin-top: 4px;
}
.date-list li {
  display: inline-block;
  width: 28px;
  height: 28px;
  line-height: 30px;
  text-align: center;
  cursor: pointer;
  color: # 000;
  border: 1px solid #fff;
  border-radius: 4px;
}
.date-list .selected {
  border: 1px solid #2d8cf0;
}
.date-list .invalid {
  background: #2d8cf0;
  color: #fff;
}
.date-list .preMonth..date-list .nextMonth {
  color: #c5c8ce;
}
.date-list li:hover {
  background: #e1f0fe;
}
input {
  display: inline-block;
  box-sizing: border-box;
  width: 100%;
  height: 32px;
  line-height: 1.5;
  padding: 4px 7px;
  font-size: 12px;
  border: 1px solid #dcdee2;
  border-radius: 4px;
  color: #515a6e;
  background-color: #fff;
  background-image: none;
  position: relative;
  cursor: text;
  transition: border 0.2 s ease-in-out, background 0.2 s ease-in-out,
    box-shadow 0.2 s ease-in-out;
  margin-bottom: 6px;
}
.fadeDownBig-enter-active..fadeDownBig-leave-active..fadeInDownBig {
  -webkit-animation-duration: 0.5 s;
  animation-duration: 0.5 s;
  -webkit-animation-fill-mode: both;
  animation-fill-mode: both;
}
.fadeDownBig-enter-active {
  -webkit-animation-name: fadeInDownBig;
  animation-name: fadeInDownBig;
}
.fadeDownBig-leave-active {
  -webkit-animation-name: fadeOutDownBig;
  animation-name: fadeOutDownBig; } @ -webkit-keyframes fadeInDownBig {
  from {
    opacity: 0.8;
    -webkit-transform: translate3d(0, -4px, 0);
    transform: translate3d(0, -4px, 0);
  }
  to {
    opacity: 1;
    -webkit-transform: none;
    transform: none; }} @keyframes fadeInDownBig {
  from {
    opacity: 0.8;
    -webkit-transform: translate3d(0, -4px, 0);
    transform: translate3d(0, -4px, 0);
  }
  to {
    opacity: 1;
    -webkit-transform: none;
    transform: none; @ -}}webkit-keyframes fadeOutDownBig {
  from {
    opacity: 1;
  }
  to {
    opacity: 0.8;
    -webkit-transform: translate3d(0, -4px, 0);
    transform: translate3d(0, -4px, 0); }} @keyframes fadeOutDownBig {
  from {
    opacity: 1;
  }
  to {
    opacity: 0; }}Copy the code