TODOアプリ
前回に引き続きReactを用いてTODOアプリを作成していこうと思います。
ただし、React初心者なので以下の手順で作成していこうと思います。
- JavaScriptで作成(サーバー側はJSON Serverを使用しaxiosを用いてデータの取得、更新を行う)
- JavaScript → TypeScriptに変換
- Material UIを使用してデザインをつける
- Java(Spring Boot)を用いてサーバー側を構築
- AWS?を使用してデプロイする
今回は、2. JavaScript → TypeScriptに変換
を行っていきます。
前回まで
前回はJavaScriptを用いてTODOアプリの基盤を作成しました。
GitHub
TypeScriptとは
TypeScriptはTypeScript = JavaScript + 型
のように表すことができます。
つまり、TypeScriptとはJavaScriptに型付けを行った言語になります。
TypeScriptを使用するメリットとしては以下の例が挙げられます。
- アプリの安全性が高まる
- 開発効率が高まる
集団で開発を行う際はバグを無くすことが重要になりますが、型付を行うことで予期せぬ挙動やコードミスなどを減らすことができます。
成果物
https://www.netlify.com/ を利用してここまで開発したアプリをデプロイしました。
まだ必要最低限の機能しか実装しておらず、側だけしか作っておらず押しても何の処理も実行されない部分もあります。
TypeScript化する前の準備
JavaScript → TypeScriptに変更するための事前準備を行います。
必要パッケージのインストール
ターミナルに以下の内容を入力します。
// npmを使用する場合
npm install typescript @types/node @types/react @types/reactdom @types/react-router-dom
// yarnを使用する場合
yarn add typescript @types/node @types/react @types/reactdom @types/react-router-dom
必要なパッケージのインストールはtypescript @types/インスールしたいパッケージ名
と入力することでインストールすることができます。
開発を行う中でパッケージをインストールしている場合は、適宜TypeScriptに必要なパッケージもインストールする必要があります。
例えば、今回の開発ではモーダル表示を行うためのreact-modal
をインストールしましたが、TypeScriptに必要なパッケージもインストールする必要があります。
// TypeScriptに必要なreact-modalのパッケージのインストールする
yarn add typescript @types/react-modal
インストールしたパッケージの状態が表示されているファイルpackage.json
の現状は以下になります。
現段階で使用していないパッケージ(Material UIなど)がありますが、一旦先にインストールしておきました。
{
"name": "todo-app-arrange",
"version": "0.1.0",
"private": true,
"dependencies": {
"@emotion/react": "^11.10.6",
"@emotion/styled": "^11.10.6",
"@mui/material": "^5.12.1",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/node": "^18.16.2",
"@types/react": "^18.2.0",
"@types/react-beautiful-dnd": "^13.1.4",
"@types/react-datepicker": "^4.10.0",
"@types/react-dom": "^18.2.1",
"axios": "^1.3.5",
"react": "^18.2.0",
"react-beautiful-dnd": "^13.1.1",
"react-datepicker": "^4.11.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.43.9",
"react-modal": "^3.16.1",
"react-scripts": "5.0.1",
"typescript": "^5.0.4",
"ulid": "^2.3.0",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@types/react-modal": "^3.13.1"
}
}
tsconfig.jsonの作成
型の厳しさの設定などのTypeScriptに関する様々な設定を書き込むtsconfig.json
を作成します。
マニュアルでも作成することができますが、以下のコマンドを入力することでも作成することができます。
// npx/yarn関係なく入力することでtsconfig.jsonを作成することができる
npx tsc --init
tsconfig.json
の初期状態は大半がコメントアウトだと思いますが、全て削除し以下で上書きします。
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src"
]
}
ツリー構成
TypeScriptを適用する前後のツリー構成は以下のようになります。
適用前後での主な変更点は以下になります。
- 拡張子
js
→tsx
に変更(tsx
はReactのTypeScript) - 使用する型をまとめたファイル
types.ts
の作成 - ファイルの名前
TodoAdd
→TodoAddModal
に変更 - TypeScriptに関する様々な設定を書き込むファイル
tsconfig.json
の作成
src
|-apis ← モックサーバーと通信するファイルを格納するディレクトリ
| |-todos.tsx ← サーバーとの通信用のファイル(CRUD)
|-components ← コンポーネントを格納するディレクトリ
| |-App.tsx ← コンポーネントをまとめるファイル
| |-TodoAddCheckItem.tsx ← チェックリスト単体用のコンポーネント
| |-TodoAddCheckList.tsx ← チェックリストをまとめるコンポーネント
| |-TodoAddModal.tsx ← TODOを新規追加するコンポーネント(TodoAdd→TodoAddModalに名前変更)
| |-TodoItem.tsx ← TODO単体用のコンポーネント
| |-TodoList.tsx ← TODOをリスト化するコンポーネント
| |-TodoTitle.tsx ← タイトル用のコンポーネント
|-hooks ← カウタムフックを格納するディレクトリ
| |-useTodo.tsx ← TODOの状態を管理するカスタムフック(todos.jsの具体的な実装部分)
|-index.tsx ← TODOアプリのトップルート
|-types.ts ← 使用する型をまとめたファイル
tsconfig.json ← TypeScriptに関する様々は設定を書き込むファイル
src
|-apis ← モックサーバーと通信するファイルを格納するディレクトリ
| |-todos.js ← サーバーとの通信用のファイル(CRUD)
|-components ← コンポーネントを格納するディレクトリ
| |-App.js ← コンポーネントをまとめるファイル
| |-TodoAdd.js ← TODOを新規追加するコンポーネント
| |-TodoAddCheckItem.js ← チェックリスト単体用のコンポーネント
| |-TodoAddCheckList.js ← チェックリストをまとめるコンポーネント
| |-TodoItem.js ← TODO単体用のコンポーネント
| |-TodoList.js ← TODOをリスト化するコンポーネント
| |-TodoTitle.js ← タイトル用のコンポーネント
|-hooks ← カウタムフックを格納するディレクトリ
| |-useTodo.js ← TODOの状態を管理するカスタムフック(todos.jsの具体的な実装部分)
|-index.js ← TODOアプリのトップルート
TypeScriptに変換する
実際にTypeScriptに変更していきますが、変更が簡単な順に記述していきます。
index.js → index.tsx
import { createRoot } from "react-dom/client";
import App from "./components/App";
const rootElement = document.getElementById("root");
const root = createRoot(rootElement);
root.render(<App />);
import { createRoot } from "react-dom/client";
import App from "./components/App";
// as HTMLElementという型情報のコードを追加する
const rootElement = (document.getElementById("root") as HTMLElement);
const root = createRoot(rootElement);
root.render(<App />);
TodoTitle.js → TodoTitle.tsx
// TodoTitleコンポーネントを作成する
// 親コンポーネント(App)から受け取ったprops(as)の値により使用するタグを変更
const TodoTitle = (props) => {
// asがh1ならば、タイトルはh1タグを使用
if (props.as === "h1") {
return <h1>{props.title}</h1>
// asがh2ならば、タイトルはh2タグを使用
} else if (props.as === "h2") {
return <h2>{props.title}</h2>
// それ以外ならば、タイトルはpタグを使用
} else {
return <p>{props.title}</p>
}
}
export default TodoTitle;
TodoTitle
コンポーネントは親コンポーネントのApp.tsx
からprops
でtitle
、as
を受け取ります。
ここで受け取るtitle
、as
はどちらもstring(文字列)
型なので、まとめた型定義TodoTitleProps
を設定します。
// propsで渡される値の型定義を行う
type TodoTitleProps = {
title: string
as : string
}
// TodoTitleコンポーネントを作成する
// 親コンポーネント(App)から受け取ったprops(as)の値により使用するタグを変更
const TodoTitle = (props: TodoTitleProps) => {
// asがh1ならば、タイトルはh1タグを使用
if (props.as === "h1") {
return <h1>{props.title}</h1>
// asがh2ならば、タイトルはh2タグを使用
} else if (props.as === "h2") {
return <h2>{props.title}</h2>
// それ以外ならば、タイトルはpタグを使用
} else {
return <p>{props.title}</p>
}
}
export default TodoTitle;
以下のように書くこともできます。
// パターン①: 個別に型を設定する
const TodoTitle = ({title: string, as: string}) => {
// 省略
}
// パターン②: 型定義をまとめて行う(propsを使用しない場合)
// 定義する型をまとめておく
type TodoTitleType = {
title: string
as : string
}
// 定義した型をまとめたTodoTitleTypeを設定する
const TodoTitle = ({title, as}: TodoTitleType) => {
// 省略
}
TodoAddCheckItem.js → TodoAddCheckItem.tsx
const TodoCheckItem = (props) => {
return (
// チェックリストの各要素
<>
{/* 現在時点でチェックボックスは機能していない */}
<input type="checkbox"></input>
<input
type="text"
value={props.checkItem.checkItem}
onChange={e => props.updateCheckList(props.index, e)}
/>
<button onClick={() => props.deleteCheckList(props.index)}>削除</button>
</>
);
}
export default TodoCheckItem;
TypeScriptはprops
で渡される変数以外にも関数も型定義を行わないといけません。
関数の型定義を行う場合、定義元の関数にカーソルを置くと以下の画像のように定義が表示されるのでその定義を型に記述します。
また、型checkItem
にはid(string)
、checkItem(string)
、done(boolean)
の3つの要素を含んでいますが、今回はその中でcheckItem
を使用します。よって以下のように記述します。
checkItem : {
checkItem: string
}
// propsで渡される値の型定義を行う
type TodoAddCheckItemProps = {
updateCheckList: (index: number, e: React.ChangeEvent<HTMLInputElement>) => void
deleteCheckList: (index: number) => void
checkItem : {
checkItem: string
}
index: number
}
const TodoCheckItem = (props: TodoAddCheckItemProps) => {
return (
// チェックリストの各要素
<>
{/* 現在時点でチェックボックスは機能していない */}
<input type="checkbox"></input>
<input
type="text"
value={props.checkItem.checkItem}
onChange={e => props.updateCheckList(props.index, e)}
/>
<button onClick={() => props.deleteCheckList(props.index)}>削除</button>
</>
);
}
export default TodoCheckItem;
todos.js → todos.tsx
// 作成したモックサーバーとの通信にaxiosを利用する
import axios from "axios";
// ローカルに準備したモックサーバーのURL
const dataUrl = "http://localhost:3100/todos";
/*
##############################
サーバー上の全TODO取得処理
##############################
*/
// axios.get()でGETリクエストを送信
// サーバー上の全てのTODO(todos)を取得する関数getAllTodosDataを宣言する
// 他ファイルでgetAllTodosData()を利用できるようにするためexportする
export const getAllTodosData = async () => {
// 引数に指定したURL(http://localhost:3100/todos)へGETリクエストを送り、
// 戻される値は全てresponseに保存される
const response = await axios.get(dataUrl);
// 通信後、response.dataでレスポンスデータを返す
return response.data;
};
/*
##############################
TODO追加処理
##############################
*/
// axios.post()で新規TODOを追加する
// TODOを追加する関数addTodoDataを宣言する
// 他ファイルでaddTodoData()を利用できるようにするためexportする
export const addTodoData = async (todo) => {
// 第2引数に、送信したいデータを指定してPOST送信する
// サーバーに転送することで新規にデータを追加する
const response = await axios.post(dataUrl, todo);
// 通信後、response.dataでレスポンスデータを返す
return response.data
};
/*
##############################
TODO削除処理
##############################
*/
// axios.delete()で一致したidのTODOを削除する
// TODOを削除する関数deleteTodoDataを宣言する
// 他ファイルでdeleteTodoData()を利用できるようにするためexportする
export const deleteTodoData = async (id) => {
await axios.delete(`${dataUrl}/${id}`);
// 通信後、削除したTODOのidを返す
return id;
};
/*
##############################
TODO更新処理
##############################
*/
// axios.put()で一致したidのTODOを更新する
// TODOを更新する関数updateTodoDataを宣言する
// 他ファイルでupdateTodoData()を利用できるようにするためexportする
export const updateTodoData = async (id, todoItem) => {
// 第2引数に更新したいデータを渡す
const response = await axios.put(`${dataUrl}/${id}`, todoItem);
// 通信後、response.dataでレスポンスデータを返す
return response.data;
};
// 作成したモックサーバーとの通信にaxiosを利用する
import axios from "axios";
// 型TodoItemTypeをインポートする
import { TodoItemType } from "../types"
// ローカルに準備したモックサーバーのURL
const dataUrl = "http://localhost:3100/todos";
/*
##############################
サーバー上の全TODO取得処理
##############################
*/
// axios.get()でGETリクエストを送信
// サーバー上の全てのTODO(todos)を取得する関数getAllTodosDataを宣言する
// 他ファイルでgetAllTodosData()を利用できるようにするためexportする
export const getAllTodosData = async () => {
// 引数に指定したURL(http://localhost:3100/todos)へGETリクエストを送り、
// 戻される値は全てresponseに保存される
const response = await axios.get(dataUrl);
// 通信後、response.dataでレスポンスデータを返す
return response.data;
};
/*
##############################
TODO追加処理
##############################
*/
// axios.post()で新規TODOを追加する
// TODOを追加する関数addTodoDataを宣言する
// 他ファイルでaddTodoData()を利用できるようにするためexportする
export const addTodoData = async (todoItem: TodoItemType) => {
// 第2引数に、送信したいデータを指定してPOST送信する
// サーバーに転送することで新規にデータを追加する
const response = await axios.post(dataUrl, todoItem);
// 通信後、response.dataでレスポンスデータを返す
return response.data
};
/*
##############################
TODO削除処理
##############################
*/
// axios.delete()で一致したidのTODOを削除する
// TODOを削除する関数deleteTodoDataを宣言する
// 他ファイルでdeleteTodoData()を利用できるようにするためexportする
export const deleteTodoData = async (id: string) => {
await axios.delete(`${dataUrl}/${id}`);
// 通信後、削除したTODOのidを返す
return id;
};
/*
##############################
TODO更新処理
##############################
*/
// axios.put()で一致したidのTODOを更新する
// TODOを更新する関数updateTodoDataを宣言する
// 他ファイルでupdateTodoData()を利用できるようにするためexportする
export const updateTodoData = async (id: string, todoItem: TodoItemType) => {
// 第2引数に更新したいデータを渡す
const response = await axios.put(`${dataUrl}/${id}`, todoItem);
// 通信後、response.dataでレスポンスデータを返す
return response.data;
};
引数のtodoItem
の型であるTodoItemType
は他のファイルでも使用されるため、ファイルtypes.ts
に型を別定義しエクスポートしたものを使用します。
// チェックアイテムの型定義
export type CheckItemType = {
id: string,
checkItem: string,
done: boolean
}
// チェックリストの型定義
export type CheckListType = CheckItemType[]
// TODOアイテムの型定義
export type TodoItemType = {
id?: string,
title?: string,
memo?: string,
checkList?: CheckItemType[],
packet?: string,
done?: boolean,
priority?: string,
difficulty?: string,
deadLine?: Date,
createDate?: Date
}
// TODOリストの型定義
export type TodoListType = TodoItemType[]
型TodoItemType
の各要素に?
をつけることでその要素が省略可能になります。今回、関数によってはTodoItemType
の要素を全て使用するわけではないため全ての要素に?
をつけています。ただし、?
を付けた要素は最後に書かなくてはなりません。今回は全要素に?
を付けているため気にしなくても良いですがそうでない場合は気をつけましょう。
また、型に[]
をつけることで配列の型を定義することができます。
今回、CheckItemType
の配列型の定義CheckListType(=CheckItemType[])
、TodoItemType
の配列型の定義TodoListType(=TodoItemType[])
の2つを設定しています。
useTodo.js → useTodo.tsx
import {useState, useEffect} from "react";
// 一意なidを生成するulidをインポートする
import {ulid} from "ulid";
// src/apis/todos.js内で宣言してexportした関数をimportする
// getAllTodosData, addTodoData, deleteTodoData, updateTodoDataを
// todoDataオブジェクトとしてまとめてimportする
import * as todoData from "../apis/todos";
// useTodo()カスタムフックを外部で使用できるようexportする
export const useTodo = () => {
// todoListは現在のTODOの状態、setTodoListは現在のtodoListの状態を更新する関数
const [todoList, setTodoList] = useState([]);
/*
##############################
TODO取得処理
##############################
*/
// データの取得処理はuseEffectを利用してコンポーネントのマウント後に実装する
// useEffect()の第2引数には空の依存配列[]を設定しているため、コンポーネントの初回レンダリング時のみ実行
useEffect(()=> {
// モックサーバーからTODOデータを取得するgetAllTodoData()を実行する
// モックサーバーからレスポンスデータの取得に成功した場合、then()以降の処理を実行する
// 引数todoにはモックサーバーから送り返されたresponse.dataが設定される
todoData.getAllTodosData().then((todo) => {
// モックサーバーからTODOデータを取得後、取得したTODOデータを反転させ、上から順に表示
// todoListの状態(state)を更新する
setTodoList([...todo].reverse());
});
}, []);
/*
##############################
完了/未完了変更処理
##############################
*/
const toggleTodoItemStatus = (id, done) => {
// find()メソッドを利用し一致するTODOを取得する
// done(完了/未完了)の状態を反転させたいTODOをTODOリストから見つける
const changeTodoItem = todoList.find((todoItem) => todoItem.id === id);
// 対象のTODOの完了/未完了を反転させる
const newTodoItem = {...changeTodoItem, done: !done };
// updataTodoData()を利用して対象のidを持つTODOを更新したら、todoListの状態を更新する
// モックサーバーからレスポンスデータの取得に成功した場合、then()以降の処理を実行する
// 引数updatedTodoItemにはモックサーバーから送り返された対象のidを持つTODOが設定される
todoData.updateTodoData(id, newTodoItem).then((updatedTodoItem) => {
// TODOリストからTODOをmap()メソッドを利用してひとつ1つ処理する
const newTodoList = todoList.map((todoItem) =>
// idが異なる場合、todoListから取り出したtodoItemをそのまま返す
// idが同じ場合、done(完了/未完了)の状態を反転させたupdatedTodoを返し、
// 新しい配列newTodoListを作成する
todoItem.id !== updatedTodoItem.id ? todoItem : updatedTodoItem
);
// todoListの現在の状態(state)をnewTodoListの内容に更新
setTodoList(newTodoList);
});
}
/*
##############################
TODO編集処理
##############################
*/
const changeTodoItem = (id, title, memo, checkList, priority, difficulty, deadLine) => {
// 編集するTODOをidを用いてTODOリストから検索する
const changeTodoItem = todoList.find((todoItem) => todoItem.id === id);
// 対象のTODOの内容を変更する
const newTodoItem = {...changeTodoItem,
title: title, // titleにタイトルをセット
memo: memo, // memoにメモをセット
checkList: checkList, // checkListにチェックリストをセット
priority: priority, // priotiryに重要度をセット
difficulty: difficulty, // difficultyに難易度をセット
deadLine: deadLine, // deadLineに期限をセット
createDate: new Date() // createDateに作成日時をセット
};
// updataTodoData()を利用して対象のidを持つTODOを更新する
todoData.updateTodoData(id, newTodoItem).then((updatedTodoItem) => {
// idが異なる場合、todoListから取り出したtodoItemをそのまま返す
// idが同じ場合、TODOの内容を更新したupdatedTodoItemを返し、
// 新しい配列newTodoListを作成する
const newTodoList = todoList.map((todoItem) =>
todoItem.id !== updatedTodoItem.id ? todoItem : updatedTodoItem
);
// 最新のtodoListに内容を更新する
setTodoList(newTodoList);
});
}
/*
##############################
新規TODO取得処理
##############################
*/
const addTodoItem = (title, memo, checkList, priority, difficulty, deadLine) => {
const newTodoItem = {
id: ulid(), // idにulidで生成された一意な値をセット
title: title, // titleにタイトルをセット
memo: memo, // memoにメモをセット
checkList: checkList, // checkListにチェックリストをセット
done: false, // doneに完了/未完了をセット(初期値は未完了(false))
priority: priority, // priotiryに重要度をセット
difficulty: difficulty, // difficultyに難易度をセット
deadLine: deadLine, // deadLineに期限をセット
createDate: new Date() // createDateに作成日時をセット
};
// addTodoData()を利用して新規TODOを追加する
// 引数addTodoItemにはモックサーバーから送り返された追加されたTODOが設定される
todoData.addTodoData(newTodoItem).then((addTodo) => {
// todoListにaddTodoItemが追加された状態に更新する
setTodoList([addTodoItem, ...todoList]);
});
};
/*
##############################
TODO削除処理
##############################
*/
const deleteTodoItem = (id) => {
// deleteTodoData()を利用して指定されたidのTODOを削除する
// deleteTodoData()は一致したidのTODOを削除する関数
todoData.deleteTodoData(id).then((deleteItemId) => {
// 削除したTODOとidが一致しないTODOのみの新しいリストを返す
const newTodoList = todoList.filter((todoItem) =>
todoItem.id !== deleteItemId
);
// todoListの状態を更新する
setTodoList(newTodoList);
});
};
// 作成した関数と、現在のTODOリストの状態変数todoListを返す
return {todoList, setTodoList, toggleTodoItemStatus, changeTodoItem, addTodoItem, deleteTodoItem};
};
import {useState, useEffect} from "react";
// 一意なidを生成するulidをインポートする
import {ulid} from "ulid";
// TODOリスト、チェックリストの型をインポートする
import { TodoListType, CheckListType } from "../types"
// src/apis/todos.js内で宣言してexportした関数をimportする
// getAllTodosData, addTodoData, deleteTodoData, updateTodoDataを
// todoDataオブジェクトとしてまとめてimportする
import * as todoData from "../apis/todos";
// useTodo()カスタムフックを外部で使用できるようexportする
export const useTodo = () => {
// todoListは現在のTODOリストの状態、setTodoListは現在のtodoListの状態を更新する関数
const [todoList, setTodoList] = useState<TodoListType>([]);
/*
##############################
TODO取得処理
##############################
*/
// データの取得処理はuseEffectを利用してコンポーネントのマウント後に実装する
// useEffect()の第2引数には空の依存配列[]を設定しているため、コンポーネントの初回レンダリング時のみ実行
useEffect(()=> {
// モックサーバーからTODOデータを取得するgetAllTodosData()を実行する
// モックサーバーからレスポンスデータの取得に成功した場合、then()以降の処理を実行する
// 引数todoにはモックサーバーから送り返されたresponse.dataが設定される
todoData.getAllTodosData().then((todoList) => {
// モックサーバーからTODOデータを取得後、取得したTODOデータを反転させ、上から順に表示
// todoListの状態(state)を更新する
setTodoList([...todoList].reverse());
});
}, []);
/*
##############################
完了/未完了変更処理
##############################
*/
const toggleTodoItemStatus = (id: string, done: boolean) => {
// find()メソッドを利用し一致するTODOを取得する
// done(完了/未完了)の状態を反転させたいTODOをTODOリストから見つける
const changeTodoItem = todoList.find((todoItem) => todoItem.id === id);
// 対象のTODOの完了/未完了を反転させる
const newTodoItem = {...changeTodoItem, done: !done };
// updataTodoData()を利用して対象のidを持つTODOを更新したら、todoListの状態を更新する
// モックサーバーからレスポンスデータの取得に成功した場合、then()以降の処理を実行する
// 引数updatedTodoItemにはモックサーバーから送り返された対象のidを持つTODOが設定される
todoData.updateTodoData(id, newTodoItem).then((updatedTodoItem) => {
// TODOリストからTODOをmap()メソッドを利用してひとつ1つ処理する
const newTodoList = todoList.map((todoItem) =>
// idが異なる場合、todoListから取り出したtodoItemをそのまま返す
// idが同じ場合、done(完了/未完了)の状態を反転させたupdatedTodoを返し、
// 新しい配列newTodoListを作成する
todoItem.id !== updatedTodoItem.id ? todoItem : updatedTodoItem
);
// todoListの現在の状態(state)をnewTodoListの内容に更新
setTodoList(newTodoList);
});
}
/*
##############################
TODO編集処理
##############################
*/
const changeTodoItem = (
id: string,
title: string,
memo: string,
checkList: CheckListType,
priority: string,
difficulty: string,
deadLine: Date
) => {
// 編集するTODOをidを用いてTODOリストから検索する
const changeTodoItem = todoList.find((todoItem) => todoItem.id === id);
// 対象のTODOの内容を変更する
const newTodoItem = {...changeTodoItem,
title: title, // titleにタイトルをセット
memo: memo, // memoにメモをセット
checkList: checkList, // checkListにチェックリストをセット
priority: priority, // priotiryに重要度をセット
difficulty: difficulty, // difficultyに難易度をセット
deadLine: deadLine, // deadLineに期限をセット
createDate: new Date() // createDateに作成日時をセット
};
// updataTodoData()を利用して対象のidを持つTODOを更新する
todoData.updateTodoData(id, newTodoItem).then((updatedTodoItem) => {
// idが異なる場合、todoListから取り出したtodoItemをそのまま返す
// idが同じ場合、TODOの内容を更新したupdatedTodoItemを返し、
// 新しい配列newTodoListを作成する
const newTodoList = todoList.map((todoItem) =>
todoItem.id !== updatedTodoItem.id ? todoItem : updatedTodoItem
);
// 最新のtodoListに内容を更新する
setTodoList(newTodoList);
});
}
/*
##############################
新規TODO追加処理
##############################
*/
// 簡易入力ではtitle以外入力しないためundifinedを許容するために?をつける
const addTodoItem = (
title: string,
memo: string,
checkList: CheckListType,
priority: string,
difficulty: string,
deadLine: Date
) => {
const newTodoItem = {
id: ulid(), // idにulidで生成された一意な値をセット
title: title, // titleにタイトルをセット
memo: memo, // memoにメモをセット
checkList: checkList, // checkListにチェックリストをセット
packet: "todo", // packetにtodoをセット(現状はtodo固定にしておく)
done: false, // doneに完了/未完了をセット(初期値は未完了(false))
priority: priority, // priotiryに重要度をセット
difficulty: difficulty, // difficultyに難易度をセット
deadLine: deadLine, // deadLineに期限をセット
createDate: new Date() // createDateに作成日時をセット
};
// addTodoData()を利用して新規TODOを追加する
// 引数addTodoItemにはモックサーバーから送り返された追加されたTODOが設定される
todoData.addTodoData(newTodoItem).then((addTodoItem) => {
// todoListにaddTodoItemが追加された状態に更新する
setTodoList([addTodoItem, ...todoList]);
});
};
/*
##############################
TODO削除処理
##############################
*/
const deleteTodoItem = (id: string) => {
// deleteTodoData()を利用して指定されたidのTODOを削除する
// deleteTodoData()は一致したidのTODOを削除する関数
todoData.deleteTodoData(id).then((deleteItemId) => {
// 削除したTODOとidが一致しないTODOのみの新しいリストを返す
const newTodoList = todoList.filter((todoItem) =>
todoItem.id !== deleteItemId
);
// todoListの状態を更新する
setTodoList(newTodoList);
});
};
// 作成した関数と、現在のTODOリストの状態変数todoListを返す
return {todoList, setTodoList, toggleTodoItemStatus, changeTodoItem, addTodoItem, deleteTodoItem};
};
App.js → App.tsx
// useRefを利用できるようインポートする
import React, { useState } from "react";
// useTodo()カスタムフックをインポートする
import { useTodo } from "../hooks/useTodo";
// TodoTitleコンポーネントをインポートする
import TodoTitle from "./TodoTitle";
// TodoListコンポーネントをインポートする
import TodoList from "./TodoList";
// TodoAddコンポーネントをインポートする
import TodoAdd from "./TodoAdd";
const App = () => {
// useTodo()カスタムフックで作成したtodoList、addTodoItemを利用する
const {todoList, setTodoList, addTodoItem, toggleTodoItemStatus, changeTodoItem, deleteTodoItem} = useTodo();
// 選択されたTODOリストのinputId、idを更新する関数setInputId
const [inputId, setInputId] = useState("");
// 現在のタイトルの現在の状態変数inputTitle、inputTitleを更新する関数setInputTitle
const [inputTitle, setInputTitle] = useState("");
// 現在のメモの現在の状態変数inputMemo、inputTitleを更新する関数setInputMemo
const [inputMemo, setInputMemo] = useState("");
// 現在のチェックリストの状態変数todoList、todoListを変更する関数setTodoList
const [checkList, setCheckList] = useState([]);
// 現在の重要度の状態変数priority、priorityを更新する関数setPriority
const [priority, setPriority] = useState("低");
// 現在の難易度の状態変数difficulty、difficultyを更新する関数setDifficulty
const [difficulty, setDifficulty] = useState("普");
// 現在の期限の状態変数inputDeadLine、inputDeadLineを更新する関数setInputDeadLine
const [inputDeadLine, setInputDeadLine] = useState(new Date());
// モーダルの表示の有無を設定する変数isShowModal、sShowModalを更新する関数setIsShowModal
const [isShowModal, setIsShowModal] = useState(false);
// 追加ボタンと編集ボタンの変更を管理する変数changeFlg、changeFlgを更新する関数setChageFlg
// False = 追加ボタン、True = 編集ボタン
const [changeFlg, setChangeFlg] = useState(false);
// 漢字変換・予測変換(サジェスト)選択中か否かの判定
// 変換中か否かの判定を行い、変換を確定させるエンターに反応しないように振り分ける
// true=変換中、false=変換中ではない
const [composing, setComposition] = useState(false);
const startComposition = () => setComposition(true);
const endComposition = () => setComposition(false);
/*
##############################
未完了のTODOリストを表示する
##############################
*/
// filter()メソッドを使用してTODOリスト内のdoneがfalseのTODOを取得する
const imCompletedList = todoList.filter((todoItem) => {
return !todoItem.done;
});
/*
##############################
完了済みのTODOリストを表示する
##############################
*/
// filter()メソッドを使用してTODOリスト内のdoneがfalseのTODOを取得する
// 現在は使用していない
const completedList = todoList.filter((todoItem) => {
return todoItem.done;
});
/*
##############################
TODO追加処理
##############################
*/
const handleAddTodoItem = () => {
// TODO入力フォームで入力された内容を新しいTODOに登録する
addTodoItem(
inputTitle,
inputMemo,
checkList,
priority,
difficulty,
inputDeadLine
);
// 新たなTODOを登録後モーダル画面を閉じる
closeModal();
}
/*
##############################
TODO編集処理
##############################
*/
const handleChangeTodoItem = () => {
// 選択されたTODOの内容を編集する
changeTodoItem(
inputId,
inputTitle,
inputMemo,
checkList,
priority,
difficulty,
inputDeadLine
);
}
/*
##############################
リセット処理
##############################
*/
const reset = () => {
setInputTitle("");
setInputMemo("");
setCheckList([]);
setPriority("低");
setDifficulty("普");
setInputDeadLine(new Date());
}
/*
##############################
TODOリストの順番変更処理
##############################
*/
const reorder = (list, startIndex, endIndex) => {
// Array.from()メソッドは、反復可能オブジェクトや配列風オブジェクトから
// シャローコピーされた、新しいArrayインスタンスを生成する
const result = Array.from(list);
// Array.splice()メソッドは、配列を操作するメソッド
// 第1引数には操作を開始する配列のインデックス、第1引数のみの場合、指定したインデックス以降を取り除く
// 第2引数はオプション、第1引数に3、第2引数に1を指定した場合、3番目の要素を配列から取り出す
const [removed] = result.splice(startIndex, 1);
// 第3引数はオブション、第3引数に設定した値が配列に追加される
result.splice(endIndex, 0, removed);
return result;
};
/*
##############################
モーダルを非表示処理
##############################
*/
const closeModal = () => {
setIsShowModal(false);
setChangeFlg(!changeFlg);
// モーダルを閉じる際に入力された内容をリセットする
reset();
};
/*
##############################
TODOリスト簡易追加処理(エンターキーによる操作)
##############################
*/
const onKeyDown = (e, key) => {
switch (key) {
// エンターキーが押された際に以下の処理を実行する
case "Enter":
// input入力中にエンターを押すとデフォルトではsubmit(送信)になるため
// e.preventDefault();で阻止する
e.preventDefault();
// 変換中ならそのまま何の処理も行わない
if (composing) break;
// 変換中でないなら、TODOを追加
addTodoItem(
inputTitle,
inputMemo,
checkList,
priority,
difficulty,
inputDeadLine
);
// 追加後に入力フォームからフォーカスを外す
document.getElementById("simpleAddInput").blur();
// 入力内容をクリアする
setInputTitle("");
break;
default:
break;
}
}
return (
<>
{/* 現在は使わないのでコメントアウト中 */}
{/* <button>新しいパケットの追加</button> */}
<div>
<TodoTitle title="TODO" as="h2" />
{/* TODOを作成するためのモーダルを表示する */}
<button
onClick={() => {
setIsShowModal(true);
setChangeFlg(false);
}}
>
TODOの作成
</button>
{/* onKeyDownでキーが押された際に処理を実行する */}
{/* onCompositionStart/onCompositionEndで入力が確定しているかどうかを判断する */}
<div>
<input
type="text"
id="simpleAddInput"
placeholder="TODOの追加"
value={inputTitle}
onChange={(e) => setInputTitle(e.target.value)}
onKeyDown={(e) => onKeyDown(e, e.key)}
onCompositionStart={startComposition}
onCompositionEnd={endComposition}
/>
</div>
{/* TODOを追加するモーダルを表示する */}
{!isShowModal ? "" :
<TodoAdd
title={inputTitle}
memo={inputMemo}
checkList={checkList}
deadLine={inputDeadLine}
priority={priority}
difficulty={difficulty}
inputDeadLine={inputDeadLine}
changeFlg={changeFlg}
isShowModal={isShowModal}
composing={composing}
setInputTitle={setInputTitle}
setInputMemo={setInputMemo}
setCheckList={setCheckList}
setPriority={setPriority}
setDifficulty={setDifficulty}
setInputDeadLine={setInputDeadLine}
handleAddTodoItem={handleAddTodoItem}
handleChangeTodoItem={handleChangeTodoItem}
setIsShowModal={setIsShowModal}
closeModal={closeModal}
reorder={reorder}
startComposition={startComposition}
endComposition={endComposition}
/>
}
{/* 登録されたTODOリストを表示する */}
<TodoList
todoList={imCompletedList}
isShowModal={isShowModal}
toggleTodoItemStatus={toggleTodoItemStatus}
changeTodoItem={changeTodoItem}
deleteTodoItem={deleteTodoItem}
setTodoList={setTodoList}
setInputId={setInputId}
setInputTitle={setInputTitle}
setInputMemo={setInputMemo}
setCheckList={setCheckList}
setPriority={setPriority}
setDifficulty={setDifficulty}
setInputDeadLine={setInputDeadLine}
setIsShowModal={setIsShowModal}
setChangeFlg={setChangeFlg}
reorder={reorder}
/>
</div>
</>
)
}
export default App;
// useRefを利用できるようインポートする
import { useState } from "react";
// useTodo()カスタムフックをインポートする
import { useTodo } from "../hooks/useTodo";
// チェックリストの型をインポートする
import { CheckListType } from "../types"
// TodoTitleコンポーネントをインポートする
import TodoTitle from "./TodoTitle";
// TodoListコンポーネントをインポートする
import TodoList from "./TodoList";
// TodoAddコンポーネントをインポートする
import TodoAdd from "./TodoAddModal";
const App = () => {
// useTodo()カスタムフックで作成したのを利用する
// カスタムフックで定義したコンポーネントはApp.tsxで定義し、propsを用いて下の階層に渡す
// 下の階層で定義すると親コンポーネント(App.tsx)がレンダリングされず処理は実行されているが画面は更新されない
const {todoList, addTodoItem, toggleTodoItemStatus, changeTodoItem, deleteTodoItem} = useTodo();
// useStateの名前は一部変更(例:inputId → id/setInputId → setId)
// 選択されたTODOリストのinputId、idを更新する関数setInputId
const [id, setId] = useState<string>("");
// 現在のタイトルの現在の状態変数inputTitle、inputTitleを更新する関数setInputTitle
const [title, setTitle] = useState<string>("");
// 現在のメモの現在の状態変数inputMemo、inputTitleを更新する関数setInputMemo
const [memo, setMemo] = useState<string>("");
// 現在のチェックリストの状態変数todoList、todoListを変更する関数setTodoList
const [checkList, setCheckList] = useState<CheckListType>([]);
// 現在の重要度の状態変数priority、priorityを更新する関数setPriority
const [priority, setPriority] = useState<string>("低");
// 現在の難易度の状態変数difficulty、difficultyを更新する関数setDifficulty
const [difficulty, setDifficulty] = useState<string>("普");
// 現在の期限の状態変数inputDeadLine、inputDeadLineを更新する関数setInputDeadLine
const [deadLine, setDeadLine] = useState<Date>(new Date());
// モーダルの表示の有無を設定する変数isShowModal、sShowModalを更新する関数setIsShowModal
const [isShowModal, setIsShowModal] = useState<boolean>(false);
// 追加ボタンと編集ボタンの変更を管理する変数changeFlg、changeFlgを更新する関数setChageFlg
// False = 追加ボタン、True = 編集ボタン
const [changeFlg, setChangeFlg] = useState<boolean>(false);
// 漢字変換・予測変換(サジェスト)選択中か否かの判定
// 変換中か否かの判定を行い、変換を確定させるエンターに反応しないように振り分ける
// true=変換中、false=変換中ではない
const [composing, setComposition] = useState<boolean>(false);
/*
##############################
各State更新用の関数
##############################
*/
// これまではuseStateの関数set*を直接propsとしてコンポーネントに渡していたが、
// 今回は別関数を作成し、その関数からset*に値を渡す。
// 理由として今回は使用しなかったが、今後useCallbackを使用することを考えて別関数を定義した
// id確認用の関数handleSetId
const handleSetId = (id: string) => setId(id);
// タイトル更新用の関数handleSetTitle
const handleSetTitle = (title: string) => setTitle(title);
// メモ更新用の関数handleSetMemo
const handleSetMemo = (memo: string) => setMemo(memo);
// チェックリスト更新用の関数handleSetCheckList
const handleSetCheckList = (checkList: CheckListType) => setCheckList(checkList);
// 重要度更新用の関数handleSetDifficulty
const handleSetDifficulty = (difficulty: string) => setDifficulty(difficulty);
// 難易度更新用の関数handleSetPriority
const handleSetPriority = (priority: string) => setPriority(priority);
// 期限更新用の関数handleSetDeadLine
const handleSetDeadLine = (deadLine: Date) => setDeadLine(new Date(deadLine));
// モーダル表示更新用の関数handleSetIsShowModal
const handleSetIsShowModal = (isShowModal: boolean) => setIsShowModal(isShowModal);
// 作成/編集更新用の関数handleSetChangeFlg
const handleSetChangeFlg = (changeFlg: boolean) => setChangeFlg(changeFlg);
// 変換開始
const startComposition = () => setComposition(true);
// 変換終了
const endComposition = () => setComposition(false);
/*
##############################
未完了のTODOリストを表示する
##############################
*/
// filter()メソッドを使用してTODOリスト内のdoneがfalseのTODOを取得する
const inCompletedList = todoList.filter((todoItem) => {
return !todoItem.done;
});
/*
##############################
完了済みのTODOリストを表示する
##############################
*/
// filter()メソッドを使用してTODOリスト内のdoneがfalseのTODOを取得する
// 現在は使用していない
const completedList = todoList.filter((todoItem) => {
return todoItem.done;
});
/*
##############################
リセット処理
##############################
*/
const reset = () => {
setTitle("");
setMemo("");
setCheckList([]);
setDifficulty("普");
setPriority("低");
setDeadLine(new Date());
}
/*
##############################
TODOリストの順番変更処理
##############################
*/
const reorder = (
// listは呼び出し元によって渡される配列の型が変わるため
// 配列型であればなんでも渡せるよう型をArray<any>とする
list: Array<any>,
startIndex: number,
endIndex: number) => {
// Array.from()メソッドは、反復可能オブジェクトや配列風オブジェクトから
// シャローコピーされた、新しいArrayインスタンスを生成する
const result = Array.from(list);
// Array.splice()メソッドは、配列を操作するメソッド
// 第1引数には操作を開始する配列のインデックス、第1引数のみの場合、指定したインデックス以降を取り除く
// 第2引数はオプション、第1引数に3、第2引数に1を指定した場合、3番目の要素を配列から取り出す
const [removed] = result.splice(startIndex, 1);
// 第3引数はオブション、第3引数に設定した値が配列に追加される
result.splice(endIndex, 0, removed);
return result;
};
/*
##############################
モーダルを非表示処理
##############################
*/
const closeModal = () => {
setIsShowModal(false);
setChangeFlg(!changeFlg);
// 入力された内容をリセットする
reset();
};
/*
##############################
TODOリスト簡易追加処理(エンターキーによる操作)
##############################
*/
// 引数のe、keyの型は関数onKeyDownの呼び出し元にカーソルを合わせることで確認することができる
const onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>, key: string) => {
switch (key) {
// エンターキーが押された際に以下の処理を実行する
case "Enter":
// input入力中にエンターを押すとデフォルトではsubmit(送信)になるため
// e.preventDefault();で阻止する
e.preventDefault();
// 変換中ならそのまま何の処理も行わない
if (composing) break;
// 変換中でないなら、TODOを追加
addTodoItem(
title,
memo,
checkList,
difficulty,
priority,
deadLine
);
// 追加後に入力フォームからフォーカスを外す
// as HTMLElementをつける
(document.getElementById("simpleAddInput") as HTMLElement).blur();
// 入力内容をクリアする
setTitle("");
break;
default:
break;
}
}
return (
<>
<div>
{/* TODOを作成するためのモーダルを表示する */}
<button
onClick={() => {
setIsShowModal(true);
setChangeFlg(false);
}}
>
TODOの作成
</button>
</div>
{/* onKeyDownでキーが押された際に処理を実行する */}
{/* onCompositionStart/onCompositionEndで入力が確定しているかどうかを判断する */}
<div>
<input
type="text"
id="simpleAddInput"
placeholder="TODOの追加"
value={title}
onChange={(e) => setTitle(e.target.value)}
onKeyDown={(e) => onKeyDown(e, e.key)}
onCompositionStart={startComposition}
onCompositionEnd={endComposition}
/>
</div>
{/* 現在は使わないのでコメントアウト中 */}
{/* <button>新しいパケットの追加</button> */}
<div>
<TodoTitle title="TODO" as="h2" />
{/* TODOを追加するモーダルを表示する */}
<TodoAdd
id={id}
title={title}
memo={memo}
checkList={checkList}
priority={priority}
difficulty={difficulty}
deadLine={deadLine}
isShowModal={isShowModal}
changeFlg={changeFlg}
composing={composing}
handleSetId={handleSetId}
handleSetTitle={handleSetTitle}
handleSetMemo={handleSetMemo}
handleSetCheckList={handleSetCheckList}
handleSetDifficulty={handleSetDifficulty}
handleSetPriority={handleSetPriority}
handleSetDeadLine={handleSetDeadLine}
addTodoItem={addTodoItem}
toggleTodoItemStatus={toggleTodoItemStatus}
changeTodoItem={changeTodoItem}
deleteTodoItem={deleteTodoItem}
closeModal={closeModal}
startComposition={startComposition}
endComposition={endComposition}
reorder={reorder}
/>
{/* 登録されたTODOリストを表示する */}
<TodoList
todoList={inCompletedList}
id={id}
title={title}
memo={memo}
checkList={checkList}
priority={priority}
difficulty={difficulty}
deadLine={deadLine}
isShowModal={isShowModal}
changeFlg={changeFlg}
composing={composing}
handleSetId={handleSetId}
handleSetTitle={handleSetTitle}
handleSetMemo={handleSetMemo}
handleSetCheckList={handleSetCheckList}
handleSetDifficulty={handleSetDifficulty}
handleSetPriority={handleSetPriority}
handleSetDeadLine={handleSetDeadLine}
handleSetIsShowModal={handleSetIsShowModal}
handleSetChangeFlg={handleSetChangeFlg}
addTodoItem={addTodoItem}
toggleTodoItemStatus={toggleTodoItemStatus}
changeTodoItem={changeTodoItem}
deleteTodoItem={deleteTodoItem}
closeModal={closeModal}
startComposition={startComposition}
endComposition={endComposition}
reorder={reorder}
/>
</div>
</>
)
}
export default App;
useTodoフックから渡された関数deleteTodoItem
などはApp.tsx
では使わないが、App.tsx
でインポートしておかないとTODOを削除しても結果が画面に反映されない(レンダリングされない)。よって、親コンポーネント側で定義し、propsとして子コンポーネントに渡す。
TodoAdd.js → TodoAddModal.tsx
// モーダル表示するためにreact-modalをインポートする
import Modal from "react-modal";
// 期限を入力するためのライブラリとしてDatePickerをインポートする
import DatePicker from "react-datepicker";
// DatePickerで使用するカレンダーのCSSをインポートする
import "react-datepicker/dist/react-datepicker.css"
// バリデーションを行うためのreact-hook-formをインポートする
// 現時点では使用しない
import { useForm } from 'react-hook-form';
import TodoCheckList from "./TodoAddCheckList";
// 重要度用の配列priorityItemsを定義する
const priorityItems = [
{id: 1, value: "低"},
{id: 2, value: "高"}
]
// 難易度用の配列difficultyItemsを定義する
const diffcultyItems = [
{id: 1, value: "易"},
{id: 2, value: "普"},
{id: 3, value: "難"}
]
// モーダル画面のデザインを設定
const customStyles = {
//モーダルの中身
content: {
width: "500px",
height: "700px",
top: "0",
left: "0",
right: "0",
bottom: "0",
margin: "auto",
border: "none",
padding: "30px 120px",
background: "white",
},
//モーダルの外側の部分はoverlayを使用する
overlay: {
background: "rgba(62, 62, 62, 0.75)"
}
};
// react-modalを使用するために宣言する必要あり
// 任意のアプリを設定する create-react-appなら#root
Modal.setAppElement("#root");
const TodoAdd = (props) => {
// react-hook-formの初期設定(現状は使用しない)
// const { register, handleSubmit, watch, formState: { errors } } = useForm();
return (
<div>
{/* モーダル表示したい部分をModalタグで囲む */}
<Modal
// モーダルをの表示処理isOpen
// 表示/非表示はStateのisShowModalで管理する
isOpen={props.isShowModal}
// モーダルが表示された後の処理
// モーダルが表示されている間、背景のスクロールを禁止する
onAfterOpen={() => document.getElementById("root").style.position = "fixed"}
// モーダルが非表示になった後の処理
// モーダルを閉じた後に画面スクロールできるようにする
onAfterClose={() => document.getElementById("root").style.position = "unset"}
// ↓を記述するとモーダル画面の外側をクリックした際にモーダルが閉じる
// onRequestClose={closeModal}
// モーダルの中身/背景のデザインを設定する
style={customStyles}
>
<div>
<div>
<p>タイトル*</p>
<input
type="text"
value={props.title}
onChange={(e) => props.setInputTitle(e.target.value)}
/>
</div>
<p>メモ</p><textarea value={props.memo} onChange={(e) => props.setInputMemo(e.target.value)}/>
<p>チェックリスト</p>
{/* チェックリストはコンポーネント化して別定義する */}
<TodoCheckList
checkList={props.checkList}
setCheckList={props.setCheckList}
reorder={props.reorder}
composing={props.composing}
startComposition={props.startComposition}
endComposition={props.endComposition}
/>
<div>
<p>重要度</p>
{/* map()メソッドを使用して重要度用の配列priorityItemsから要素を取り出す */}
{priorityItems.map((priorityItem) => (
<label key={priorityItem.id}>
<input
type="radio"
value={priorityItem.value}
onChange={(e) => props.setPriority(e.target.value)}
checked={props.priority === priorityItem.value}
/>
{priorityItem.value}
</label>
))}
</div>
<div>
<p>難易度</p>
{/* map()メソッドを使用して難易度用の配列difficultyItemsから要素を取り出す */}
<select defaultValue={props.difficulty} onChange={(e) => props.setDifficulty(e.target.value)}>
{diffcultyItems.map((diffcultyItem) => (
<option key={diffcultyItem.id} value={diffcultyItem.value}>
{diffcultyItem.value}
</option>
))}
</select>
</div>
<div>
<p>期限</p>
{/* 期限はDatePickerを使用する */}
{/* minDateを設定することで選択できる日付を制限できる */}
{/* minDate={new Date()}のように設定すると本日より前の日付は選択できないようになる */}
<DatePicker
selected={props.inputDeadLine}
onChange={(date) => props.setInputDeadLine(date)}
minDate={new Date()}
/>
</div>
<div>
{/* changeFlgの値により表示するボタンを変更する */}
{props.changeFlg ?
<button
type="submit"
onClick={() => {
props.handleChangeTodoItem();
props.closeModal();
}}
>
編集する
</button> :
<button
type="submit"
onClick={() => {
props.handleAddTodoItem();
props.closeModal();
}}
>
作成する
</button>
}
<button onClick={props.closeModal}>キャンセル</button>
</div>
</div>
</Modal>
</div>
);
}
export default TodoAdd;
// モーダル表示するためにreact-modalをインポートする
import Modal from "react-modal";
// 期限を入力するためのライブラリとしてDatePickerをインポートする
import DatePicker from "react-datepicker";
// DatePickerで使用するカレンダーのCSSをインポートする
import "react-datepicker/dist/react-datepicker.css"
// バリデーションを行うためのreact-hook-formをインポートする
// 現時点では使用しない
import { useForm } from 'react-hook-form';
// チェックリストの型をインポートする
import { CheckListType } from "../types"
import TodoAddCheckList from "./TodoAddCheckList";
// propsで渡される値の型定義を行う
// 関数の型定義はこれまでと同様に定義元にカーソルを合わせることで取得する
type TodoAddProps = {
id: string
title: string
memo: string
checkList: CheckListType
priority: string
difficulty: string
deadLine: Date
isShowModal: boolean
changeFlg: boolean
composing: boolean
handleSetId: (id: string) => void
handleSetTitle: (title: string) => void
handleSetMemo: (memo: string) => void
handleSetCheckList: (checkList: CheckListType) => void
handleSetDifficulty: (difficulty: string) => void
handleSetPriority: (priority: string) => void
handleSetDeadLine: (deadLine: Date) => void
addTodoItem: (
title: string,
memo: string,
checkList: CheckListType,
priority: string,
difficulty: string,
deadLine: Date
) => void
toggleTodoItemStatus: (id: string, done: boolean) => void
changeTodoItem: (
id: string,
title: string,
memo: string,
checkList: CheckListType,
priority: string,
difficulty: string,
deadLine: Date
) => void
deleteTodoItem: (id: string) => void
closeModal: () => void
startComposition: () => void
endComposition: () => void
reorder: (list: Array<any>, startIndex: number, endIndex: number) => any[]
}
// 重要度用の配列priorityItemsを定義する
const priorityItems = [
{id: 1, value: "低"},
{id: 2, value: "高"}
]
// 難易度用の配列difficultyItemsを定義する
const diffcultyItems = [
{id: 1, value: "易"},
{id: 2, value: "普"},
{id: 3, value: "難"}
]
// モーダル画面のデザインを設定
const customStyles = {
//モーダルの中身
content: {
width: "500px",
height: "700px",
top: "0",
left: "0",
right: "0",
bottom: "0",
margin: "auto",
border: "none",
padding: "30px 120px",
background: "white",
},
//モーダルの外側の部分はoverlayを使用する
overlay: {
background: "rgba(62, 62, 62, 0.75)"
}
};
// react-modalを使用するために宣言する必要あり
// 任意のアプリを設定する create-react-appなら#root
Modal.setAppElement("#root");
const TodoAddModal = (props: TodoAddProps) => {
// react-hook-formの初期設定(現状は使用しない)
// const { register, handleSubmit, watch, formState: { errors } } = useForm();
/*
##############################
TODO編集処理
##############################
*/
const handleChangeTodoItem = () => {
props.changeTodoItem(
props.id,
props.title,
props.memo,
props.checkList,
props.priority,
props.difficulty,
props.deadLine
);
props.closeModal();
}
/*
##############################
TODO追加処理
##############################
*/
const handleAddTodoItem = () => {
props.addTodoItem(
props.title,
props.memo,
props.checkList,
props.priority,
props.difficulty,
props.deadLine
)
props.closeModal();
}
return (
<div>
{/* モーダル表示したい部分をModalタグで囲む */}
<Modal
// モーダルをの表示処理isOpen
// 表示/非表示はStateのisShowModalで管理する
isOpen={props.isShowModal}
// モーダルが表示された後の処理
// モーダルが表示されている間、背景のスクロールを禁止する
onAfterOpen={() => (document.getElementById("root") as HTMLElement).style.position = "fixed"}
// モーダルが非表示になった後の処理
// モーダルを閉じた後に画面スクロールできるようにする
onAfterClose={() => (document.getElementById("root") as HTMLElement).style.position = "unset"}
// ↓を記述するとモーダル画面の外側をクリックした際にモーダルが閉じる
// onRequestClose={closeModal}
// モーダルの中身/背景のデザインを設定する
style={customStyles}
>
<div>
<div>
<p>タイトル*</p>
<input
type="text"
defaultValue={props.title}
onChange={(e) => props.handleSetTitle(e.target.value)}
/>
</div>
<div>
<p>メモ</p>
<textarea value={props.memo} onChange={(e) => props.handleSetMemo(e.target.value)}/>
</div>
<div>
<p>チェックリスト</p>
{/* チェックリストはコンポーネント化して別定義する */}
<TodoAddCheckList
checkList={props.checkList}
handleSetCheckList={props.handleSetCheckList}
reorder={props.reorder}
composing={props.composing}
startComposition={props.startComposition}
endComposition={props.endComposition}
/>
</div>
<div>
<p>重要度</p>
{/* map()メソッドを使用して重要度用の配列priorityItemsから要素を取り出す */}
{priorityItems.map((priorityItem) => (
<label key={priorityItem.id}>
<input
type="radio"
value={priorityItem.value}
onChange={(e) => props.handleSetPriority(e.target.value)}
checked={props.priority === priorityItem.value}
/>
{priorityItem.value}
</label>
))}
</div>
<div>
<p>難易度</p>
{/* map()メソッドを使用して難易度用の配列difficultyItemsから要素を取り出す */}
<select defaultValue={props.difficulty} onChange={(e) => props.handleSetDifficulty(e.target.value)}>
{diffcultyItems.map((diffcultyItem) => (
<option key={diffcultyItem.id} value={diffcultyItem.value}>
{diffcultyItem.value}
</option>
))}
</select>
</div>
<div>
<p>期限</p>
{/* 期限はDatePickerを使用する */}
{/* slectedをで初期値を設定できる */}
{/* minDateを設定することで選択できる日付を制限できる */}
{/* minDate={new Date()}のように設定すると本日より前の日付は選択できないようになる */}
<DatePicker
dateFormat="yyyy/MM/dd"
selected={props.deadLine}
// null許容型に対して、非nullアサーション(!)を使用することで、
// その型がnull | undefinedではなくTであることをコンパイラに明示できます
// 引数dateを使用する際にdateに値が割り当てられていることを明示する
onChange={(date) => props.handleSetDeadLine(date!)}
minDate={new Date()}
/>
</div>
<div>
{/* changeFlgの値により表示するボタンを変更する */}
{props.changeFlg ?
<button type="submit" onClick={handleChangeTodoItem}>編集する</button> :
<button type="submit" onClick={handleAddTodoItem}>作成する</button>
}
<button onClick={props.closeModal}>キャンセル</button>
</div>
</div>
</Modal>
</div>
);
}
export default TodoAddModal;
TodoAddCheckList.js → TodoAddCheckList.tsx
import React, { useState } from 'react';
// ドラッグ&ドロップのライブラリreact-beautiful-dndをインポートする
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
import TodoCheckItem from "./TodoAddCheckItem";
const TodoCheckList = (props) => {
// チェックリストの現在の個数itemCount
// itemCountを変更する関数setItemCountを定義する
const [itemCount, setItemCount] = useState(0);
// チェックリストを追加するinputの現在の状態変数inputValue
// inputValueを更新する関数setInputValueを定義する
const [inputValue, setInputValue] = useState("");
/*
##############################
チェックリストのCSS
##############################
*/
// 引数:isDraggingOver を使用してドラッグ中とそうでない時のCSSを変更することができる
const getListStyle = (isDraggingOver) => ({
background: 'white',
/* isDraggingOverの型は真偽値、true=ドラッグ中、false=ドラッグ中ではない */
/* border: isDraggingOver ? 'solid 5px lightgray' : 'solid 5px white', */
textAlign: 'left',
});
/*
##############################
チェックアイテムのCSS
##############################
*/
const getItemStyle = (draggableStyle) => ({
marginBottom: '0.5rem',
...draggableStyle
});
/*
##############################
チェックリスト追加処理(エンターキーによる操作)
##############################
*/
// エンターキーで新たなチェックリストを追加できるようにする
const onKeyDown = (e, key, number) => {
switch (key) {
// 変換中でない時にEnterキーでinputを増やす
case "Enter":
// input入力中にエンターを押すとデフォルトではsubmit(送信)になるため
// e.preventDefault();で阻止する
e.preventDefault();
// 変換中ならそのまま何の処理も行わない
if (props.composing) break;
// 変換中でないなら、addCheckList()メソッドでチェックリストを追加
if (number === 0) addCheckList();
break;
default:
break;
}
}
/*
##############################
チェックリストの順番変更処理
##############################
*/
const onDragEnd = (result) => {
// ドロップ先がない場合、そのまま処理を抜ける
if (!result.destination) return;
// 配列の順番を入れ替える
let movedCheckItem = props.reorder(
props.checkList, // 順番を入れ替えたい配列
result.source.index, // 元の配列での位置
result.destination.index // 移動先の配列での位置
);
props.setCheckList(movedCheckItem);
};
/*
##############################
チェックリスト追加処理
##############################
*/
const addCheckList = () => {
// inputが空白ならそのまま何の処理も行わない
if (inputValue === "") return;
// 既存の配列に新たにチェックリストを加える
// チェックリスト内要素の識別に使用されるidはstring(文字列)型でないと警告文が発生してしまう
props.setCheckList([...props.checkList, ...[{id: `item-${itemCount}`, checkItem: inputValue}]]);
// チェックリストを加えたのでカウントアップ
setItemCount(itemCount + 1);
// チェックリストに追加した後、入力内容をクリアする
setInputValue("");
}
/*
##############################
チェックリスト内容変更処理
##############################
*/
const updateCheckList = (index, e) => {
// slice()メソッドを使用してチェックリストのコピーを作成する
const copyCheckList = props.checkList.slice();
// index を使用して対象のチェックリストの内容を書き換える
copyCheckList[index].checkItem = e.target.value;
props.setCheckList(copyCheckList);
}
/*
##############################
チェックリスト削除処理
##############################
*/
const deleteCheckList = (index) => {
// Array.from()メソッドは、反復可能オブジェクトや配列風オブジェクトから
// シャローコピーされた、新しいArrayインスタンスを生成する
const result = Array.from(props.checkList);
// Array.splice()メソッドは、配列を操作するメソッド
// 第2引数はオプション、第1引数に3、第2引数に2を指定した場合、3、4番目の要素を配列から取り出す
result.splice(index, 1);
props.setCheckList(result);
}
return (
// onDragEnd={onDragEnd}→ドラッグ後のイベント処理、タスクの状態や順番を変更する
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="droppable">
{/* Droppableタグでsnapshotは以下のプロパティを持っている */}
{/* snapshot.isDraggingOver:リスト上でアイテムがドラッグ中かどうか */}
{(provided, snapshot) => (
<div
{...provided.droppableProps}
ref={provided.innerRef}
style={getListStyle(snapshot.isDraggingOver)}
>
{props.checkList.map((checkItem, index) => (
<Draggable key={checkItem.id} draggableId={checkItem.id} index={index}>
{/* Draggaleタグでsnapshotは以下のプロパティを持っている */}
{/* snapshot.isDragging:アイテムがドラッグ中かどうか */}
{(provided) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={getItemStyle(provided.draggableProps.style)}
>
<TodoCheckItem
index={index}
checkItem={checkItem}
updateCheckList={updateCheckList}
deleteCheckList={deleteCheckList}
/>
</div>
)}
</Draggable>
))}
{/* ここにドラッグ可能なアイテムを配置 */}
{provided.placeholder}
// 新しいチェックリストを追加するボタン/入力フォーム
<button onClick={() => addCheckList()}>追加</button>
<input
type="text"
value={inputValue}
placeholder="新しいチェックリストを追加"
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={(e) => onKeyDown(e, e.key, 0)}
onCompositionStart={props.startComposition}
onCompositionEnd={props.endComposition}
>
</input>
</div>
)}
</Droppable>
</DragDropContext>
);
}
export default TodoCheckList;
import React, { useState } from 'react';
// 一意なidを生成するulidをインポートする
import {ulid} from "ulid";
// ドラッグ&ドロップのライブラリreact-beautiful-dndをインポートする
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
// チェックリストの型をインポートする
import { CheckListType } from "../types"
import TodoCheckItem from "./TodoAddCheckItem";
// propsで渡される値の型定義を行う
type TodoCheckListProps = {
composing: boolean
reorder: (list: CheckListType, startIndex: number, endIndex: number) => any[]
checkList: CheckListType
handleSetCheckList: (checkList: CheckListType) => void
startComposition: () => void
endComposition: () => void
}
const TodoAddCheckList = (props: TodoCheckListProps) => {
// ↓使用しなくなったのでコメントアウト
// const [itemCount, setItemCount] = useState<number>(0);
// チェックリストを追加するinputの現在の状態変数inputValue
// inputValueを更新する関数setInputValueを定義する
const [inputValue, setInputValue] = useState<string>("");
/*
##############################
チェックリストのCSS
##############################
*/
// 引数:isDraggingOver を使用してドラッグ中とそうでない時のCSSを変更することができる
const getListStyle = (isDraggingOver: boolean) => ({
background: 'white',
/* isDraggingOverの型は真偽値、true=ドラッグ中、false=ドラッグ中ではない */
/* border: isDraggingOver ? 'solid 5px lightgray' : 'solid 5px white', */
textAlign: 'left',
});
/*
##############################
チェックアイテムのCSS
##############################
*/
// 引数draggableStyleの型が分からなかったため、anyで代用
// anyはどんな値でも対応することができるが、TypeScript化する意味が意味が無くなってしまうため
// 一時的な型として用いる
const getItemStyle = (draggableStyle: any) => ({
marginBottom: '0.5rem',
...draggableStyle
});
/*
##############################
チェックリスト追加処理(エンターキーによる操作)
##############################
*/
// エンターキーで新たなチェックリストを追加できるようにする
const onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>, key: string) => {
switch (key) {
// 変換中でない時にEnterキーでinputを増やす
case "Enter":
// input入力中にエンターを押すとデフォルトではsubmit(送信)になるため
// e.preventDefault();で阻止する
e.preventDefault();
// 変換中ならそのまま何の処理も行わない
if (props.composing) break;
// 変換中でないなら、addCheckList()メソッドでチェックリストを追加
addCheckList();
break;
default:
break;
}
}
/*
##############################
チェックリストの順番変更処理
##############################
*/
// resultの型をanyで代用
const onDragEnd = (result: any) => {
// ドロップ先がない場合、そのまま処理を抜ける
if (!result.destination) return;
// 配列の順番を入れ替える
const movedCheckItem = props.reorder(
props.checkList, // 順番を入れ替えたい配列
result.source.index, // 元の配列での位置
result.destination.index // 移動先の配列での位置
);
props.handleSetCheckList(movedCheckItem);
};
/*
##############################
チェックリスト追加処理
##############################
*/
const addCheckList = () => {
// inputが空白ならそのまま何の処理も行わない
if (inputValue === "") return;
// 既存の配列に新たにチェックリストを加える
// チェックリスト内要素の識別に使用されるidはstring(文字列)型でないと警告文が発生してしまう
// idはitemCountを用いたものからulid()を用いた値に変更
props.handleSetCheckList([...props.checkList, ...[{id: ulid(), checkItem: inputValue, done: false}]]);
// チェックリストに追加した後、入力内容をクリアする
setInputValue("");
}
/*
##############################
チェックリスト内容変更処理
##############################
*/
const updateCheckList = (index: number, e:React.ChangeEvent<HTMLInputElement>) => {
// slice()メソッドを使用してチェックリストのコピーを作成する
const copyCheckList = props.checkList.slice();
// index を使用して対象のチェックリストの内容を書き換える
copyCheckList[index].checkItem = e.target.value;
props.handleSetCheckList(copyCheckList);
}
/*
##############################
チェックリスト削除処理
##############################
*/
const deleteCheckList = (index: number) => {
// Array.from()メソッドは、反復可能オブジェクトや配列風オブジェクトから
// シャローコピーされた、新しいArrayインスタンスを生成する
const result = Array.from(props.checkList);
// Array.splice()メソッドは、配列を操作するメソッド
// 第2引数はオプション、第1引数に3、第2引数に2を指定した場合、3、4番目の要素を配列から取り出す
result.splice(index, 1);
props.handleSetCheckList(result);
}
return (
// onDragEnd={onDragEnd}→ドラッグ後のイベント処理、タスクの状態や順番を変更する
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="droppable">
{/* Droppableタグでsnapshotは以下のプロパティを持っている */}
{/* snapshot.isDraggingOver:リスト上でアイテムがドラッグ中かどうか */}
{(provided, snapshot) => (
<div
{...provided.droppableProps}
ref={provided.innerRef}
// Reactコンポーネント内でCSSを使用するとエラーが発生してしまうので as any をつける
style={getListStyle(snapshot.isDraggingOver) as any}
>
{props.checkList.map((checkItem, index: number) => (
<Draggable key={checkItem.id} draggableId={checkItem.id} index={index}>
{/* Draggaleタグでsnapshotは以下のプロパティを持っている */}
{/* snapshot.isDragging:アイテムがドラッグ中かどうか */}
{(provided) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={getItemStyle(provided.draggableProps.style)}
>
<TodoCheckItem
index={index}
checkItem={checkItem}
updateCheckList={updateCheckList}
deleteCheckList={deleteCheckList}
/>
</div>
)}
</Draggable>
))}
{/* ここにドラッグ可能なアイテムを配置 */}
{provided.placeholder}
{/* 新しいチェックリストを追加するボタン/入力フォーム */}
<button onClick={() => addCheckList()}>追加</button>
<input
type="text"
value={inputValue}
placeholder="新しいチェックリストを追加"
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={(e) => onKeyDown(e, e.key)}
onCompositionStart={props.startComposition}
onCompositionEnd={props.endComposition}
>
</input>
</div>
)}
</Droppable>
</DragDropContext>
);
}
export default TodoAddCheckList;
TodoList.js → TodoList.tsx
// TodoItemコンポーネントをインポートする
import TodoItem from "./TodoItem";
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
const TodoList = (props) => {
/*
##############################
TODOリストの順番変更処理
##############################
*/
const onDragEnd = (result) => {
// ドロップ先がない場合、そのまま処理を抜ける
if (!result.destination) return;
// 配列の順番を入れ替える
let movedCheckItem = props.reorder(
props.todoList, // 順番を入れ替えたい配列
result.source.index, // 元の配列での位置
result.destination.index // 移動先の配列での位置
);
props.setTodoList(movedCheckItem);
};
/*
##############################
TODOリストのCSS
##############################
*/
const getListStyle = (isDraggingOver) => ({
background: 'white',
/* isDraggingOverの型は真偽値、true=ドラッグ中、false=ドラッグ中ではない */
/* border: isDraggingOver ? 'solid 5px lightgray' : 'solid 5px white', */
textAlign: 'left',
});
/*
##############################
TODOアイテムのCSS
##############################
*/
const getItemStyle = (draggableStyle) => ({
marginBottom: '0.5rem',
...draggableStyle
});
return (
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="droppable">
{/* Droppableタグでsnapshotは以下のプロパティを持っている */}
{/* snapshot.isDraggingOver:リスト上でアイテムがドラッグ中かどうか */}
{(provided, snapshot) => (
<div
{...provided.droppableProps}
ref={provided.innerRef}
style={getListStyle(snapshot.isDraggingOver)}
>
{/* ドラッグできる要素 */}
{props.todoList.map((todoItem, index) => (
<Draggable key={todoItem.id} draggableId={todoItem.id} index={index}>
{/* Draggaleタグでsnapshotは以下のプロパティを持っている */}
{/* snapshot.isDragging:アイテムがドラッグ中かどうか */}
{(provided) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={getItemStyle(provided.draggableProps.style)}
>
<TodoItem
todoItem={todoItem}
toggleTodoItemStatus={props.toggleTodoItemStatus}
changeTodoItem={props.changeTodoItem}
deleteTodoItem={props.deleteTodoItem}
getSingleTodosData={props.getSingleTodosData}
setInputId={props.setInputId}
setInputTitle={props.setInputTitle}
setInputMemo={props.setInputMemo}
setCheckList={props.setCheckList}
setPriority={props.setPriority}
setDifficulty={props.setDifficulty}
setInputDeadLine={props.setInputDeadLine}
isShowModal={props.isShowModal}
setIsShowModal={props.setIsShowModal}
setChangeFlg={props.setChangeFlg}
/>
</div>
)}
</Draggable>
))}
{/* ここにドラッグ可能なアイテムを配置 */}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
)
}
export default TodoList;
// TodoItemコンポーネントをインポートする
import TodoItem from "./TodoItem";
// ドラッグ&ドロップのライブラリreact-beautiful-dndをインポートする
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
// TODOリスト、チェックリストの型をインポートする
import { CheckListType, TodoListType } from "../types";
type TodoListProps = {
todoList: TodoListType
id: string
title: string
memo: string
checkList: CheckListType
priority: string
difficulty: string
deadLine: Date
isShowModal: boolean
changeFlg: boolean
composing: boolean
handleSetId: (id: string) => void
handleSetTitle: (title: string) => void
handleSetMemo: (memo: string) => void
handleSetCheckList: (checkList: CheckListType) => void
handleSetDifficulty: (difficulty: string) => void
handleSetPriority: (priority: string) => void
handleSetDeadLine: (deadLine: Date) => void
handleSetIsShowModal: (isShowModal: boolean) => void
handleSetChangeFlg: (changeFlg: boolean) => void
addTodoItem: (
title: string,
memo: string,
checkList: CheckListType,
priority: string,
difficulty: string,
deadLine: Date
) => void
toggleTodoItemStatus: (id: string, done: boolean) => void
changeTodoItem: (
id: string,
title: string,
memo: string,
checkList: CheckListType,
priority: string,
difficulty: string,
deadLine: Date
) => void
deleteTodoItem: (id: string) => void
closeModal: () => void
startComposition: () => void
endComposition: () => void
reorder: (list: Array<any>, startIndex: number, endIndex: number) => any[]
}
const TodoList = (props: TodoListProps) => {
/*
##############################
TODOリストの順番変更処理
##############################
*/
const onDragEnd = (result: any) => {
// ドロップ先がない場合、そのまま処理を抜ける
if (!result.destination) return;
// 配列の順番を入れ替える
const movedTodoItem = props.reorder(
props.todoList, // 順番を入れ替えたい配列
result.source.index, // 元の配列での位置
result.destination.index // 移動先の配列での位置
);
};
/*
##############################
TODOリストのCSS
##############################
*/
const getListStyle = (isDraggingOver: boolean) => ({
background: 'white',
/* isDraggingOverの型は真偽値、true=ドラッグ中、false=ドラッグ中ではない */
/* border: isDraggingOver ? 'solid 5px lightgray' : 'solid 5px white', */
textAlign: 'left',
});
/*
##############################
TODOアイテムのCSS
##############################
*/
const getItemStyle = (draggableStyle: any) => ({
marginBottom: '0.5rem',
...draggableStyle
});
return (
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="droppable">
{/* Droppableタグでsnapshotは以下のプロパティを持っている */}
{/* snapshot.isDraggingOver:リスト上でアイテムがドラッグ中かどうか */}
{(provided, snapshot) => (
<div
{...provided.droppableProps}
ref={provided.innerRef}
// Reactコンポーネント内でCSSを使用するとエラーが発生してしまうので as any をつける
style={getListStyle(snapshot.isDraggingOver) as any}
>
{/* ドラッグできる要素 */}
{props.todoList.map((todoItem, index) => (
<Draggable key={todoItem.id} draggableId={todoItem.id!} index={index}>
{/* Draggaleタグでsnapshotは以下のプロパティを持っている */}
{/* snapshot.isDragging:アイテムがドラッグ中かどうか */}
{(provided) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={getItemStyle(provided.draggableProps.style)}
>
<TodoItem
todoItem={todoItem}
id={props.id}
title={props.title}
memo={props.memo}
checkList={props.checkList}
priority={props.priority}
difficulty={props.difficulty}
deadLine={props.deadLine}
isShowModal={props.isShowModal}
changeFlg={props.changeFlg}
composing={props.composing}
handleSetId={props.handleSetId}
handleSetTitle={props.handleSetTitle}
handleSetMemo={props.handleSetMemo}
handleSetCheckList={props.handleSetCheckList}
handleSetDifficulty={props.handleSetDifficulty}
handleSetPriority={props.handleSetPriority}
handleSetDeadLine={props.handleSetDeadLine}
handleSetIsShowModal={props.handleSetIsShowModal}
handleSetChangeFlg={props.handleSetChangeFlg}
addTodoItem={props.addTodoItem}
toggleTodoItemStatus={props.toggleTodoItemStatus}
changeTodoItem={props.changeTodoItem}
deleteTodoItem={props.deleteTodoItem}
closeModal={props.closeModal}
startComposition={props.startComposition}
endComposition={props.endComposition}
reoreder={props.reorder}
/>
</div>
)}
</Draggable>
))}
{/* ここにドラッグ可能なアイテムを配置 */}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
)
}
export default TodoList;
TodoItem.js → TodoItem.tsx
import { useState, useEffect } from "react";
import TodoAdd from "./TodoAdd";
const TodoItem = (props) => {
// 期限までの時間を設定する変数deadLine
// deadLineを更新する関数setDeadLineを定義する
const [deadLine, setDeadLine] = useState("");
/*
##############################
完了/未完了変更処理
##############################
*/
const handleToggleTodoItemStatus = () => {
props.toggleTodoItemStatus(props.todoItem.id, props.todoItem.done);
}
/*
##############################
TODOリスト削除処理
##############################
*/
const handleDeleteTodoItem = () => {
props.deleteTodoItem(props.todoItem.id);
}
/*
##############################
TODO追加モーダル表示処理
##############################
*/
const handleChangeTodo = () => {
props.setIsShowModal(true); // TODO追加モーダルを表示する
props.setChangeFlg(true); // 編集/作成を切り替える
props.setInputId(props.todoItem.id);
props.setInputTitle(props.todoItem.title);
props.setInputMemo(props.todoItem.memo);
props.setCheckList(props.todoItem.checkList);
props.setPriority(props.todoItem.priority);
props.setDifficulty(props.todoItem.difficulty);
props.setInputDeadLine(new Date(props.todoItem.deadLine)); // DatePickerに日付をDate型にしてから渡す
}
/*
##############################
期限までの残り時間を表示する
##############################
*/
const handleCountDown = () => {
const nowDate = new Date(); // 本日の日時を取得
const deadLineDate = new Date(props.todoItem.deadLine); // 期限の日時を取得
const diffDate = deadLineDate.getTime() - nowDate.getTime(); // 期限までの残り時間を取得
// 期限が過ぎていない場合
if (diffDate > 0) {
setDeadLine(`${Math.floor(diffDate / 1000 / 60 / 60 / 24)}日後`);
// 期限が過ぎている場合
} else {
setDeadLine("期限切れ")
}
}
// useEffectを使用してコンポーネントのマウント後に関数handleCountDownを実行する
// useEffectの第2引数を空の依存配列[]にすることで初回の画面レンダリング時に関数handleCountDownを実行する
useEffect(handleCountDown, []);
/*
##############################
チェックボックス更新処理
##############################
*/
// 現在時点で関数handleChangeCheckは機能していない
const handleChangeCheck = () => {
};
return (
<div>
<h3>{props.todoItem.title}</h3>
<p>{props.todoItem.memo}</p>
{/* map()を利用してcheckListの要素を1つひとつ取り出す */}
{props.todoItem.checkList.map((checkItem) => (
<label key={checkItem.id} style={{display: "block"}}>
<input type="checkbox" value={checkItem.chckItem} onChange={handleChangeCheck}/>
{checkItem.checkItem}
</label>
))}
<p>期限:
{/* 最初にタスクの完了/未完了を判定する、その後期限が過ぎていないか判定する */}
{/* 上記の条件をクリアした場合、期限までの時間を表示する */}
{props.todoItem.done ? "完了済み" : deadLine}
</p>
{/* ボタンをクリックすることで関数handleToggleTodoItemStatusを実行する */}
{/* ボタンをクリックすることでTODOの状態(完了/未完了)を反転させる */}
<button onClick={handleToggleTodoItemStatus}>
{props.todoItem.done ? "未完了リストへ" : "完了リストへ"}
</button>
{/* ボタンをクリックすることで関数handleDeleteTodoItemを実行する */}
{/* ボタンをクリックすることでTODOを削除する */}
<button onClick={handleDeleteTodoItem}>削除</button>
{/* ボタンをクリックすることで関数handleChangeTodoItemを実行する */}
{/* 関数handleChangeTodoItemが実行されるとモーダル画面が表示される */}
<button onClick={handleChangeTodo}>編集</button>
{props.isShowModal && <TodoAdd />}
</div>
);
}
export default TodoItem;
import { useState, useEffect } from "react";
// TODOリスト、チェックリストの型をインポートする
import { TodoItemType, CheckListType } from "../types"
import TodoAddModal from "./TodoAddModal";
type TodoItemProps = {
todoItem: TodoItemType
id: string
title: string
memo: string
checkList: CheckListType
priority: string
difficulty: string
deadLine: Date
isShowModal: boolean
changeFlg: boolean
composing: boolean
handleSetId: (id: string) => void
handleSetTitle: (title: string) => void
handleSetMemo: (memo: string) => void
handleSetCheckList: (checkList: CheckListType) => void
handleSetDifficulty: (difficulty: string) => void
handleSetPriority: (priority: string) => void
handleSetDeadLine: (deadLine: Date) => void
handleSetIsShowModal: (isShowModal: boolean) => void
handleSetChangeFlg: (changeFlg: boolean) => void
addTodoItem: (
title: string,
memo: string,
checkList: CheckListType,
priority: string,
difficulty: string,
deadLine: Date
) => void
toggleTodoItemStatus: (id: string, done: boolean) => void
changeTodoItem: (
id: string,
title: string,
memo: string,
checkList: CheckListType,
priority: string,
difficulty: string,
deadLine: Date
) => void
deleteTodoItem: (id: string) => void
closeModal: () => void
startComposition: () => void
endComposition: () => void
reoreder: (list: Array<any>, startIndex: number, endIndex: number) => any[]
}
const TodoItem = (props: TodoItemProps) => {
// 期限までの時間を設定する変数deadLine
// deadLineを更新する関数setDeadLineを定義する
const [deadLine, setDeadLine] = useState("");
/*
##############################
モーダル表示処理
##############################
*/
const handleChangeTodo = () => {
// todoItemの型である`TodoItemType`の要素は`undifined`を許容する型になっており、
// その型をidやtitleなどのundifinedを許容しない型に設定することはできないため、
// !をつけることで値が設定されていることを明示してあげる必要がある
props.handleSetId(props.todoItem.id!); // 対象のTODOからIDを取得する
props.handleSetTitle(props.todoItem.title!); // 対象のTODOからタイトルを取得する
props.handleSetMemo(props.todoItem.memo!); // 対象のTODOからメモを取得する
props.handleSetCheckList(props.todoItem.checkList!); // 対象のTODOからチェックリストを取得する
props.handleSetDifficulty(props.todoItem.difficulty!); // 対象のTODOから難易度を取得する
props.handleSetPriority(props.todoItem.priority!); // 対象のTODOから重要度を取得する
props.handleSetDeadLine(props.todoItem.deadLine!); // 対象のTODOから期限を取得する
// 編集ボタンを表示するよう変更する
props.handleSetChangeFlg(true);
// モーダルを表示する
props.handleSetIsShowModal(true);
}
/*
##############################
期限までの残り時間を表示する
##############################
*/
const handleCountDown = () => {
const nowDate = new Date(); // 本日の日時を取得
const deadLineDate = new Date(props.todoItem.deadLine!); // 期限の日時を取得
const diffDate = deadLineDate.getTime() - nowDate.getTime(); // 期限までの残り時間を取得
// 期限が過ぎていない場合
if (diffDate > 0) {
setDeadLine(`${Math.floor(diffDate / 1000 / 60 / 60 / 24)}日後`);
// 期限が今日の場合
} else if (diffDate === 0) {
setDeadLine("今日");
// 期限が過ぎている場合
} else {
setDeadLine("期限切れ")
}
}
// useEffectを使用してコンポーネントのマウント後に関数handleCountDownを実行する
// useEffectの第2引数にprops.todoItem.deadLineを設定することでprops.todoItem.deadLineが
// 更新される度に関数handleCountDownを実行する
useEffect(handleCountDown, [props.todoItem.deadLine]);
/*
##############################
チェックボックス更新処理
##############################
*/
// 現在時点で関数handleChangeCheckは機能していない
const handleChangeCheck = () => {
};
return (
<div>
<h3>{props.todoItem.title}</h3>
<input type="hidden"></input>
<p>{props.todoItem.memo}</p>
{/* map()を利用してcheckListの要素を1つひとつ取り出す */}
{/* map()メソッドを使用する対象の配列に「! = undifined」を設定するとエラー */}
{props.todoItem.checkList?.map((checkItem) => (
<label key={checkItem.id} style={{display: "block"}}>
<input type="checkbox" value={checkItem.checkItem} onChange={handleChangeCheck}/>
{checkItem.checkItem}
</label>
))}
<p>期限:
{/* 最初にタスクの完了/未完了を判定する、その後期限が過ぎていないか判定する */}
{/* 上記の条件をクリアした場合、期限までの時間を表示する */}
{props.todoItem.done ? "完了済み" : deadLine}
</p>
{/* ボタンをクリックすることで関数handleToggleTodoItemStatusを実行する */}
{/* ボタンをクリックすることでTODOの状態(完了/未完了)を反転させる */}
<button onClick={() => {props.toggleTodoItemStatus(props.todoItem.id!, props.todoItem.done!)}}>
{props.todoItem.done ? "未完了リストへ" : "完了リストへ"}
</button>
{/* ボタンをクリックすることで関数handleDeleteTodoItemを実行する */}
{/* ボタンをクリックすることでTODOを削除する */}
<button onClick={() => {props.deleteTodoItem(props.todoItem.id!)}}>削除</button>
{/* ボタンをクリックすることで関数handleChangeTodoItemを実行する */}
{/* 関数handleChangeTodoItemが実行されるとモーダル画面が表示される */}
<button onClick={handleChangeTodo}>編集</button>
{props.isShowModal &&
// JavaScript時にはpropsをTodoAddModalコンポーネントに渡さなくても良かったが、
// TypeScript時ではTodoAddModalコンポーネントで定義している型と同じpropsを
// 渡さなければならない
<TodoAddModal
id={props.id}
title={props.title}
memo={props.memo}
checkList={props.checkList}
priority={props.priority}
difficulty={props.difficulty}
deadLine={props.deadLine}
isShowModal={props.isShowModal}
changeFlg={props.changeFlg}
composing={props.composing}
handleSetId={props.handleSetId}
handleSetTitle={props.handleSetTitle}
handleSetMemo={props.handleSetMemo}
handleSetCheckList={props.handleSetCheckList}
handleSetDifficulty={props.handleSetDifficulty}
handleSetPriority={props.handleSetPriority}
handleSetDeadLine={props.handleSetDeadLine}
addTodoItem={props.addTodoItem}
toggleTodoItemStatus={props.toggleTodoItemStatus}
changeTodoItem={props.changeTodoItem}
deleteTodoItem={props.deleteTodoItem}
closeModal={props.closeModal}
startComposition={props.startComposition}
endComposition={props.endComposition}
reorder={props.reoreder}
/>
}
</div>
);
}
export default TodoItem;
最後に
useEffectに関して、第2引数に空の依存配列[]
を設定するとWarning文が出てしまう可能性がありますが、以下で対策することができます。
useEffect(sampleFunc,
// ↓のコメント文を記述することで空の依存配列を定義していてもWarning文が表示されない
// eslint-disable-next-line react-hooks/exhaustive-deps
[]);
今回参考にした?
、!
の解説については以下のサイトを参考にしました。
以上でJavaScript → TypeScriptへの変換は完了しました。
次回はMaterial UI
を使用して画面レイアウトを作成していきます。