Build flexible react date components using primitives ⚛️ + 📅fns
You want a date component that's:
✔️ Laid out the way you want
✔️ Functions the way you want
✔️ Flexible for your use case
Kalendaryo is a React component that provides the toolsets for you to build calendar components that works for your use cases. It has no layout or functionalities other than the ones you can think of to build through its API.
The component uses the render props pattern which helps in exposing various variables for you to use and also gives you the flexibility to build your calendar's layout anyway you want due to its inline rendering nature.
See the Basic Usage section to see how you can build a basic calendar component using Kalendaryo or see the Examples section to see more examples built with Kalendaryo.
This package expects you to have >= [email protected]
, >= prop-types@15
, and [email protected]
Once you're done installing these peer dependencies, install the package like so:
npm i -d kalendaryo // <-- for npm peeps
yarn add kalendaryo // <-- for yarn peeps
After doing all of those, hopefully everything should work and be ready for use 👍
// Step 1: Import the component
import Kalendaryo from 'kalendaryo'
// Step 2: Invoke and pass your desired calendar as a function in the render prop
const BasicCalendar = () => <Kalendaryo render={MyCalendar} />
// Step 3: Build your calendar!
function MyCalendar(kalendaryo) {
const {
getFormattedDate,
getWeeksInMonth,
getDatePrevMonth,
getDateNextMonth,
setSelectedDate,
setDate
} = kalendaryo
const currentDate = getFormattedDate("MMMM YYYY")
const weeksInCurrentMonth = getWeeksInMonth()
const setDateNextMonth = () => setDate(getDateNextMonth())
const setDatePrevMonth = () => setDate(getDatePrevMonth())
const selectDay = date => () => setSelectedDate(date)
/* For this basic example we're going to build a calendar that has:
* 1. A header where you have:
* 1.1 Controls for moving to the previous/next month of the current date
* 1.2 A label for current month & year of the current date
* 2. A body where you have:
* 2.1 A row for the label of the days of a week
* 2.2 Rows containing the days of each week in the current date's month where you can:
* 2.2.1 Select a date by clicking on a day
*/
return (
<div className="my-calendar">
// (1)
<div className="my-calendar-header">
// (1.1)
<button onClick={setDatePrevMonth}>←</button>
// (1.2)
<span className="text-white">{currentDate}</span>
// (1.1)
<button onClick={setDateNextMonth}>→</button>
</div>
// (2)
<div className="my-calendar-body">
// (2.1)
<div className="week day-labels">
<div className="day">Sun</div>
<div className="day">Mon</div>
<div className="day">Tue</div>
<div className="day">Wed</div>
<div className="day">Thu</div>
<div className="day">Fri</div>
<div className="day">Sat</div>
</div>
// (2.2)
{weeksInCurrentMonth.map((week, i) => (
<div className="week" key={i}>
{week.map(day => (
<div
key={day.label}
// (2.2.1)
onClick={selectDay(day.dateValue)}
>
{day.label}
</div>
))}
</div>
))}
</div>
</div>
)
}
See this basic usage snippet in action here!
This section contains descriptions of the various things the <Kalendaryo />
component has to offer which are split into three parts:
state
: Description of the component's state that could changeprops
: Description of the component's props that you can change or hook intomethods
: Description of the component's various helper methods you can use from the render prop
type: Date
Is the state for the current date the component is in. By convention, you should only change this when the calendar you're building changes it's current date, i.e. moving to and from a month or year on the calendar. Defaults to today's date if startCurrentDateAt prop is not set.
type: Date
Is the state for the selected date on the component. By convention, you should only change this when the calendar you're building receives a date selection input from the user, i.e. selecting a day on the calendar. Defaults to today's date if startSelectedDateAt prop is not set.
type: Date required: false default: new Date()
Modifies the initial value of #date
. Great for when you want your calendar to boot up in some date other than today.
Passing non-Date
types to this prop sets the #date
state to today.
const birthday = new Date(1988, 4, 27)
<Kalendaryo startCurrentDateAt={birthday} />
type: Date required: false default: new Date()
Modifies the initial value of #selectedDate
. Great for when you want your calendar's selected date to boot up in some other date than today.
Passing non-Date
types to this prop sets the #selectedDate
state to today.
const birthday = new Date(1988, 4, 27)
<Kalendaryo startSelectedDateAt={birthday} />
type: String required: false default: 'MM/DD/YY'
Modifies the default format value on the #getFormattedDate
method. Accepts any format that date-fns' format
function can support.
const myFormat = 'yyyy-mm-dd'
<Kalendaryo defaultFormat={myFormat} />
type: Number required: false default: 0
Modifies the starting day index of the weeks returned from the #getWeeksInMonth
. Defaults to 0 (sunday)
const monday = 1
<Kalendaryo startWeekAt={monday} />
type: func(state: Object): void required: false
Callback for listening to state changes on the #date
& #selectedDate
states.
const logState = (state) => console.log(state)
<Kalendaryo onChange={logState}/>
type: func(date: Date): void required: false
Callback for listening to state changes only to the #date
state.
const logDateState = (date) => console.log(date)
<Kalendaryo onDateChange={logDateState} />
type: func(date: Date): void required: false
Callback for listening to state changes only to the #selectedDate
state.
const logSelectedDateState = (selectedDate) => console.log(selectedDate)
<Kalendaryo onSelectedChange={logSelectedDateState} />
type: func(kalendaryo: Object): void required: true
Callback for rendering your date component. This function receives an object which has <Kalendaryo />
's state
, methods
, as well as props you pass that are invalid(see passing variables to the render prop for more information).
const MyCalendar = (kalendaryo) => {
console.log(kalendaryo)
return <p>Some layout</p>
}
<Kalendaryo render={MyCalendar} />
Sometimes you may need to have states other than the #date
and #selectedDate
state, i.e for a date range calendar component you may need to have a state for startDate
and endDate
and may need to create the calendar component as a method inside the date range calendar's class like so:
class DateRangeCalendar extends React.Component {
state = {
startDate: null,
endDate: null
}
Calendar = () => {
const { startDate, endDate } = this.state
return // Your calendar layout
}
setDateRange = (selectedDate) => {
// Logic for updating the start and end date states
}
render() {
return <Kalendaryo onSelectedChange={this.setDateRange} render={this.Calendar} />
}
}
This approach however, leaves the Calendar
render callback tightly coupled to the DateRangeCalendar
component and bloats it with an unnecessary method for rendering UI.
An approach I decided to go for and highly suggest is to pass any variables you want to pass through the Kalendaryo
component's prop, any invalid or unknown props that Kalendaryo
isn't concerned about gets passed to the #render
callback's object parameter, this makes the render
callback more pure and simple!
class DateRangeCalendar extends React.Component {
state = {
startDate: null,
endDate: null
}
setDateRange = (selectedDate) => {
// Logic for updating the start and end date states
}
render() {
return (
<Kalendaryo
startDate={this.state.startDate}
endDate={this.state.endDate}
onSelectedChange={this.setDateRange}
render={Calendar}
/>
)
}
}
// Pure and simple!
function Calendar(kalendaryo) {
const { startDate, endDate } = kalendaryo
return // Your calendar component
}
You can also use this functionality to add helper functions inside the render
callback!
import { differenceInDays, isWithinRange } from 'date-fns'
const dateIsInRange = (date, startDate, endDate) => {
if (!isDate(data) || !isDate(startDate) || !isDate(endDate)) {
throw new Error('Argument is not an instance of Date')
}
return differenceInDays(startDate, endDate) < 1 && isWithinRange(date, startDate, endDate)
}
function MyCalendar(kalendaryo) {
const { dateIsInRange } = kalendaryo
return // Your calendar layout
}
<Kalendaryo dateIsInRange={dateIsInRange} render={MyCalendar} />
type: func(date?: Date | format?: String, format?: String): String
Returns the date formatted by the given format string. You can invoke this in four ways:
-
getFormattedDate()
- When no arguments are given, by default#getFormattedDate
returns the current value in the#date
state formatted as the value given in the#defaultFormat
prop -
getFormattedDate(date)
- When a date object is given in the first argument and the second argument is not given,#getFormattedDate
returns the given date object formatted as the value given in the#defaultFormat
prop -
getFormattedDate(formatString)
- When a string is given in the first argument and the second argument is not given,#getFormattedDate
returns the current value in the#date
state formatted as the value from the given string -
getFormattedDate(date, formatString)
- When the second argument is given, the first argument must be a date object, this will return the given date object formatted as the value from the given string value on the second argument
NOTE: Throws an error if an invalid argument value is passed to the function
function MyCalendar(kalendaryo) {
const birthday = new Date(1988, 4, 27)
const myFormattedDate = kalendaryo.getFormattedDate(birthday, 'yyyy-mm-dd')
return <p>My birthday is at {myFormattedDate}</p>
}
<Kalendaryo render={MyCalendar} />
type: func(date?: Date | integer?: Integer, integer?: Integer): Date
Returns a date object with months added from some given integer. You can invoke this in four ways:
-
getDateNextMonth()
- When no arguments are given, by default#getDateNextMonth
will add 1 month to the value of the#date
state -
getDateNextMonth(date)
- When a date object is given in the first argument and the second argument is not given,#getDateNextMonth
will add 1 month by default to the given date object -
getDateNextMonth(integer)
- When an integer is given in the first argument and the second argument is not given,#getDateNextMonth
will add a month to the value of the#date
state by the specified integer value -
getDateNextMonth(date, integer)
- When the second argument is given, the first value must be a date object, this will return the given date object from the first argument that has a month added from the specified integer value on the second argument
NOTE: Throws an error if an invalid argument value is passed to the function
function MyCalendar(kalendaryo) {
const nextMonth = kalendaryo.getDateNextMonth()
const nextMonthFormatted = kalendaryo.getFormattedDate(nextMonth, 'MMMM')
return <p>The next month from today is: {nextMonthFormatted}</p>
}
<Kalendaryo render={MyCalendar} />
type: func(date?: Date | integer?: Integer, integer?: Integer): Date
Returns a date object with months subtracted from some given integer. You can invoke this in four ways:
-
getDatePrevMonth()
- When no arguments are given, by default#getDatePrevMonth
will subtract 1 month to the value of the#date
state -
getDatePrevMonth(date)
- When a date object is given in the first argument and the second argument is not given,#getDatePrevMonth
will subtract 1 month by default to the given date object -
getDatePrevMonth(integer)
- When an integer is given in the first argument and the second argument is not given,#getDatePrevMonth
will subtract a month to the value of the#date
state by the specified integer value -
getDatePrevMonth(date, integer)
- When the second argument is given, the first value must be a date object, this will return the given date object from the first argument that has a month subtracted from the specified integer value on the second argument
NOTE: Throws an error if an invalid argument value is passed to the function
function MyCalendar(kalendaryo) {
const prevMonth = kalendaryo.getDatePrevMonth()
const prevMonthFormatted = kalendaryo.getFormattedDate(prevMonth, 'MMMM')
return <p>The previous month from today is: {prevMonthFormatted}</p>
}
<Kalendaryo render={MyCalendar} />
type: func(date?: Date): Array: { label: Integer, dateValue: Date }
Returns an array of day objects which are objects that have data for the label
of the day as well as the dateValue
for that day. You can invoke this in two ways:
-
getDaysInMonth()
- When no argument is given, by default#getDaysInMonth
returns an array of day objects from the#date
state's value -
getDaysInMonth(date)
- When a date object is given,#getDaysInMonth
returns an array of day objects for the given date
NOTE: Throws an error if an invalid argument value is passed to the function
function MyCalendar(kalendaryo) {
const nextMonth = kalendaryo.getDateNextMonth()
const daysNextMonth = kalendaryo.getDaysInMonth(nextMonth)
return (
<div>
{daysNextMonth.map((day) => (
<p
key={day.label}
onClick={() => console.log(day.dateValue)}
>
{day.label}
</p>
))}
</div>
)
}
<Kalendaryo render={MyCalendar} />
type: func(date?: Date, startingDayIndex?: Integer): WeekArray: DayArray: { label: Integer, dateValue: Date }
Returns an array of each weeks for the month of the given date, each array of weeks contain an array of days for that week. You can invoke this in three ways:
-
getWeeksInMonth()
- Returns an array for the weeks in the month of the#date
state with the days starting at the value of#startWeekAt
prop -
getWeeksInMonth(date)
- Returns an array for the weeks in the month of the givendate
argument, with the days starting at the value of#startWeekAt
prop -
getWeeksInMonth(date, startingDayIndex)
- Returns an array for the weeks in the givendate
argument, with the days starting at the value of thestartingDayIndex
argument
NOTE: Throws an error if an invalid argument value is passed to the function
function MyCalendar(kalendaryo) {
const prevMonth = kalendaryo.getDatePrevMonth()
const weeksPrevMonth = kalendaryo.getWeeksInMonth(prevMonth, 1)
return (
<div>
{weeksPrevMonth.map((week, i) => (
<div class="week" key={i}>
{week.map((day) => (
<p
key={day.label}
onClick={() => console.log(day.dateValue)}
>
{day.label}
</p>
))}
</div>
))}
</div>
)
}
<Kalendaryo render={MyCalendar} />
type: func(date: Date): void
Updates the #date
state to the given date object
NOTE: Throws an error if an invalid argument value is passed to the function
function MyCalendar(kalendaryo) {
const birthday = new Date(1988, 4, 27)
const currentDate = kalendaryo.getFormattedDate()
const setDateToBday = () => kalendaryo.setDate(birthday)
return (
<div>
<p>The date is: {currentDate}</p>
<button onClick={setDateToBday}>Set date to my birthday</button>
</div>
)
}
<Kalendaryo render={MyCalendar} />
type: func(date: Date): void
Updates the #selectedDate
state to the given date object
NOTE: Throws an error if an invalid argument value is passed to the function
function MyCalendar(kalendaryo) {
const birthday = new Date(1988, 4, 27)
const currentDate = kalendaryo.getFormattedDate()
const selectedDate = kalendaryo.getFormattedDate(kalendaryo.selectedDate)
const selectBdayDate = () => kalendaryo.setSelectedDate(birthday)
return (
<div>
<p>The date is: {currentDate}</p>
<p>The selected date is: {selectedDate}</p>
<button onClick={selectBdayDate}>Set selected date to my birthday!</button>
</div>
)
}
<Kalendaryo render={MyCalendar} />
type: func(date: Date): void
Updates both the #date
& #selectedDate
state to the given date object
NOTE: Throws an error if an invalid argument value is passed to the function
function MyCalendar(kalendaryo) {
const birthday = new Date(1988, 4, 27)
const currentDate = kalendaryo.getFormattedDate()
const selectedDate = kalendaryo.getFormattedDate(kalendaryo.selectedDate)
const selectBday = () => kalendaryo.pickDate(birthday)
return (
<div>
<p>The date is: {currentDate}</p>
<p>The selected date is: {selectedDate}</p>
<button onClick={selectBday}>Set date and selected date to my birthday!</button>
</div>
)
}
<Kalendaryo render={MyCalendar} />
This project is heavily inspired from Downshift by Kent C. Dodds, a component library that uses render props to expose certain APIs for you to build flexible and accessible autocomplete, dropdown, combobox, etc. components.
Without it, I would not have been able to create this very first OSS project of mine, so thanks Mr. Dodds and Contributors for it! ❤️