iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 30
1
自我挑戰組

菜雞們,讓我們一起征服JS及React吧系列 第 30

React菜雞-Day30:React實戰~寫個日曆 最終篇 - 賦予日曆靈魂吧!

  • 分享至 

  • xImage
  •  
tags: 鐵人賽 React javascript nodejs

鐵人賽第30天,今天我們要將日曆的互動功能完成,不囉唆,立馬上工! /images/emoticon/emoticon08.gif

設計重點

  • Calendar是日曆最上層的元件,而子元件HeaderDateContent均有使用到moment物件

  • 因此,我們要在Calendar設一個state來存放moment,並傳給子元件,達到子元件共同存取,確保資料一致。

  • 日曆component的結構不深,其實用props傳遞即可,不過,我們還是練習一下使用useContext + useReducer的方式,設計箭頭按鈕對應month切換的部分。

  • useReducer中的state,我們設定moment.format("YYYY-MM-DD")輸出的字串值。

程式碼整理好來~

  • 將各個元件整理到各個對應的js檔案中,如下`
  • Header.js: Header component
  • WeekDay.js: WeekDay component
  • DateContent.js: DateContent component

加入timeReduce,並與Header建立連結

Calendar.js

// Calendar.js
import React, {useReducer, createContext} from "react";
import moment from "moment";
import * as Styles from "./styles";
import reducers from "./reducers"

// components
import Header from "./components/Header";
import WeekDay from "./components/WeekDay";
import DateContent from "./components/DateContent";

// context store
export const AppStore = createContext();

// Calendar component
export default function Calendar() {
  const mmt = new moment();
  const timeReducer = useReducer(reducers, mmt.format("YYYY-MM-DD"));

  return (
    <AppStore.Provider value={{timeReducer:timeReducer}}>
      <div>
        <h1>{"Calendar Demo"}</h1>
        <div className="calendar-container" style={Styles.calendarContainer}>
          <Header />
          <WeekDay />
          <DateContent />
        </div>
      </div>
    </AppStore.Provider>
  );
}

reducers.js

  • 利用console.log快速驗證,只要確定reducer連線上,就可以開始在對應的action建立對應的程式碼
// reducers.js
import moment from "moment";

// reducers func
export default function reducers(stateTime, action){
    let type = action.type; 
    const mmt = new moment(stateTime);
  
    switch(type){
      case "LAST_MONTH":
        mmt.subtract(1, "month");
        break;
  
      case "NEXT_MONTH":
        mmt.add(1, "month");
        break;

      default: 
        break
    }
  
    console.log(mmt.format("YYYY-MM-DD"));
    return mmt.format("YYYY-MM-DD");
  }

Header.js

  • 先將Header元件
// Header.js
import React, {useContext} from "react";
import * as Styles from "../styles";
import { AppStore }from "../Calendar";
import moment from "moment";

export default function Header(){
  const { timeReducer } = useContext(AppStore);
  const dateInfo = timeReducer[0];
  const mmt = new moment(dateInfo);

  const dispatch = timeReducer[1];

  return (
    <div className="header-container" style={Styles.headerContainer}>
      <span style={Styles.headerMonthYearStyle} className="month-year">{mmt.format("MMM-YYYY")}</span>
      <span style={Styles.headerButtonStyle} onClick={()=>dispatch({type:"LAST_MONTH"})}> {"<"} </span>
      <span style={Styles.headerButtonStyle} onClick={()=>dispatch({type:"NEXT_MONTH"})}> {">"} </span>
    </div>
  )
}
  • 驗證Reducer是否正常運作

將與DateContent連結

  • 整個系統使用同一個moment變數,因此,我們要將Calendarmoment state傳遞到getWeeksInMonth(mmt)

DateContent

import getWeeksInMonth from "../utils";
import * as Styles from "../styles";
import { AppStore }from "../Calendar";
import moment from "moment";

export default function DateContent(){
    const {timeReducer} =  useContext(AppStore);
    const date = timeReducer[0];
    const mmt = new moment(date);

    let weekContentList = getWeeksInMonth(mmt);
    let result = [];
    return (
      <div className="DateContainer" style={Styles.DateContainer}>
        {
          weekContentList.map((week, wIdx)=>{
            let aWeek = [];
            aWeek = week.map((day, dIdx)=>
              <span className="dateContent-day" style={Styles.dayStyle} key={`${day}-${dIdx}`}>{day===0?"":day}</span>)
            return <div className="aweek" style={Styles.aWeekStyle} key={`${week}-${wIdx}`}>{aWeek}</div>;
            
          })
        }
        {result}
      </div>
      )
  }

utils.js

  • getWeeksInMonth()要接收上層的moment,故別忘了修改utils.js的程式喔!
// utils.js
// GLobal Vars
const SEVENDAYS = 7; // 建立一個通用變數,存放一週有7天

function processWeekDays(mmt, isFirstWeek=false){
  // get first day of a week, ex: Thursday
  const totalDays = mmt.daysInMonth();
  const startDay = isFirstWeek? 
  mmt.startOf("month").day():0;

  const weekDays = Array(SEVENDAYS).fill(0);
  let isFinished = false;
  for(let d=startDay; d<SEVENDAYS; d++){
    weekDays[d] = mmt.date();
    if(mmt.date()!==totalDays)
      mmt.add(1, "day");
    else{
      isFinished=true;
      break;
    }
      
  }

  // console.log("weekDays", weekDays);
  return {weekDays, isFinished};
}

export default function getWeeksInMonth(mmt){
  const weekDayList = [];

  // first weekDays
  const {weekDays} = processWeekDays(mmt, true);
  weekDayList.push(weekDays);

  let loopStatus = false;
  // create
  while(!loopStatus){
    let result = processWeekDays(mmt);
    const {weekDays} = result;
    weekDayList.push( weekDays );

    loopStatus = result["isFinished"];
  }
  
  return weekDayList;
}

改善!調整框框的界線

  • 眼尖的朋友一定注意到,某些月份的週數會超出我們的設定,導致最後一週超出框框,我們調整一下styles.js裡的Height,讓它顯示範圍大一點
// styles.js
export const STYLES = {
  WIDTH: "280px", 
  HEIGHT: "320px",  //<--adjust 280px -> 320px 
  ONE_COLUMN_HEIGHT: "40px",

Demo

  • 看到成品,相信你的嘴角已經上揚!很棒~真的很棒!

後續

疑問待解

  • 在最上面的設計重點,大家可能會疑問,為何不直接給予moment物件當作初始值即可?而要透過轉換成為字串後,再當作state的值。
  • 經過我測試,這樣的設計會導致component無法更新,詳細的原因我會持續探究,屆時再更新給朋友們! 如果你有答案,也別忘了指點一下喔!

可優化的部分:

  • 動態的調整height,來因應日期的變化!是個不錯的方法,想練習的朋友可別以挑戰看看!!

我抵達終點啦!

/images/emoticon/emoticon62.gif

  • 終於! 完賽了!

  • 30天的堅持,證明了自己鐵人的意志,滿滿的肺腑之言我會找時間po上,假日先讓我好好的與家人相聚,因為家人是我陪伴完賽的強力後盾,特別是老婆,這30天感謝有你們的支持,愛你們了!

  • 另外,我也要好好謝謝我的好馬吉Andy Lu,感謝他揪我一起參加鐵人賽,我們都撐過來了,完成新的里程碑!Rock~~!

  • 最後,照慣例要來個鼓勵,你的堅持與勇氣,讓你散發鑽石般耀眼的光芒!加油!繼續朝前端之神邁進!!


上一篇
React菜雞-Day29:React實戰~寫個日曆 part2 - 來搞定日曆的外觀吧!
系列文
菜雞們,讓我們一起征服JS及React吧30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
Andy 安迪
iT邦新手 4 級 ‧ 2020-10-03 17:26:19

恭喜完賽~~~/images/emoticon/emoticon08.gif

我要留言

立即登入留言