The date-picker in Element-UI is an excellent time picker with a wide range of options and a high degree of customization. However, date-Picker did not support custom cell styles prior to 2.12, the cellClassName feature in 2.12. So if you use pre-2.12, you can’t change the style of the cells directly, so you can’t mark important dates (like holidays) on the calendar.

The company project uses version 2.3.9 of Element-UI, but requires the cellClassName function from version 2.12. If you’re asking why you’re not upgrading to the latest version, it’s because companies are looking for stability. Of course, if you upgrade to the latest version, there will be no article.

purpose

  1. Pass in an array to store itYYYY-MM-DDFormat time, in the panel for the data matching the corresponding class
  2. Data already marked when switching panels will not be lost
  3. Cannot upgrade to version 2.12

The source code parsing

First look directly at the source code structure.

date-pickerIs the core ofpicker.vueTo operate the entire picker initialization, hide, show, etc. The specific daily presentation isdate-table.vueTo control. date-tableThe HTML source code is as follows and we can see for eachTDThat is, the cell increment class is usedgetCellClassesThis method. Traversal data is usedrows

<template>
	<table cellspacing="0" cellpadding="0" class="el-date-table" @click="handleClick" @mousemove="handleMouseMove" :class="{ 'is-week-mode': selectionMode === 'week' }">
		<tbody>
			<tr>
				<th v-if="showWeekNumber">{{ t('el.datepicker.week') }}</th>
				<th v-for="(week, key) in WEEKS" :key="key">{{ t('el.datepicker.weeks.' + week) }}</th>
			</tr>
			<tr class="el-date-table__row" v-for="(row, key) in rows" :class="{ current: isWeekActive(row[1]) }" :key="key">
				<td v-for="(cell, key) in row" :class="getCellClasses(cell)" :key="key">
					<div>
						<span>
							{{ cell.text }}
						</span>
					</div>
				</td>
			</tr>
		</tbody>
	</table>
</template>
<script>
methods: {
		getCellClasses(cell) {
			const selectionMode = this.selectionMode;
			const defaultValue = this.defaultValue
				? Array.isArray(this.defaultValue)
					? this.defaultValue
					: [this.defaultValue]
				: [];

			let classes = [];
			if (
				(cell.type === 'normal' || cell.type === 'today') &&
				!cell.disabled
			) {
				classes.push('available');
				if (cell.type === 'today') {
					classes.push('today'); }}else {
				classes.push(cell.type);
			}

			if (
				cell.type === 'normal' &&
				defaultValue.some((date) = > this.cellMatchesDate(cell, date))
			) {
				classes.push('default');
			}

			if (
				selectionMode === 'day' &&
				(cell.type === 'normal' || cell.type === 'today') &&
				this.cellMatchesDate(cell, this.value)
			) {
				classes.push('current');
			}

			if (
				cell.inRange &&
				(cell.type === 'normal' ||
					cell.type === 'today' ||
					this.selectionMode === 'week')
			) {
				classes.push('in-range');

				if (cell.start) {
					classes.push('start-date');
				}

				if (cell.end) {
					classes.push('end-date'); }}if (cell.disabled) {
				classes.push('disabled');
			}

			if (cell.selected) {
				classes.push('selected');
			}
			console.log(classes);
			return classes.join(' '); }}</script>
Copy the code

Let’s see if there’s any way we can sneak in. After repeated observation (about an hour), you can see that in the first if statement, as long as type is not “normal” or “today” and not disabled, it goes to else, and type is used as class. Therefore, we have the opportunity to change the class.

