95

GitHub - geeofree/kalendaryo: Build flexible react date components + fns

 5 years ago
source link: https://github.com/geeofree/kalendaryo
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

README.md

kalendaryo

Coverage Status Build Status npm npm license

Build flexible react date components using primitives ⚛️ + ?fns

Problem

You want a date component that's:
✔️ Laid out the way you want
✔️ Functions the way you want
✔️ Flexible for your use case

This solution

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.


Table of Contents

Installation

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 ?

Basic Usage

// 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}>&larr;</button>

        // (1.2)
        <span className="text-white">{currentDate}</span>

        // (1.1)
        <button onClick={setDateNextMonth}>&rarr;</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!

API

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 change
  • props: Description of the component's props that you can change or hook into
  • methods: Description of the component's various helper methods you can use from the render prop

State

#date

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.

#selectedDate

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 startCurrentDateAt prop is not set.

Props

#startCurrentDateAt

type: Date
required: false
default: new Date()

Modifies the initial value of #date & #selectedDate states. Great for when you want your calendar to boot up in some date other than today.

const birthday = new Date(1988, 4, 27)

<Kalendaryo startCurrentDateAt={birthday} />

#defaultFormat

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

#onChange

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

#onDateChange

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

#onSelectedChange

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

#render

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

Passing variables to the render prop

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

Methods

#getFormattedDate

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

#getDateNextMonth

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

#getDatePrevMonth

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

#getDaysInMonth

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

#getWeeksInMonth

type: func(date?: Date): 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 two ways:

  • getWeeksInMonth() - When no argument is given, by default #getWeeksInMonth returns an array of weeks for the month of the #date state's value

  • getWeeksInMonth(date) - When a date object is given, #getWeeksInMonth returns an array of weeks for the month of the given date value

NOTE: Throws an error if an invalid argument value is passed to the function

function MyCalendar(kalendaryo) {
  const prevMonth = kalendaryo.getDateNextMonth()
  const weeksPrevMonth = kalendaryo.getWeeksInMonth(prevMonth)

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

#setDate

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

#setSelectedDate

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

#pickDate

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

Examples

Inspiration

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! ❤️


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK