Touch your hand and write a calendar with React (with online demonstration and source code)

Posted by itpvision on Mon, 24 Jan 2022 08:42:58 +0100

Design and implement a simple version of the calendar. It supports defining the discharge order of the calendar, starting with the day of the week. As shown below:

  • First look at the effect: https://rodchen-king.github.io/react-calendar/components/calendar

  • Source code: https://github.com/rodchen-king/react-calendar

Design (with the most commonly used calendar by month)

In fact, everyone is very familiar with the calendar. All designs are based on functions, which is fundamental. The functions of the calendar are divided into two parts.

  • Calendar header: current year / month.

  • Calendar entity: the specific date information of the current month.

  • Number of lines in the calendar body: the calendar we see now is basically 6 lines, because the maximum number of days in a month is 31. It is assumed that the first day of the current month is the last day of the last week of the previous month. If there are five rows of data, only 29 days are displayed, which is why six rows of data are displayed.

Function point

  • The calendar initial rendering date is the current month

  • When the head slides left and right, the calendar data needs to display the information of the corresponding month

  • You can set the weekly data of the calendar to start with week *, Sunday or Monday according to the call.

Core issues

How to get the year and month of the current date

// Calender/lib/utils.ts
/**
 * Get calendar header content in the format: * * * * year * * month
 * @param {*} date
 */
export const getHeaderContent = function (date: Date) {
  let _date = new Date(date);

  return dateFormat(_date, 'yyyy year MM month');
};

How to obtain 42 pieces of data (6 * 7) to be displayed in the current month? What are these 42 pieces of data?

The core of this question is: what is the first day of the 42 data displayed in the current month?

The solution to this problem also starts from the above design. When referring to the number of lines of the calendar theme, it says "assuming that the first day of the current month is the last day of the last week of the previous month", then the first data of the content displayed by 42 pieces of data should also be based on the day of the week in which the first day of the current month is located.

Example: February 1, 2019
The first day of February is Friday, so the first day of the current monthly calendar is

var date = new Date()
date.setDate(date.getDate() - date.getDay() + 1) //Get the first day of the current month as January 28, 2019

What's the problem here?