rows() {
			// TODO: refactory rows / getCellClasses
			const date = new Date(this.year, this.month, 1);
			let day = getFirstDayOfMonth(date); // day of first day
			const dateCountOfMonth = getDayCountOfMonth(
				date.getFullYear(),
				date.getMonth()
			);
			const dateCountOfLastMonth = getDayCountOfMonth(
				date.getFullYear(),
				date.getMonth() === 0 ? 11 : date.getMonth() - 1
			);

			day = day === 0 ? 7 : day;

			const offset = this.offsetDay;
			const rows = this.tableRows;
			let count = 1;
			let firstDayPosition;

			const startDate = this.startDate;
			const disabledDate = this.disabledDate;
			const selectedDate = this.selectedDate || this.value;
			const now = clearHours(new Date());

			for (let i = 0; i < 6; i++) {
				const row = rows[i];

				if (this.showWeekNumber) {
					if(! row[0]) {
						row[0] = {
							type: 'week'.text: getWeekNumber(nextDate(startDate, i * 7 + 1))}; }}for (let j = 0; j < 7; j++) {
					let cell = row[this.showWeekNumber ? j + 1 : j];
					if(! cell) { cell = {row: i,
							column: j,
							type: 'normal'.inRange: false.start: false.end: false
						};
					}

					cell.type = 'normal';

					const index = i * 7 + j;
					const time = nextDate(startDate, index - offset).getTime();
					cell.inRange =
						time >= clearHours(this.minDate) &&
						time <= clearHours(this.maxDate);
					cell.start =
						this.minDate && time === clearHours(this.minDate);
					cell.end =
						this.maxDate && time === clearHours(this.maxDate);
					const isToday = time === now;

					if (isToday) {
						cell.type = 'today';
					}

					if (i >= 0 && i <= 1) {
						if (j + i * 7 >= day + offset) {
							cell.text = count++;
							if (count === 2) {
								firstDayPosition = i * 7+ j; }}else {
							cell.text =
								dateCountOfLastMonth -
								(day + offset - (j % 7)) +
								1 +
								i * 7;
							cell.type = 'prev-month'; }}else {
						if (count <= dateCountOfMonth) {
							cell.text = count++;
							if (count === 2) {
								firstDayPosition = i * 7+ j; }}else {
							cell.text = count++ - dateCountOfMonth;
							cell.type = 'next-month'; }}let newDate = new Date(time);
					cell.disabled =
						typeof disabledDate === 'function' &&
						disabledDate(newDate);
					cell.selected =
						Array.isArray(selectedDate) &&
						selectedDate.filter(
							(date) = > date.toString() === newDate.toString()
						)[0];

					this.$set(row, this.showWeekNumber ? j + 1 : j, cell);
				}

				if (this.selectionMode === 'week') {
					const start = this.showWeekNumber ? 1 : 0;
					const end = this.showWeekNumber ? 7 : 6;
					const isWeekActive = this.isWeekActive(row[start + 1]);

					row[start].inRange = isWeekActive;
					row[start].start = isWeekActive;
					row[end].inRange = isWeekActive;
					row[end].end = isWeekActive;
				}
			}

			rows.firstDayPosition = firstDayPosition;

			return rows;
		}
Copy the code

If we look at the data iterated over, we can see that there is a calculation property “rows” that uses the data on the table. If we need to create a new cell object every time, we’re going to get stuck. Unfortunately, this is created when the cell is empty, so we can change the class as long as we can change the value of the table.

Of course, there is a pitfall here, that is, can not trigger the update of the calculated property. This is because the computed property fires and sets type to Normal, which causes the data to be re-rendered, overwriting the previous type. $set(); Vue.$set();

Another problem is that the text stored in each cell is only day, not a complete date. Therefore, we need to obtain the date of the current date-table.

The solution

After analyzing the above requirements, we need to complete the following tasks:

  1. Access to thetableRows, find the value we need (judging by the current date)
  2. Modify thetableRows, and cannot trigger the calculation property.
  3. Encapsulate into separate components

To retrieve the table ws we need to use $refs to retrieve the component’s data. The code is as follows:

/ / get tableRows
this.$refs.datePicker.picker.$children[0].tableRows;
// Get the current date of the panel
this.$refs.datePicker.picker.$children[0].date;
Copy the code

The datePicker is the ref of the native component, and the picker is a child component within the component. The picker is divided into panel and input. $children[0] is the panel.

