iT邦幫忙

2022 iThome 鐵人賽

DAY 23
0
Modern Web

React Hook 不求人,建立自己的 Hook Libary系列 第 23

[DAY 23] 自己的 Hook 自己做! useDates 兩個時間恰恰好

  • 分享至 

  • xImage
  •  

承繼上篇,useDate 變兩個就成了 useDates :)

DEMO 在這裡

情境

除了用一個 useDate 之外,有時候需要兩個關聯的時間(區間)來提供進階的搜尋,這個就稍微麻煩了一些,例如:前後的時間不能交錯、不能選過去的時間、或不能選大於幾個月的時間...等等

外加今天比較晚開工,只好先粗暴一點了

功能

useDate 既有功能之外,再加上一些限制:

  • 不可選未來時間
  • 不可選過去時間
  • 時間不能交錯(第一個不能大於第二個)
  • 限制最短間距
  • 限制最長間距

開始!

date-fns

這次有用到

  • add
  • sub
  • format
  • isFuture
  • isPast
  • isAfter
  • intervalToDuration

由於有兩個日期,我們改存 array [new Date(), new Date()],程式碼通通給他 map 下去 (#`д´)ノ

原本的 Hook 稍加修改

function datesReducer(state, action) {
  const type = action.type
  const payload = action.payload
  const configs = action.configs

  switch (type.toUpperCase()) {
    case "BACKWARD": {
      return state.map((date) =>
        sub(date, { [configs.transitionUnit]: Math.abs(payload || 1) })
      )
    }
    case "FORWARD": {
      return state.map((date) =>
        add(date, { [configs.transitionUnit]: Math.abs(payload || 1) })
      )
    }
    case "NOW": {
      return [new Date(), new Date()]
    }
    case "FIRST_TO": {
      return [new Date(payload), state[1]]
    }
    case "SECOND_TO": {
      return [state[0], new Date(payload)]
    }
    default: {
      return state
    }
  }
}

function useDates(configs) {
  const mergedConfigs = merge(defaultConfigs, configs)

  const [dates, dispatch] = useReducer(
    datesReducer,
    mergedConfigs.defaultDate,
    (defaultDate) =>
      defaultDate
        ? [new Date(defaultDate), new Date(defaultDate)]
        : [new Date(), new Date()]
  )

  const datesDispatch = useCallback(
    (type, payload) => dispatch({ type, payload, configs: mergedConfigs }),
    [configs]
  )

  const offsetDates = dates.map((date) =>
    offset(date, mergedConfigs.offsetUnit, mergedConfigs.offsetValue)
  )

  const formatDate = offsetDates.map((date) =>
    format(date, mergedConfigs.formatPattern)
  )

  return [formatDate, datesDispatch]
}

新的 config

const defaultConfigs = {
  defaultDate: null,
  formatPattern: "yyyy-MM-dd",
  transitionUnit: "days",
  offsetUnit: "days",
  offsetValue: 0,
  /* NEW */
  durationUnit: "days",
  durationDateOnIssue: 0, // 1 || 2  || 0
  minDuration: 0,
  maxDuration: 0,
  noPastDate: false,
  noFutureDate: false,
  noGreaterThenSecondDate: false,
}

Duration

來控制兩個日期的區間

  • durationUnit: 區間單位,預設為天
  • durationDateOnIssue: 當區間不符合時,要校正回來的日期,0 不進行任何變更(回傳修改前的時間),1 就是校正第一個(index 0),2 就是第二個(ind
  • minDuration: 最短區間
  • maxDuration: 最大區間

Limitations

  • noPastDate: 不能回到過去
  • noFutureDate: 也不能穿梭未來
  • noGreaterThenSecondDate: 更不能讓時間交疊 (? (意即第一天比第二天晚)

流程

由於多了很多內容,我們把 reducer 修改為這個流程

  1. dateChanger: 先進行日期的修正
  2. datesDurationChecker: 再進行區間的判斷與校正
  3. datesLimitationChecker: 再進行限制上的判斷
  4. offset/format: 最後在進行微調與樣式調整 (這個則是放在hook裡面)

dateChanger

就是原本的 dateReducer 改名為 dateChanger

dateDurationChecker

當有設定 (minDuration || maxDuration)durationDateOnIssue 為 1 or 2 時,這邊會進行日期的校正,超過太多,就扣會來;日期不過,就加回來,最後回傳調整完的日期。

durationDateOnIssue 則是指定要校正的日期是第一個還是第二個,可以再進一步透過durationUnit 來設定要判斷以及校正的單位。

durationDateOnIssue 為 0,且不符合任一 min/max 時,會 fallback,回傳修改前的日期。

function datesDurationChecker(prevDates, changedDates, action) {
  const configs = action.configs
  const durationDateOnIssue = configs.durationDateOnIssue
  const minDuration = configs.minDuration
  const maxDuration = configs.maxDuration
  const durationUnit = configs.durationUnit

  const duration = intervalToDuration({
    start: changedDates[0],
    end: changedDates[1],
  })

  const isLesser = duration[durationUnit] < minDuration
  const lesserDelta = minDuration - duration[durationUnit]
  const isLarger = duration[durationUnit] > maxDuration
  const largerDelta = duration[durationUnit] - maxDuration

  if (minDuration > 0 && isLesser) {
    if (!durationDateOnIssue) return prevDates

    return changedDates.map((date, index) => {
      if (index + 1 === durationDateOnIssue) {
        return add(date, { [durationUnit]: lesserDelta })
      }
      return date
    })
  }

  if (maxDuration > 0 && isLarger) {
    if (!durationDateOnIssue) return prevDates

    return changedDates.map((date, index) => {
      if (index + 1 === durationDateOnIssue) {
        return sub(date, { [durationUnit]: largerDelta })
      }
      return date
    })
  }

  return changedDates
}

datesLimitationChecker

這個就比較單純,判斷有沒有通過各種限制,沒有就沒有,有就有,白紙黑字(??

  • 沒通過回傳 false
  • 全通過回傳 true
function datesLimitationChecker(dates, action) {
  const configs = action.configs

  if (configs.noPastDate && dates.some(isPast)) {
    return false
  }

  if (configs.noFutureDate && dates.some(isFuture)) {
    return false
  }

  if (configs.noGreaterThenSecondDate && isAfter(dates[0], dates[1])) {
    return false
  }

  return true
}

reducer

按照流程稍微編排一下,並最後透過 datesLimitationChecker 的 boolean 進一步判斷要回傳新的日期或是舊的日期。

function datesReducer(state, action) {
  const changedDate = datesChanger(state, action)
  const fixedDate = datesDurationChecker(state, changedDate, action)
  return datesLimitationChecker(fixedDate, action) ? fixedDate : state
}

這樣就完成啦!

DEMO 在這裡

結語

useDate 以及 useDates 都在 date-fns 加持下都讓我們很方便操控時間,也能專心處理邏輯。

由於可以設定的限制邊多,功能上可以新增 error message 進一步提醒使用者哪裡不對,提升一些 UX。

或者,安裝別人寫好的 date picker,可選擇可惜不在同一隊的查理狐狐刻了 mui-date-picker,應該也是不錯的選擇啦 XDD


上一篇
[DAY 22] 自己的 Hook 自己做! useDate 來操控日期吧!
下一篇
[DAY 24] 自己的Hook自己做!useImage 來預覽圖片吧!
系列文
React Hook 不求人,建立自己的 Hook Libary30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言