iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 10
0
Modern Web

React + GraphQL 全端練習筆記系列 第 10

仿Trello - 建立編輯Todo介面與以下省略

本系列文以製作專案為主軸,紀錄小弟學習React以及GrahQL的過程。主要是記下重點步驟以及我覺得需要記憶的部分,有覺得不明確的地方還請留言多多指教。

編輯功能:

  1. 滑鼠移到Todo上時顯示編輯圖示
  2. 點擊編輯圖示跳出編輯輸入框
  3. 跳出輸入框的同時全選文字
  4. 點擊Enter或編輯Todo輸入框外部分時執行編輯Todo,並關閉輸入框

加入Font Awesome

Trello的編輯圖示是支筆,我們從Font Awsome上找類似的圖示來替代。

安裝指令:

npm i --save @fortawesome/fontawesome-svg-core
npm install --save @fortawesome/free-solid-svg-icons
npm install --save @fortawesome/react-fontawesome

Font Awesome提供了完整的React Component套件用於載入icon,使用方法:

import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' //React component
import { faCoffee } from '@fortawesome/free-solid-svg-icons' //載入需要的icon

const element = <FontAwesomeIcon icon={faCoffee} /> //JSX表達式

建立編輯按鈕

首先要在Todo上加入帶有筆圖案的按鈕,並且只有當滑鼠在該Todo上時顯示。

加上帶icon的按鈕:

// Todo.jsx
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faPencilAlt } from "@fortawesome/free-solid-svg-icons";

export default function Todo({name}) {
  
  ...
  
  return (
    <div className="todo text-wrap my-1 p-2 rounded">
      {name}
      <Button className="edit-button m-1" size="sm">   //加入按鈕
        <FontAwesomeIcon icon={faPencilAlt} />
      </Button>
    </div>
  );
}

要帶入的圖案名稱可以從以往的class命名方式推導出,像是 fa-pencil-alt 變成 camel case 的 faPencilAlt,不過也可以藉 VS Code的自動完成功能查詢:

接著要利用state來切換顯示:

// Todo.jsx
export default function Todo({name}) {
    const [isOver, setIsOver] = useState(false);
    
    //滑鼠進入Todo時
    function handleOnOver() {
        setIsOver(true);
    }

    //滑鼠離開Todo時
    function handleOnLeave() {
        setIsOver(false);
    }
    
      return (
        <div
          className="todo text-wrap my-1 p-2 rounded"
          onMouseEnter={handleOnOver}  //添加事件
          onMouseLeave={handleOnLeave}
        >
          {name}
          {isOver && (  //使用isOver切替顯示
            <Button className="edit-button m-1" size="sm" onClick={handelClickEdit}>
              <FontAwesomeIcon icon={faPencilAlt} />
            </Button>
          )}
        </div>
  );

}

建立編輯輸入框

看一下Trello的編輯輸入框:

顯示的時候不影響其他組件的排列,同時間只會有一個輸入框,當輸入框開啟時KanBan上其他部分(像是edit按鈕)是不能點的,且點擊輸入框組件以外的部分就會關閉輸入框。

所以這部分我覺得不像NewTodo一樣每個List配一個,而是配在KanBan下,當顯示的時候以半透明層覆蓋整個KanBan,並以相對位置顯示輸入框會比較好。

這麼做可以在半透明層加入點擊事件,觸發輸入框的關閉,而不是用onBlur的方式,不然像是點儲存按鈕也會關閉輸入框。

不過這樣就有個問題,輸入框的位置應該要覆蓋在對應的Todo上,這個位置要怎麼取得?

答案是利用 ref ,取得dom後使用dom的getBoundingClientRect()方法,就能得到 top跟left的數據。

首先在KanBan建立State:

//KanBan.jsx
const [editState, updateEditState] = useState({
show: false,  //顯示狀態
dimensions: { top: 0, left: 0, width: 0 },  //顯示位置
value: "", //編輯的值
});

帶入Edit組件的位置要在KanBan的最末,以確保顯示在最上面

