I. Component introduction

Website links: Calendar component | Element (gitee. IO)

Calendar is used to display dates.

The date processing in Element Plus uses day.js, a 2KB library that is a lightweight version of moment.js.

1.1 attributes

  • V-model: Indicates the Date type, indicating the selected Date.
  • Range: Date[] indicates the range of dates to be displayed. If the range is not transmitted, the current month is default and the month can be changed.

1.2 slot

  • Data-cell: the named slot is used to customize the date display content.

Second, source code analysis

2.1 Calendar component code

2.1.1 the template

<template>
  <div class="el-calendar">
    <div class="el-calendar__header">// Title: Format: July 2021<div class="el-calendar__title">{{ i18nDate }}</div>// Non-range mode<div v-if="validatedRange.length === 0" class="el-calendar__button-group">// In non-range mode, you can switch month // button groups: last month, today, next Month<el-button-group>
          <el-button
            size="mini"
            @click="selectDate('prev-month')"
          >
            {{ t('el.datepicker.prevMonth') }}
          </el-button>
          <el-button size="mini" @click="selectDate('today')">
            {{
              t('el.datepicker.today')
            }}
          </el-button>
          <el-button
            size="mini"
            @click="selectDate('next-month')"
          >
            {{ t('el.datepicker.nextMonth') }}
          </el-button>
        </el-button-group>
      </div>
    </div>// Table display of dates in non-range mode<div v-if="validatedRange.length === 0" class="el-calendar__body">// date-table is an internal component used to render the date table<date-table
        :date="date"
        :selected-day="realSelectedDay"
        @pick="pickDay"
      >
        <template v-if="$slots.dateCell" #dateCell="data">// date-cell Named slot<slot name="dateCell" v-bind="data"></slot>
        </template>
      </date-table>
    </div>// Range mode displays only the dates in the range<div v-else class="el-calendar__body">
      <date-table
        v-for="(range_, index) in validatedRange"
        :key="index"
        :date="range_[0]"
        :selected-day="realSelectedDay"
        :range="range_"
        :hide-header="index ! = = 0"
        @pick="pickDay"
      >
        <template v-if="$slots.dateCell" #dateCell="data">
          <slot name="dateCell" v-bind="data"></slot>
        </template>
      </date-table>
    </div>
  </div>
</template>
Copy the code

2.1.2 script

import { t } from '@element-plus/locale'
import dayjs, { Dayjs } from 'dayjs'
import DateTable from './date-table.vue'

