Tue Nov 14 2017
Several months ago, I set out to write part 3 in my series on building an extendable calendar using Vue.js. Unfortunately, I’m sad to report that this is not that article! :(
However, it’s not all sad faces because what bore from it was a full-blown calendar plugin. It has been in the works for these past few months, and I’m excited to finally share it with others who love to make things with Vue.js.
It’s called V-Calendar, and while it was born from my desire for an all in one calendar solution, I also just wanted to see how far I could get with stock Vue.js and all of the features it provides. While this plugin doesn’t satisfy all of my needs as of yet, it still goes a long way towards my ultimate utopian calendar solution. Here are some preliminary goals for the plugin:
html<template>
<v-calendar
is-double-paned>
</v-calendar>
</template>
The base calendar is designed with as little flair as possible, but still provides an understated, simplified appearance that should integrate well with most applications. Here are some other points to note about the base calendar upon examination:
Attributes are the most important concept to understand with V-Calendar. Fortunately, they are simple to understand and they provide a powerful way to communicate information to your users. Think of an attribute like a decorator for calendar day cells. Any single attribute may target multiple dates or date ranges (with a start and end date), and each may include one of each of the following objects:
Reference the attribute definition below
javascript
// Attribute structure
{
key: String, // Used to uniquely identify attribute, may affect how it is animated
highlight: Object, // Highlight to display for the attribute (reference structure below)
dot: Object, // Dot indicator to display for the attribute (reference structure below)
bar: Object, // Bar indicator to display for the attribute (reference structure below)
contentStyle: Object, // Style object to apply to day cell content
contentHoverStyle: Object, // Style object to apply to day cell content when hovered (merged with contentStyle)
dates: Array[Date or Object], // List of date like values or date ranges (w/ start and end date) to apply the attribute
order: Number // By default, attributes are layered to display the most data possible, but this may be used for manual ordering
}
// Highlight structure
{
animated: Boolean, // Default: true
height: String, // Default: "1.8rem"
backgroundColor: String, // Default: undefined
borderColor: String, // Default: undefined
borderWidth: String, // Default: "0"
borderStyle: String, // Default: "solid"
borderRadius: String // Default: "1.8rem"
}
// Dot indicator structure
{
diameter: String, // Default: "5px"
backgroundColor: String, // Default: undefined
borderColor: String, // Default: undefined
borderWidth: String, // Default: "0"
borderStyle: String, // Default: "solid"
borderRadius: String, // Default: "50%"
}
// Bar indicator structure
{
height: String, // Default: "5px"
backgroundColor: String, // Default: undefined
borderColor: String, // Default: undefined
borderWidth: String, // Default: "0"
borderStyle: String, // Default: "solid"
}
For example, let’s take a look at how to use highlights in practice.
Highlights appear as spanned regions of styled bordered backgrounds. Usually, you’ll want to pair a highlight with a content style as well so that text is legible within the highlighted region. Highlights that span date ranges appear as continuous background regions from the defined start date to the end date. The border radius only applies to the highlighted end caps.
The example above can be represented using 3 attributes. Each attribute has a defined highlight with an associated content style (a style that is just applied to the day label content) so that the days are clearly legible when displayed within the highlighted backgrounds. Finally, we also need to provide an array of dates or date ranges over which each attribute is to be applied.
html<template>
<v-calendar
:attributes='attributes'
is-double-paned>
</v-calendar>
</template>
<script>
import { getExampleMonthComps } from '@/utils/helpers';
export default {
data() {
const {
thisMonth,
thisMonthYear,
nextMonth,
nextMonthYear } = getExampleMonthComps();
return {
attributes: [
{
highlight: {
backgroundColor: '#ff8080', // Red
borderColor: '#ff6666',
borderWidth: '2px',
borderStyle: 'solid',
},
contentStyle: {
color: 'white',
},
dates: [
// Use date ranges
{
start: new Date(thisMonthYear, thisMonth, 1),
end: new Date(thisMonthYear, thisMonth, 4),
},
// Or single dates
new Date(nextMonthYear, nextMonth, 6),
new Date(nextMonthYear, nextMonth, 23),
],
},
{
highlight: {
backgroundColor: '#9f80ff', // Purple
borderColor: '#8c66ff',
borderWidth: '2px',
},
contentStyle: {
color: 'white',
},
dates: [
new Date(thisMonthYear, thisMonth, 1),
new Date(thisMonthYear, thisMonth, 10),
new Date(thisMonthYear, thisMonth, 12),
{
start: new Date(nextMonthYear, nextMonth, 22),
end: new Date(nextMonthYear, nextMonth, 26),
},
],
},
{
highlight: {
backgroundColor: '#66b3cc', // Turquoise
borderColor: '#53a9c6',
borderWidth: '2px',
borderRadius: '5px',
},
contentStyle: {
color: 'white',
},
dates: [
new Date(thisMonthYear, thisMonth, 14),
{
start: new Date(thisMonthYear, thisMonth, 24),
end: new Date(thisMonthYear, thisMonth, 25),
},
new Date(thisMonthYear, thisMonth, 28),
new Date(nextMonthYear, nextMonth, 4),
{
start: new Date(nextMonthYear, nextMonth, 16),
end: new Date(nextMonthYear, nextMonth, 17),
},
],
},
],
};
},
};
</script>
Dot indicators are another kind of object that can be defined within attributes. They follow the same rules as highlights, and can even be applied over date ranges as well. When a date range is used, the indicator is simply repeated for each day within the range.
html<template>
<v-calendar
:attributes='attributes'
is-double-paned>
</v-calendar>
</template>
<script>
import { getExampleMonthComps } from '@/utils/helpers';
export default {
data() {
const { thisMonth, thisMonthYear, nextMonth, nextMonthYear } = getExampleMonthComps();
return {
attributes: [
{
dot: {
backgroundColor: '#ff4d4d', // Red
},
dates: [
new Date(thisMonthYear, thisMonth, 1),
new Date(thisMonthYear, thisMonth, 10),
new Date(thisMonthYear, thisMonth, 22),
new Date(nextMonthYear, nextMonth, 6),
new Date(nextMonthYear, nextMonth, 16),
],
},
{
dot: {
backgroundColor: '#398fac', // Turquoise
},
dates: [
new Date(thisMonthYear, thisMonth, 4),
new Date(thisMonthYear, thisMonth, 10),
new Date(thisMonthYear, thisMonth, 15),
new Date(nextMonthYear, nextMonth, 1),
new Date(nextMonthYear, nextMonth, 12),
{
start: new Date(nextMonthYear, nextMonth, 20),
end: new Date(nextMonthYear, nextMonth, 25),
},
],
},
{
dot: {
backgroundColor: '#794dff', // Purple
},
dates: [
new Date(thisMonthYear, thisMonth, 12),
new Date(thisMonthYear, thisMonth, 26),
new Date(thisMonthYear, thisMonth, 15),
new Date(nextMonthYear, nextMonth, 9),
new Date(nextMonthYear, nextMonth, 5),
new Date(nextMonthYear, nextMonth, 6),
new Date(nextMonthYear, nextMonth, 20),
new Date(nextMonthYear, nextMonth, 25),
],
},
],
};
},
};
</script>
Bars indicators behave exactly the same as dot indicators. They just appear as bars spread evenly across the bottom of day cells.
html<template>
<v-calendar
:attributes='attributes'
is-double-paned>
</v-calendar>
</template>
<script>
import { getExampleMonthComps } from '@/utils/helpers';
export default {
data() {
const { thisMonth, thisMonthYear, nextMonth, nextMonthYear } = getExampleMonthComps();
return {
attributes: [
{
bar: {
backgroundColor: '#ff4d4d', // Red
},
dates: [
new Date(thisMonthYear, thisMonth, 1),
new Date(thisMonthYear, thisMonth, 10),
new Date(thisMonthYear, thisMonth, 22),
new Date(nextMonthYear, nextMonth, 6),
new Date(nextMonthYear, nextMonth, 16),
],
},
{
bar: {
backgroundColor: '#398fac', // Turquoise
},
dates: [
new Date(thisMonthYear, thisMonth, 4),
new Date(thisMonthYear, thisMonth, 10),
new Date(thisMonthYear, thisMonth, 15),
new Date(nextMonthYear, nextMonth, 1),
new Date(nextMonthYear, nextMonth, 12),
{
start: new Date(nextMonthYear, nextMonth, 20),
end: new Date(nextMonthYear, nextMonth, 25),
},
],
},
{
bar: {
backgroundColor: '#794dff', // Purple
},
dates: [
new Date(thisMonthYear, thisMonth, 12),
new Date(thisMonthYear, thisMonth, 26),
new Date(thisMonthYear, thisMonth, 15),
new Date(nextMonthYear, nextMonth, 9),
new Date(nextMonthYear, nextMonth, 5),
new Date(nextMonthYear, nextMonth, 6),
new Date(nextMonthYear, nextMonth, 20),
new Date(nextMonthYear, nextMonth, 25),
],
},
],
};
},
};
</script>
In addition to being used in conjunction with other attribute objects, content styles can be used independently.
html<template>
<v-calendar
:attributes='attributes'
is-double-paned
:min-page='minPage'>
</v-calendar>
</template>
<script>
import { getExampleMonthComps } from '@/utils/helpers';
export default {
data() {
const {
thisMonth,
thisMonthYear,
nextMonth,
nextMonthYear } = getExampleMonthComps();
return {
minPage: {
month: 4,
year: 2016,
},
attributes: [
{
contentStyle: {
color: '#ff4d4d', // Red
fontWeight: 600,
fontSize: '1em',
},
dates: [
{
start: new Date(thisMonthYear, thisMonth, 1),
end: new Date(thisMonthYear, thisMonth, 4),
},
new Date(nextMonthYear, nextMonth, 6),
new Date(nextMonthYear, nextMonth, 23),
],
},
{
contentStyle: {
color: '#398fac', // Turquoise
fontWeight: 600,
fontStyle: 'italic',
},
dates: [
new Date(thisMonthYear, thisMonth, 1),
new Date(thisMonthYear, thisMonth, 10),
new Date(thisMonthYear, thisMonth, 12),
{
start: new Date(nextMonthYear, nextMonth, 22),
end: new Date(nextMonthYear, nextMonth, 26),
},
],
},
{
contentStyle: {
color: '#794dff', // Purple
fontWeight: 600,
},
dates: [
new Date(thisMonthYear, thisMonth, 14),
{
start: new Date(thisMonthYear, thisMonth, 24),
end: new Date(thisMonthYear, thisMonth, 25),
},
new Date(thisMonthYear, thisMonth, 28),
new Date(thisMonthYear, thisMonth + 1, 4),
{
start: new Date(nextMonthYear, nextMonth, 16),
end: new Date(nextMonthYear, nextMonth, 17),
},
],
},
],
};
},
};
</script>
A semantic-inspired navigation panel allows the user to jump to desired months and years when the header title is hovered (on desktop) or clicked (on mobile). One really nice additional feature with the drop-down navigation panel is that it can use any supplied attributes to display indicators for calendar month cells. The aim with this design was to help provide some additional context for users of your web applications.
Because of the flexibility built in to v-calendar, adding date picker support out of the box was pretty trivial, as v-date-picker is just a v-calendar wrapper that proxies all props and events. It merely provides some additional state and attribute definitions needed to support the following date selection modes:
Other date picker features include options for inline display, custom input slots, custom selection attribute, date formatting and more.
For the sake of brevity I won’t get into other plugin features, but be on the look out for tutorials on other topics such as custom theming and easily integrating your apps via the API. In summary, my hope is that others will find this plugin useful and get inspired to make great apps with Vue.js.
Please keep in mind that this project is still in beta and there are still plenty of things to tidy up. Having said that, if you would like to research or contribute, check the project out on github or visit the project website to view API documentation and follow along with examples.