用React hooks寫了一個日曆組件,來看看?

前言

在最近的項目中,大量的嘗試了react hooks,咱們的組件庫用的是Next,除了一個地方由於要使用Form + Field的組合,因此使用了class組件,通過了這個項目,也算是總結一些使用的經驗,因此準備本身封裝一個日曆組件來分享一下。如下也會經過git中的commit記錄,來分析個人思路。react

下來看下效果(由於軟件的緣由,轉的gif居然如此抖動,順便求一個mac上轉換gif比較好用的軟件)git

同時奉上代碼地址: 倉庫github

初始化項目

只是使用了create-react-app進行了項目的搭建,而後整體就是hooks + typescript,固然寫的不夠好,就沒有往npm上發的意願。typescript

工具方法,保存狀態

首先是寫了一個工具方法,做用是將時間戳轉化成 年月日 三個屬性。npm

interface Res {
    year: number;
    month: number;
    day: number;
}

export const getYearMonthDay = (value: number):Res => {
    const date = new Date(value);
    return {
        year: date.getFullYear(),
        month: date.getMonth(),
        day: date.getDay()
    }    
} 
複製代碼

而後使用useState,在input的dom上綁定了value,很簡單。數組

const Test:React.FC<TestProps> = memo(({value = Date.now(), onChange}) => {
  console.log("render -------");
  const [date, setDate] = useState<Date>(new Date(value));
  const { year, month, day } = getYearMonthDay(date.getTime());
  console.log(year, month, day);
  return (
    <div> <input type="text" value={`${year} - ${month} - ${day}`} /> </div> ) }); 複製代碼

日曆遍歷的思路

這一次的提交主要是肯定了日曆組件,怎麼寫,具體思路看下面的代碼,儘可能經過註釋把思路講的清楚一些。app

// 如下兩個數組只是爲了遍歷
const ary7 = new Array(7).fill("");
const ary6 = new Array(6).fill("");

