本系列文以製作專案為主軸,紀錄小弟學習React以及GrahQL的過程。主要是記下重點步驟以及我覺得需要記憶的部分,有覺得不明確的地方還請留言多多指教。
接著要來模仿Trello建立新Todo的方式,大致有以下步驟:
把按鈕加在Todo列表下方
// List.jsx
export default function List({ title, todos }) {
return (
<div className="list p-2 m-1 rounded-lg">
<div className="title">{title}</div>
{todos.map((todo) => (
<Todo key={todo.name} {...todo} />
))}
<div className="footer pt-2 d-flex">
<Button className="py-1 flex-grow-1 text-left">+ New</Button>
</div>
</div>
);
}
輸入框預計會有許多事件,先作為獨立的部件加入
// NewTodo.jsx
import React from "react";
import { Form } from "react-bootstrap";
export default function NewTodo() {
return (
<Form>
<Form.Control as="textarea" />
</Form>
);
}
然後因為在點擊 +New按鈕前不顯示輸入框,所以在List上加一個showNew的state控制NewTodo的顯示。
// List.jsx
export default function List({ title, todos }) {
const [showNew, updateShowNew] = useState(false); //預設不顯示NewTodo
return (
<div className="list p-2 m-1 rounded-lg">
<div className="title">{title}</div>
{todos.map((todo) => (
<Todo key={todo.name} {...todo} />
))}
<NewTodo /> //NewTodo接在List中
<div className="footer pt-2 d-flex">
<Button className="py-1 flex-grow-1 text-left">+ New</Button>
</div>
</div>
);
}
新增一個toggleShowNew的fucntion讓showNew在true,false間切替。
// List.jsx
export default function List({ title, todos}) {
const [showNew, updateShowNew] = useState(false);
//切換NewTodo顯示
function toggleShowNew(e) {
updateShowNew(!showNew);
}
return (
<div className="list p-2 m-1 rounded-lg">
<div className="title">{title}</div>
{todos.map((todo) => (
<Todo key={todo.name} {...todo} />
))}
//切換"新增Todo輸入框"跟"+New按鈕"顯示
{showNew ? (
<NewTodo/>
) : (
<div className="footer d-flex">
<Button
className="py-1 flex-grow-1 text-left"
onClick={toggleShowNew} //註冊click事件
>
+ New
</Button>
</div>
)}
</div>
);
}
JSX的部分用inline if else的方式切換顯示"新增Todo輸入框"跟"+New按鈕",複習一下格式:
{ [條件判斷式] ? [true時回傳的JSX]:[false時回傳的JSX]}
目前為止的畫面:
點擊+New按鈕的話就會切換顯示輸入框。
我們希望當輸入框出現的時候,就出現輸入游標在框中,
這邊搭配useEffect,useRef兩個hook來達成。
//NewTodo.jsx
export default function NewTodo() {
const newTodoRef = useRef(null);
useEffect(() => {
newTodoRef.current.focus();
}, []);
return (
<Form>
<Form.Control as="textarea" ref={newTodoRef} onBlur={toggleShowNew} />
</Form>
);
}
先從之前沒介紹到的useRef說明。
先說ref這個屬性在class component上用於存取dom,跟document.getElementById等抓取dom element的功能有一樣的作用,他會將抓到的dom存在current當中。
而存取到dom就能做一些操作,像是focus,或是全選文字等等。
原本這是專屬class compoent的功能,不過Hooks推出後,useRef()這個hook也賦予了function component一樣的功能。
像上面的範例,首先宣告ref:
const newTodoRef = useRef(null);
然後綁到要參照的dom上頭:
<Form.Control as="textarea" ref={newTodoRef} onBlur={toggleShowNew} />
這樣就可以用 newTodoRef.current取得dom。
然後我們用useEffect,在部件被mount之後對輸入框的dom做focus:
useEffect(() => {
newTodoRef.current.focus();
}, []);
複習: useEffect基本會在每一次render的時候被執行,不過當依賴值鎮列為空時,就會變成只在部件第一次被render(相當於mount)後執行一次。
現在確定了只要輸入框顯示就會focus在上面,接著要設定當不在focus輸入框,或是打字按Enter時會關閉輸入框。
要切換輸入框的顯示要更新List裡的showNew,更新的function已經有了,就是toggleShowNew,所以把toggleShowNew傳給NewTodo使用。
//List.jsx
<NewTodo toggleShowNew={toggleShowNew}/>
然後在目標的兩個事件中執行toggleShowNew:
//NewTodo.jsx
<Form>
<Form.Control
as="textarea"
style={textareaStyle}
ref={newTodoRef}
onBlur={toggleShowNew} //不再Focus輸入框時
onKeyDown={(e) => { //輸入ENTER時
if (e.key === "Enter") {
toggleShowNew();
}
}}
/>
</Form>
完成關閉輸入框的事件。
目前在輸入框裡輸入超長字串的話會出現滾動條:
應該要讓輸入框的高度跟著字串變化,不顯示滾動條。
首先,我們要讓輸入框有個動態的高度。
定義一個掌管高度的state,用在inline style物件上,然後把這個style綁定到目標上:
//NewTodo.jsx
export default function NewTodo({ toggleShowNew }) {
const [autoHeight, updateAutoHeight] = useState(75);//定義高度state
const textareaStyle = { //inline style 物件
height: `${autoHeight}px`,
};
...
return (
<Form>
<Form.Control
as="textarea"
style={textareaStyle} //inline style
ref={newTodoRef}
onBlur={toggleShowNew}
/>
</Form>
);
}
然後在每一次輸入的時候更新高度值,而目標的高度可以用dom的scrollHeight取得:
//NewTodo.jsx
export default function NewTodo({ toggleShowNew }) {
...
function autoResize(e) {
//利用ref取得輸入框的scrollHeight
updateAutoHeight(newTodoRef.current.scrollHeight);
}
return (
<Form>
<Form.Control
as="textarea"
style={textareaStyle}
ref={newTodoRef}
onBlur={toggleShowNew}
onInput={autoResize} //每次輸入就更新高度
/>
</Form>
);
}
這下高度就會跟著變化,不會再出現滾動條了。
到這邊先完成UI相關的動作,下一篇製作新增資料的功能。