Vue use-calendar
A vue 3 composable to create any kind of calendar!
- Open source
- SSR compliant
- Fully typed with Typescript
- Full customize your calendar style without thinking about the logic
- Extendable
- Uses
date-fns
functions internally for lightweight and consistent Dates operations
This can be used to hold the dates logic for your calendar components.
>>> See the DEMO <<<
// Considering today is the 15th of March 2022
const { useMontlyCalendar } = useCalendar()
const {
nextMonth,
prevMonth,
currentMonthAndYear,
currentMonth,
selectedDates,
listeners,
} = useMonthlyCalendar({ fullWeeks: false, infinite: true });
/*
currentMonthAndYear === { year: 2022, month: 2 }
currentMonth === { month: 2, year: 2022, days: [...], index: 24266 }
selectedDates === []
*/
// Go to next month
nextMonth()
/*
currentMonthAndYear === { year: 2022, month: 3 }
currentMonth === { month: 3, year: 2022, days: [...], index: 24267 }
selectedDates === []
*/
listeners.selectSingle(currentMonth.days[3])
/*
selectedDates === [{ date: <4th March 2020>, isSelected: true, ... }]
*/
The composables
The entry point of the library is use-calendar
.
use-calendar
import { isSameDay } from 'date-fns';
import { es } from 'date-fns/locale';
const pricesByDay = [
{ price: 55, date: '2022-05-12' },
]
const { useMontlyCalendar } = useCalendar({
startOn: new Date(2025, 5, 1),
minDate: '2025-05-12',
maxDate: new Date(2025, 5, 18),
disabled: [new Date(2025, 5, 15)],
firstDayOfWeek: 1, // Monday
locale: es, // Spanish
preSelection: [new Date(2025, 5, 13)],
factory: (calendarDate) => {
// Extends the generated date by adding the associated price
const correspondingPrice = pricesByDay.find(({ date }) => isSameDay(date, calendarDate.date));
const customDate: CustomDate = {
...calendarDate,
price: correspondingPrice?.price || 0,
};
return customDate;
}
})
const { currentMonth } = useMontlyCalendar()
Parameters
name | type | optional | default | description |
---|---|---|---|---|
startOn | string | Date |
true | undefined |
The date to initiate the calendar on |
minDate | string | Date |
true | undefined |
The minimum selectionable date. All dates before will be disabled |
maxDate | string | Date |
true | undefined |
The maximum selectionable date. All dates after will be disabled |
disabled | Array\<string | Date> |
true | [] |
A date or array of date to disable |
firstDayOfWeek | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
true | 0 |
Tells on which day the week starts. 0 being Sunday. |
locale | date-fns’s Locale |
true | undefined |
The locale object to use for translating weekdays.Import like import { fr } from 'date-fns/locale'; . See date-fns |
preSelection | Array\<Date> | Date |
true | [] |
A date or array of date to be preselected on calendar generation |
factory | (date: ICalendarDate) => \<extends ICalendarDate\> |
true | undefined |
A custom factory function to extend the default ICalendarDate objects. See [exemple](TODO example link) |
Outputs
The composable returns the following sub-composables
name | description |
---|---|
useMonthlyCalendar | Generate a calendar where days are grouped by months |
useWeeklyCalendar | Generate a calendar where days are grouped by weeks |
useWeekdays | Gets the list of weekdays, translated and formatted |
use-monthly-calendar
const { useMontlyCalendar } = useCalendar()
const { currentMonth } = useMonthlyCalendar({ infinite: false, fullWeeks: false })
Parameters
name | type | optional | default | description |
---|---|---|---|---|
infinite | boolean |
true | true |
Indicates if navigating throught the calendar should generate new months on the fly |
fullWeeks | boolean |
true | true |
Indicates if each month should display the “other month” days to complete each week |
Outputs
name | type | description |
---|---|---|
days | ComputedRef<Array<C>> |
An array of all days currently generated in the calendar. Excludes copied days so the array is sorted by day without duplicates |
selectedDates | Array<Date> |
Reactive array of current dates selection. |
listeners | Listeners<C> |
Object of methods for changing dates states (see ##listeners) |
currentMonthAndYear | ShallowReactive<{ month: number; year: number }>; |
Reactive object containing the current month and year displayed. Can be mutated. |
currentMonth | ComputedRef<Month<C>>; |
The current month displayed. Contains data about this month and the array of days it includes. |
months | ShallowReactive<Month<C>[]>; |
An array of all the months generated in the calendar. |
nextMonth | () => void; |
Method to navigate to the next month |
prevMonth | () => void; |
Method to navigate to the previous month |
nextMonthEnabled | ComputedRef<boolean>; |
A computed boolean that indicates if it is allowed to go to the next month |
prevMonthEnabled | ComputedRef<boolean>; |
A computed boolean that indicates if it is allowed to go to the previous month |
use-weekly-calendar
const { useWeeklyCalendar } = useCalendar()
const { currentWeek } = useWeeklyCalendar({ infinite: false })
Parameters
name | type | optional | default | description |
---|---|---|---|---|
infinite | boolean |
true | true |
Indicates if navigating throught the calendar should generate new weeks on the fly |
Outputs
name | type | description |
---|---|---|
days | ComputedRef<Array<C>> |
An array of all days currently generated in the calendar. Excludes copied days so the array is sorted by day without duplicates |
selectedDates | Array<Date> |
Reactive array of current dates selection. |
listeners | Listeners<C> |
Object of methods for changing dates states (see ##listeners). |
weeks | Array<Week<C>>; |
A reactive array of all generated weeks in the calendar. |
currentWeekIndex | Ref<number>; |
The current week index displayed. |
currentWeek | ComputedRef<Week<C>>; |
The current week wrapper. Contains data about this week and the array of days it includes. |
nextWeek | () => void; |
Method to navigate to the next week |
prevWeek | () => void; |
Method to navigate to the previous week |
nextWeekEnabled | ComputedRef<boolean>; |
A computed boolean that indicates if it is allowed to go to the next week |
prevWeekEnabled | ComputedRef<boolean>; |
A computed boolean that indicates if it is allowed to go to the previous week |
use-weekdays
const { useWeekdays } = useCalendar()
// [Mon, Tue, Wed, ..., Sun]
const weekDays = useWeekdays('iii')
Parameters
name | type | optional | default | description |
---|---|---|---|---|
weekdayFormat | 'i' | 'io' | 'ii' | 'iii' | 'iiii' | 'iiiii' | 'iiiiii' |
true | 'iiiii' |
The format to use while translating the weekdays. See date-fns ISO day of week |
Outputs
name | reactive | description |
---|---|---|
false |
The array of week days, translated and formatted. |
Listeners
The listeners
object returned by the sub composables contains the following methods:
selectSingle
Call this method to select a single date. It will unselect all currently selected dates.
Example:
const { useMontlyCalendar } = useCalendar()
const { currentMonth, listeners: { selectSingle } } = useMonthlyCalendar()
/*
March 2022
Su Mo Tu We Th Fr Sa
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
*/
selectSingle(currentMonth.days[10])
/*
March 2022
Su Mo Tu We Th Fr Sa
1 2 3 4 5
6 7 8 9 10 [11] 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
*/
selectSingle(currentMonth.days[20])
/*
Selecting another date resets all previously selected dates
March 2022
Su Mo Tu We Th Fr Sa
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 [21] 22 23 24 25 26
27 28 29 30 31
*/
selectRange
Use this method to select a range of two dates.
Selecting a third date will remove the previous selected dates.
Selecting an already selected date will unselect it. It won’t unselect any other date, only the one in parameter.
Example:
const { useMontlyCalendar } = useCalendar()
const { currentMonth, listeners: { selectRange } } = useMonthlyCalendar()
/*
March 2022
Su Mo Tu We Th Fr Sa
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
*/
selectRange(currentMonth.days[10])
selectRange(currentMonth.days[20])
/*
The 11th and the 21st are marked as `selected`
March 2022
Su Mo Tu We Th Fr Sa
1 2 3 4 5
6 7 8 9 10 [11] 12
13 14 15 16 17 18 19
20 [21] 22 23 24 25 26
27 28 29 30 31
*/
selectRange(currentMonth.days[27])
/*
A third date is selected. All previously selected date are reset and we select the new date only (the 28th here)
March 2022
Su Mo Tu We Th Fr Sa
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 [28] 29 30 31
*/
selectRange(currentMonth.days[27])
/*
Selecting an already selected date reset its state
March 2022
Su Mo Tu We Th Fr Sa
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
*/
selectMultiple
Select multiple dates without limitation.
Selecting an already selected date will unselect it.
Example:
const { useMontlyCalendar } = useCalendar()
const { currentMonth, listeners: { selectMultiple } } = useMonthlyCalendar()
/*
March 2022
Su Mo Tu We Th Fr Sa
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
*/
selectMultiple(currentMonth.days[10])
selectMultiple(currentMonth.days[20])
/*
The 11th and the 21st are marked as `selected`
March 2022
Su Mo Tu We Th Fr Sa
1 2 3 4 5
6 7 8 9 10 [11] 12
13 14 15 16 17 18 19
20 [21] 22 23 24 25 26
27 28 29 30 31
*/
selectMultiple(currentMonth.days[27])
/*
A third date is selected. It just adds it to the selected date without wiping out the others
March 2022
Su Mo Tu We Th Fr Sa
1 2 3 4 5
6 7 8 9 10 [11] 12
13 14 15 16 17 18 19
20 [21] 22 23 24 25 26
27 [28] 29 30 31
*/
selectMultiple(currentMonth.days[27])
/*
Selecting an already selected date reset its state
March 2022
Su Mo Tu We Th Fr Sa
1 2 3 4 5
6 7 8 9 10 [11] 12
13 14 15 16 17 18 19
20 [21] 22 23 24 25 26
27 28 29 30 31
*/
hoverMultiple
Set the hover status on all dates between the first selected date and the one passed in parameter. It can be used to style the dates in between two selected dates while you hover them.
Example:
const { useMontlyCalendar } = useCalendar()
const { currentMonth, listeners: { hoverMultiple } } = useMonthlyCalendar()
/*
March 2022
Su Mo Tu We Th Fr Sa
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
*/
hoverMultiple(currentMonth.days[10])
hoverMultiple(currentMonth.days[20])
/*
All the days between the 11th and 21st are marked as `hovered`
March 2022
Su Mo Tu We Th Fr Sa
1 2 3 4 5
6 7 8 9 10 [11 12
13 14 15 16 17 18 19
20 21] 22 23 24 25 26
27 28 29 30 31
*/
resetHover
Reset the hover
state to false
for all dates.
Example:
const { useMontlyCalendar } = useCalendar()
const { currentMonth, listeners: { hoverMultiple, resetHover } } = useMonthlyCalendar()
hoverMultiple(currentMonth.days[10])
hoverMultiple(currentMonth.days[20])
/*
All the days between the 11th and 21st are marked as `hovered`
March 2022
Su Mo Tu We Th Fr Sa
1 2 3 4 5
6 7 8 9 10 [11 12
13 14 15 16 17 18 19
20 21] 22 23 24 25 26
27 28 29 30 31
*/
resetHover()
/*
All days marked as `hovered` are reset to normal state
March 2022
Su Mo Tu We Th Fr Sa
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
*/
How does it work?
Each date is represented by a custom object called CalendarDate
.
CalendarDate object
Property | Type | Description |
---|---|---|
date | Date |
The real javascript Date object |
isToday | boolean |
True if is the same day as today |
isWeekend | boolean |
True if is saturday or sunday |
otherMonth | boolean |
True if the date is included |
disabled | Ref<boolean> |
Reactive boolean if the date is disabled |
isSelected | Ref<boolean> |
Reactive boolean if the date is selected |
isBetween | Ref<boolean> |
Reactive boolean if the date is between two selected dates |
isHovered | Ref<boolean> |
Reactive boolean if the date is currently hovered |
monthYearIndex | number |
The date’s month index. See What’s the monthYear index? |
dayId | ${year}-${month}-${day} |
A string representing a uniq id for this day. In the form ${year}-${month}-${day} . Mainly used internally. |
_copied | boolean |
True if the day is a copy from another day. See ## Linked dates |
Extending the CalendarDate
objects
You can provide an optional custom function in the useCalendar
composable to add extra properties to your date objects.
// Imagine you have this kind of array somewhere in your app / components
const pricesByDay = [
{ price: 55, date: '2022-05-12' },
]
// If you want the calendar to use this object, you can provide a function to make the mapping yourself.
const { useMontlyCalendar } = useCalendar({
factory: (calendarDate) => {
// Find the price object related to the generated date
const correspondingPrice = pricesByDay.find(({ date }) => isSameDay(date, calendarDate.date));
// Creates a custom date object, with an additionnal `price` property.
const customDate: CustomDate = {
...calendarDate,
price: correspondingPrice?.price || 0,
};
// Return the new object date created from the original one + the price
return customDate;
}
})
It’s a function that takes as a parameter a CalendarDate
object generated by the composable.
Anytime the composable generates a new date, it will trigger this function.
You must return an object containing the same properties as the original, eventually with extra ones.
Careful with this function! If you don’t return the complete original object, it may break the composables. Be sure that you don’t touch the existing properties of the CalendarDate object!
What’s the monthYear index?
What we call the “monthYear” index is not a real concept. We use this index to identify every month with a number having the following properties:
- All monthYearIndex is uniq for a given month in a year.
- Two consecutives months should have a consecutive number (also true between “december year 1” and “january year 2”)
The formula to compute it is as simple as : <month index> + <year> * 12
Example:
2020/08/18 => (08 - 1) + 2020 * 12 => 24247
2020/12/25 => (12 - 1) + 2020 * 12 => 24251
2021/01/21 => (01 - 1) + 2021 * 12 => 24252
This is internally used to easily navigate thought months (and years) with an classical index as an iterator.
Linked dates
The composable use a mechanism to link two identical dates so that when the state of one changes, the other is updated too.
For instance, when using fullWeeks
prop on the use-monthly-calendar
composable, you will have duplicates dates across your months.
Month 1 will show some dates from Month 2, because they are part of its last week.
On Month 2, these dates are the first dates of the month. But it also shows the last dates from Month 1.
March 2022 April 2022
Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa
1 2 3 4 5 [27 28 29 30 31] 1 2
6 7 8 9 10 11 12 3 4 5 6 7 8 9
13 14 15 16 17 18 19 10 11 12 13 14 15 16
20 21 22 23 24 25 26 17 18 19 20 21 22 23
27 28 29 30 31 [1 2] 24 25 26 27 28 29 30
In the calendars above, the dates between squared brackets are copies from the dates in the original month. There are linked.
So when the date 30 in “March 2022” is selected, so is the [30th] in “April 2022”.
Technical note: A date linked to another is marked by the internal attribute
_copied
Contributing
Run the project
You can fork and clone the project on github.
Then install the dependencies:
yarn install
Start the example on localhost:3000:
yarn example
Submit changes
If you’re willing to participate in the development of this library, you are warmly welcome!
Here are the steps to follow:
- Fork the repository on github
- Clone your fork (or update your remote target branch to your fork)
- Open a branch, name it according the changes you are planning on doing.
- Submit a PullRequest.
I’ll review it as soon as possible! ?
Todos
- Try a LinkedList implementation to simplify the code (without class instance for SSR)
- avoid updating currentMonthAndYear if
infinite === false
- use date-fns locale’s weekStartOn by default
- weekly calendar selection
- move
otherMonth
andmonthYearIndex
attribute of the default CalendarDate object. Should be a custom one for month-calendar only.