Building a Date Picker
Continuing to work on my rebuild of the GPX app, I decided to build my own datepicker using Vue 3, Bootstrap 5, and TypeScript
Steve Furches
Final Code on GitHub: github.com/MSF42/VueDatePicker
GPX App
The GPX app is coming along quite well. I've learned a lot about TypeScript and working with dates in JavaScript. My next blog post will be an update on that. For now, I just want to talk about the datepicker I built.
Datepickers
There are a lot of datepickers to choose from out there. When I needed one for this app, I tried three or four from the vast selection on npm. Out of the few that were aesthetically pleasing, putting them to use required more effort than I cared to expend. So, I decided to build my own! I thought it would be a nice learning opportunity. Developers often complain about working with dates in JavaScript, so I thought this would be a great chance to get into it.
Problem Solving
First, the basics:
Getting and Emitting Date
I needed the datepicker to be a reusable component, of course. So I set it up to receive the date from the parent and emit the chosen date back to the parent.
props: {
date: {
required: true,
type: Date,
},
},
emits: ["selected-date"],
Basic Info
const { date } = toRefs(props);
// GET THE YEAR, MONTH, AND DAY FOR DROPDOWN DISPLAY
const dateYear = computed(() => {
return date.value.getFullYear();
});
// A NUMBER VERSION OF THE MONTH TO WORK WITH
const dateMonthInt = computed(() => {
return date.value.getMonth();
});
// A STRING VERSION OF THE MONTH TO DISPLAY
const dateMonthStr = computed(() => {
let month = months[dateMonthInt.value];
return month.length === 4
? month
: months[dateMonthInt.value].slice(0, 3);
});
// ADD A "0" IF THE DATE IS LESS THAN 10
const dateDate = computed(() => {
return ((date.value.getDate() < 10 ? "0" : "") +
date.value.getDate()) as unknown as number;
});
// MONTH AND DAY LISTS
const months = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
const days = ["Sun", "Mon", "Tue", "Wed", "Thur", "Fri", "Sat"];
Toggle Calendar
const showDate = ref(false);
const toggleDateView = () => {
showDate.value = !showDate.value;
};
Emit the Chosen Date
Letting the parent know the date has been chosen:
const dateChosen = (row: number, col: number) => {
let newdate = monthArray.value[row][col]
context.emit('selected-date', newdate)
}
Time Zone Offset
If I don't use a timezone offset, the chosen date and displayed date can get out of sync. Adding this offset effectively sets the time for each day to midnight.
// add this to all new dates (milliseconds)
const tzOffset = computed(() => {
return date.value.getTimezoneOffset() * 1000;
});
Month Length and First Day of Month
Since JavaScript will return February 1 if you pass in January 32, this creates a handy way to calculate the length of a month: Simply subtract the date value from 32.
Example: January 32 will return February 1, so 32 - 1 = 31
const getMonthLength = computed(() => {
return 32 - new Date(dateYear.value, dateMonthInt.value, 32).getDate();
});
// GET THE FIRST DAY OF THE MONTH
const getMonthFirstDay = computed(() => {
let weekStartsOnSun = new Date(dateYear.value, dateMonthInt.value).getDay();
return weekStartsOnSun === 0 ? 6 : weekStartsOnSun - 1;
// I subtracted 1 so the week could start on Monday
});
Month Array (this was a bit tricky)
- I created an array of 42 dates, since some months will need to display six weeks.
- I took the first day of the month and subtracted the 'day' value in order to find out what date needed to be in the first square. For example, if February 1 is a Thursday, the 'day' value for Thursday is 3. The first column (Monday) will start on February 1st - 3, or January 29.
- I used 86400000 to account for the number of milliseconds in a day.
- I added a day for each item in the array.
- Finally, I returned six slices of the array, representing each week. This results in an array of six arrays (i.e. 'weeks').
const monthArray = computed(() => {
let tempArray = [];
let firstDay = new Date(dateYear.value, dateMonthInt.value);
for (let i = 0; i < 42; i++) {
tempArray.push(
new Date(
firstDay.getTime() +
i * 86400000 +
tzOffset.value -
getMonthFirstDay.value * 86400000,
),
);
}
return [
tempArray.slice(0, 7),
tempArray.slice(7, 14),
tempArray.slice(14, 21),
tempArray.slice(21, 28),
tempArray.slice(28, 35),
tempArray.slice(35),
];
});
Advancing the Calendar
I made a single function to move forward/backward by one month or year. If moving back one month, "0" is passed in for the year (no change) and "-1" is passed in for the month.
One issue I didn't think about initially - if the selected date is January 31, for example, and the user advanced one month, the calendar advanced to March 3 (i.e. "February 31"). Therefore I had to set up a month length check. Now this example will result in advancing to February 28 (or 29).
const dateChange = (year: number, month: number) => {
let tempdate
// CHECK LENGTH OF THE MONTH WE ARE ADVANCING TO
let newMonthLength =
32 - new Date(dateYear.value, dateMonthInt.value + month, 32).getDate()
if (year === 0 && dateDate.value > newMonthLength) {
tempdate = new Date(
new Date(
dateYear.value,
dateMonthInt.value + month,
newMonthLength
).getTime() + tzOffset.value
)
} else {
tempdate = new Date(
new Date(
dateYear.value + year,
dateMonthInt.value + month,
dateDate.value
).getTime() + tzOffset.value
)
}
context.emit('selected-date', tempdate)
}
Highlighting Dates
// HIGHLIGHT THE SELECTED DATE IN RED
const isSelectedDate = (date1: Date, date2: Date) => {
return date1.getFullYear() === date2.getFullYear() &&
date1.getMonth() === date2.getMonth() &&
date1.getDate() === date2.getDate()
? 'bg-red-500 text-bold text-gray-200'
: ''
}
// HIGHLIGHT TODAY'S DATE IN BLUE
const isCurrentDate = (date: Date) => {
let today = new Date(Date.now())
return date.getFullYear() === today.getFullYear() &&
date.getMonth() === today.getMonth() &&
date.getDate() === today.getDate()
? 'bg-blue-500 text-bold text-gray-200'
: ''
}
// MAKE DAYS OF CURRENTLY VIEWED MONTH BOLD, MAKE OTHERS LIGHTER
const isNotCurrentMonth = (date1: Date, date2: Date) => {
return date1.getFullYear() === date2.getFullYear() &&
date1.getMonth() === date2.getMonth()
? 'text-bold'
: 'text-gray-500'
}
Lastly, a Directive to Highlight on Hover
directives: {
hover: {
beforeMount(el, binding) {
const { value = [] } = binding;
el.addEventListener("mouseenter", () => {
el.classList.add(...value);
});
el.addEventListener("mouseleave", () => {
el.classList.remove(...value);
});
},
unmounted(el, binding) {
el.removeEventListener("mouseenter", binding);
el.removeEventListener("mouseleave", binding);
},
},
},
////// and in the element above
v-hover="['bg-blue-700', 'text-gray-300']"
Summary
I really enjoyed this little challenge! It was great practice in several areas - directives, JavaScript date functions, and TypeScript.
Perhaps someone else will find a use for my datepicker!