0.0.2 • Published 2 years ago

vue-use-calendar v0.0.2

Weekly downloads
-
License
MIT
Repository
github
Last release
2 years ago

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 <<<

Install it

# npm
npm install vue-use-calendar

# yarn
yarn add vue-use-calendar

Basic example

// 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

nametypeoptionaldefaultdescription
startOnstring \| DatetrueundefinedThe date to initiate the calendar on
minDatestring \| DatetrueundefinedThe minimum selectionable date. All dates before will be disabled
maxDatestring \| DatetrueundefinedThe maximum selectionable date. All dates after will be disabled
disabledArray\<string \| Date>true[]A date or array of date to disable
firstDayOfWeek0 \| 1 \| 2 \| 3 \| 4 \| 5 \| 6true0Tells on which day the week starts. 0 being Sunday.
localedate-fns's LocaletrueundefinedThe locale object to use for translating weekdays.Import like import { fr } from 'date-fns/locale';. See date-fns
preSelectionArray\<Date> \| Datetrue[]A date or array of date to be preselected on calendar generation
factory(date: ICalendarDate) => \<extends ICalendarDate\>trueundefinedA custom factory function to extend the default ICalendarDate objects. See exemple(TODO example link)

Outputs

The composable returns the following sub-composables

namedescription
useMonthlyCalendarGenerate a calendar where days are grouped by months
useWeeklyCalendarGenerate a calendar where days are grouped by weeks
useWeekdaysGets the list of weekdays, translated and formatted

use-monthly-calendar

const { useMontlyCalendar } = useCalendar()

const { currentMonth } = useMonthlyCalendar({ infinite: false, fullWeeks: false })

Parameters

nametypeoptionaldefaultdescription
infinitebooleantruetrueIndicates if navigating throught the calendar should generate new months on the fly
fullWeeksbooleantruetrueIndicates if each month should display the "other month" days to complete each week

Outputs

nametypedescription
daysComputedRef<Array<C>>An array of all days currently generated in the calendar. Excludes copied days so the array is sorted by day without duplicates
selectedDatesArray<Date>Reactive array of current dates selection.
listenersListeners<C>Object of methods for changing dates states (see ##listeners)
currentMonthAndYearShallowReactive<{ month: number; year: number }>;Reactive object containing the current month and year displayed. Can be mutated.
currentMonthComputedRef<Month<C>>;The current month displayed. Contains data about this month and the array of days it includes.
monthsShallowReactive<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
nextMonthEnabledComputedRef<boolean>;A computed boolean that indicates if it is allowed to go to the next month
prevMonthEnabledComputedRef<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

nametypeoptionaldefaultdescription
infinitebooleantruetrueIndicates if navigating throught the calendar should generate new weeks on the fly

Outputs

nametypedescription
daysComputedRef<Array<C>>An array of all days currently generated in the calendar. Excludes copied days so the array is sorted by day without duplicates
selectedDatesArray<Date>Reactive array of current dates selection.
listenersListeners<C>Object of methods for changing dates states (see ##listeners).
weeksArray<Week<C>>;A reactive array of all generated weeks in the calendar.
currentWeekIndexRef<number>;The current week index displayed.
currentWeekComputedRef<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
nextWeekEnabledComputedRef<boolean>;A computed boolean that indicates if it is allowed to go to the next week
prevWeekEnabledComputedRef<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

nametypeoptionaldefaultdescription
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

namereactivedescription
falseThe 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.

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.

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.

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.

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.

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

PropertyTypeDescription
dateDateThe real javascript Date object
isTodaybooleanTrue if is the same day as today
isWeekendbooleanTrue if is saturday or sunday
otherMonthbooleanTrue if the date is included
disabledRef<boolean>Reactive boolean if the date is disabled
isSelectedRef<boolean>Reactive boolean if the date is selected
isBetweenRef<boolean>Reactive boolean if the date is between two selected dates
isHoveredRef<boolean>Reactive boolean if the date is currently hovered
monthYearIndexnumberThe 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.
_copiedbooleanTrue 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: 1. Fork the repository on github 2. Clone your fork (or update your remote target branch to your fork) 3. Open a branch, name it according the changes you are planning on doing. 4. 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 and monthYearIndex attribute of the default CalendarDate object. Should be a custom one for month-calendar only.