Customizing Element UI Calendar Component for Vue.js Applications

  1. Toggle between week and month calendar views
  2. Click on dates in month view to switch to corresponding week view
  3. View specific day's data in week view
  4. Navigate between weeks/months using previous/next buttons
  5. Quick return to today's date in either view

By levearging dayjs for date calculations and Element UI's calendar component, we can implement these features effectively. It's important to note that when dsiplaying the calendar in week view, the date range should be calculated based on the target week. For example, if Sunday is the first day of the week, we need to provide both Sunday and Saturday dates as the range. For month view, we use the :value attribute rather than v-model since value provides one-way data binding while v-model creates two-way binding.

Implementation Code

<template>
<div class="calendarContainer">
  <ComponentBar name="Calendar" iconName="calendar.png" titleName="Today" @handleBarClick="resetToToday">
  <div class="calendar">
     <div class="calendar-header">
        <span class="current-month">
            <i class="el-icon-arrow-left" @click="navigatePrevious"></i>
            <i class="el-icon-arrow-right" @click="navigateNext"></i>
        </span>
        <el-radio-group v-model="viewMode" size="mini">
            <el-radio-button label="Week"></el-radio-button>
            <el-radio-button label="Month"></el-radio-button>
        </el-radio-group>
    </div>
    <MonthView v-if="viewMode==='Month'" :calendarValue="monthValue" :selectedDate="selectedDate" :dateData="dateData" @fetchData="loadData"></MonthView>
    <WeekView v-else :dateRange="weekRange" :selectedDate="selectedDate" :dateData="dateData"         
@fetchData="loadData"></WeekView>
  </div>
  <tabs :class="viewMode==='Month'?'monthTabs':'weekTabs'" v-model="activeTab" v-loading="isLoading">
    <el-tab-pane name="tabPendingPlans">
// Data list component
</el-tab-pane>
    <el-tab-pane name="tabCompletedPlans"></el-tab-pane>
  </tabs>
</div>
</template>

<script>
import dayjs from 'dayjs';
var weekPlugin = require('dayjs/plugin/weekday');
dayjs.extend(weekPlugin);

