Learn プログ

-マナビメモ-

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の作成

最終的に完成した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アプリの完成。