DEV Community

Owais Khan
Owais Khan

Posted on • Edited on

How to create a calendar in Vue

Calendar

In this article we will create a calendar using vue.js.

Setup

First create a vue project using vite. After creating the project delete all unnecessary code and files created by vite.



npm init vite <project-name> // creates vue project using vite 
cd <project-name> // move in to the project created by vite
npm install // installs all necessary dependecies for our project
npm run dev // runs development server at http://localhost:5173/


Enter fullscreen mode Exit fullscreen mode

Creating calendar

Our calendar will be divided into two main components Header and calendarGrid. Header component will contain name of month, year and chevrons/arrows for changing date.calendarGrid component will contain days and their corresponding dates. And both of these components will live inside one calendar component. So, lets first create calendar.vue in src/components folder and import Header and calendarGrid components into it.

Calendar Image

calendar.vueπŸ‘‡πŸ»



<script setup>
    import Header from './Header.vue'
    import calendarGrid from './calendarGrid.vue'
</script>
<template>
    <main>
        <!-- Using Header and calendarGrid Component -->
        <Header/>
        <calendarGrid/>
    </main>
</template>
<style scoped lang="less">
    main{
        width: 500px;
        padding-bottom: 30px;
        border-radius: 5px;
        background-color: azure;
        box-shadow: 5px 5px 20px 0 rgba(0, 0, 0, 0.5);
        @media(max-width: 500px){
            width: 90vw;
        }
    }
</style>


Enter fullscreen mode Exit fullscreen mode

For vue to compile our calendar.vue we need to import it into App.vue component because vue passes App.vue to createApp function in main.js for compiling. So, lets do that:

App.vueπŸ‘‡πŸ»



<script setup>
    import calendar from './components/calendar.vue'
</script>
<template>
    <main>
        <!-- Using calendar Component -->
        <calendar/>
    </main>
</template>


Enter fullscreen mode Exit fullscreen mode

Now, lets create Header.vue component in src/components folder. In Header.vue we will create a div with class header inside of which we will create further two div elements with classes info and wrapper. .info element will contain year and .wrapper element will contain chevrons and name of month. And at last we will add some styles to it.

Header.vueπŸ‘‡πŸ»



<script setup>
    import chev from '../assets/chev.svg' // importing chevron image
</script>
<template>
    <div class="header">
        <div class="info">
            <div class="year">
                <p>2023</p>
            </div>
        </div>
        <div class="wrapper">
            <div class="left-chev chev">
            <img :src="chev" alt="left-chevron">
            </div>
            <div class="month">
                <h2>February</h2>
            </div>
            <div class="right-chev chev">
                <img :src="chev" alt="right-chevron">
            </div>
        </div>
    </div>
</template>
<style scoped lang="less">
    .header{
        display: flex;
        flex-direction: column;
        padding: 10px;
        background-color: antiquewhite;
        border-radius: 5px 5px 0 0;
        .wrapper{
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 20px 0;
            .month{
                h2{
                    font-size: 32px;
                    font-weight: bolder;
                    letter-spacing: 5.5px;
                }
            }
            .chev{
                user-select: none;
                width: 40px;
                height: 40px;
                border-radius: 50%;
                display: flex;
                justify-content: center;
                align-items: center;
                cursor: pointer;
                transition: background-color .5s;
                &:hover{
                    background-color: rgb(243, 206, 158);
                }
                img{
                    width: 100%;
                    height: auto;
                    transform: translateX(2px);
                }
            }
            .left-chev{
                transform: rotate(180deg);
            }
        }
    }
</style>


Enter fullscreen mode Exit fullscreen mode

With that we have created Header.vue. Now, we will create calendarGrid.vue component.

In calendarGrid component we will create a div with calendar-grid class which will contains two div elements with classes days-grid and date-grid. .days-grid will contain 7 div elements with class day which will contain days of week sunday to saturday. But instead of creating indivisual div elements we will create an array of week days and we will loop over that in our template using v-for directive. .date-grid element will contain 31, 30, 28 or 29(depending on the month) div elements with class date which will contain dates in .val element. But for now we will render 31 days using range function and v-for directive for the sake of layout. We will change it when we will implement functionality for our calendar.

calendarGrid.vueπŸ‘‡πŸ»



<script setup>
    const DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
