Riot.js Redux Rails 8 / 8 「チェックしてあるタスクを削除出来るようにする」
はじめに
フロントにRiot.jsとReduxを使いサーバーサイドにRailsを使い、初めに環境構築し、次にReduxの使い方の流れをみて、最終的に簡単なTodoAppを作成する。
環境構築
- Riot.js Redux Rails 1 / 8 「環境構築」
簡単な例でReduxの流れをみる
- Riot.js Redux Rails 2 / 8 「Reduxを使ってtitleを表示させる」
- Riot.js Redux Rails 3 / 8 「formを追加しtitleの変更を可能にする」
- Riot.js Redux Rails 4 / 8 「3で作成したformを、title-formタグとして独立させる」
Todo Appの作成
- Riot.js Redux Rails 5 / 8 「Todoリストを表示する」
- Riot.js Redux Rails 6 / 8 「新たなTodoを追加出来るようにする」
- Riot.js Redux Rails 7 / 8 「完了したTodoをチェック出来るようにする」
- Riot.js Redux Rails 8 / 8 「チェックしてあるタスクを削除出来るようにする」
最終的に完成したTodo App
https://github.com/atfeo/Riot_Redux_Rails
参考
Riot公式のTodoのようにチェックしてあるタスクを全て削除するボタンを作成する。
Rails
routes
まず一度に複数削除する場合、サーバー側のurlはどうすれば良いのかわからなかった。 試しに
# ./config/routes.rb namespace :api, format: 'json' do resources :tasks do delete :del_tasks end end
としたところ
$ rails routes
でルーティングを確認するとurlが
/api/tasks/:task_id/del_tasks(.:format)の様になった。
複数削除なので:task_idがいらないと思い、
# ./config/routes.rb namespace :api, format: 'json' do resources :tasks do delete :del_tasks, on: :collection end end
としたところ
/api/tasks/del_tasks(.:format)の様になったのでこれを使う。
controller
一度に複数削除する方法がわからないのでidを配列で受け取り一つずつ処理するようにした。
# ./app/controllers/api/tasks_controller.rb def del_tasks ids = params[:ids] ids.each do |id| if Task.find(id).destroy head :no_content else head :unprocessable_entity end end end private def task_params # idsを追加 params.permit(:name, :isComplete, :ids) end
サーバー側はこれで終了。
Del Tasksボタン
todo-app.tagにDel Tasksのbuttonタグをマウントさせ、処理のためのメソッドも追加。
// ./client/src/tags/todo-app.tag </task-form> // 追加 <button class="del" onclick={handleDeleteTasks} disabled={this.state.tasks.filter(complete).length == 0} > Del Tasks X {this.state.tasks.filter(complete).length} </button> <div style="clear: both"></div> // scriptに追加 complete(task) { return task.isComplete == true } handleDeleteTasks() { // 大変だった箇所。チェックしてあるタスクのidをどのように取得するか迷った。 let ids = [] // まずチェックされているタスクだけの配列を作成 const comp = this.state.tasks.filter((task) => this.complete(task)) // チェックされているタスクの数だけfor文で処理 for (let i = 0; i < comp.length; i += 1) { // チェックされているタスクのidをidsに格納 ids[i] = comp[i].id } store.dispatch(actions.deleteTasks(ids)) }
Del Tasksボタンをformの隣に配置するためにtask-formタグにstyleを加える。
// ./client/src/tags/task-form.tag <style scoped> :scope form { float: left; } </style>
先程決めたサーバーのURLに配列idsを送るメソッドなどをactions.jsに作成。
// ./client/src/actions.js module.exports = { loadTasks, addTask, textExists, toggleComplete, // 追加 deleteTasks, }; function deleteTasks(ids) { return (dispatch) => { $.ajax({ url: '/api/tasks/del_tasks', type: 'DELETE', dataType: 'json', data: { ids }, success: () => { dispatch(deletedTasks(ids)); }, error: (xhr, status, err) => { dispatch(toggleLoading(false)); console.log('/api/tasks/del_tasks', status, err.toString()); }, }); }; } function deletedTasks(ids) { return { type: 'DELETED_TASKS', data: ids }; }
一番大変だった箇所。どうやって削除後の新しい配列を作るかかなり迷った。
// ./client/src/index.js case 'DELETED_TASKS': { // stateを直接いじってはいけないらしいのでコピー let newTasks = state.tasks.slice(); // 配列で渡されたidの数だけfor文で繰り返す。 for (let i = 0; i < action.data.length; i += 1) { // 配列で渡されたidから、配列中のインデックスを取得 const taskIndex = newTasks.findIndex((task) => { return task.id == action.data[i]; }); // 取得したインデックスを利用しコピーした配列から直接、破壊的に削除。 newTasks.splice(taskIndex, 1); } return Object.assign({}, state, { tasks: newTasks }); }
cssに以下を追加しDel Tasksボタンの色を変える。
/* ./app/assets/stylesheets/application.css */ .del { background-color: #d21073; border: 1px solid rgba(0,0,0,.2); }
ブラウザでlocalhost:5000を更新する。チェックの数によってDel Tasksボタンの数値が変わること、ボタンを押して削除出来ることを確認。
ここまででほぼ完成。ついでに一つずつ削除する処理も追加しておく。
Rails
Controller
# ./app/controllers/api/tasks_controller.rb def destroy if Task.find(params[:id]).destroy head :no_content else head :unprocessable_entity end end
Delボタンの追加
いつもの流れ。まずタグをtask-list.tagに追加。
// ./client/src/tags/task-list.tag </label> // 追加 <button class="del" id={task.id} show={task.isComplete} onclick={handleClick}> Del </button> // scriptに追加 handleClick(e) { this.opts.handledeletetask(e.target.id) } // styleを追加 <style scoped> :scope .del { float: right; } </style>
todo-app.tagにもろもろ追加。
// ./client/src/tags/todo-app.tag <task-list tasks={this.state.tasks} handlecheck={handleTaskCompletionChange} // 追加 handledeletetask={handleDeleteTask}> </task-list> // scriptに追加 handleDeleteTask(id) { store.dispatch(actions.deleteTask(id)) }
サーバーと通信するメソッドなどをactions.jsに追加。
// ./client/src/actions.js module.exports = { loadTasks, addTask, textExists, toggleComplete, deleteTasks, // 追加 deleteTask, }; function deleteTask(id) { return (dispatch) => { dispatch(toggleLoading(true)); $.ajax({ url: `/api/tasks/${id}`, type: 'DELETE', dataType: 'json', success: () => { dispatch(deletedTask(id)); dispatch(toggleLoading(false)); }, error: (xhr, status, err) => { dispatch(toggleLoading(false)); dispatch(tempErrorMessage('API Error')); console.log('/api/tasks/del_tasks', status, err.toString()); }, }); }; } function deletedTask(id) { return { type: 'DELETED_TASK', data: id }; }
アクションを送られたときの処理をindex.jsに追加。
// ./client/src/index.js case 'DELETED_TASK': { const newTasks = state.tasks.filter((task) => { return task.id != action.data; }); return Object.assign({}, state, { tasks: newTasks }); }
ブラウザでlocalhost:5000を更新する。チェックしてあるタスクの右にDelボタンが表示されること、Delボタンを押して削除出来ることを確認。
Todoアプリ完成。