The above code logic assumes that the calendar is arranged at the beginning of the week (if your calendar also puts Sunday on the first day of the calendar, it's no problem, but in China, it puts Sunday on the last day), which means that the previous implementation also needs to consider the placement order of the calendar, because the calendar is based on the ordinary Monday to Sunday, From Sunday to Monday, the first day of the calendar of the month we get is different. Therefore, the above code also depends on the discharge order of the calendar.

The discharge order here will be the first parameter of the calendar component that can be controlled by the caller. My assumption here is to match the passed in value of this parameter with date Getday() matches.

  • 0: Sunday

  • 1: Monday

  • .....

  • 5: Friday

  • 6: Saturday

So the above formula is:

date.setDate(date.getDate() - date.getDay() + x)

However, if the date after adding the x value here is greater than the first day of the current month, the current date value needs to be subtracted by 7 days. There is no need to explain this reason.

/**
 * Gets the first day of the current month calendar
 * @param {*} date
 */
export const getFirstDayOfCalendar = function (
  date: Date,
  weekLabelIndex: number,
) {
  let _date = new Date(date);
  _date = new Date(
    _date.setDate(_date.getDate() - _date.getDay() + weekLabelIndex),
  );
  //If the current date is greater than the first day of the current month, you need to subtract 7 days
  if (_date > date) {
    _date = new Date(_date.setDate(_date.getDate() - 7));
  }

  return _date;
};

Next, it's easy to do. Just add 1 to the current date to get the next day's date each time.
 

How to set the left and right switching months

The above designs take today as the initial value. How to design the initial value of left-right switching?

The first reaction is to add or subtract 1 from the month of the current date. This is not possible, because if today is the 31st, when the next month is only 30, you will click next month and switch directly for two months. Not to mention February, a month with variable days. So here's another problem.

My solution is: when you click the switch in the month, the initial calculated value is designed as the first day of the current month.

/**
 * Get the first day of the next month based on the passed in parameters
 * @param {*} firstDayOfCurrentMonth
 */
export const getFirstDayOfNextMonth = function (firstDayOfCurrentMonth: Date) {
  return new Date(
    firstDayOfCurrentMonth.getFullYear(),
    firstDayOfCurrentMonth.getMonth() + 1,
    1,
  );
};

/**
 * Obtain the first day of the previous month based on the passed in parameters
 * @param {*} firstDayOfCurrentMonth
 */
export const getFirstDayOfPrevMonth = function (firstDayOfCurrentMonth: Date) {
  return new Date(
    firstDayOfCurrentMonth.getFullYear(),
    firstDayOfCurrentMonth.getMonth() - 1,
    1,
  );
};

Switch the month data transfer mode from left to right (observer mode)

For the calendar component itself, the header and body belong to the same parent component at the same level. Data transmission can depend on the parent component. Here I use the observer mode.

/*
 * Subject
 * Three methods are created internally and an ObserverList is maintained internally.
 */

export class Subject {
  private _observers = new ObserverList();

  //addObserver: call the add method of the internally maintained ObserverList
  public addObserver(observer: Observer) {
    this._observers.add(observer);
  }

  //removeObserver: call the removeat method of the internally maintained ObserverList
  public removeObserver(observer: Observer) {
    this._observers.removeAt(this._observers.indexOf(observer, 0));
  }

  // Notify: notification function, which is used to notify the observer and execute the update function. Update is a method to implement the interface and a trigger method for notification.
  public notify(context: any) {
    let observerCount = this._observers.count();
    for (let i = 0; i < observerCount; i++) {
      (<Observer>this._observers.get(i)).update(context);
    }
  }
}

/*
 * ObserverList
 * An array is internally maintained and four methods are used for array operations. The relevant contents here still belong to the subject, because the existence of the ObserverList is to separate the subject from the internally maintained observers and have a clear function.
 */
class ObserverList {
  private _observerList: Observer[] = [];

  public add(obj: Observer) {
    return this._observerList.push(obj);
  }

  public count() {
    return this._observerList.length;
  }

  public get(index: number) {
    if (index > -1 && index < this._observerList.length) {
      return this._observerList[index];
    }

    throw new Error(`_observerList ${index} Unknown as null`);
  }

  public indexOf(obj: Observer, startIndex: number) {
    let i = startIndex;

    while (i < this._observerList.length) {
      if (this._observerList[i] === obj) {
        return i;
      }
      i++;
    }

    return -1;
  }

  public removeAt(index: number) {
    this._observerList.splice(index, 1);
  }
}

export class Observer {
  public update: Function = () => {};
}

CalendarBody observer registration

CalendarHeader notification message


 

file structure

Calendar                   
├─ Components              
│  ├─ CalendarBody.tsx     
│  ├─ CalendarHeader.tsx   
│  ├─ calenderBody.less    
│  └─ calenderHeader.less  
├─ lib                     
│  ├─ subject.ts           
│  └─ utils.ts                         
└─ index.tsx

All code files

// index.ts
import React from 'react';
import CalendarBody from './components/CalendarBody';
import CalendarHeader from './components/CalendarHeader';
import { initObserver } from './lib/utils';
import { Subject } from './lib/subject';

export default ({ weekLabelIndex = 1 }: { weekLabelIndex: number }) => {
  let calendarObserver: Subject = initObserver();

  return (
    <div>
      <CalendarHeader observer={calendarObserver} />
      <CalendarBody
        observer={calendarObserver}
        weekLabelIndex={weekLabelIndex}
      />
    </div>
  );
};
// lib/subject.ts
export class Subject {
  private _observers = new ObserverList();

  //addObserver: call the add method of the internally maintained ObserverList
  public addObserver(observer: Observer) {
    this._observers.add(observer);
  }

  //removeObserver: call the removeat method of the internally maintained ObserverList
  public removeObserver(observer: Observer) {
    this._observers.removeAt(this._observers.indexOf(observer, 0));
  }

  // Notify: notification function, which is used to notify the observer and execute the update function. Update is a method to implement the interface and a trigger method for notification.
  public notify(context: any) {
    let observerCount = this._observers.count();
    for (let i = 0; i < observerCount; i++) {
      (<Observer>this._observers.get(i)).update(context);
    }
  }
}

/*
 * ObserverList
 * An array is internally maintained and four methods are used for array operations. The relevant contents here still belong to the subject, because the existence of the ObserverList is to separate the subject from the internally maintained observers and have a clear function.
 */
class ObserverList {
  private _observerList: Observer[] = [];

  public add(obj: Observer) {
    return this._observerList.push(obj);
  }

  public count() {
    return this._observerList.length;
  }

  public get(index: number) {
    if (index > -1 && index < this._observerList.length) {
      return this._observerList[index];
    }

    throw new Error(`_observerList ${index} Unknown as null`);
  }

  public indexOf(obj: Observer, startIndex: number) {
    let i = startIndex;

    while (i < this._observerList.length) {
      if (this._observerList[i] === obj) {
        return i;
      }
      i++;
    }

    return -1;
  }

  public removeAt(index: number) {
    this._observerList.splice(index, 1);
  }
}

export class Observer {
  public update: Function = () => {};
}

// lib/utils.ts
import { Subject } from './Subject';

let transfer = function (this: any, fmt: string) {
  let o: {
    [k: string]: string | number;
  } = {
    'M+': this.getMonth() + 1, //Month
    'd+': this.getDate(), //Day
    'h+': this.getHours(), //Hours
    'm+': this.getMinutes(), //Cent
    's+': this.getSeconds(), //Second
    'q+': Math.floor((this.getMonth() + 3) / 3), //Quarter
    S: this.getMilliseconds(), //Millisecond
  };

  if (/(y+)/.test(fmt)) {
    fmt = fmt.replace(
      RegExp.$1,
      (this.getFullYear() + '').substr(4 - RegExp.$1.length),
    );
  }
  for (let k in o) {
    if (new RegExp('(' + k + ')').test(fmt)) {
      fmt = fmt.replace(
        RegExp.$1,
        RegExp.$1.length === 1
          ? o[k] + ''
          : ('00' + o[k]).substr(('' + o[k]).length),
      );
    }
  }

  return fmt;
};

/**
 * Format for format date
 * @param {*} timeSpan
 * @param {*} fmt
 * @param {*} formatDateNullValue
 */
export const dateFormat = function (
  timeSpan: Date,
  fmt: string,
  formatDateNullValue?: string,
) {
  if (!timeSpan) {
    if (formatDateNullValue) {
      return formatDateNullValue;
    }
    return 'nothing';
  }

  let date = new Date(timeSpan);

  return transfer.call(date, fmt);
};

/**
 * Get calendar header content in the format: * * * * year * * month
 * @param {*} date
 */
export const getHeaderContent = function (date: Date) {
  let _date = new Date(date);

  return dateFormat(_date, 'yyyy year MM month');
};

/**
 * Gets the first day of the current month
 * @param {*} date
 */
export const getFirstDayOfMonth = function (date: Date) {
  let _date = new Date(date);
  _date.setDate(1);

  return _date;
};

/**
 * Gets the first day of the current month calendar
 * @param {*} date
 */
export const getFirstDayOfCalendar = function (
  date: Date,
  weekLabelIndex: number,
) {
  let _date = new Date(date);
  _date = new Date(
    _date.setDate(_date.getDate() - _date.getDay() + weekLabelIndex),
  );
  //If the current date is greater than the first day of the current month, you need to subtract 7 days
  if (_date > date) {
    _date = new Date(_date.setDate(_date.getDate() - 7));
  }

  return _date;
};

/**
 * Confirm the order of weeklabel s according to the passed in index
 * @param {*} weekIndexOfFirstWeekDay
 */
export const getWeekLabelList = function (weekIndexOfFirstWeekDay: number) {
  let weekLabelArray: string[] = [
    'Sunday',
    'Monday',
    'Tuesday',
    'Wednesday',
    'Thursday',
    'Friday',
    'Saturday',
  ];

  for (let index = 0; index < weekIndexOfFirstWeekDay; index++) {
    let weekLabel = weekLabelArray.shift() || '';
    weekLabelArray.push(weekLabel);
  }

  return weekLabelArray;
};

/**
 * Start observer mode and initialize
 */
export const initObserver = function () {
  let subject = new Subject();

  return subject;
};

/**
 * The format date is two words. For example, the format of '1' is' 01 '
 * @param {*} dateNumber
 */
export const formatDayWithTwoWords = function (dateNumber: number) {
  if (dateNumber < 10) {
    return '0' + dateNumber;
  }

  return dateNumber;
};

/**
 * Compare whether the current date is the date of this month for highlighting the data of this month
 * @param {*} firstDayOfMonth
 * @param {*} date
 */
export const isCurrentMonth = function (firstDayOfMonth: Date, date: Date) {
  return firstDayOfMonth.getMonth() === date.getMonth();
};

/**
 * Compare whether the current date is the current system date
 * @param {*} date
 */
export const isCurrentDay = function (date: Date) {
  let _date = new Date();
  return (
    date.getFullYear() === _date.getFullYear() &&
    date.getMonth() === _date.getMonth() &&
    date.getDate() === _date.getDate()
  );
};

/**
 * Get the first day of the next month based on the passed in parameters
 * @param {*} firstDayOfCurrentMonth
 */
export const getFirstDayOfNextMonth = function (firstDayOfCurrentMonth: Date) {
  return new Date(
    firstDayOfCurrentMonth.getFullYear(),
    firstDayOfCurrentMonth.getMonth() + 1,
    1,
  );
};

/**
 * Obtain the first day of the previous month based on the passed in parameters
 * @param {*} firstDayOfCurrentMonth
 */
export const getFirstDayOfPrevMonth = function (firstDayOfCurrentMonth: Date) {
  return new Date(
    firstDayOfCurrentMonth.getFullYear(),
    firstDayOfCurrentMonth.getMonth() - 1,
    1,
  );
};

// Components/CalendarHeader.tsx
import React, { useEffect, useCallback, useState } from 'react';
import { Subject } from '../lib/subject';
import {
  getHeaderContent,
  getFirstDayOfMonth,
  getFirstDayOfNextMonth,
  getFirstDayOfPrevMonth,
} from '../lib/utils';
import './calenderHeader.less';

export default ({ observer }: { observer: Subject }) => {
  //Page binding data
  const [headerContent, setHeaderContent] = useState<string>('');
  const [firstDayOfMonth, setFirstDayOfMonth] = useState<Date>(new Date());

  let leftArrow = '<';
  let rightArrow = '>';

  useEffect(() => {
    setHeaderContent(getHeaderContent(new Date()));
    setFirstDayOfMonth(new Date());
  }, []);

  /**
   * The subject publishes information and notifies the observer
   */
  const observerNotify = (currentFirstDayOfMonth: Date) => {
    setHeaderContent(getHeaderContent(currentFirstDayOfMonth));
    observer.notify(currentFirstDayOfMonth);
  };

  /**
   * Page operation
   */
  const goPrev = () => {
    const preFirstDayOfMonth = getFirstDayOfPrevMonth(firstDayOfMonth);
    setFirstDayOfMonth(preFirstDayOfMonth);
    observerNotify(preFirstDayOfMonth);
  };

  const goNext = () => {
    const nextFirstDayOfMonth = getFirstDayOfNextMonth(firstDayOfMonth);

    setFirstDayOfMonth(nextFirstDayOfMonth);
    observerNotify(nextFirstDayOfMonth);
  };

  return (
    <div className="calendar-header">
      <div className="header-center">
        <span className="prev-month" onClick={goPrev}>
          {leftArrow}
        </span>
        <span className="title">{headerContent}</span>
        <span className="next-month" onClick={goNext}>
          {rightArrow}
        </span>
      </div>
    </div>
  );
};

// Components/CalendarBody.tsx
import React, { useEffect, useCallback, useState } from 'react';
import { Subject } from '../lib/subject';
import {
  getFirstDayOfMonth,
  getFirstDayOfCalendar,
  formatDayWithTwoWords,
  isCurrentMonth,
  isCurrentDay,
  getWeekLabelList,
} from '../lib/utils';
import './calenderBody.less';

interface DayItem {
  date: Date;
  monthDay: number | string;
  isCurrentMonth: boolean;
  isCurrentDay: boolean;
}

export default ({
  observer,
  weekLabelIndex = 1,
}: {
  observer: Subject;
  weekLabelIndex?: number;
}) => {
  const [firstDayOfMonth, setFirstDayOfMonth] = useState(new Date());
  const [weekList, setWeekList] = useState<DayItem[][]>([]);
  const [weekLabelArray, setWeekLabelArray] = useState<string[]>([]);

  useEffect(() => {
    //Register observer object
    observer.addObserver({
      update: update,
    });

    //Set the first day of the current month, which is used for initial data session and judging whether the date is the current month
    setFirstDayOfMonth(getFirstDayOfMonth(new Date()));

    //Set weekly label identification data
    setWeekLabelArray(getWeekLabelList(weekLabelIndex));

    //Initial setting of current month calendar data
    setWeekListValue(getFirstDayOfMonth(new Date()));
  }, []);

  /**
   * Calendar method
   */
  //Click calendar
  const onClickDay = (dayItem: DayItem) => {
    // this.$emit('dayClick', dayItem)
  };

  //Set weekList value
  const setWeekListValue = (firstDayOfmonth: Date) => {
    let newWeekList = [];
    let dayOfCalendar = getFirstDayOfCalendar(firstDayOfmonth, weekLabelIndex);

    //The number of traversal layers is 6, because the calendar displays 6 rows of current month data
    for (let weekIndex = 0; weekIndex < 6; weekIndex++) {
      let weekItem = [];
      //7 days per week
      for (let dayIndex = 0; dayIndex < 7; dayIndex++) {
        let dayItem: DayItem = {
          date: dayOfCalendar,
          monthDay: formatDayWithTwoWords(dayOfCalendar.getDate()),
          isCurrentMonth: isCurrentMonth(firstDayOfMonth, dayOfCalendar),
          isCurrentDay: isCurrentDay(dayOfCalendar),
        };
        weekItem.push(dayItem);

        //Add 1 to the current date, and so on to get 42 records
        dayOfCalendar.setDate(dayOfCalendar.getDate() + 1);
      }

      newWeekList.push(weekItem);

      setWeekList(newWeekList);
    }
  };

  /**
   * Observer mode correlation method
   */
  //Switch months to update body data
  const update = (content: Date) => {
    setFirstDayOfMonth(content);
    setWeekListValue(content);
  };

  /**
   * Tool method
   */
  //Saturday / Sunday logo red font
  const isShowRedColorForWeekLable = (index: number) => {
    return (
      index + weekLabelIndex === 6 ||
      index + weekLabelIndex === 7 ||
      (index === 0 && weekLabelIndex === 0)
    );
  };

  return (
    <div className="calendar-body">
      {/* <!-- Calendar week label ID -- >*/}
      <div className="calendar-body-week-label">
        {weekLabelArray.map((item, index) => (
          <div
            className={`calendar-body-week-label-day ${
              isShowRedColorForWeekLable(index) ? 'red-font' : ''
            }`}
          >
            <span>{item}</span>
          </div>
        ))}
      </div>
      {/* <!-- Calendar data, traverse the calendar binary array to get the data of each week -- >*/}
      {weekList.map((weekItem: DayItem[]) => (
        <div className="calendar-body-week">
          {/* <!-- Traverse weekly data -- >*/}
          {weekItem.map((dayItem: DayItem, index: number) => (
            <div
              className={`calendar-body-week-day ${
                dayItem.isCurrentMonth ? 'calendar-body-current-month' : ''
              } ${dayItem.isCurrentDay ? 'calendar-body-current-day' : ''} ${
                isShowRedColorForWeekLable(index) ? 'red-font' : ''
              }`}
              onClick={() => onClickDay(dayItem)}
            >
              <span>{dayItem.monthDay}</span>
            </div>
          ))}
        </div>
      ))}
    </div>
  );
};

Turn from https://mp.weixin.qq.com/s/eddOOYGmxvWCFELj3w_LLA

Topics: Front-end React