# React + TypeScript TodoList npx create-react-app todo-typescript --template typescript ### App.css書き換え ``` *, *:active, *:hover, *:before, *:after { box-sizing: border-box; } .inner { max-width: 700px; margin: 0 auto; } .task-list { list-style: none; margin: 0; padding: 0; border-top: 1px solid #d7d2cd; } .task-list li { padding: 20px 10px; border-bottom: 1px solid #d7d2cd; display: flex; align-items: center; position: relative; } .task-list li.done span { text-decoration: line-through; } .task-list li label { width: calc(100% - 100px); display: block; cursor: pointer; } .task-list li span { display: block; } .task-list li .btn { display: none; width: 80px; position: absolute; right: 10px; padding: 0.4em 1em; font-size: 13px; z-index: 10; } .task-list li:hover .btn { display: block; } .inputForm { margin-bottom: 40px; background: #f9f3ee; padding: 40px 0; border-bottom: 1px solid #d7d2cd; } .inputForm .inner { display: flex; } .inputForm .input { width: 80%; font-size: 15px; outline: none; border: solid 3px #d7d2cd; padding: 10px; border-radius: 7px; margin-right: 10px; } .inputForm .input:focus { background: #f9f9f0; } .inputForm .btn { width: 20%; } .btn { font-size: 15px; cursor: pointer ; font-weight: bold; display: inline-block; padding: 10px 15px; text-decoration: none; background: #668ad8; color: #FFF; border-bottom: solid 4px #627295; border-radius: 7px; outline: none; } .btn:active { transform: translateY(4px); border-bottom: none; margin-bottom: 4px; } .btn.is-delete { background-color: #d86681; border-bottom-color: #956270; } .checkbox-input { display: none; } .checkbox-label { padding-left: 30px; position: relative; margin-right: 20px; } .checkbox-label::before { content: ""; display: block; position: absolute; top: 2px; left: 0; width: 18px; height: 18px; border: 1px solid #d7d2cd; border-radius: 4px; } .checkbox-input:checked + .checkbox-label { color: #999; } .checkbox-input:checked + .checkbox-label::after { content: ""; display: block; position: absolute; top: 0; left: 5px; width: 7px; height: 14px; transform: rotate(40deg); border-bottom: 3px solid #d01137; border-right: 3px solid #d01137; } ``` mkdir src/components ### 型の設定 src/components/Types.ts ``` export type Task = { id: number; title: string; done: boolean; }; ``` ### TaskItem作成 src/components/TaskItem.tsx ``` import React from 'react' import { Task } from './Types' type Props = { task: Task handleDone: (task: Task) => void handleDelete: (task: Task) => void } const TaskItem: React.FC<Props> = ({ task, handleDone, handleDelete }) => { return ( <li className={task.done ? 'done' : ''}> <label> <input type="checkbox" className="checkbox-input" onClick={() => handleDone(task)} defaultChecked={task.done} /> <span className="checkbox-label">{ task.title }</span> </label> <button onClick={() => handleDelete(task)} className="btn is-delete" >削除</button> </li> ) } export default TaskItem ``` ### TaskList.tsx src/components/TaskList.tsx ``` import React from 'react' import TaskItem from './TaskItem' import { Task } from './Types' type Props = { tasks: Task[] setTasks: React.Dispatch<React.SetStateAction<Task[]>> } const TaskList: React.FC<Props> = ({ tasks, setTasks }) => { const handleDone = (task: Task) => { setTasks(prev => prev.map(t => t.id === task.id ? { ...task, done: !task.done } : t )) } const handleDelete = (task: Task) => { setTasks(prev => prev.filter(t => t.id !== task.id )) } return ( <div className="inner"> { tasks.length <= 0 ? '登録されたTODOはありません。' : <ul className="task-list"> { tasks.map( task => ( <TaskItem key={task.id} task={task} handleDelete={handleDelete} handleDone={handleDone} /> )) } </ul> } </div> ) } export default TaskList ``` ### TaskInput.tsx src/components/TaskInput.tsx ``` import React, { useState } from 'react'; import { Task } from './Types'; type Props = { setTasks: React.Dispatch<React.SetStateAction<Task[]>>; tasks: Task[]; }; const TaskInput: React.FC<Props> = ({ setTasks, tasks }) => { const [inputTitle, setInputTitle] = useState<string>(''); const [count, setCount] = useState<number>(tasks.length + 1); const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { setInputTitle(e.target.value); }; const handleSubmit = () => { setCount(count + 1); const newTask: Task = { id: count, title: inputTitle, done: false, }; setTasks([newTask, ...tasks]); setInputTitle(''); }; return ( <div> <div className='inputForm'> <div className='inner'> <input type='text' className='input' value={inputTitle} onChange={handleInputChange} /> <button onClick={handleSubmit} className='btn is-primary'> 追加 </button> </div> </div> </div> ); }; export default TaskInput; ``` ### App.tsx書き換え ``` import React, { useState } from 'react'; import TaskList from './components/TaskList'; import TaskInput from './components/TaskInput'; import { Task } from './components/Types'; import './App.css'; const initialState: Task[] = [ { id: 2, title: '次にやるやつ', done: false, }, { id: 1, title: 'はじめにやるやつ', done: true, }, ]; const App: React.FC = () => { const [tasks, setTasks] = useState(initialState); return ( <div> <TaskInput setTasks={setTasks} tasks={tasks} /> <TaskList setTasks={setTasks} tasks={tasks} /> </div> ); }; export default App; ``` <!-- ## styled-componentsに一意のclassを張れるようにする。 yarn add styled-components @types/styled-components --> <!-- あとは授業内でやればいいかな。 -->