</script>
<template>
    <div class="calendar-grid">
        <div class="days-grid">
            <div
                v-for="day, index in DAYS"
                :key="index"
                class="day"
                :title="day"
            >{{ day[0] }}</div>
        </div>
        <div class="date-grid">
            <div class="date"
                v-for="date in range(31)"
            >
                <div 
                    class="val">
                    {{ date }}
                </div>
            </div>
        </div>
    </div>
</template>
<style scoped lang="less">
    .calendar-grid{
        margin-top: 15px;
        display: grid;
        gap: 20px;
        user-select: none;
        .days-grid{
            display: grid;
            grid-template-columns: repeat(7, 1fr);
            justify-content: space-between;
            align-items: center;
            .day{
                text-align: center;
                font-weight: bold;
                &.today{
                    color: blueviolet;
                }
            }
        }
        .date-grid{
            display: grid;
            grid-template-columns: repeat(7, 1fr);
            justify-content: space-between;
            align-items: center;
            row-gap: 20px;
            .date{
                display: flex;
                justify-content: center;
                cursor: pointer;
                .val{
                    width: 20px;
                    height: 20px;
                    text-align: center;
                    border-radius: 50%;
                    display: flex;
                    align-items: center;
                    justify-content: center;
                    padding: 15px;
                    position: relative;
                    background-color: antiquewhite;
                }
            }
        }
    }
</style>


Enter fullscreen mode Exit fullscreen mode

And with that layout of our calendar is completed.

Adding Functionality

Basic functionality of our app includes changing dates and to highlight current day and date and also to arrange dates in correct order with respect to their corresponding days. For that we have to create quite a few reactive states and then we have to pass those states from one component to another and evetually we will extend the functionality of our app for adding events for which we have to pass state between sibling components which makes state management bit difficult. So, to manage state easily we will manage states at global level so that any component can access that state. To manage state at global level we can use state managers like vuex or pinia but this app not that big and complex so we will use vue's built in provide/inject functions.

Provide function

Vue's provide function allows us to share state/data from higher component to the lower components. E.g: If we have five components A, B C, D and E where B and C are children of A, D is child of C and E is child of D. Component A contains a state x which we want to use in component E which we can do by passing state x from A to C, C to D, D to E. In this example, for using state x in component E we had to pass it to components C and D which do not need this state.This process of passing data/state from parent component to child component (where child component can be at any depth in component tree) as props is known prop drilling.

Component tree with prop drilling



                                      A ----> contains state 'x'
                                      |x <---- passing 'x' from A -> C
                                      |x x x
                                ------------x
                                |          |x
                                |          |x
                                B          C <--- Doest not use 'x'
                                            \x <---- passing 'x' from C -> D
                                             \x
                                              D <--- Doest not use 'x'
                                               \x<---- passing 'x' from D -> E
                                                \x
                                                 E ----> uses state 'x'



Enter fullscreen mode Exit fullscreen mode

provide function helps us to avoid prop drilling meaning we can pass state x directly from A to E without prop drilling. Lets see how to do it.

Provide function takes two arguments first is key and second is the state that we want to share. Key is used to access the shared state in child component.

In component A



import {ref, provide} from 'vue'
const x = ref(0)
provide('store', x) // sharing state x and setting 'store' as key


Enter fullscreen mode Exit fullscreen mode

Note: value in provide function can be of any type like string, number, array, object e.t.c

Inject Function

Vue's inject functions allows us to use state shared by parent component in child component. Inject function takes one argument which is the key that we passed to provide function in parent component.

In component E



import {inject} from 'vue'
const x = inject('store') // accessing state 'x'
console.log(x.value) // prints 0


Enter fullscreen mode Exit fullscreen mode

Component tree without prop drilling



                                      A ----> contains state 'x' and share it using provide
                                      |
                                      |
                                ------------
                                |          |
                                |          |
                                B          C
                                            \
                                             \
                                              D
                                               \
                                                \
                                                 E ----> uses state 'x' using inject 



Enter fullscreen mode Exit fullscreen mode

Lets see how to use provide/inject in our calendar app. Our app have a state called date which is used in almost every component so, lets make it a global state or app level state. To create app level state we will use provide function inside main.js.

main.js πŸ‘‡πŸ»



