To-Do Calendar - Day18 實作年曆日期圈選功能

這篇要來實作原子習慣追蹤的年曆日期圈選功能,包含打圈和取消打圈。

原子習慣追蹤圈選日期功能

  • 開發流程:
    • 根據前端傳來的參數 op 判斷要執行新增或移除日期,並將結果並返回前端
    • 最後實作前端判斷圈選操作後呼叫對應的 API,然後顯示更新後返回的結果

設計 RESTful API

image

實作年曆日期圈選功能

image

原子習慣追蹤(圈選或取消年曆上的日期)


addPickedDay API

關於要使用哪個 HTTP 方法糾結了許久,考慮用 POST 是因為這功能有「新增」日期,考慮用 PUT 是因為這功能實際是「修改」原子習慣追蹤,考慮用 PATCH 是因為這功能實際是「部分修改」原子習慣追蹤,最後還是決定以對應到資料庫操作去設計,因為從 URI 來看,原子習慣追蹤才是主體資源,而這個新增的日期只是原子習慣追蹤的 pickedDays 陣列中的一個元素,為了更新一個屬性上傳整個 habitTracker Document 或上傳整個新的 pickedDays 陣列都不太好(資料傳輸量浪費 server 頻寬),所以最後採用的是 PATCH,只上傳要新增的日期,而 PATCH 請求的格式參考的是 JSON Patch

  • Request URL
    PATCH http://localhost:8080/users/{userId}/habitTrackers?habitId=<habitId>&year=<year>
    
  • Request Body
    {
      "op":"add",
      "path":"/pickedDays",
      "value":{"id": "2022-06-06", "date": "2022-06-05T16:00:00.000Z"}
    }
    
  • Response Body
    {
      "id": "6294f0123d1b551c205e3b71",
      "habitId": "habit01",
      "year": "2022",
      "pickedDays":[
        {
          "id": "2022-01-06",
          "date": "2022-01-05T16:00:00.000Z"
        },
        {
          "id": "2022-01-07",
          "date": "2022-01-06T16:00:00.000Z"
        },
        {
          "id": "2022-01-08",
          "date": "2022-01-07T16:00:00.000Z"
        },
        {
          "id": "2022-01-09",
          "date": "2022-01-08T16:00:00.000Z"
        },
        {
          "id": "2022-01-10",
          "date": "2022-01-09T16:00:00.000Z"
        },
        {
          "id": "2022-06-04",
          "date": "2022-06-03T16:00:00.000Z"
        },
        {
          "id": "2022-06-06",
          "date": "2022-06-05T16:00:00.000Z"
        }
      ],
      "createdTime": 1653405213436,
      "lastModifiedTime": 1653405213436
    }
    

removePickedDay API

和 addPickedDay API 的 Request Body 差在 op 參數的值。

  • Request URL
    PATCH http://localhost:8080/users/{userId}/habitTrackers?habitId=<habitId>&year=<year>
    
  • Request Body
    {
      "op":"remove",
      "path":"/pickedDays",
      "value":{"id": "2022-06-06", "date": "2022-06-05T16:00:00.000Z"}
    }
    
  • Response Body
    {
      "id": "6294f0123d1b551c205e3b71",
      "habitId": "habit01",
      "year": "2022",
      "pickedDays":[
        {
          "id": "2022-01-06",
          "date": "2022-01-05T16:00:00.000Z"
        },
        {
          "id": "2022-01-07",
          "date": "2022-01-06T16:00:00.000Z"
        },
        {
          "id": "2022-01-08",
          "date": "2022-01-07T16:00:00.000Z"
        },
        {
          "id": "2022-01-09",
          "date": "2022-01-08T16:00:00.000Z"
        },
        {
          "id": "2022-01-10",
          "date": "2022-01-09T16:00:00.000Z"
        },
        {
          "id": "2022-06-04",
          "date": "2022-06-03T16:00:00.000Z"
        }
      ],
      "createdTime": 1653405213436,
      "lastModifiedTime": 1653405213436
    }
    

實作 Controller 層