const Test: React.FC<TestProps> = memo(({ value = Date.now(), onChange }) => {
  const [date, setDate] = useState<Date>(new Date(value));
  // useState保存下面彈窗收起/展開的狀態
  const [contentVisible, setContentVisible] = useState<boolean>(true);
  const { year, month, day } = getYearMonthDay(date.getTime());
  // 獲取當前選中月份的第一天
  const currentMonthFirstDay = new Date(year, month, 1);
  // 判斷出這一天是星期幾
  const dayOfCurrentMonthFirstDay = currentMonthFirstDay.getDay();
  // 而後當前日曆的第一天就應該是月份第一天向前移幾天
  const startDay = new Date(currentMonthFirstDay.getTime() - dayOfCurrentMonthFirstDay * 1000 * 60 * 60 * 24);
  const dates:Date[] = [];
  // 生成一個長度爲42的數組,裏面記錄了從第一天開始的每個date對象
  for (let index = 0; index < 42; index++) {
    dates.push(new Date(startDay.getTime() + 1000 * 60 * 60 * 24 * index));
  }
  return (
    <div> <input type="text" value={`${year} - ${month} - ${day}`} /> { contentVisible && ( <div> <div> <span>&lt;</span> <span>&lt; &lt;</span> <span></span> <span>&gt;</span> <span>&gt; &gt;</span> </div> <div> // 生成的日曆應該是7*6的,而後遍歷出42個span, 這就是以前生成的兩個常量數組的做用 { ary6.map((_, index) => { return ( <div> { ary7.map((__, idx) => { const num = index * 7 + idx; console.log(num); const curDate = dates[num] return ( <span>{curDate.getDate()}</span> ) }) } </div> ) }) } </div> </div> ) } </div> ) }); 複製代碼

處理document點擊事件

const Test: React.FC<TestProps> = memo(({ value = Date.now(), onChange }) => {
  // 使用useRef保存最外一層包裹的div
  const wrapper = useRef<HTMLDivElement>(null);
  // 展開收起的方法,都使用了useCallback,傳入空數組,讓每次渲染都返回相同的函數
  const openContent = useCallback(
    () => setContentVisible(true),
    []
  );
  const closeContent = useCallback(
    () => setContentVisible(false),
    []
  );
  const windowClickhandler = useCallback(
    (ev: MouseEvent) => {
      let target = ev.target as HTMLElement;
      if(wrapper.current && wrapper.current.contains(target)) {
      } else {
        closeContent();
      }
    },
    []
  )
  // 使用useEffect模擬componentDidMount和componentWillUnmount的生命週期函數,來綁定事件
  useEffect(
    () => {
      window.addEventListener("click",windowClickhandler);
      return () => {
        window.removeEventListener('click', windowClickhandler);
      }
    },
    []
  )
  return (
    <div ref={wrapper}> // 以前的那些東西,沒有變化 </div>
  )
});
複製代碼

處理每一個日期的點擊事件

// 使用setDate,處理,這裏其實第二個參數傳遞一個空數組便可,由於這個函數是不依賴當前的date狀態來變化的。
  const dateClickHandler = useCallback(
    (date:Date) => {
      setDate(date);
      const { year, month, day } = getYearMonthDay(date.getTime());
      onChange(`${year}-${month}-${day}`);
      setContentVisible(false);
    },
    [date]
  )

<span onClick={() => dateClickHandler(curDate)}>{curDate.getDate()}</span>

複製代碼

支持value傳遞

// 先判斷如下value是否傳遞,若是沒傳默認就是當前的時間
const DatePicker: React.FC<TestProps> = ({ value = "", onChange = () => { } }) => {
  let initialDate: Date;
  if (value) {
    let [year, month, day] = value.split('-');
    initialDate = new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
  } else {
    initialDate = new Date();
  }
  const [date, setDate] = useState<Date>(initialDate);
 }
複製代碼

月份切換的事件處理

月份處理就是當前月份的第一天向前移動一個月或者一個月等等。dom

const DatePicker: React.FC<TestProps> = ({ value = "", onChange = () => { } }) => {
  // 以前這裏的currentMonthFirstDay是直接由date得出的,如今成爲組件的state就可讓他支持變化。
  const { year, month, day } = getYearMonthDay(date.getTime());
  const [currentMonthFirstDay, setCurrentMonthFirstDay] = useState<Date>(new Date(year, month, 1));
  const { year: chosedYear, month: chosedMonth } = getYearMonthDay(currentMonthFirstDay.getTime());
  const dayOfCurrentMonthFirstDay = currentMonthFirstDay.getDay();
  const startDay = new Date(currentMonthFirstDay.getTime() - dayOfCurrentMonthFirstDay * 1000 * 60 * 60 * 24);
  const dates: Date[] = [];
  for (let index = 0; index < 42; index++) {
    dates.push(new Date(startDay.getTime() + 1000 * 60 * 60 * 24 * index));
  }

  const openContent = useCallback(
    () => setContentVisible(true),
    []
  );
  const closeContent = useCallback(
    () => setContentVisible(false),
    []
  );
  const windowClickhandler = useCallback(
    (ev: MouseEvent) => {
      let target = ev.target as HTMLElement;
      if (wrapper.current && wrapper.current.contains(target)) {
      } else {
        closeContent();
      }
    },
    []
  );
  const dateClickHandler = useCallback(
    (date: Date) => {
      const { year, month, day } = getYearMonthDay(date.getTime());
      onChange(`${year}-${month + 1}-${day}`);
      setContentVisible(false);
    },
    [date]
  );
  // 這裏全部的月份切換事件都選擇了使用了函數式更新
  const prevMonthHandler = useCallback(
    () => {
      setCurrentMonthFirstDay(value => {
        let { year, month } = getYearMonthDay(value.getTime());
        if (month === 0) {
          month = 11;
          year--;
        } else {
          month--;
        }
        return new Date(year, month, 1)
      })
    },
    []
  );
  const nextMonthHandler = useCallback(
    () => {
      setCurrentMonthFirstDay(value => {
        let { year, month } = getYearMonthDay(value.getTime());
        if (month === 11) {
          month = 0;
          year++;
        } else {
          month++;
        };
        return new Date(year, month, 1);
      })
    },
    []
  );
  const prevYearhandler = useCallback(
    () => {
      setCurrentMonthFirstDay(value => {
        let { year, month } = getYearMonthDay(value.getTime());
        return new Date(--year, month, 1)
      })
    },
    []
  );
  const nextYearHandler = useCallback(
    () => {
      setCurrentMonthFirstDay(value => {
        let { year, month } = getYearMonthDay(value.getTime());
        return new Date(++year, month, 1)
      })
    },
    []
  )
  return (
    <div ref={wrapper}> <input type="text" value={`${year} - ${month + 1} - ${day}`} onFocus={openContent} /> { contentVisible && ( <div className="content"> <div className="header"> <span onClick={prevYearhandler}>&lt; &lt;</span> <span onClick={prevMonthHandler}>&lt;</span> <span>{`${chosedYear} - ${chosedMonth + 1}`}</span> <span onClick={nextMonthHandler}>&gt;</span> <span onClick={nextYearHandler}>&gt; &gt;</span> </div> </div> ) } </div> ) }; 複製代碼

處理porps變化

工具方法函數

export const getDateFromString = (str: string):Date => {
    let [year, month, day] = str.split('-');
    return new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
}
複製代碼

組件工具

const DatePicker: React.FC<TestProps> = ({ value = "", onChange = () => { } }) => {
  let initialDate: Date;
  if (value) {
    initialDate = getDateFromString(value);
  } else {
    initialDate = new Date();
  };
  
  const [date, setDate] = useState<Date>(initialDate);
  
  // 使用了useRef來保存上一次渲染時傳遞的value值,
  const prevValue = useRef<string>(value);

  useEffect(
    () => {
      // 僅當value值變化且不一樣與上一次值時,纔會從新進行改變自身date狀態。
      if (prevValue.current !== value) {
        let newDate = value ? getDateFromString(value) : new Date();
        setDate(newDate);
        const { year, month } = getYearMonthDay(newDate.getTime());
        setCurrentMonthFirstDay(new Date(year, month, 1))
      }
    },
    [value]
  )

  return (
    ...
  )
};
複製代碼

這裏如今想來其實也能夠用useMemo來處理傳遞進來的value值,這也是一種思路。稍後實現一下。

最後

最後貼下代碼github地址

相關文章
相關標籤/搜索