#概要
今回はアプリ制作の登竜門とも言える、ToDo
アプリを作成してみました。
実はこれ、単純に見えて意外と複雑!
でもReactでstate・propsなどの流れを確認するにはちょうどいいレベルですね。
小さくてすみません。。。
##開発環境
・macOS Catalina ver.10.15.4
・Editor:VScode
・node.js(create-react-app)
主に使うやつだけ載せました!
ざっとですが、流れとしてはindex.html←index.js←App.js・index.css←componentsって感じです。
ここの位置関係をちゃんと把握しておくと、後々楽になってきます。
##解説
今回はUIのパーツごと・ファイルごとにコードの成り立ちを解説している形になっています。
ですので、実際に作っていく流れとは違うとは思います!ご了承を。
1.TodoHeader.jsx
2.TodoForm.jsx
3.TodoList.jsx
4.App.js
5.index.css
##1.TodoHeader.jsx
これはリストの最上部を描画しているコンポーネントです。
ここで実装されている主な機能は
・複数で削除が可能、Alert機能
・Check/総数 でカウント
ですね。
全体のコードはこちら!
import React from 'react';
export default function Todoheader(props) {
const remaining = props.todos.filter((todo) => {
return !todo.isDone;
});
return (
<h1>
<button onClick={props.purge}>Purge</button>
Today's Task
<span>
({remaining.length}/{props.todos.length})
</span>
</h1>
);
}
remainingではfilterで、isDone=false=□ の反対(!)であるチェックされた項目だけが集められています。これによって{remaining.length}/{props.todos.length}で チェック/全体 を表示しています。
isDone,purge,todosについては4.App.jsからの引用なのでそちらで紹介をします。
##2.TodoForm.jsx
これはリストの中部を描画しているコンポーネントです。
ここで実装されている主な機能は
・チェックをつけたら斜線が入る
・何もToDoがない時はPerfectを表示
・単独で削除が可能、Alert機能
ですね。
全体のコードはこちら!
import React from 'react';
export default function TodoList(props) {
const todos = props.todos.map((todo) => {
return (
<TodoItem
key={todo.id}
todo={todo}
checkTodo={props.checkTodo}
deleteTodo={props.deleteTodo}
/>
);
});
return <ul>{props.todos.length ? todos : <li>Perfect!</li>}</ul>;
}
function TodoItem(props) {
return (
<li>
<label>
<input
type="checkbox"
checked={props.todo.isDone}
onChange={() => props.checkTodo(props.todo)}
/>
<span className={props.todo.isDone ? "done" : ""}>
{props.todo.title}
</span>
</label>
<span className="cmd" onClick={() => props.deleteTodo(props.todo)}>
[×]
</span>
</li>
);
}
ここではTodoListで要素をコピーし、TodoItemで実際に処理を行っていく流れになります。
まずmapメソッドを用いてtodosをコピーします。その際にTodoItemにkeyやtodoなどの属性を付与するのですが、checkTodo,deleteTodoについては後で見ていきます。
簡単に言うとcheckTodoはチェックボックスを使えるようにするため、deleteTodoは消せるようにするためのメソッドです。
ulでは、要素があったらtodosを表示、なかったらPerfect!を表示できるように演算子を使用します。
最初に出てくるspanではcssで斜線を引けるようにtrueの時にdoneクラスを付与します。
次に出てくるspanではクリックで要素を消せるように、onClickで機能を追加しています。
##3.TodoList.jsx
これはリストの最下部を描画しているコンポーネントです。
ここで実装されている主な機能は
・入力+Add ボタンで追加が可能
ですね。
全体のコードはこちら!
import React from 'react';
export default function TodoForm(props) {
return (
<form onSubmit={props.addTodo}>
<input type="text" value={props.item} onChange={props.updateItem} />
<input type="submit" value="Add" />
</form>
);
}
ここではaddTodo,updateItemメソッドを用いて実際に記入した要素が反映されるようにしていますね。
これもApp.jsで見ていきましょう(丸投げ)
##4.App.js
ここでは上3つのコンポーネントが集まり、stateを用いた機能を補完しています
さらにプラスで実装されている主な機能は
・リロードしても記録が残る
ですね。
全体のコードはこちら!
このコンポーネントは複雑なので注釈をつけていくスタイルにします。
import React from "react";
import TodoForm from "./components/TodoForm";
import TodoList from "./components/TodoList";
import Todoheader from "./components/Todoheader";
const todos = [];
function getUniqueId() {
//乱数を発生させて、Itemに一意の番号を付与しています
return new Date().getTime().toString(36) + Math.random().toString(36);
}
class App extends React.Component {
constructor() {
super();
this.state = {
todos: todos,
item: "",
};
this.deleteTodo = this.deleteTodo.bind(this);
this.checkTodo = this.checkTodo.bind(this);
this.updateItem = this.updateItem.bind(this);
this.addTodo = this.addTodo.bind(this);
this.purge = this.purge.bind(this);
}
purge() {
if (!window.confirm("Are you sure?")) {
//falseならreturnを返す
return;
}
const todos = this.state.todos.filter((todo) => {
//Trueのときの判定
//filterでToDoにtodosの1つ1つが入っていって
//まだチェックしていないToDoだけを集めて更新する
// =>チェックされている項目だけが消えているように見える
return !todo.isDone;
});
this.setState({
todos: todos,
});
}
addTodo(e) {
e.preventDefault(); //(*1)
if (this.state.item.trim() === "") {
return; //空文字を処理しない(ToDoに何もない要素が追加されないようにする)
}
const item = {
id: getUniqueId(),
title: this.state.item,
isDone: false, // t/fを判断する箱。
// 初期状態をfalseにすることで、チェックボックスが□で出てくる
};
const todos = this.state.todos.slice();
//オブジェクトのプロパティをいじらない時のコピーなのでslice
todos.push(item);
this.setState({
todos: todos,
item: "", //更新した時に空にする
});
}
checkTodo(todo) {
const todos = this.state.todos.map((todo) => {
return { id: todo.id, title: todo.title, isDone: todo.isDone };
});
//オブジェクトのコピーはmapで行う。todosはstateなので直接変更できない
const pos = this.state.todos
.map((todo) => {
return todo.id;
})
.indexOf(todo.id);
//idのみのtodoをmapで配列に集め、indexOfで渡されてきたtodoが何番目かを最終的な値とする
todos[pos].isDone = !todos[pos].isDone;
//取ってきた値のisDone(t/f判定)が反転できるようにする
this.setState({
todos: todos,
});
//それらを全てstateに反映する
}
deleteTodo(todo) {
if (!window.confirm("Are you sure?")) {
return;
}
const todos = this.state.todos.slice();
//オブジェクトのプロパティをいじらない時のコピーなのでslice
const pos = this.state.todos.indexOf(todo);
todos.splice(pos, 1); //pos番目の要素を1つ取り除く
this.setState({
todos: todos,
});
}
updateItem(e) {
this.setState({
item: e.target.value,
});
//formの値はイベントオブジェクトから取得できるので、eを引数にしつつthis.setState()として、
//stateの中のitemはformのtarget.valueとするとformに入力された値がUIに反映される
}
//(*2)ここはリロードしても値を保持するデータの永続化を行っています。
componentDidUpdate() {
localStorage.setItem("todos", JSON.stringify(this.state.todos));
}//ここでlocalStorageに値を保持し
componentDidMount() {
this.setState({
todos: JSON.parse(localStorage.getItem("todos")) || [],
});//ここで値を読み込ませています
}
render() {
return (
<div className="container">
<Todoheader
todos={this.state.todos}
purge={this.purge}
/>
<TodoList
todos={this.state.todos}
checkTodo={this.checkTodo}
deleteTodo={this.deleteTodo}
/>
<TodoForm
item={this.state.item}
updateItem={this.updateItem}
addTodo={this.addTodo}
/>
</div>
);
}
}
export default App;
*1:(https://qiita.com/tochiji/items/4e9e64cabc0a1cd7a1ae)
*2:(https://qiita.com/jima-r20/items/73b78c4c8cf5af2fed58)
##5.index.css
ここでは全体の見た目を整えています。
今回は特にフレームワーク等を使わずに書いているので、コードだけ載せて省略させていただきます。
全体のコードはこちら!
body {
font-size: 16px;
font-family: Arial, Helvetica, sans-serif;
}
.container {
width: 300px;
margin: auto;
}
.container h1 {
font-size: 16px;
border-bottom: 1px solid #ddd;
padding: 16px 0;
}
.container ul {
padding: 0;
list-style: none;
}
.container li {
line-height: 1.5;
}
.container input[type="checkbox"] {
margin-right: 8px;
}
.container input[type="text"] {
padding: 2px;
margin-right: 5px;
}
h1 > span {
color: #ccc;
font-size: 12px;
font-weight: normal;
margin-left: 7px;
}
h1 > button {
float: right;
}
.cmd {
font-size: 12px;
cursor: pointer;
color: #08c;
margin-left: 5px;
}
.done {
text-decoration: line-through;
color: #ccc;
}
##終わり
少し後半雑になってしまったのは自分自身、完璧に理解できていないからですね
もっと噛み砕いて説明できるように精進したいなと思います。