##【仕様】
- テキスト入力欄にタスクを書き込んでエンターキーを押すと下のリストに追加される。
- 完了したタスクは、チェックボックスを『ON』にする。視覚効果として、完了済みのタスクは文字を薄くなる。
- タブによるフィルタリング機能もあり、『All』を選択すると全てのタスク、『ToDo』を選択すると未完了のタスクのみ、『Done』を選択すると完了済みのタスクのみが表示される。
- 下部には表示中のタスク件数が表示される。
##【コンポーネント設計する】
#####アプリの作成(実装)を始める前に、どうアプリを作成するのかを下記の点を踏まえ、考える。
- どのようにコンポーネントを分けるか。
- それぞれのコンポーネントはどのような属性(props)を受け取り、どのような状態(state)を管理するか。
#####※ 『フォーム』や『ボタン』『リンク』『タブ』などインタラクティブな UI 要素がある場合、『ハンドラ』が必要。 誰が『ハンドラ』を定義して、誰が実行するかを考えること。
##1. 作成する『Todoアプリ』のコンポーネントを設計する
#####① 『Todo』コンポーネントは、アプリの全体
➡︎ アプリの大元になるので、外部からの props は不要。
#####② 『Input』コンポーネントは、タスクの入力欄
➡︎ 入力欄なので、入力値を『state』として管理する。この入力値は、エンターキーが押されるまで親(Todo)が知る必要はないので、『Input』コンポーネント内で管理するが、エンターキーが押されたら、親のタスクリストを更新する必要がある。
#####③ 『Filter』コンポーネントは、 タブのフィルタリング部分
➡︎ 『Input』と同様、それぞれのタブが押された時のイベントハンドラ関数を『props』 として受け取る必要がある。そのほか、選択中のタブは見た目を変える必要がある。
#####④ 『TodoItem』 コンポーネントは、タスク一個分
➡︎ 一個分のタスク情報を親から『props』で渡してもらう必要がある。さらに、チェックボックスがあり、チェックすると、何らかの形でタスクの完了状態を更新する。『TodoItem』はタスク情報をもらうだけで、自分では管理しない。管理するのは『 Todo』で、子でイベントが発生したタイミングで、親の『state』が更新される必要があるので、『Todo』から『TodoItem』にイベントハンドラを渡すパターンが良い。言い換えるなら、『TodoItem』は、どのタスクがチェックされたか、親である『Todo』 知らせる必要がある。
#####※「知らせる」とは「イベント」のこと
###Propsとは
[Reactを基本からまとめてみた【5】【Props(プロップス)】]
(https://qiita.com/kanfutrooper/items/c0e75dc3c2ad8cbd32e5)
###Stateとは
[Reactを基本からまとめてみた【6】【State(ステート)】]
(https://qiita.com/kanfutrooper/items/340760f49fc253f612da)
##2. HTMLを作成する
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>⚛️ React ToDo</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.8.2/css/bulma.min.css" />
<style>
.container { margin-top: 2rem; }
</style>
</head>
<body>
<div id="root"></div>
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/classnames/2.2.6/index.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text.js">
// ここにコードを書いていく
</script>
</body>
</html>
###CSS フレームワーク 『Bulma』 とは
公式サイト : [Bulma]
(https://bulma.io/)
『Bulma』は、 JavaScript 無しのプレーンな CSS フレームワーク。予め用意されたコンポーネントに対応するクラス名をHTML要素に付与することでスタイリングを行う。
###React ライブラリ 『classnames』 とは
パッケージドキュメント : [classnames]
(https://www.npmjs.com/package/classnames#usage-with-reactjs)
htmlではclassを使ってスタイリングをするが、Reactが使っているjsxではclassNameを使ったスタイリングを行う。
##3. レンダリングされるルートコンポーネントとして『App』を作成する
function Todo() {
return null;
}
function App() {
return (
<div className="container is-fluid">
<Todo />
</div>
);
}
const root = document.getElementById('root');
ReactDOM.render(<App />, root);
##4. タスクを表示する機能を作成する
#####①Todo に以下の state を生成する。
タスクは複数存在するので、配列で表現する。
const [items, setItems] = React.useState([]);
この配列に入るのはデータは、タスクの文字列をそのまま配列に入れると、完了したかどうか分からないので、以下の形式で管理する。
{
key: String,
text: String,
done: Boolean
}
key は、タスクを一意に特定する ID で、本格的なアプリだとデータベースに格納した結果の ID 値などになるが、今回は以下の関数でランダムな文字列を生成する。
この関数はコードの一番上に追加する。
const getKey = () => Math.random().toString(32).substring(2);
#####② state の初期値にテストデータを入れる。
その state をループで表示する JSX コードを追加する。
function Todo() {
const [items, setItems] = React.useState([
{ key: getKey(), text: 'Learn JavaScript', done: false },
{ key: getKey(), text: 'Learn React', done: false },
{ key: getKey(), text: 'Get some good sleep', done: false },
]);
return (
<div className="panel">
<div className="panel-heading">
⚛️ React ToDo
</div>
{items.map(item => (
<label className="panel-block">
<input type="checkbox" />
{item.text}
</label>
))}
<div className="panel-block">
{items.length} items
</div>
</div>
);
}
#####③ タスクの部分を『TodoItem』コンポーネントに切り出す。
function TodoItem({ item }) {
return (
<label className="panel-block">
<input type="checkbox" />
{item.text}
</label>
);
}
TodoItem から返される JSX は以下のようになる。
ループで生成される要素には『key』が必要で、タスクに用意した『key』プロパティを利用する。
<div className="panel">
<div className="panel-heading">
⚛️ React ToDo
</div>
{items.map(item => (
<TodoItem key={item.key} item={item} />
))}
<div className="panel-block">
{items.length} items
</div>
</div>
##5.タスクの完了状態を切り替える
#####①チェックボックスで完了状態を切り替えられるようにする。
TodoItem では、チェックされた(もしくは外された)時に、ハンドラ関数 onCheck を実行する。ハンドラにはタスク情報を渡す。子から親に知らせるイメージ。
function TodoItem({ item, onCheck }) {
const handleChange = () => {
onCheck(item);
};
return (
<label className="panel-block">
<input
type="checkbox"
checked={item.done}
onChange={handleChange}
/>
{item.text}
</label>
);
}
#####②Todo にハンドラを実装する。
items から map で新しいリストを作成して setItems する。map の中では、key で同一判定をして、チェック対象の done の真偽を反転させる。
const handleCheck = checked => {
const newItems = items.map(item => {
if (item.key === checked.key) {
item.done = !item.done;
}
return item;
});
setItems(newItems);
};
#####③実装したハンドラを TodoItem の onCheck props に指定する。
{items.map(item => (
<TodoItem
key={item.key}
item={item}
onCheck={handleCheck}
/>
))}
#####④TodoItem の JSXをBulma のヘルパークラスを用い、完了済みのタスクは文字色を灰色に変化するように編集する。
<label className="panel-block">
<input
type="checkbox"
checked={item.done}
onChange={handleChange}
/>
<span
className={classNames({
'has-text-grey-light': item.done
})}
>
{item.text}
</span>
</label>
#####⑤ {item.text} に CSS クラスを適用させるために で囲った上で、classnames ライブラリを利用する。
※ IとIIは同じ意味
I
className={classNames({
'has-text-grey-light': item.done // 真偽値
})}
II
className={item.done ? 'has-text-grey-light' : ''}
######⑥TodoItem コンポーネントは完成。
チェックの切り替えと、ON 時の文字色の変化を確認する。
##6.タスクを作成する
#####① 入力欄の値を管理するだけの Input コンポーネントを作成する。
function Input() {
const [text, setText] = React.useState('');
const handleChange = e => setText(e.target.value);
return (
<div className="panel-block">
<input
class="input"
type="text"
placeholder="Enter to add"
value={text}
onChange={handleChange}
/>
</div>
);
}
#####② エンターキーを押したときのハンドラを実装する。
onKeyDown のハンドラで一旦イベントを受け取り、エンターキーであった時のみ props で受け取る想定の onAdd を実行する。引数には入力されたテキストを渡す。さらに、追加処理の後は入力欄をクリアにする。
function Input({ onAdd }) {
// 中略
const handleKeyDown = e => {
if (e.key === 'Enter') {
onAdd(text);
setText('');
}
};
return (
<div className="panel-block">
<input
class="input"
type="text"
placeholder="Enter to add"
value={text}
onChange={handleChange}
onKeyDown={handleKeyDown}
/>
</div>
);
}
#####⑤Todo に、追加処理を行うハンドラ関数を実装する。
テキストは子から渡されるので、key と done を追加して、タスクリストに追加する。
const handleAdd = text => {
setItems([...items, { key: getKey(), text, done: false }]);
};
######⑥.panel-heading の下に Input を配置する。
<Input onAdd={handleAdd} />
##7.フィルタリング機能を作成する
#####①Todo に state を追加する。
フィルタリング条件は、ALL / TODO / DONE の文字列で、今回は表現する。
const [filter, setFilter] = React.useState('ALL');
######② Filter コンポーネントを実装する。
function Filter({ value, onChange }) {
const handleClick = (key, e) => {
e.preventDefault();
onChange(key);
};
return (
<div className="panel-tabs">
<a
href="#"
onClick={handleClick.bind(null, 'ALL')}
>All</a>
<a
href="#"
onClick={handleClick.bind(null, 'TODO')}
>ToDo</a>
<a
href="#"
onClick={handleClick.bind(null, 'DONE')}
>Done</a>
</div>
);
}
###bind() メソッドとは
bind() メソッドは、呼び出された際に this キーワードに指定された値が設定される新しい関数を生成。この値は新しい関数が呼び出された時、一連の引数の前に置かれる。
#####③Todo に戻り、フィルタリング条件を更新する関数を作成する。
const handleFilterChange = value => setFilter(value);
#####④items を直接表示するのではなく、条件に応じてフィルタリングされた結果を表示する。
以下のコードを Todo に追加する。
const displayItems = items.filter(item => {
if (filter === 'ALL') return true;
if (filter === 'TODO') return !item.done;
if (filter === 'DONE') return item.done;
});
#####⑤タスク表示箇所と件数表示箇所を displayItems を使うように編集する。
displayItems.map(item => (
// 中略
))}
<div className="panel-block">
{displayItems.length} items
</div>
#####⑥タブの表示切り替えを実装する。
classnames を使用する。
<a
href="#"
onClick={handleClick.bind(null, 'ALL')}
className={classNames({ 'is-active': value === 'ALL' })}
>All</a>
<a
href="#"
onClick={handleClick.bind(null, 'TODO')}
className={classNames({ 'is-active': value === 'TODO' })}
>ToDo</a>
<a
href="#"
onClick={handleClick.bind(null, 'DONE')}
className={classNames({ 'is-active': value === 'DONE' })}
>Done</a>
##7.完成
const getKey = () => Math.random().toString(32).substring(2);
function Todo() {
const [items, setItems] = React.useState([]);
const [filter, setFilter] = React.useState('ALL');
const handleAdd = text => {
setItems([...items, { key: getKey(), text, done: false }]);
};
const handleFilterChange = value => setFilter(value);
const displayItems = items.filter(item => {
if (filter === 'ALL') return true;
if (filter === 'TODO') return !item.done;
if (filter === 'DONE') return item.done;
});
const handleCheck = checked => {
const newItems = items.map(item => {
if (item.key === checked.key) {
item.done = !item.done;
}
return item;
});
setItems(newItems);
};
return (
<div className="panel">
<div className="panel-heading">
⚛️ React ToDo
</div>
<Input onAdd={handleAdd} />
<Filter
onChange={handleFilterChange}
value={filter}
/>
{displayItems.map(item => (
<TodoItem
key={item.text}
item={item}
onCheck={handleCheck}
/>
))}
<div className="panel-block">
{displayItems.length} items
</div>
</div>
);
}
function Input({ onAdd }) {
const [text, setText] = React.useState('');
const handleChange = e => setText(e.target.value);
const handleKeyDown = e => {
if (e.key === 'Enter') {
onAdd(text);
setText('');
}
};
return (
<div className="panel-block">
<input
class="input"
type="text"
placeholder="Enter to add"
value={text}
onChange={handleChange}
onKeyDown={handleKeyDown}
/>
</div>
);
}
function Filter({ value, onChange }) {
const handleClick = (key, e) => {
e.preventDefault();
onChange(key);
};
return (
<div className="panel-tabs">
<a
href="#"
onClick={handleClick.bind(null, 'ALL')}
className={classNames({ 'is-active': value === 'ALL' })}
>All</a>
<a
href="#"
onClick={handleClick.bind(null, 'TODO')}
className={classNames({ 'is-active': value === 'TODO' })}
>ToDo</a>
<a
href="#"
onClick={handleClick.bind(null, 'DONE')}
className={classNames({ 'is-active': value === 'DONE' })}
>Done</a>
</div>
);
}
function TodoItem({ item, onCheck }) {
const handleChange = () => {
onCheck(item);
};
return (
<label className="panel-block">
<input
type="checkbox"
checked={item.done}
onChange={handleChange}
/>
<span
className={classNames({
'has-text-grey-light': item.done
})}
>
{item.text}
</span>
</label>
);
}
function App() {
return (
<div className="container is-fluid">
<Todo />
</div>
);
}
const root = document.getElementById('root');
ReactDOM.render(<App />, root);
##参考サイト
[React入門チュートリアル (5) ToDoアプリを作ってみよう]
(https://www.hypertextcandy.com/react-tutorial-05-wrap-up-with-todo-app)
[CSSフレームワークのススメ - BULMAの導入と覚え書き]
(https://qiita.com/belq/items/10ec41f656e47ee2b540)
[CSS フレームワーク Bulma チートシート]
(https://blog1.mammb.com/entry/2021/06/20/235459)
[Reactのスタイリング(classNameやclassNamesの使い方)]
(https://qiita.com/terry_6518/items/de53123253cf67e048b3)