import { createApp, ref } from 'vue'
import './less/global.less'
import App from './App.vue'

const date = ref({date: {
        str: new Date(),
        dateString: new Date().toDateString()
    }})
createApp(App)
    .provide('store', date) // providing date state to every component in our app.
    .mount('#app')



Enter fullscreen mode Exit fullscreen mode

date is simply an object which contains two properties str and dateString. str store a date object which gives us current date and dateString gives us date in more simpler format like this 'Sat Feb 27 2023'. But other than date we have more states in our app that we want to make global. For that we will create a single object which will contain all of our global states. And to keep our main.js clean and clutter free we will create our global state in different file and then we will import that file into main.js.

Create store.js in src/store folder. store.js will contain global state.

store.jsπŸ‘‡πŸ»



import { ref } from 'vue'
export const state = ref({
    date: {
        str: new Date(),
        dateString: new Date().toDateString()
    }
})


Enter fullscreen mode Exit fullscreen mode

state in store.js is a single global reactive state that contains all states for our app. Now, lets import state in main.js.



import { createApp } from 'vue'
import './less/global.less'
import App from './App.vue'
import { state } from './store/store.js'

createApp(App)
    .provide('store', state) // providing single global state object to every component in our app.
    .mount('#app')


Enter fullscreen mode Exit fullscreen mode

Now, we can use state object in any component of our app. Lets use state in Header.vue. In Header.vue we need month and year which we can extract from dateString property of date object inside state. We will also create a component level state current which will contain month and year that we will extract from dateString. One thing to note that dateString gives only first 3 intials of month name meaning for January it will give Jan and so on. But we want full month names so, we will create a MONTHS object with months first 3 initials as key and months fullname as value that way we can get fullname of months from first 3 initials.

Header.vueπŸ‘‡πŸ»



<script setup>
    import { ref, inject } from 'vue'
    import chev from '../assets/chev.svg'

    const MONTHS = {
        Jan: "January",
        Feb: "February",
        Mar: "March",
        Apr: "April",
        May: "May",
        Jun: "June",
        Jul: "July",
        Aug: "August",
        Sep: "September",
        Oct: "October",
        Nov: "November",
        Dec: "December"
    }
    const state = inject('store') // accessing state using inject function
    const [ _, month, __, year ] = state.value.date.dateString.split(' ') // extracting month & year from dateString
    const current = ref({
        month: MONTHS[month], // getting month fullname from MONTHS object using months first 3 initials
        year
    })
</script>

<template>
    <div class="header">
        <div class="info">
            <div class="year">
                <p>{{ current.year }}</p> <!-- Using year from current state above -->
            </div>
        </div>
        <div class="wrapper">
            <div class="left-chev chev">
            <img :src="chev" alt="left-chevron">
            </div>
            <div class="month">
                <h2>{{ current.month }}</h2> <!-- Using month from current state above -->
            </div>
            <div class="right-chev chev">
                <img :src="chev" alt="right-chevron">
            </div>
        </div>
    </div>
</template>
<style scoped lang="less">
    /* styling goes here */
</style>


Enter fullscreen mode Exit fullscreen mode

Until now our calendar is only showing current month and year but we want it to change month and year depending on the chevron we click E.g: if we click on right chevron and current month is february calendar should change it to march and if month is december and year is 2023 then calendar should change month to january and year to 2024. And reverse should happen if we click on left chevron. Now all of this to happen we have to update date object inside global reactive state. We can update date in Header.vue component but vue documentation recommends not to change global reactive state in child components rather create a function at global level which will be responsible for updaing/changing global state. We will create and export this function from store.js file and we will name it dispatch and after that we will import it in main.js and then instead of passing state as value to provide function we will pass an object which contains state and dispatch function as properties to provide function because of which we can access dispatch in components for updating/changing global state.

store.jsπŸ‘‡πŸ»



import { ref } from 'vue'
export const state = ref({
    date: {
        str: new Date(),
        dateString: new Date().toDateString()
    }
})

export const dispatch = () => {} // we will implement it in a bit


Enter fullscreen mode Exit fullscreen mode

main.jsπŸ‘‡πŸ»



import { createApp } from 'vue'
import './less/global.less'
import App from './App.vue'
import { state, dispatch } from './store/store.js' // importing dispatch