這次從 Controller 層開始實作

  • 在 HabitTrackerController class 新增 patchHabitTracker 方法,返回類型為 ResponseEntity<HabitTracker>
  • 在方法上加上 @PatchMapping 註解,表示前端要使用 PATCH 方法來請求 API
  • @PatchMapping 註解括號中指定 url 路徑,並使用 @RequestParam 註解去取得 url 路徑的參數
  • 使用 @RequestBody 去接住前端傳來的參數,並使用 @Valid 註解讓寫在 PatchRequest class 的驗證請求參數的註解生效
    Info
    如果 Spring Boot 版本是 2.3 之後的版本,要使用驗證請求參數的註解的話,是需要額外在 pom.xml 增加一組 dependency validation 設定。
  • 接著實作 patchHabitTracker 方法
    • 將前端傳過來的參數的值一一 set 到 habitTrackerQueryParams 的屬性
    • call habitTrackerService 的 patchHabitTracker 方法

image

pom.xml

image

PatchRequest class

image

HabitTrackerController class


實作 Service 層

  • 在 HabitTrackerService interface 宣告 patchHabitTracker 方法
  • 接著再到 HabitTrackerServiceImpl class,實作 patchHabitTracker 方法
    • 依 patchRequest 的 op 參數的值決定要 call habitTrackerDao 的哪個方法

image

HabitTrackerService interface

image

HabitTrackerServiceImpl class


實作 Dao 層

  • 在 HabitTrackerDao interface 宣告 addPickedDay 方法和 removePickedDay 方法
  • 實作之前可先在 Query Console 測試 MongoDB 原生語法
  • 接著再回到 HabitTrackerDaoImpl class,實作 addPickedDay 方法和 removePickedDay 方法
    • addPickedDay 方法中會先檢查指定的 habitTracker Document 是否存在,如果尚未存在則以查詢條件建立一個,然後才去執行更新語法(使用前才會去建立使用者的 habitTracker Document)
      image

      Query Console(圈選日期)

      image

      Query Console(取消圈選日期)

      image

      HabitTrackerDao interface

      image

      HabitTrackerDaoImpl class(addPickedDay)

      image

      HabitTrackerDaoImpl class(removePickedDay)

  • 接著運行 Spring Boot 程式,使用 API Tester 測試一下效果
    image
    image

    addPickedDay API

    image

    removePickedDay API


實作前端串接 API

  • 在 src/axios/index.js 新增 apiPickedDayAdd 方法和 apiPickedDayRemove 方法,然後再 export 出去給外面的組件 import
      export const apiPickedDayAdd = (userId, habitId, year, day) =>
        instance.patch(
          `/users/${userId}/habitTrackers?habitId=${habitId}&year=${year}`,
          {
            op: "add",
            path: "/pickedDays",
            value: {
              id: day.id,
              date: day.date,
            },
          }
        );
    
      export const apiPickedDayRemove = (userId, habitId, year, day) =>
        instance.patch(
          `/users/${userId}/habitTrackers?habitId=${habitId}&year=${year}`,
          {
            op: "remove",
            path: "/pickedDays",
            value: {
              id: day.id,
              date: day.date,
            },
          }
        );
    
  • 在 habitTracker.vue 中根據所選的日期是否已存在 pickedDays 陣列
    • 如果尚未存在則呼叫 addPickedDay API
    • 如果尚未存在則呼叫 removePickedDay API
    • 最後顯示更新後的結果
      import { apiHabitsQuery, apiHabitTrackerQuery, apiPickedDayAdd, apiPickedDayRemove } from "../../api/index.js";
      ...
      onDayClick(day) {
        let self = this;
        let idx = self.pickedDays.findIndex((d) => d.id === day.id);
        if (idx >= 0) {
          apiPickedDayRemove(self.$store.state.userId, self.pickedHabit.habitId, self.pageYear, day).then(
            (res) => {
              this.pickedDays = res.data.pickedDays;
            }
          )
        } else {
          apiPickedDayAdd(self.$store.state.userId, self.pickedHabit.habitId, self.pageYear, day).then(
            (res) => {
              this.pickedDays = res.data.pickedDays;
            }
          )
        }
      }
    

運行結果

image

年曆日期圈選功能



延伸閱讀

參考資料

comments

comments powered by Disqus