//KanBan.jsx
 return (
    <span>
      <KanBanNav />
      <div className="board p-1">
        {lists.map(...)}
        {editState.show && <Edit editState={editState}></Edit>}//帶入Edit組件
      </div>
    </span>
  );

Edit組件:

//Edit.jsx
import React from "react";
import { Form } from "react-bootstrap";

export default function Edit({ editState }) {
  const styles = {
    position: "relative",
    margin: 0,
    ...editState.dimensions, //展開dimensions中的 top,left,width
  };

  return (
    <Form className="edit-form"> //這部分是半透明層
      <Form.Control style={styles} as="textarea" rows="3" /> //以inline style帶入相對位置顯示的輸入框
    </Form>
  );
}

回到Todo建立觸發顯示輸入框的事件,首先要建立ref方便取得Todo的相對位置:

// Todo.jsx
export default function Todo({name}) {
    
    const targetRef = useRef(null); //建立ref物件

    return (
    <div
      ...
      ref={targetRef}  //綁定ref
    >
      {name}
      {isOver && (...)}
    </div>
     );

}

傳遞updateEditState給Todo,建立更新editState的方法:

// Todo.jsx
export default function Todo({name,updateEditState}) {
    
    ...

    //點擊edit事件
    function handelClickEdit(e) {
        //取得Todo的相對位置、寬度
        const { top, left, width } = targetRef.current.getBoundingClientRect();

        updateEditState({
          show: true,  //顯示Edit
          dimensions: { //更新Edit位置、寬度
            top: top,
            left: left,
            width: width,
          },
          value: name //提供Edit目前Todo的值
        });
    }
    
    ...
    
    return (
        <div ... ref={targetRef} >//目標ref  
          {name}
          {isOver && (
            <Button className="edit-button m-1" size="sm" onClick={handelClickEdit}> //加入點擊事件
              ...
            </Button>
          )}
        </div>
     );

}

這下就能顯示編輯輸入框了。

關閉輸入框

接著到Edit製作自動focus,輸入Enter或點及外部時關閉輸入框等動作,這部分跟NewTodo有重複性就不詳細解說,看結果:

export default function Edit({ editState, updateEditState }) {
  const editRef = useRef(null);
  const styles = {
    position: "relative",
    margin: 0,
    ...editState.dimensions,
  };

  // 自動focus
  useEffect(() => {
    editRef.current.focus();
  }, []);

  // 關閉輸入框
  function toggleEditShow() {
    updateEditState({
      ...editState,
      show: false,
    });
  }

  return (
    <Form className="edit-form" onClick={toggleEditShow}>
      <div style={styles}>
        <Form.Control
          ref={editRef}
          as="textarea"
          rows="3"
          onClick={(e) => {
            e.stopPropagation();
          }}
        />
      </div>
    </Form>
  );
}

雖說重複性高不過有兩點可以注意:

  1. 輸入框(textarea)加了一個點擊事件,呼叫 e.stopPropagation(),防止觸發上層的點擊事件toggleEditShow導致關閉輸入框。
  2. 複習: 用useState建立物件型state的話,update該state時要將所有的key都包含在一起更新,不能只更新目標key,否則其他key會遺失,因為useState的更新方法是將整個state覆蓋。
    一般做法是利用展開運算子確保不會遺漏key:
updateEditState({
  ...editState,
  show: false,
});

到這邊建立好編輯相關介面。

接下來製作編輯Todo,然後List的CRUD的部分基本做法都差不多,沒什麼新的重點,
就直接略過,可以自己試著寫寫看,或是利用這個brancch:

React Trello Clone 前端布局

完成Todo跟List的CRUD的功能後,我想接著加入Redux,做為學習,不過在那之前會先插播一篇Sass的筆記,這之前的過程都沒特別提style的設定,就在下篇統整一些Sass的用法。

References:


上一篇
仿Trello - 建立新增Todo功能
下一篇
SASS/SCSS 簡介
系列文
React + GraphQL 全端練習筆記30

尚未有邦友留言

立即登入留言