createApp(App)
    .provide('store', { state, dispatch }) // providing an object containing global state and function to update/change global state to every component in our app.
    .mount('#app')


Enter fullscreen mode Exit fullscreen mode

Now, lets understand how dispatch function will work. In our app we will change date in different ways E.g; clicking on right chevron will change date in inreasing order and clicking on left chevron will change date in decreasing order and we may perform different actions on our app which can change calendar in different ways then how a single function is able to handle every type of change and the answer is that we will create separate functions for each type of action we perform on calendar and dispatch function will only decide which function to execute. Lets say we have a function increase which increases the date when we click on right chevron but we cannot invoke increase function directly because to increase date we have to update date object which is in global state object and to update global state we have to use dispatch function so, when user clicks on right chevron we will call dispatch and then dispatch will call increase function and same thing will happen when user clicks left chevron but instead of calling increase function dispatch will call decrease function which decreases the date. So, how does dispatch knows which function to invoke when user performs any action? When we call dispatch function we will pass an argument known as action to it. Based on this argument dispatch is able to decide which function to call. action is simply an object with property type whose value is simply a string which describes what the action is. If we want to increase date by clicking on right chevron for that we will first call dispatch and pass action object as an argument like this dispatch({type: 'INCREASE'}) and in dispatch function we will recieve this action as parameter and inside that function we will check the type of action object and if it is INCREASE it will call increase function which increases the date. In addition to type we can also add more properties to action object if we want to send additional data to dispatch function.

Lets add click event listeners on right and left chevrons in Header.vue component. When user clicks on any of the chevron it calls a handleClick function and we also pass an argument to this function and that argument is 'next-month' or 'previous-month' depending on the chevron user clicked. In handleClick function we call dispatch and pass an action as argument and that argument looks like this



const handleClick = (direction) => dispatch({type: 'updateDate', payload: {direction}})


Enter fullscreen mode Exit fullscreen mode

In above code handleClick function recieves a parameter direction which is either 'next-month' or 'previous-month' depending on the chevron user clicked and inside the function we are calling dispatch with an object as argument and this argument is an action which contains two properties type and payload. The value of type specifies what type of action is being performed meaning in this case user has clicked one of the chevrons so we have to update date either in increasing or decreasing order that is why value of type is 'updateDate' and payload is an object which is used to send additional data to our dispatch function.

Now, inside dispatch function we have to check the type of the action and based on that we will call an appropriate function for updating date.

store.jsπŸ‘‡πŸ»



import { ref } from 'vue'
export const state = ref({
    date: {
        str: new Date(),
        dateString: new Date().toDateString()
    }
})

export const dispatch = (action) => {
    // checking the type of action
    if(action.type === 'updateDate'){
        const newDate = updateDate(state.value.date, action.payload) // calling updateDate function for updating date
        state.value = {...state.value, date: newDate} // updating state
    }
}


Enter fullscreen mode Exit fullscreen mode

In above code we are checking if action.type is equal to 'updateDate' and if it is we are calling an updateDate function which updates date by increasing or decreasing depending on the value of direction which we passed in payload object of action. If value of direction is 'next-month' then updateDate function will increase date and if value is 'previous-month' then date will be decreased.

Lets implement updateDate function



const updateDate = (date, payload) => {
    const year = date.str.getFullYear()
    const month = date.str.getMonth() + 1
    let newDate = {}
    if(payload.direction === 'next-month') {
        newDate = increase(month, year)
    }
    else newDate = decrease(month, year)
    return newDate
}


Enter fullscreen mode Exit fullscreen mode

updateDate function takes 2 parameters date and payload. date is current date which we will use to get the next date or previous date and payload is the object which we passed with action to dispatch and payload have a direction property which is used to decide whether to increase the date or decrease. First, we will get current year and month using getFullYear and getMonth methods on date.str and note that getMonth returns a month number from 0 to 11, 0 -> January & 11 -> December that is why we adding 1 to month. Then we check if direction equals to 'next-month' and if yes then we increase the date by calling increase funtion with current month and year as arguments else we decrease date by calling decrease in same way. And the result object returned by these function is store in newDate variable which is then returned.

As we can see that we are using increase and decrease functions inside updateDate which are responsible for increasing and decreasing date respectively. So, lets implement these functions.



