- Toggle between week and month calendar views
- Click on dates in month view to switch to corresponding week view
- View specific day's data in week view
- Navigate between weeks/months using previous/next buttons
- 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:
- Get all row elements using
document.querySelectorAll('.el-calendar-table__row') - Iterate through these rows
- If a row contains an element with the
.currentclass, it means it has dates from the current month, so leave it as is - If a row doesn't contain any
.currentclass elements, it means all dates are from previous/next months, so adddisplay: noneto 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.