Learn プログ

-マナビメモ-

Riot.js Redux Rails 8 / 8 「チェックしてあるタスクを削除出来るようにする」

はじめに

フロントにRiot.jsとReduxを使いサーバーサイドにRailsを使い、初めに環境構築し、次にReduxの使い方の流れをみて、最終的に簡単なTodoAppを作成する。

環境構築

簡単な例でReduxの流れをみる

Todo Appの作成

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