const increase = (month, year) => {
    const obj = {}
    // checking month is not DecemberπŸ‘‡πŸ»
    if(month !== 11){
        obj.str = new Date(`${year}-${month+1}`)
        obj.dateString = new Date(`${year}-${month+1}`).toDateString()
    }else{
        obj.str = new Date(`${year+1}-01`)
        obj.dateString = new Date(`${year+1}-01`).toDateString()
    }
    return obj
}
const decrease = (month, year) => {
    const obj = {}
    // checking month is not JanuaryπŸ‘‡πŸ»
    if(month !== 0){
        obj.str = new Date(`${year}-${month-1}`)
        obj.dateString = new Date(`${year}-${month-1}`).toDateString()
    }else{
        obj.str = new Date(`${year-1}-12`)
        obj.dateString = new Date(`${year-1}-12`).toDateString()
    }
    return obj
}


Enter fullscreen mode Exit fullscreen mode

In increase function we are checking if month is not December and if it is not then we just have to increase month and year will be same but month is December then we have to increase year and change month to '01' and the result is stored in obj object which is then returned. And reverse happens in decrease function.

Now, when user clicks on any of the chevron it will update date correctly but we will not be able to see those changes on our screen because in Header.vue we are storing date in component level state known as current and this state do not know that date has updated so, whenever date updates we also have to update current for that we have to watch date object in global state so, lets do that using watch function.



<script setup>
    import { ref, inject, watch } from 'vue'
    import chev from '../assets/chev.svg'

    const MONTHS = {
        Jan: "January",
        Feb: "February",
        Mar: "March",
        Apr: "April",
        May: "May",
        Jun: "June",
        Jul: "July",
        Aug: "August",
        Sep: "September",
        Oct: "October",
        Nov: "November",
        Dec: "December"
    }
    const state = inject('store') // accessing state using inject function
    const [ _, month, __, year ] = state.value.date.dateString.split(' ') // extracting month & year from dateString
    const current = ref({
        month: MONTHS[month], // getting month fullname from MONTHS object using monts first 3 initials
        year
    })

    // watching date so that we can update current πŸ‘‡πŸ»
    watch(()=>state.value.date, (updatedDate) => {
        const [ _, month, __, year ] = updatedDate.dateString.split(' ') // extracting updated month and year

        // updating currentπŸ‘‡πŸ»
        current.value = {
            month: MONTHS[month],
            year
        }
    })
</script>

<template>
    <div class="header">
        <div class="info">
            <div class="year">
                <p>{{ current.year }}</p> <!-- Using year from current state above -->
            </div>
        </div>
        <div class="wrapper">
            <div class="left-chev chev">
            <img :src="chev" alt="left-chevron">
            </div>
            <div class="month">
                <h2>{{ current.month }}</h2> <!-- Using month from current state above -->
            </div>
            <div class="right-chev chev">
                <img :src="chev" alt="right-chevron">
            </div>
        </div>
    </div>
</template>
<style scoped lang="less">
    /* styling goes here */
</style>


Enter fullscreen mode Exit fullscreen mode

watch function takes 2 arguments first argument is dependency state meaning what state we want to watch and second argument is a function which gets executed when dependency state changes this function takes a parameter which is updated version of dependency state.

Now, we can see the changes on screen

Calendar Gif

Now, lets add functionality to CalenderGrid.vue component. In this component we first have to render correct number of days for each month and then we have to make sure that each date falls under correct day in every month. E.g; In 01-february-2023 was 'wednesday' so, in calendar '1' should be under day 'wednesday'. If we dont arrange dates in each month in correct order then in every month '1' will fall under day 'sunday'.

Calendar Image

To get the correct number of dates in each month we will create a state called dateGrid with initial value of empty array inside global reactive state in store.js. dateGrid array will be updated with current number of dates every time date changes by clicking on any chevrons in header. To update dateGrid we will create a function updateDateGrid which will update dateGrid whenever date changes.

store.jsπŸ‘‡πŸ»



import { ref } from 'vue'
export const state = ref({
    date: {
        str: new Date(),
        dateString: new Date().toDateString()
    },
    dateGrid: []
})

export const dispatch = (action) => {
    // checking the type of action
    if(action.type === 'updateDate'){
        const newDate = updateDate(state.value.date, action.payload) // calling updateDate function for updating date
        state.value = {...state.value, date: newDate} // updating state
    }
}