setup(props, ctx) {
    // The selected date
    const selectedDay = ref(null)
    // Generate a dayJS instance with a date pointing to today
    // Use the date I wrote the article as an example. Today is 2021/07/19
    const now = dayjs()
    
    // Calculate property, currently selected date, note that this is an instance of dayJS
    const date: ComputedRef<Dayjs> = computed(() = > {
      // No V-model case is passed in
      // The default v-model attribute in vue3 is modelValue, and the default event is update:modelValue
      if(! props.modelValue) {if (realSelectedDay.value) {
          return realSelectedDay.value
        } else if (validatedRange.value.length) {
          return validatedRange.value[0] [0]}return now
      } else {
        // Upload the v-model information
        return dayjs(props.modelValue)
      }
    })
    
    // Calculate attributes, based on the selected date, calculate today of last month
    // According to the date I wrote the article, it is 2021/06/19
    const prevMonthDayjs = computed(() = > {
      // Subtract API of dayJS makes month -1
      return date.value.subtract(1.'month')})// Calculate the attributes and format them to YYYY-MM according to the selected date
    const curMonthDatePrefix = computed(() = > {
      return dayjs(date.value).format('YYYY-MM')})// Calculate attributes, based on the selected date, calculate today of last month
    // The date I wrote the article is 2021/09/19
    const nextMonthDayjs = computed(() = > {
      // Use the dayjs add API to make month +1
      return date.value.add(1.'month')})// Use i18n to process the current date
    const i18nDate = computed(() = > {
      const pickedMonth = `el.datepicker.month${date.value.format('M')}`
      // T method is an internalized method of Elment Plus, compatible with VUE-I18N
      return `${date.value.year()} ${t('el.datepicker.year')} ${t(pickedMonth)}`
    })
    
    // The actual selected date, through the get set mode set calculation properties, when the access to trigger get method, when the change of the set method trigger
    const realSelectedDay = computed({
      get() {
        if(! props.modelValue)return selectedDay.value
        return date.value
      },
      set(val: Dayjs) {
        selectedDay.value = val
        const result = val.toDate()

        ctx.emit('input', result)
        ctx.emit('update:modelValue', result)
      },
    })

    // In range mode, validatedRange will be a two-dimensional array
    const validatedRange = computed(() = > {
      if(! props.range)return []
      // Pass the start and end dates in the props. Range property to dayJS to generate two dayJS instances
      const rangeArrDayjs = props.range.map(_= > dayjs(_))
      // Deconstruct the assignment and get the dayJS instances of the start and end dates
      const [startDayjs, endDayjs] = rangeArrDayjs
      // The start date is greater than the end date
      if (startDayjs.isAfter(endDayjs)) {
        console.warn(
          '[ElementCalendar]end time should be greater than start time'.)return[]}// Start date and end date are in the same month
      // Use the isSame API of DayJS to determine
      if (startDayjs.isSame(endDayjs, 'month')) {
        // Returns a two-dimensional array containing one element
        return [[
          If 2021/07/19 is Monday, then the first day of the week is 2021/07/18
          startDayjs.startOf('week'),
          // The end of the week of the end date is a Saturday. If today is Monday on 2021/07/19, the end of the week is Saturday on 2021/07/24
          endDayjs.endOf('week'),]]}else {
        // If the interval between the start date and the end date is more than 1 month, an error message is displayed
        if (startDayjs.add(1.'month').month() ! == endDayjs.month()) {console.warn(
            '[ElementCalendar]start time and end time interval must not exceed two months'.)return[]}// The first day of the month in which the end date occurs
        const endMonthFirstDay = endDayjs.startOf('month')
        // [end of month] the first day of the week
        const endMonthFirstWeekDay = endMonthFirstDay.startOf('week')
        let endMonthStart = endMonthFirstDay
        // The first day of the end month and the first day of the end month are not in the same month
        // This indicates that the Monday is partly last month
        if(! endMonthFirstDay.isSame(endMonthFirstWeekDay,'month')) {
          endMonthStart = endMonthFirstDay.endOf('week').add(1.'day')}return [
          // The first element is the date range of the starting month
          [
            startDayjs.startOf('week'),
            startDayjs.endOf('month')],// The second element is the date range for the starting month
          [
            endMonthStart,
            endDayjs.endOf('week'),],]}})// Switch months
    const selectDate = type= > {
      let day: Dayjs
      if (type= = ='prev-month') {
        // Last month today
        day = prevMonthDayjs.value
      } else if (type= = ='next-month') {
        // This day next month
        day = nextMonthDayjs.value
      } else {
        / / today
        day = now
      }
      // Click on the same date as the current selection, no processing
      if (day.isSame(date.value, 'day')) return
      pickDay(day)
    }
    
    const pickDay = (day: Dayjs) = > {
      realSelectedDay.value = day
    }
    
    return {
      selectedDay,
      curMonthDatePrefix,
      i18nDate,
      realSelectedDay,
      date,
      validatedRange,
      pickDay,
      selectDate,
      t,
    }
  },
Copy the code

2.2 Date-table component code

2.2.1 the template

<template>// Use native table tables<table
    :class="{ 'el-calendar-table': true, 'is-range': isInRange }"
    cellspacing="0"
    cellpadding="0"
  >
    <thead v-if=! "" hideHeader">// Table header: Sunday ~ Saturday<th v-for="day in weekDays" :key="day">{{ day }}</th>
    </thead>
    <tbody>/ / table rows<tr
        v-for="(row, index) in rows"
        :key="index"
        :class="{ 'el-calendar-table__row': true, 'el-calendar-table__row--hide-border': index === 0 && hideHeader }"
      >
        // column
        <td
          v-for="(cell, key) in row"
          :key="key"
          :class="getCellClass(cell)"
          @click="pickDay(cell)"
        >
          <div class="el-calendar-day">
            <slot
              name="dateCell"
              :data="getSlotData(cell)"
            >
              <span>{{ cell.text }}</span>
            </slot>
          </div>
        </td>
      </tr>
    </tbody>
  </table>
</template>
Copy the code

2.2.2 script

