本系列文以製作專案為主軸,紀錄小弟學習React以及GrahQL的過程。主要是記下重點步驟以及我覺得需要記憶的部分,有覺得不明確的地方還請留言多多指教。
編輯功能:
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>
);
}
雖說重複性高不過有兩點可以注意:
updateEditState({
...editState,
show: false,
});
到這邊建立好編輯相關介面。
接下來製作編輯Todo,然後List的CRUD的部分基本做法都差不多,沒什麼新的重點,
就直接略過,可以自己試著寫寫看,或是利用這個brancch:
完成Todo跟List的CRUD的功能後,我想接著加入Redux,做為學習,不過在那之前會先插播一篇Sass的筆記,這之前的過程都沒特別提style的設定,就在下篇統整一些Sass的用法。