Enter fullscreen mode Exit fullscreen mode

Now, lets implement updateDateGrid function. This function recieves an argument which is current date object then we are extracting month name and year from dateString property of date object. After that we need to know number of days in current month for that we are using getNumberOfDays function and we are passing month name as an argument to this function. After recieving number of days in numberOfDays variable from getNumberOfDays function and then we are creating currentMonthDates array which will contain dates of current month. Each element in currentMonthDates array will be an object with three properties id, dt, day. dtis date from 1 to numberOfDays, day is simply day on particular date dt.



const DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
const updateDateGrid = date => {
    const [ _, monthName, __, year ] = date.dateString.split(' ') // extracting month name and year
    const numberOfDays = getNumberOfDays(monthName, year) // recieving number of days
    const currentMonthDates = [] // will contain dates of current month
    const monthNumber = date.str.getMonth() + 1 // current month number 1 - 12
    for(let i = 0; i < numberOfDays; i++){
        const item = {} // creating an object that will contain day & date
        const day = new Date(`${year}-${monthNumber}-${i+1}`).getDay() // getting day number 0 - 6
        item.id = i + 1
        item.dt = i + 1
        item.day = DAYS[day] // getting day from DAYS array
        currentMonthDates.push(item) // adding item object containing day and date to currentMonthDates array
    }
    return currentMonthDates
}


Enter fullscreen mode Exit fullscreen mode

In above code you can see that we are using getNumberOfDays function which returns number of days in given month lets implement this function.



const getNumberOfDays = (month, year) => {
    const thirtyOneDays = ['Jan', 'Mar', 'May', 'Jul', 'Aug', 'Oct', 'Dec'] // months having 31 days
    if(thirtyOneDays.includes(month)) return 31
    else if(month === 'Feb') return isLeapYear(year) ? 29 : 28 // checking if year is a leap year
    return 30
}


Enter fullscreen mode Exit fullscreen mode

First we are creating an array of months thirtyOneDays that have 31 days then we are checking that if the given month is in this array and if it is then we are returning 31 but if it is not in this array then we are checking that if month is February and if it is then we have to check that if the year is leap year or not if it is leap year then we are returning 29 else we are returning 28 but if month is not February then we return 30.

In above code we have used isLeapYear function which returns true if the year is a leap year else it returns false. Now, lets implement it.



const isLeapYear = year => {
    const firstCondition = year % 4 === 0 && year % 100 !== 0 // If a year is divisible by 4 and is not divisible by 100
    const secondCondition = year % 4 === 0 && year % 100 === 0 && year % 400 === 0 // If a year is divisible by 4 and 100 and 400
    if(firstCondition || secondCondition) return true
    return false
}


Enter fullscreen mode Exit fullscreen mode

A year is leap if it specifies two condition:

  • If a year is divisible by 4 and is not divisible by 100.
  • If a year is divisible by 4 and 100 and 400.

Now, our calendar renders correct number of days for each month but there is still a problem which is that these days are not in correct order meaning the first date of every month comes under sunday as shown in picture above.

Now, lets arrange these dates in correct order and for that we will create a function which takes currentMonthDates as a parameter then it first checks the day on first date which it gets from first element of array. Then we will get index of that day from DAYS array which will let us know at which position we have to render the first date in our calendar. E.g: day on first date of February is Wednesday so the date 1 should be under W in calendar for that we will first get the index of Wed from DAYS array and the index is 3. So, we will prepend 3 empty objects in currentMonthDates array so that when we loop over currentMonthDates array it will render 3 empty objects which will leave first 3 blocks in calendar empty and the date 1 will be rendered on 4th block which is under W and all other dates automatically falls under correct day. And same process will be repeated for each month.

Lets implement this function



const DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
const arrangeGridAccordingToDays = currentMonthDates => {
    let newDateGrid = []
    const firstDay = DAYS.indexOf(currentMonthDates[0].day) // getting index of first day
    for(let i = 0; i <= firstDay; i++){
        if(i < firstDay){
            newDateGrid = [...newDateGrid, {
                id: Math.random(),
                dt: '', //  adding empty date value
                day: DAYS[i]
            }]
        }else{
            newDateGrid = [...newDateGrid, ...currentMonthDates]
        }
    }
    return newDateGrid
}