Then based on these two we can write a method to modify the table ws, the code is as follows:

	    /** * Retrieve the date in YYYY-MM-DD format based on the current date of the datePicker * date-table is a 6*7 table, so a maximum of three months will be displayed */
		getFormatDate(val) {
			const date = this.$refs.datePicker.picker.$children[0].date;
			let formatDate = moment(date);
			formatDate.set('date', val.text);
			if (val.type == 'prev-month') {
				formatDate.subtract(1.'M');
			} else if (val.type == 'next-month') {
				formatDate.add(1.'M');
			}
			return formatDate.format('YYYY-MM-DD');
		},
		// Check if cell dates need to be marked
		checkMarked(cell) {
			return this.mark.indexOf(this.getFormatDate(cell)) ! = -1;
		},
        // Mark cells
		markDate() {
			// Get the array inside the el-date-picker
			const rows = this.$refs.datePicker.picker.$children[0].tableRows;
			// Change the data to
			for (let i = 0; i < rows.length; i++) {
				for (let j = 0; j < rows[i].length; j++) {
					let cell = rows[i][j];
					if (this.checkMarked(cell)) {
						cell.type = this.cellClassName; }}}// The el-date-picker uses the calculated property internally. If Vue.$set is used, the calculated property will be called to override the set class
			this.$refs.datePicker.picker.$children[0].tableRows = rows;
		}
Copy the code

The function of the Vue.$set method is specified in the comments of the code. The key is not to let the component’s calculated property fire, so do not use vue. $set.

Inside the packaged component, I also used timers to ensure that I could change the page number to class in real time. This solution isn’t elegant, but I don’t see a page-turning callback event in the source code. In theory I should capture the mouse’s behavior and trigger the markDate() method when the mouse is clicked, but I can’t do that yet. If you have a better solution, let us know in the comments section.

Component source

The complete component source code is given below:

<template> <el-date-picker v-model="bindingDate" :align="align" :default-value="defaultDate" :type="type" :placeholder="placeholder" :picker-options="pickerOptions" ref='datePicker' @focus="handleFocus"> </el-date-picker> </template> <script> import moment from 'moment'; export default { props: { value: { default: Date.now() }, //type type: { default: () => { return 'date'; Placeholder: {default: () => {return 'please select date '; }}, // Editable: {type: Boolean, default: true}, // Array to be tagged (yyyY-MM-DD format) mark: {type: Array}, // defaultDate: {default: () => {return new Date(); }}, // Custom cell tag cellClassName: {type: String, default: 'marked'}, align: {type: String, default: 'left'}, pickerOptions: {default: {}}, // Whether filterable: {default: () => {return true; }}, data() {return {// timer timer: ''}; }, mounted() { let _this = this; / / forced datePicker initialization enclosing $refs. DatePicker. MountPicker (); // Refresh the cell with timer this.timer = window.setInterval(() => {_this.markdate (); }, 1000); }, // Destroy timer beforeDestroy() {clearInterval(this.timer); }, computed: { bindingDate: { get: function() { return this.value; }, set: function(value) { this.$emit('input', value); } } }, watch: { mark: function(val) { if (val && val.length > 0) { this.markDate(); } } }, methods: {/** * datePicker datePicker datePicker datePicker datePicker datePicker / getFormatDate(val) {const date = const date = const date = const date = const date = const date = const date = const date = const date = const date = const date = this.$refs.datePicker.picker.$children[0].date; let formatDate = moment(date); formatDate.set('date', val.text); if (val.type == 'prev-month') { formatDate.subtract(1, 'M'); } else if (val.type == 'next-month') { formatDate.add(1, 'M'); } return formatDate.format('YYYY-MM-DD'); }, // Check if the cell date needs to be marked checkMarked(cell) {return this.mark.indexof (this.getFormatDate(cell))! = 1; }, //focus event handleFocus() {this.markDate(); }, // Mark cell markDate() {const rows = this.$refs.datepicker.picker.$children[0]. For (let I = 0; i < rows.length; i++) { for (let j = 0; j < rows[i].length; j++) { let cell = rows[i][j]; if (this.checkMarked(cell)) { cell.type = this.cellClassName; }}} // The el-date-picker uses the calculated attribute internally, $this.$refs.datepicker. picker.$children[0]. TableRows = rows; }}}; </script>Copy the code

conclusion

To sum up, the purpose of this article is to add the ability to mark important dates to the DatePicker without upgrading the element-UI version (again, consider upgrading if you can). The main use of date-table internal access to a class statement vulnerability and directly to the object assignment will not trigger the calculation of attributes this feature. The packaged component uses timers internally to ensure that the class can be changed when the page is turned.