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><<</span> <span><</span> <span class="header_time"> <span>{{time.year}}year</span> <span>{{time.month + 1}}month</span> </span> <span>></span> <span>>></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"><<</span> <span @click="preMonth"><</span> <span class="header_time"> <span>{{time.year}}year</span> <span>{{time.month + 1}}month</span> </span> <span @click="nextMonth">></span> <span @click="nextYear">>></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