Enter fullscreen mode Exit fullscreen mode

Now, use above function in updateDateGrid function.



const DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
const updateDateGrid = date => {
    const [ _, monthName, __, year ] = date.dateString.split(' ') // extracting month name and year
    const numberOfDays = getNumberOfDays(monthName, year) // recieving number of days
    const currentMonthDates = [] // will contain dates of current month
    const monthNumber = date.str.getMonth() + 1 // current month number 1 - 12
    for(let i = 0; i < numberOfDays; i++){
        const item = {} // creating an object that will contain day & date
        const day = new Date(`${year}-${monthNumber}-${i+1}`).getDay() // getting day number 0 - 6
        item.id = i + 1
        item.dt = i + 1
        item.day = DAYS[day] // getting day from DAYS array
        currentMonthDates.push(item) // adding item object containing day and date to currentMonthDates array
    }
    return arrangeGridAccordingToDays(currentMonthDates) // arranging dates in correct order
}


Enter fullscreen mode Exit fullscreen mode

CalenderGrid.vue πŸ‘‡πŸ»



<script setup>
    import { ref, computed, inject } from 'vue'

    const DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
    const {state, dispatch} = inject('store')
    const current = ref({
        currentDay: DAYS[new Date().getDay()],
        currentDate: new Date().getDate(),
        currentMonth: new Date().getMonth(),
        currentYear: new Date().getFullYear()
    })

    // Checking current month and year πŸ‘‡πŸ»
    const isCurrentMonthAndYear = computed(() => (
        state.value.date.str.getMonth() === current.value.currentMonth && 
        state.value.date.str.getFullYear() === current.value.currentYear
    ))
</script>

<template>
    <div class="calender-grid">
        <div class="days-grid">
            <div
                v-for="day, index in DAYS"
                :key="index"
                :class="['day', {today: day === current.currentDay && isCurrentMonthAndYear}]"
                :title="day"
            >{{ day[0] }}</div>
        </div>
        <div class="date-grid">
            <div class="date"
                v-for="date in state.dateGrid" // Looping over dateGrid
                :key="date.id"
            >
                <div 
                    :class="['val', {today: date.dt === current.currentDate && isCurrentMonthAndYea}]" 
                    v-if="date.dt">
                    {{ date.dt }}
                </div>
                <div v-else>&#10060;</div>
            </div>
        </div>
    </div>
</template>

<style scoped lang="less">
    .calender-grid{
        margin-top: 15px;
        display: grid;
        gap: 20px;
        user-select: none;
        .days-grid{
            display: grid;
            grid-template-columns: repeat(7, 1fr);
            justify-content: space-between;
            align-items: center;
            .day{
                text-align: center;
                font-weight: bold;
                &.today{
                    color: blueviolet;
                }
            }
        }
        .date-grid{
            display: grid;
            grid-template-columns: repeat(7, 1fr);
            justify-content: space-between;
            align-items: center;
            row-gap: 20px;
            .date{
                display: flex;
                justify-content: center;
                cursor: pointer;
                .val{
                    width: 20px;
                    height: 20px;
                    text-align: center;
                    border-radius: 50%;
                    display: flex;
                    align-items: center;
                    justify-content: center;
                    padding: 15px;
                    position: relative;
                    background-color: antiquewhite;
                    &.today{
                        color: blueviolet;
                        font-weight: bold;
                    }
                    &.event{
                        // border: 1px solid salmon;
                        background-color: antiquewhite;
                        font-weight: bold;
                        &::before{
                            content: '';
                            width: 5px;
                            height: 5px;
                            border-radius: 50%;
                            background-color: salmon;
                            position: absolute;
                            top: 2px;
                            right: 2px;
                        }
                    }
                }
            }
        }
    }
</style>


Enter fullscreen mode Exit fullscreen mode

And with that our basic calendar is ready.

Final Calendar

Top comments (2)

Collapse
 
pa4ozdravkov profile image
Plamen Zdravkov • Edited

Thank you for sharing your experience with this task. It seems like not a trivial task, indeed. I was wondering have you considered using some ready component instead of building it from scratch?

Collapse
 
owais11-art profile image
Owais Khan • Edited

I am glad that you enjoyed it.... I built it from scratch only with vue.js