Implement a calendar component from zero

Keywords: Front-end Vue npm Attribute JSON

I. Introduction to calendar components

The calendar component is mainly composed of a text input box. After clicking the text input box, the calendar panel will be displayed below the text box. The calendar panel consists of three parts: the head area (which mainly displays the month and year corresponding to the face-to-face calendar panel and the up and down buttons of four months), the content area (which displays the week and 42 days), the bottom area (today's shortcut button, click to jump to today directly). At the same time, click the outside of the calendar panel to close the calendar panel.

II. Key points of calendar components

① the key point of the calendar component is the display of the calendar panel. When observing the calendar, it can be found that each calendar panel will display 42 days, but there are 28-31 days in a month, so there must be some non current month time in these 42 days. These non current month time need to be grayed out. There are 7 columns in each row (because there are 7 days in each week, each day corresponds to a week's day). There are 6 rows in total. As for The reason why six lines are needed is that the first line must show the first day of the month, but if the first day of a month is Saturday, then the first line only shows the first day of the month in seven days, and a month may have 31 days. If there are only four lines later, then at most only 1 + 28 = 29 days can be displayed, and 31 days can't be displayed, so a total of six lines must be displayed to show all the days of the month. Number.

② when observing the calendar, we can also find a rule, that is, the day of the week corresponding to the first day of the month, so that we can move forward a few days according to the time of the first day of the 42 days, find the corresponding time of the first day of the 42 days, and then traverse, one day plus one day at a time, until the 42 days, we can display the time on the calendar panel of the month.

3. Implement a calendar component from scratch

① create a new folder named calendar
② enter the calendar project, execute npm init --yes to initialize the project and generate the corresponding package.json file.
③ the rapid prototype development mode is used here, NPM install - G @ Vue / cli service global
④ create a new App.vue file in the root directory of calendar project, such as:

<template>
    <div id="app">
        hello calendar
    </div>
</template>

⑤ start the project through the vue serve, the App.vue root component under the calendar project root directory will be automatically loaded and executed. Enter http://localhost:8080 in the browser, if the hello calendar is printed out, it indicates that the environment is built successfully.

⑥ next, we start to write the calendar component. First, create a new components directory under the root directory of the calendar project, and then create a new calendar.vue component in it. The calendar component receives a value attribute. The data type is Date date type, and the default value is current time. The content is as follows:

<template>
    <div class="calendar">
        //Calendar component {{value}}
    </div>
</template>
<script>
export default {
    props: {
        value: {
            type: Date,
            default: () => new Date()
        }
    }
}
</script>

Modify App.vue and introduce calendar.vue calendar component, such as:

<template>
    <div id="app">
        <calendar v-model="now"></calendar>
    </div>
</template>
<script>
import Calendar from "./components/calendar"
export default {
    components: {
        calendar: Calendar
    },
    data () {
        return {
            now: new Date()
        }
    }
}
</script>

⑦ at this time, our calendar component can render normally. Next, we start to write the contents of the calendar. The calendar component includes a text input box and a calendar panel. The contents of the calendar panel are implemented later. In this step, we first write the style of the text box and the non content part of the calendar panel, such as:
//Add iconfont font style, mainly used for calendar icon in text box
//Create a new CSS folder in the components folder and a new iconcont.css

@font-face {font-family: "iconfont";
    src:url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAALwAAsAAAAAB8QAAAKkAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHEIGVgCCcAqDUIMmATYCJAMICwYABCAFhG0HLhvKBhHVkz1kPwrj9qSlmDfJebNDpSCSnDR9XwTPox31fpKZzVo6SC3E6nqoP3dgB5dEPfs/Z9kkCxthinLICnUpv8BpduBOq3vTbgHwx73TvwIKZD6gnObY+KmLoy7cGtDeGEVWICmGmTeM3UR5ELchgB9JFCAdXZc7WAxgkQCyannogk3pMDXFgkVwS3Ya5BgOVu1XjwGO8vfLVygTCwpHA8pGlmwDaPmYB9P0Nu9vFkXgj2cBtH2ggQLAgEyU2obQYawAjZ8TM6TBuooFPuZ5H8pb7R8PBMQFFAYAkCDyzomPBadaqAAwrQYvA9d7FUNAjE0JAPM3ypkoP7adP3BRJICf6XcqgtUh6nRk8NnoOf4HL2C2nfcLKU1ztl/y9xfCyeoJlCWL6jga4tfK9kuT8TdMrd9Xo7LXufPOaEGhCaFBhR181BnHXefNP7jOrzDz3PP/oNCgD1jRIulutzbRt3aI1Ls/dTzaUODWxM88+8gjaAHAe2uoWPzAz3C/L2fd3GHDf+tvAHj17t4d7vHeBto5wN6mXeB38VvWGFcI9MrY/FKH4vJtL1SAH36AB7IrjPd9HZEQWwSr80VQ+JAIGksGaigF4OBPBbhYmsGPfLr3+xPOBjRifIE8dgsghHANFEHcAU0IT1BDeQcOUXwHlxDR4McUCT/RnyxJ4s6ayRUK0PvF2C8LhYzSCYqvFL4yl5NCTnsSN3EQLd3MJvdUEI+xpvkKbRGFisscd8J9lGUlVlwm5IseiVQjw1BlT9L9MocOtDO5QgHi/SKxXxaKNpdO7vVXCl+ZyzWkDvuTuImHRyx0zBboXla0Il3LI81XaCOiEMVljuwEC2UwViJV+bSEfNGJekSqEYZUT7WV6fMr8qfbBkAHgLrdgtUaw3EWAwA=') format('woff2')
  }
  
  .iconfont {
    font-family: "iconfont" !important;
    font-size: 16px;
    font-style: normal;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
  }
  
  .iconrili:before {
    content: "\e72a";
  }

//Modify calendar.vue

<template>
    <div class="calendar">
        <input type="text" placeholder="Selection date" class="calendar_input"/>
        <span class="input_prefix">
            <i class="iconfont iconrili"></i>
        </span>
        <!--Calendar panel-->
        <div class="calendar_box">
            <span class="triangle"></span> <!--Upper triangle of panel-->
        </div>
    </div>
</template>
<script>
export default {
    props: {
        value: {
            type: Date,
            default: () => new Date()
        }
    }
}
</script>
<style scoped>
@import url("./css/iconfont.css");
.calendar {
    position: relative;
}
.calendar_input {
    border: 1px solid #c0c4cc;
    padding: 0 30px;
    height: 40px;
    line-height: 40px;
    border-radius: 4px;
    outline: none;/* Remove outline outside border */
}
.calendar_input:focus {
    border: 1px solid #409eff;
}
.input_prefix {
    height: 100%;
    width: 25px;
    text-align: center;
    position: absolute;
    left: 5px;
    top: 0;
    color: #c0c4cc;
}
.input_prefix i {
    line-height: 40px;
}
.calendar_box {
    position: absolute;
    top: 50px;
    width: 400px;/* The fixed width and height are temporarily used, and then the width and height will be removed for content adaptive reality. */
    height: 300px;
    border: 1px solid #e4e7ed;
    box-shadow: 0 2px 12px 0 rgba(0,0,0,.1);
    border-radius: 4px;
}
.calendar_box .triangle {
    position: absolute;
    width: 0;
    height: 0;
    top: -14px;
    left: 25px;
    border: 7px solid transparent;
    border-bottom: 7px solid white;
}
.calendar_box::before {
    position: absolute;
    content: "";
    width: 0;
    height: 0;
    top: -16px;
    left: 24px;
    border: 8px solid transparent;
    border-bottom: 8px solid #e4e7ed;
} 
</style>

The effect is as follows:

⑧ at this time, the calendar input box and panel have been drawn. Next, click the text box to display the calendar panel. Click the outside of the calendar panel to close the calendar panel. To achieve this function, you need to use custom instructions, because the instructions are to encapsulate the DOM operation, which is mainly to let the document listen to the click event. If the clicked element is in the DOM bound with the instructions, then Open the calendar panel. If the clicked element is not in the dom of the binding instruction, close the calendar panel, such as:

<div class="calendar" v-click-outside> <!--Bound instruction-->
    ...ellipsis
</div>
export default {
    directives: { // Add instruction object
        clickOutside: {
            bind(el, binding, vnode) {
                const handler = (e) => {
                    if (el.contains(e.target)) { // If you click on the text box, you need to display the calendar panel
                        if (!vnode.context.isVisible) { // Open the calendar panel if isVisible is false
                            vnode.context.focus();
                        }
                    } else { // If you click on the outside of a text box instead of a text box
                        if (vnode.context.isVisible) { // Close the calendar panel if isVisible is true
                            vnode.context.blur();
                        }
                    }
                };
                el.handler = handler; // Save the event handling function to el, that is, the DOM where the instruction is located, which is convenient to unbind and remove the event handling function.
                document.addEventListener("click", handler);
            },
            unbind(el) {
                document.removeEventListener("click", el.handler);
            }
        } 
    }
}

In a word, if you click the DOM element of the instruction, the calendar panel will be opened. If you click the DOM element of the instruction, the calendar panel will be closed.

⑨ at this time, click the text box to display the calendar panel. Click the outside of the calendar panel to close the calendar panel. Next, you need to display the specific contents of the calendar panel:
First of all, we passed a current Date object to the calendar component. According to this Date object, we should take out the corresponding year, month and day, create a new utils directory in the root directory, and create a new util.js file. The content is as follows:

const getYearMonthDay = (date) => {
    const year = date.getFullYear(); // Year of acquisition
    const month = date.getMonth(); // Acquisition month
    const day = date.getDate(); // Acquisition date
    return {year, month , day};
}
export {
    getYearMonthDay
}

Then we need to display the corresponding year and month of the current panel in the head of the calendar panel, and call the getYearMonthDay() method in the data of the calendar component to get the corresponding year.

export default {
    data () {
        const {year, month} = util.getYearMonthDay(this.value); // Get the year and month corresponding to the delivery time
        return {
             isVisible : false, // Is the control panel visible
             time: {year, month}, // Define the time object to display the current year and month
             weekDays: ["day", "One", "Two", "Three", "Four", "Five", "Six"],
        }
    }
}

//Calendar panel

<div class="calendar_box" v-if="isVisible">
            <span class="triangle"></span> <!--Upper triangle of panel-->
            <div class="calendar_header">
               <span>&lt;&lt;</span>
               <span>&lt;</span>
               <span class="header_time">
                   <span>{{time.year}}year</span>
                   <span>{{time.month + 1}}month</span>
               </span>
               <span>&gt;</span>
               <span>&gt;&gt;</span>
            </div>
            <div class="calendar_content">
                <span v-for="j in 7" :key="`_${j}`" class="cell">
                       {{weekDays[j - 1]}}
                </span>
            </div>
        </div>

//Corresponding CSS Style

.calendar_header {
    display: flex;
    justify-content: space-around;
    height: 30px;
    line-height: 30px;
    font-size: 14px;
    font-weight: 100;
}
.header_time {
    box-sizing: border-box;
    width: 50%;
    padding: 0 25px;
    height: 30px;
    line-height: 30px;
    color: #606266;
    font-size: 16px;
    font-weight: 500;
    display: flex;
    justify-content: space-between;
}
.calendar_content .cell {
    display: inline-flex;
    width: 41px;
    height: 41px;
    justify-content: center;
    align-items: center;
}

The next step is to calculate the 42 days of the month. The idea is to find out the day of the week corresponding to the first day of the month, and then move forward a few days to be the first day of the 42 days, and then cycle out 42 days, such as:
//Add a calculation attribute to calculate the 42 days displayed in the current month

export default {
    computed: {
      visibleDays() {
          // Get the Date object corresponding to the first day of the current month
          const firstDayOfMonth = new Date(this.time.year, this.time.month, 1);
          // Get the day of the week corresponding to the first day of the month
          const week = firstDayOfMonth.getDay();
          // Get the Date object corresponding to the first day of 42 days, that is, the time corresponding to the first day of each month minus the week day
          const startDay = firstDayOfMonth - week * 60 * 60 * 1000 * 24; 
          const days = [];
          for (let i= 0; i< 42; i++) { // Cycle out 42 days
             days.push(new Date(startDay + i * 60 * 60 * 1000 * 24));
          }
          return days;
      } 
}

//It's 42 days.

<div class="calendar_content">
   <span v-for="j in 7" :key="`_${j}`" class="cell">
         {{weekDays[j - 1]}}
   </span>
   <div v-for="i in 6" :key="i"> <!--Cycle from 1-->
         <span v-for="j in 7" :key="j" class="cell">
            <!--Get the corresponding date of each day date Value to display-->
            {{visibleDays[(i -1) * 7 + (j -1)].getDate()}}
         </span>
   </div>
</div>

⑪ next, we need to gray the date that is not the current month. If it is today, we need to add a red background, which is mainly to use the current date object to judge and make dynamic changes in the style, such as:
//Add two methods

export default {
    methods: {
        isCurrentMonth(date) { // Judge whether the delivery date belongs to the current month
            // Get the month and year corresponding to the delivery time
            const {year, month} = util.getYearMonthDay(date);
            // Compared with the calendar panel showing the year and month, if the year and month are the same, it is the time of the month.
            return year === this.time.year && month === this.time.month;
        },
        isToday(date) { // Determine whether the delivery date is today
             // Get the date corresponding to the delivery time
            const {year, month, day} = util.getYearMonthDay(date);
            // Get the date corresponding to today's time
            const {year:y, month:m, day:d} = util.getYearMonthDay(new Date());
            return year === y && month === m && day === d;
        }
    }
}

//Add upper style dynamically

<div class="calendar_content">
                <span v-for="j in 7" :key="`_${j}`" class="cell">
                       {{weekDays[j - 1]}}
                </span>
                <div v-for="i in 6" :key="i"> <!--Cycle from 1-->
                    <span v-for="j in 7" :key="j" class="cell" :class="[
                        {
                           notCurrentMonth: !isCurrentMonth(visibleDays[(i -1) * 7 + (j -1)])
                        },
                        {
                           today: isToday(visibleDays[(i -1) * 7 + (j -1)]) 
                        }
                    ]">
                       <!--Get the corresponding date of each day date Value to display-->
                       {{visibleDays[(i -1) * 7 + (j -1)].getDate()}}
                    </span>
                </div>
            </div>

//Add notCurrentMonth and today styles

.notCurrentMonth {
    color: grey;
}
.today {
    background: red;
    color: white;
    border-radius: 4px;
}

(13) the next step is to select the time. When the user clicks a certain time in 42 days, the corresponding time needs to be displayed in the text box. The default time displayed in the text box is the time delivered by the parent component. Because the child component cannot directly modify the time delivered by the parent component, after selecting the date, the parent component needs to be notified to modify. The parent component has delivered the time after receiving the notification. When the time of arrival is modified, the subcomponent can get the time selected by the user for display, such as:
//The time displayed in the text box is year month day, so it needs to be formatted
//Add a calculation property

export default {
    computed: {
        formatDate() {
            const {year, month, day} = util.getYearMonthDay(this.value);
            return `${year}-${month + 1}-${day}`;
      }
    },
    methods: {
        chooseDate(date) {
            // There are 12 days on the calendar panel, so users may choose other months, and the calendar panel needs to be updated accordingly.
            this.time = util.getYearMonthDay(date); // Update this.time to update the month and year displayed in the calendar panel for 42 days
            this.$emit("input", date);
            this.blur();
        }
    }
}
 <input type="text" placeholder="Selection date" class="calendar_input" :value="formatDate"/>
 <span v-for="j in 7" :key="j" class="cell" @click="chooseDate(visibleDays[(i -1) * 7 + (j -1)])">

⑬ after the user selects the time, when the panel is opened again, you need to see which date is selected, so you need to judge the date selected by the user, and then switch the dynamic style dynamically, such as:

export default {
    methods: {
        isSelect(date) { // Pass the time on the panel and judge whether it is the date selected by the user.
            // Get the year, month and day corresponding to the date on the panel
            const {year, month, day} = util.getYearMonthDay(date);
            // Get the year, month and day corresponding to the time selected by the user
            const {year:y, month:m, day:d} = util.getYearMonthDay(this.value);
            return year===y && month === m && day === d;
        }
    }
}
.select {
    border:  1px solid pink;
    box-sizing: border-box;
    border-radius: 4px;
}

⑭ the next is the previous month, the next month, the previous year and the next year. It is very simple. According to the month and year displayed on the current panel, you can arbitrarily obtain a day in the panel, such as the first day of each month, and then create a Date object. You can obtain the current month or year through the Date object and add or subtract 1, for example:

export default {
    methods: {
        preYear() {
            // Get any day in the current panel, such as the Date object corresponding to the first day of the current month
            const someDayOfCurrentMonth = new Date(this.time.year, this.time.month, 1);
            const currentYear = someDayOfCurrentMonth.getFullYear();
            // Change a day in the current panel to a day in the previous month
            someDayOfCurrentMonth.setFullYear(currentYear - 1);
            // Get the corresponding month month update this.time from a day in the previous month
            this.time = util.getYearMonthDay(someDayOfCurrentMonth);
        },
        preMonth() {
            // Get any day in the current panel, such as the Date object corresponding to the first day of the current month
            const someDayOfCurrentMonth = new Date(this.time.year, this.time.month, 1);
            const currentMonth = someDayOfCurrentMonth.getMonth();
            // Change a day in the current panel to a day in the previous month
            someDayOfCurrentMonth.setMonth(currentMonth - 1);
            // Get the corresponding month month update this.time from a day in the previous month
            this.time = util.getYearMonthDay(someDayOfCurrentMonth);

        },
        nextYear() {
            // Get any day in the current panel, such as the Date object corresponding to the first day of the current month
            const someDayOfCurrentMonth = new Date(this.time.year, this.time.month, 1);
            const currentYear = someDayOfCurrentMonth.getFullYear();
            // Change a day in the current panel to a day in the previous month
            someDayOfCurrentMonth.setFullYear(currentYear + 1);
            // Get the corresponding month month update this.time from a day in the previous month
            this.time = util.getYearMonthDay(someDayOfCurrentMonth);
        },
        nextMonth() {
            // Get any day in the current panel, such as the Date object corresponding to the first day of the current month
            const someDayOfCurrentMonth = new Date(this.time.year, this.time.month, 1);
            const currentMonth = someDayOfCurrentMonth.getMonth();
            // Change a day in the current panel to a day in the previous month
            someDayOfCurrentMonth.setMonth(currentMonth + 1);
            // Get the corresponding month month update this.time from a day in the previous month
            this.time = util.getYearMonthDay(someDayOfCurrentMonth);
        }
    }
}
<div class="calendar_header">
     <span @click="preYear">&lt;&lt;</span>
     <span @click="preMonth">&lt;</span>
     <span class="header_time">
           <span>{{time.year}}year</span>
           <span>{{time.month + 1}}month</span>
     </span>
     <span @click="nextMonth">&gt;</span>
     <span @click="nextYear">&gt;&gt;</span>
</div>

⑮ you can switch between year and month. If the user cuts a long way, it will be very difficult to choose today. Therefore, you need to provide a shortcut. Click to go back to today. Add a div content as today at the bottom of the panel, and add an event. The event only needs to get today's time, and then set the month value of this.time, such as:

<div class="calendar_footer" @click="toToday">
                //Today
</div>
.calendar_footer {
    height: 30px;
    line-height: 30px;
    padding: 5px 0;
    border: 1px solid #e4e7ed;
    border-radius: 4px;
    text-align: center;
    cursor: pointer;
}
export default {
    toToday() {
        this.time = util.getYearMonthDay(new Date());
    }
}

⑯ final effect drawing, as shown in the figure

Posted by jaikob on Tue, 29 Oct 2019 05:06:20 -0700