// Show only part of the core code
setup(props, ctx) {
    // Get the text of the week in the current locale, for example: Sunday-Monday-Tuesday-Wednesday-Thursday-Friday-Saturday
    const WEEK_DAYS = ref(dayjs().localeData().weekdaysShort())

    const now = dayjs()

    // Get the start day of the week in the current language. For example, in China, 1 is Monday
    const firstDayOfWeek = (now as any).$locale().weekStart || 0

    [Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday]
    const weekDays = computed(() = > {
      const start = firstDayOfWeek

      if (start === 0) {
        return WEEK_DAYS.value
      } else {
        return WEEK_DAYS.value
          .slice(start)
          .concat(WEEK_DAYS.value.slice(0, start))
      }
    })

    // Divide a one-dimensional array into a two-dimensional array. The subarray length is 7 bits, corresponding to the days of the week
    const toNestedArr = days= > {
      return rangeArr(days.length / 7).map((_, index) = > {
        const start = index * 7
        return days.slice(start, start + 7)})}// Calculate attributes to generate table rows
    const rows = computed(() = > {
      let days = []
      / / range model
      if (isInRange.value) {
        // Get the start date of range
        const [start, end] = props.range
        // Generates an array for the current month
        const currentMonthRange = rangeArr(
          end.date() - start.date() + 1,
        ).map((_, index) = > ({
          text: start.date() + index,
          type: 'current',}))let remaining = currentMonthRange.length % 7
        remaining = remaining === 0 ? 0 : 7 - remaining
        // Generate an array for the next month
        const nextMonthRange = rangeArr(remaining).map((_, index) = > ({
          text: index + 1.type: 'next',}))// Concatenate an array
        days = currentMonthRange.concat(nextMonthRange)
      } else {
        // Non-range mode
        // The first day of the month in which the currently selected date is located
        const firstDay = props.date.startOf('month').day() || 7
        // Last month's date array
        const prevMonthDays = getPrevMonthLastDays(
          props.date,
          firstDay - firstDayOfWeek,
        ).map(day= > ({
          text: day,
          type: 'prev',}))// An array of dates for the current month
        const currentMonthDays = getMonthDays(props.date).map(day= > ({
          text: day,
          type: 'current',
        }))
        days = [...prevMonthDays, ...currentMonthDays]
        // Next month's date array
        const nextMonthDays = rangeArr(42 - days.length).map((_, index) = > ({
          text: index + 1.type: 'next',}))// Concatenate an array
        // In non-range mode, display data for a fixed 6 weeks per month, 6*7=42 dates. Because the maximum number of days in a month is 31, it can be covered in a maximum of 6 weeks
        days = days.concat(nextMonthDays)
      }
      // Convert a one-dimensional array to a two-dimensional array
      return toNestedArr(days)
    })

    // Format the date. The result of formatting is YYYY-MM-DD
    const getFormattedDate = (day, type) :Dayjs= > {
      let result
      if (type= = ='prev') {
        result = props.date.startOf('month').subtract(1.'month').date(day)
      } else if (type= = ='next') {
        result = props.date.startOf('month').add(1.'month').date(day)
      } else {
        result = props.date.date(day)
      }
      return result
    }

    // Set cell class according to the date type
    const getCellClass = ({ text, type }) = > {
      const classes = [type]
      if (type= = ='current') {
        const date_ = getFormattedDate(text, type)
        if (date_.isSame(props.selectedDay, 'day')) {
          classes.push('is-selected')}if (date_.isSame(now, 'day')) {
          classes.push('is-today')}}return classes
    }

    const pickDay = ({ text, type }) = > {
      const date = getFormattedDate(text, type)
      ctx.emit('pick', date)
    }
    // Provide data to the slot
    const getSlotData = ({ text, type }) = > {
      const day = getFormattedDate(text, type)
      return {
        isSelected: day.isSame(props.selectedDay),
        type: `The ${type}-month`.day: day.format('YYYY-MM-DD'),
        date: day.toDate(),
      }
    }

    // Check whether it is range mode
    const isInRange = computed(() = > {
      return props.range && props.range.length
    })

    return {
      isInRange,
      weekDays,
      rows,
      getCellClass,
      pickDay,
      getSlotData,
    }
  }
Copy the code

2.3 summarize

  1. Element plusThe use ofdayjsDate processing library, the library is small in size, rich in functions;
  2. The calendar component uses table internally to generate a date table for display, which can be displayed for 6 weeks and completely cover the date of a month.
  3. Vue is a data-driven view that focuses on managing data.