export default {
  data() {
    return{
      activeTab: 'tabPendingPlans',
      viewMode: 'Week', // Default to week view
      monthValue: '', // YYYY-MM format for backend queries
      selectedDate: '', // YYYY-MM-DD format for daily data queries
      weekRange: [], // Date range for week view
      dateData: [], // Data for displaying dots on calendar
    }
  },

  watch: {
    // Watch for changes in monthValue and selectedDate
    // to trigger appropriate data fetching
  },

  methods: {
    navigatePrevious() {
      // Previous month
      if (this.viewMode === 'Month') {
        this.monthValue = dayjs(this.monthValue).subtract(1, 'month').format('YYYY-MM');
        
        // Check if selected date belongs to current month
        let currentMonth = dayjs(this.selectedDate).format('YYYY-MM');
        if (currentMonth === this.monthValue) {
          // Calculate first day of current week
          let weekStart = dayjs(this.selectedDate).startOf('week').format('YYYY-MM-DD');
          // Calculate last day of current week
          let weekEnd = dayjs(this.selectedDate).endOf('week').format('YYYY-MM-DD');
          this.weekRange = [weekStart, weekEnd];
        } else {
          let weekStart = dayjs(this.monthValue).startOf('month').startOf('week').format('YYYY-MM-DD');
          let weekEnd = dayjs(this.monthValue).startOf('month').endOf('week').format('YYYY-MM-DD');
          this.weekRange = [weekStart, weekEnd];
        }
      }
      
      // Previous week
      if (this.viewMode === 'Week') {
        let weekStart = dayjs(this.weekRange[0]).subtract(1, 'week').startOf('week').format('YYYY-MM-DD');
        let weekEnd = dayjs(this.weekRange[1]).subtract(1, 'week').endOf('week').format('YYYY-MM-DD');
        this.monthValue = dayjs(this.weekRange[0]).subtract(1, 'week').startOf('week').format('YYYY-MM');
        this.weekRange = [weekStart, weekEnd];
      }
    },
    
    navigateNext() {
      // Next month
      if (this.viewMode === 'Month') {
        this.monthValue = dayjs(this.monthValue).add(1, 'month').format('YYYY-MM');
        
        // Check if selected date belongs to current month
        let currentMonth = dayjs(this.selectedDate).format('YYYY-MM');
        if (currentMonth === this.monthValue) {
          // Calculate first day of current week
          let weekStart = dayjs(this.selectedDate).startOf('week').format('YYYY-MM-DD');
          // Calculate last day of current week
          let weekEnd = dayjs(this.selectedDate).endOf('week').format('YYYY-MM-DD');
          this.weekRange = [weekStart, weekEnd];
        } else {
          let weekStart = dayjs(this.monthValue).endOf('month').startOf('week').format('YYYY-MM-DD');
          let weekEnd = dayjs(this.monthValue).endOf('month').endOf('week').format('YYYY-MM-DD');
          this.weekRange = [weekStart, weekEnd];
        }
      }
      
      // Next week
      if (this.viewMode === 'Week') {
        let weekStart = dayjs(this.weekRange[0]).add(1, 'week').startOf('week').format('YYYY-MM-DD');
        let weekEnd = dayjs(this.weekRange[1]).add(1, 'week').endOf('week').format('YYYY-MM-DD');
        this.monthValue = dayjs(this.weekRange[0]).add(1, 'week').startOf('week').format('YYYY-MM');
        this.weekRange = [weekStart, weekEnd];
      }
    },
    
    // Reset to today
    resetToToday() {
      this.monthValue = dayjs(new Date()).format('YYYY-MM');
      this.selectedDate = dayjs(new Date()).format('YYYY-MM-DD');
      let weekStart = dayjs(new Date()).startOf('week').format('YYYY-MM-DD');
      let weekEnd = dayjs(new Date()).endOf('week').format('YYYY-MM-DD');
      this.weekRange = [weekStart, weekEnd];
      this.activeTab = 'tabPendingPlans';
    },
    
    // Handle date click in both views
    handleDateClick(dateInfo) {
      this.selectedDate = dateInfo.date;
      
      // Handle navigation when clicking dates from other months
      if (this.viewMode === 'Month') {
        if (dateInfo.type === 'next-month') {
          this.navigateNext();
        }
        if (dateInfo.type === 'prev-month') {
          this.navigatePrevious();
        }
      }
      
      if (this.viewMode === 'Week') {
        let currentMonth = dayjs(dateInfo.date).format('YYYY-MM');
        if (dateInfo.type === 'next-month') {
          if (currentMonth !== this.monthValue) {
            this.monthValue = dayjs(dateInfo.date).format('YYYY-MM');
          }
        }
        if (dateInfo.type === 'prev-month') {
          if (currentMonth !== this.monthValue) {
            this.monthValue = dayjs(dateInfo.date).format('YYYY-MM');
          }
        }
      }
      
      // Switch to week view when clicking current month date
      if (dateInfo.type === 'current-month') {
        this.viewMode = 'Week';
        // Calculate first day of clicked date's week
        let weekStart = dayjs(this.selectedDate).startOf('week').format('YYYY-MM-DD');
        // Calculate last day of clicked date's week
        let weekEnd = dayjs(this.selectedDate).endOf('week').format('YYYY-MM-DD');
        // Set month to the first day of the week's month
        this.monthValue = dayjs(weekStart).startOf('week').format('YYYY-MM');
        this.weekRange = [weekStart, weekEnd];
      }
    },
  }
}
</script>

Custom Calendar Styling

Here's how to customize the calendar appearance by overriding the default styles:

::v-deep .calendar {
    &-header {
      text-align: center;
      margin: 10px 16px 0 16px;

      .current-month {
        font-size: 16px;
        letter-spacing: 0;
        font-weight: 500;
        margin-left: 15%;
        color: #262626;
        font-family: PingFangSC-Medium;

        i {
          cursor: pointer;
        }
      }

      .el-radio-group {
        float: right;
      }

      .el-radio-button__orig-radio:checked + .el-radio-button__inner {
        background: #ffffff;
        box-shadow: -1px 0 0 0 transparent;
        border: 1px solid rgba(199, 0, 11, 1);
        font-family: PingFangSC-Medium;
        font-size: 12px;
        color: #c7000b;
        letter-spacing: -0.04px;
        font-weight: 500;
      }

      .el-radio-button__inner:hover {
        color: #c7000b;
      }
    }

    .calendar-dot-box {
      width: 100%;
      bottom: -8px;
      position: absolute;

      span {
        width: 6px;
        height: 6px;
        margin-right: 3px;
        border-radius: 50%;
        display: inline-block;

        &:last-of-type {
          margin-right: 0;
        }
      }

      .completedPlan {
        background-color: #d61212;
      }

      .pendingPlan {
        background-color: #ffd100;
      }
    }

    .el-calendar {
      &__body {
        padding: 10px 16px;
      }

      .is-today {
        .el-calendar-day {
          .calendar-date {
            width: 34px;
            height: 34px;
            margin: 0 auto;
            color: #ff534f;
            border-radius: 10px;
            background: #fff;
            box-shadow: none;
          }
        }
      }

      &__header {
        display: none;
      }

      .current {
        .el-calendar-day {
          color: #262626;
        }
      }

      .prev,
      .next {
        color: #bfbfbf;
      }

      &-day {
        padding: 0;
        font-weight: 700;
        font-size: 16px;
        letter-spacing: 0;
        text-align: center;
        position: relative;
        transition: color 0.3s;
        font-family: DINAlternate-Bold;
      }

      &-table {
        th {
          font-family: PingFangSC-Regular;
          font-size: 16px;
          color: #262626;
          letter-spacing: 0;
          text-align: center;
          line-height: 34px;
          font-weight: 400;
          padding: 0;

          &:last-of-type,
          &:first-of-type {
            color: #ff564e;
          }
        }

        td {
          border: none;

          &.is-selected {
            background-color: transparent;
          }
        }

        .el-calendar-day {
          height: 34px;
          line-height: 34px;

          &:hover {
            background-color: transparent;
          }

          .calendar-selected {
            width: 34px;
            height: 34px;
            margin: 0 auto;
            color: #fff;
            border-radius: 10px;
            background: #ff534f;
            box-shadow: 0px 0px 2px 0px rgba(238, 88, 64, 1);
          }
        }
      }
    }
}

