iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 8
0
Modern Web

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

仿Trello - 建立新增Todo介面

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

接著要來模仿Trello建立新Todo的方式,大致有以下步驟:

  1. 每個List最底部有一個 +New 按鈕
  2. 點擊 +New 按鈕顯示新增Todo輸入框
  3. 新增Todo輸入框:
    1. 取代點擊的+New按鈕位置
    2. 顯示的同時focus在輸入框上
    3. 有著基本高度
    4. 當輸入的字串超出基本高度範圍時,高度隨字串長度變化
  4. 點擊Enter或新增Todo輸入框外部分時執行新增Todo,並關閉輸入框
  5. 若輸入空字串則不執行新增Todo

+New 按鈕

把按鈕加在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>
  );
}

新增Todo輸入框

輸入框預計會有許多事件,先作為獨立的部件加入

// 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>
  );
}

點擊顯示新增Todo輸入框

新增一個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按鈕的話就會切換顯示輸入框。

自動focus輸入框

我們希望當輸入框出現的時候,就出現輸入游標在框中,
這邊搭配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相關的動作,下一篇製作新增資料的功能。

References:


上一篇
仿Trello - 建立基礎 List 部件
下一篇
仿Trello - 建立新增Todo功能
系列文
React + GraphQL 全端練習筆記30

尚未有邦友留言

立即登入留言