Riot.js Observable Rails 5 / 5 「チェックしてあるタスクを削除出来るようにする」
はじめに
Riot.js Redux Railsシリーズで作ったTodoアプリのRedux部分をObservableを使って書いた。 (Observerパターンとか全然わかってない。ReduxをObservableで置き換える意味があるかもわからない。なんとなく似ていると思ったのでやってみた。)
Riot.js Redux Railsで作ったTodoアプリと全く同じものを作成するのでコードが大分重なっている(Redux部分をObservableにしたのと、それに伴う変更以外全く同じ)が、今回の一連の記事だけ読んでも出来るように省略はしない。
環境構築
Todo Appの作成
- Riot.js Observable Rails 2 / 5 「Todoリストを表示する」
- Riot.js Observable Rails 3 / 5 「新たなTodoを追加出来るようにする」
- Riot.js Observable Rails 4 / 5 「完了したTodoをチェック出来るようにする」
- Riot.js Observable Rails 5 / 5 「チェックしてあるタスクを削除出来るようにする」
最終的に完成したTodo App
https://github.com/atfeo/Riot_Observable_Rails
参考
今回はRiot公式のTodoのようにチェックしてあるタスクを全て削除するボタンを作成する。
Rails
routes
まず複数削除のためのルートを追加
# ./config/routes.rb namespace :api, format: 'json' do resources :tasks do delete :del_tasks, on: :collection end end
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() { let ids = [] const comp = this.state.tasks.filter((task) => this.complete(task)) for (let i = 0; i < comp.length; i += 1) { ids[i] = comp[i].id } actions.deleteTasks(store, 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(store, ids) { $.ajax({ url: '/api/tasks/del_tasks', type: 'DELETE', dataType: 'json', data: { ids }, success: () => { deletedTasks(store, ids); }, error: (xhr, status, err) => { toggleLoading(store, false); tempErrorMessage(store, 'API Error'); console.log('/api/tasks/del_tasks', status, err.toString()); }, }); } function deletedTasks(store, ids) { store.trigger('DELETED_TASKS', { data: ids }); }
イベントがトリガーされた時の処理をindex.jsに追加。
// ./client/src/index.js store.on('DELETED_TASKS', (action) => { let newTasks = store.getState().tasks.slice(); for (let i = 0; i < action.data.length; i += 1) { const taskIndex = newTasks.findIndex((task) => { return task.id == action.data[i]; }); newTasks.splice(taskIndex, 1); } store.setState({ 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ボタンの追加
まず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) { actions.deleteTask(store, id) }
サーバーと通信するメソッドなどをactions.jsに追加。
// ./client/src/actions.js module.exports = { loadTasks, addTask, textExists, toggleComplete, deleteTasks, // 追加 deleteTask, }; function deleteTask(store, id) { toggleLoading(store, true); $.ajax({ url: `/api/tasks/${id}`, type: 'DELETE', dataType: 'json', success: () => { deletedTask(store, id); toggleLoading(store, false); }, error: (xhr, status, err) => { toggleLoading(store, false); tempErrorMessage(store, 'API Error'); console.log('/api/tasks/del_tasks', status, err.toString()); }, }); } function deletedTask(store, id) { store.trigger('DELETED_TASK', { data: id }); }
イベントがトリガーされた時の処理をindex.jsに追加。
// ./client/src/index.js store.on('DELETED_TASK', (action) => { const newTasks = store.getState().tasks.filter((task) => { return task.id != action.data; }); store.setState({ tasks: newTasks }); });
ブラウザでlocalhost:5000を更新する。チェックしてあるタスクの右にDelボタンが表示されること、Delボタンを押して削除出来ることを確認。
Observable版Todoアプリの完成。
Riot.js Observable Rails 4 / 5 「完了したTodoをチェック出来るようにする」
はじめに
Riot.js Redux Railsシリーズで作ったTodoアプリのRedux部分をObservableを使って書いた。 (Observerパターンとか全然わかってない。ReduxをObservableで置き換える意味があるかもわからない。なんとなく似ていると思ったのでやってみた。)
Riot.js Redux Railsで作ったTodoアプリと全く同じものを作成するのでコードが大分重なっている(Redux部分をObservableにしたのと、それに伴う変更以外全く同じ)が、今回の一連の記事だけ読んでも出来るように省略はしない。
環境構築
Todo Appの作成
- Riot.js Observable Rails 2 / 5 「Todoリストを表示する」
- Riot.js Observable Rails 3 / 5 「新たなTodoを追加出来るようにする」
- Riot.js Observable Rails 4 / 5 「完了したTodoをチェック出来るようにする」
- Riot.js Observable Rails 5 / 5 「チェックしてあるタスクを削除出来るようにする」
最終的に完成したTodo App
https://github.com/atfeo/Riot_Observable_Rails
参考
checkbox
チェックボックスを作成。
// ./client/src/tags/task-list.tag <task-list> <ul> <li each={task in this.opts.tasks}> <label class={completed: task.isComplete}> <input type="checkbox" id={task.id} checked={task.isComplete} onchange={handleCheck} > {task.name} </label> </li> </ul> <script> handleCheck(e) { this.opts.handlecheck(e.target.id, e.target.checked) } </script> </task-list>
todo-app.tagにマウントされているtask-listタグに
// ./client/src/tags/todo-app.tag <task-list tasks={this.state.tasks} handlecheck={handleTaskCompletionChange} > </task-list> handleTaskCompletionChange(id, isComplete) { actions.toggleComplete(store, id, isComplete) }
サーバーにチェックの状態を送るメソッドなどをactions.jsに追加。
// ./client/src/actions.js module.exports = { loadTasks, addTask, textExists, // 追加 toggleComplete, }; function toggleComplete(store, id, isComplete) { $.ajax({ url: `/api/tasks/${id}`, type: 'PATCH', dataType: 'json', data: { isComplete }, success: (res) => { completeChanged(store, res.id, res.isComplete); }, error: (xhr, status, err) => { console.log(`/api/tasks/${id}`, status, err.toString()); }, }); } function completeChanged(store, id, isComplete) { store.trigger('TASK_COMPLETION_CHANGED', { data: { id, isComplete } }); }
イベントがトリガーされた時の処理をindex.jsに追加。
// ./client/src/index.js store.on('TASK_COMPLETION_CHANGED', (action) => { const taskIndex = store.getState().tasks.findIndex((task) => { return task.id == action.data.id; }); const newTasks = [ ...store.getState().tasks.slice(0, taskIndex), Object.assign({}, store.getState().tasks[taskIndex], { isComplete: action.data.isComplete }), ...store.getState().tasks.slice(taskIndex + 1), ]; store.setState({ tasks: newTasks }); });
チェックした時のスタイルを追加。
/* ./app/assets/stylesheets/application.css */ .completed { text-decoration: line-through; color: #ccc; } label { cursor: pointer; }
ここまででviewは完成。
Rails
Migration
サーバー側でisCompleteを扱えるようにする。 ターミナルでctrl-cでforemanを終了させてから
$ rails g migration addIsCompleteToTasks isComplete:boolean
できたマイグレーションファイルに , default: false を追加し
# ./db/migrate/日付_add_is_complete_to_tasks.rb class AddIsCompleteToTasks < ActiveRecord::Migration[5.0] def change add_column :tasks, :isComplete, :boolean, default: false end end
マイグレーションを反映させる。
$ rails db:migrate
これでデータベースのテーブルにisCompleteカラムが追加された。
Controller
次にcontrollerでupdateについての処理を書く。
# ./app/controllers/api/tasks_controller.rb def update @task = Task.find(params[:id]) @task.assign_attributes(task_params) if @task.save render :show, status: :ok else head :unprocessable_entity end end private # isCompleteの追加 def task_params params.permit(:name, :isComplete) end
View(JSON)
JSONがisCompleteも返すようにindex.json.jbuilderにisCompleteを追加。
# ./app/views/api/tasks/index.json.jbuilder json.tasks(@tasks) { |t| json.extract! t, :id, :name, :isComplete }
同じく、JSONがisCompleteを返すようにshow.json.jbuilderにisCompleteを追加。
# ./app/views/api/tasks/show.json.jbuilder json.extract! @task, :id, :name, :isComplete
再びターミナルで
$ foreman start
でサーバーを開始。
ブラウザでlocalhost:5000を更新してチェックしてもエラーにならないことを確認。
また更新後チェックの状態が維持されることも確認。
サーバーエラー時の処理
チェックの状態が変更された時にサーバー側でエラーが起きると、サーバー側とブラウザ側で矛盾が生じる(checkされているのにisCompleteがfalseまたはその逆)。それを防ぐために、エラー時にチェックを反転させる処理をactions.jsに書く。
// ./client/src/actions.js function toggleComplete(store, id, isComplete) { $.ajax({ url: `/api/tasks/${id}`, type: 'PATCH', dataType: 'json', data: { isComplete }, success: (res) => { completeChanged(store, res.id, res.isComplete); }, error: (xhr, status, err) => { // 追加 completeChanged(store, id, !isComplete); console.log(`/api/tasks/${id}`, status, err.toString()); }, }); }; }
またこの時エラーが起こったことがわかりやすいように、エラーメッセージが表示されるようにする。
まずerror-messageタグを作成。
// ./client/src/tags/error-message.tag <error-message> <div show={this.opts.iserror}> {this.opts.message} </div> <style scoped> :scope div { margin-top: 5px; padding-left: 10px; color: white; background-color: red; } </style> </error-message>
todo-app.tagでerror-messageタグをマウントさせる。
// ./client/src/tags/todo-app.tag </task-form> <error-message message={"Text Message"} iserror={true} > </error-message> <loading-indicator
タグを使うためにindex.jsで読み込む。
// ./client/src/index.js import './tags/error-message.tag';
ブラウザでlocalhost:5000を更新して Text Message というメッセージが表示されていることを確認。
エラーメッセージが表示できるようになったので、エラーが起こった時にだけ表示されるようにする。
showErrorメソッドを作成し、サーバーのエラー時の処理に追加。
// ./client/src/action.js error: (xhr, status, err) => { // 追加 showError(store, 'API Error'); completeChanged(store, id, !isComplete); console.log(`/api/tasks/${id}`, status, err.toString()); } function showError(store, message) { store.trigger('SHOW_ERROR', { data: message }); }
イベントがトリガーされたときの処理を追加。
// ./client/src/index.js store.on('SHOW_ERROR',(action) => { store.setState({ isError: true, errorMessage: action.data }); }
先程のように固定ではなく、stateの変化に対応できるよう書き換え。
<!-- ./client/src/tags/todo-app.tag --> <error-message message={this.state.errorMessage} iserror={this.state.isError} > </error-message>
ここでわざとサーバーのurlを間違えたものにしておき、エラーが起こるようにしておく。
ブラウザでlocalhost:5000を更新する。チェックの状態を変更するとエラーが起こりエラーメッセージが表示されること、チェックが反転されることを確認。
次にエラーメッセージが2秒後消えるようにする。
// ./client/src/action.js error: (xhr, status, err) => { // 書き換え tempErrorMessage(store, 'API Error'); completeChanged(store, id, !isComplete); console.log(`/api/tasks/${id}`, status, err.toString()); }, // 追加 function hideError(store) { store.trigger('HIDE_ERROR'); } function tempErrorMessage(store, message) { showError(store, message); setTimeout(() => { hideError(store); }, 2000); }
新たなイベントの処理を追加。
// ./client/src/index.js store.on('HIDE_ERROR', () => { store.setState({ isError: false, errorMessage: '' }); });
ブラウザでlocalhost:5000を更新する。チェックの状態を変更するとエラーが起こりエラーメッセージが表示され2秒後に消えること、チェックが反転されることを確認。
一応loadTasks()やaddTask()などサーバーと通信している他のメソッドにも
error: (xhr, status, err) => { toggleLoading(store, false); // 追加 tempErrorMessage(store, 'API Error');
このように追加して、エラーメッセージが表示されるようにしておく。
次回はTodoを削除できるようにする。
Riot.js Observable Rails 3 / 5 「新たなTodoを追加出来るようにする」
はじめに
Riot.js Redux Railsシリーズで作ったTodoアプリのRedux部分をObservableを使って書いた。 (Observerパターンとか全然わかってない。ReduxをObservableで置き換える意味があるかもわからない。なんとなく似ていると思ったのでやってみた。)
Riot.js Redux Railsで作ったTodoアプリと全く同じものを作成するのでコードが大分重なっている(Redux部分をObservableにしたのと、それに伴う変更以外全く同じ)が、今回の一連の記事だけ読んでも出来るように省略はしない。
環境構築
Todo Appの作成
- Riot.js Observable Rails 2 / 5 「Todoリストを表示する」
- Riot.js Observable Rails 3 / 5 「新たなTodoを追加出来るようにする」
- Riot.js Observable Rails 4 / 5 「完了したTodoをチェック出来るようにする」
- Riot.js Observable Rails 5 / 5 「チェックしてあるタスクを削除出来るようにする」
最終的に完成したTodo App
https://github.com/atfeo/Riot_Observable_Rails
参考
- Riot公式のAPIの説明
- Riot公式のexample
- RiotJS and Redux - Part 5
- react-railsを使ってReactのTutorialをやってみる
- Riot公式のTodo App
Rails
新たなTodoを追加する際のサーバー側の処理を作成する。
Controller
tasks_controller.rbに
# ./app/controllers/api/tasks_controller.rb def create @task = Task.new(task_params) if @task.save render :show, status: :created else head :unprocessable_entity end end private def task_params params.permit(:name) end
を追加。
View(JSON)
保存に成功したときにJSON返すshow.json.jbuilderを作成。
# ./app/views/api/tasks/show.json.jbuilder json.extract! @task, :id, :name
これでサーバー側は完成。
Todoを追加するためのtask-formタグ
UIをRiot公式のTodoのように作成する。
+ フォームに入力があった時にボタンが有効になるようにする。
+ 何個目のTodoを登録しようとしているかわかるようにする。
// ./client/tags/task-form.tag <task-form> <form onsubmit={handleSubmit}> <input type="text" name="newTask" onkeyup={handleKeyup} placeholder="new task" > <button type="submit" disabled={!this.opts.istext} > Add Task # {this.opts.objects.length + 1} </button> </form> <script> handleSubmit() { if (!this.newTask.value) { return } this.opts.addtask(this.newTask.value) this.newTask.value = '' } handleKeyup() { this.opts.handlekeyup(this.newTask.value) } </script> </task-form>
サーバーに新しいタスクを送るメソッドなどをactions.jsに追加。
// ./client/src/actions.js module.exports = { loadTasks, // 追加 addTask, textExists, }; // 追加 function addTask(store, newTask) { toggleLoading(store, true); $.ajax({ url: '/api/tasks.json', type: 'POST', dataType: 'json', data: { name: newTask }, success: (res) => { newTaskAdded(store, res.id, res.name); toggleLoading(store, false); }, error: (xhr, status, err) => { toggleLoading(store, false); console.log('/api/tasks.json', status, err.toString()); }, }); } function newTaskAdded(store, id, name) { store.trigger('TASK_ADDED', { data: { id, name } }); } function textExists(store, value) { store.trigger('TEXT_EXISTS', { data: value }); }
タグをマウントするための読み込みと、イベントがトリガーされた時の処理をindex.jsに追加。
// ./client/src/index.js import './tags/task-form.tag'; store.on('TASK_ADDED', (action) => { store.setState({ tasks: store.getState().tasks.concat(action.data) }); }); store.on('TEXT_EXISTS', (action) => { store.setState({ isText: action.data }); });
todo-appタグにtask-formタグ、h3タグ、メソッドを追加
// ./client/src/tags/todo-app.tag <todo-app> <h3>Todo List</h3> <task-form addtask={this.handleNewTask} handlekeyup={handleInputForm} objects={this.state.tasks} istext={this.state.isText} > </task-form> <loading- ...> // scriptに追加 handleNewTask(task) { actions.addTask(store, task) } handleInputForm(value) { actions.textExists(store, value) }
cssを追加しUIをRiot公式のTodoに近づける。
/* ./app/assets/stylesheets/application.css */ body { font-family: 'myriad pro', sans-serif; font-size: 20px; border: 0; } todo-app { display: block; max-width: 600px; margin: 5% auto; } form input { font-size: 85%; padding: .4em; border: 1px solid #ccc; border-radius: 2px; } button { background-color: #1FADC5; border: 1px solid rgba(0,0,0,.2); font-size: 75%; color: #fff; padding: .4em 1.2em; border-radius: 2em; cursor: pointer; margin: 0 .23em; outline: none; } button[disabled] { background-color: #ddd; color: #aaa; } ul { padding: 0; } li { list-style-type: none; padding: .2em 0; }
ブラウザでlocalhost:5000を更新して確認。
フォームにタスクの名前を入力しボタンを押して登録できること、更新後も登録したTodoが維持されていることを確認。
次回は完了したTodoをチェック出来るようにする。