Month View Component

<template>
  <!-- Month calendar view -->
  <el-calendar :value="calendarValue" :first-day-of-week="7" value-format="YYYY-MM">
    <template slot="dateCell" slot-scope="{ date, data }">
      <div v-if="selectedDate === data.day" class="calendar-selected" @click="handleDateClick($event, date, data)">
        {{ date.getDate() }}
      </div>
      <div v-else class="calendar-date" @click="handleDateClick($event, date, data)">
        {{ date.getDate() }}
      </div>
      <div class="calendar-dot-box" @click="handleDateClick($event, date, data, 'dot')">
        <template v-for="(item) in dateData">
          <span class="pendingPlan" v-if="item.date === data.day && item.pendingPlanCount > 0"></span>
          <span class="completedPlan" v-if="item.date === data.day && item.completedPlanCount > 0"></span>
        </template>
      </div>
    </template>
  </el-calendar>
</template>
<script>
export default {
  components: {},
  name: "MonthView",
  props: {
    selectedDate: {
      type: String,
      default: "",
    },
    calendarValue: {
      type: String,
      default: new Date(),
    },
    dateData: {
      type: Array,
      default: () => {
        return [];
      },
    },
  },
  watch: {
    dateData: {
      handler(list) {
        this.dateList = list;
      },
      immediate: true,
    },
  },
  data() {
    return { monthDate: this.calendarValue, dateList: [] };
  },
  created() { },
  methods: {
    handleDateClick(e, date, data) {
      this.$emit("fetchData", data);
    },
  },
};
</script>

Week View Component

<template>
  <!-- Week calendar view -->
  <el-calendar :range="dateRange" :first-day-of-week="7" value-format="YYYY-MM">
    <template slot="dateCell" slot-scope="{date,data}">
      <div v-if="selectedDate === data.day" class="calendar-selected" @click="handleDateClick($event, date, data)">
        {{ date.getDate() }}</div> 
      <div v-else class="calendar-date" @click="handleDateClick($event, date, data)">{{ date.getDate() }}</div>
      <div class="calendar-dot-box" @click="handleDateClick($event, date, data)">
        <template v-for="(item) in dateData">
          <span class="pendingPlan" v-if="item.date === data.day && item.pendingPlanCount > 0"></span>
          <span class="completedPlan" v-if="item.date === data.day && item.completedPlanCount > 0"></span>
        </template>
      </div>
    </template>
  </el-calendar>
</template>
<script>
export default {
  components: {}, 
  name: 'WeekView', 
  props: {
    selectedDate: {
      type: String, 
      default: '',
    }, 
    dateRange: {
      type: Array,
      default: () => {
        return [];
      }
    }, 
    dateData: {
      type: Array, 
      default: () => {
        return [];
      }
    }
  }, 
  data() {
    return {
    }
  }, 
  created() {
  }, 
  methods: {
    handleDateClick(e, date, data) {
      this.$emit('fetchData', data)
    },
  }
}
</script>

Handling Month View Display Issues

In month view, you might notice entire rows of gray dates for previous/next months, which doesn't look visually appealing. To address this, you can manipulate the DOM using JavaScript. The basic approach involves:

  1. Get all row elements using document.querySelectorAll('.el-calendar-table__row')
  2. Iterate through these rows
  3. If a row contains an element with the .current class, it means it has dates from the current month, so leave it as is
  4. If a row doesn't contain any .current class elements, it means all dates are from previous/next months, so add display: none to that row

Week View Considerations

The week view should display only the dates within the selected week without showing dates from adjacent months. This requires proper date range calculation and styling to ensure a clean, consistent appearance.

Tags: Element UI Vue.js Calendar Component Dayjs Frontend Development

Posted on Fri, 24 Jul 2026 16:54:51 +0000 by aviavi