読者です 読者をやめる 読者になる 読者になる

Learn プログ

-マナビメモ-

Riot.js Redux Rails 7 / 8 「完了したTodoをチェック出来るようにする」

Riot.js Redux Rails Riot.js Redux Railsシリーズ

はじめに

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

環境構築

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

Todo Appの作成

最終的に完成したTodo App
https://github.com/atfeo/Riot_Redux_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) {
  store.dispatch(actions.toggleComplete(id, isComplete))
}

サーバーにチェックの状態を送るメソッドなどをactions.jsに追加。

// ./client/src/actions.js
module.exports = {
  loadTasks,
  addTask,
  textExists,
  // 追加
  toggleComplete,
};

function toggleComplete(id, isComplete) {
  return (dispatch) => {
    $.ajax({
      url: `/api/tasks/${id}`,
      type: 'PATCH',
      dataType: 'json',
      data: { isComplete },
      success: (res) => {
        dispatch(completeChanged(res.id, res.isComplete));
      },
      error: (xhr, status, err) => {
        console.log(`/api/tasks/${id}`, status, err.toString());
      },
    });
  };
}

function completeChanged(id, isComplete) {
  return {
    type: 'TASK_COMPLETION_CHANGED',
    data: { id, isComplete }
  };
}

アクションを送られたときの処理をindex.jsに追加。

// ./client/src/index.js
case 'TASK_COMPLETION_CHANGED': {
  const taskIndex = state.tasks.findIndex((task) => {
    return task.id == action.data.id;
  });
  const newTasks = [
    ...state.tasks.slice(0, taskIndex),
    Object.assign({}, state.tasks[taskIndex], { isComplete: action.data.isComplete }),
    ...state.tasks.slice(taskIndex + 1),
  ];
  return Object.assign({}, state, { 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、show.json.jbuilderにisCompleteを追加。

# ./app/views/api/tasks/index.json.jbuilder
json.tasks(@tasks) { |t| json.extract! t, :id, :name, :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(id, isComplete) {
  return (dispatch) => {
    $.ajax({
      url: `/api/tasks/${id}`,
      type: 'PATCH',
      dataType: 'json',
      data: { isComplete },
      success: (res) => {
        dispatch(completeChanged(res.id, res.isComplete));
      },
      error: (xhr, status, err) => {
        // 追加
        dispatch(completeChanged(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/actions.js
error: (xhr, status, err) => {
  // 追加
  dispatch(showError('API Error'));
  dispatch(completeChanged(id, !isComplete));
  console.log(`/api/tasks/${id}`, status, err.toString());
},

// 追加
function showError(message) {
  return { type: 'SHOW_ERROR', data: message };
}

dispatchメソッドでアクションが送られたときの処理を追加。

// ./client/src/index.js
case 'SHOW_ERROR':
      return Object.assign({}, state, { 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/actions.js
error: (xhr, status, err) => {
  // 書き換え
  dispatch(tempErrorMessage('API Error'));
  dispatch(completeChanged(id, !isComplete));
  console.log(`/api/tasks/${id}`, status, err.toString());
},

// 追加
function hideError() {
  return { type: 'HIDE_ERROR' };
}

function tempErrorMessage(message) {
  return (dispatch) => {
    dispatch(showError(message));
    setTimeout(() => {
      dispatch(hideError());
    }, 2000);
  };
}

新たなアクションの処理を追加。

// ./client/src/index.js
case 'HIDE_ERROR':
      return Object.assign({}, state, { isError: false, errorMessage: '' });

ブラウザでlocalhost:5000を更新する。チェックの状態を変更するとエラーが起こりエラーメッセージが表示され2秒後に消えること、チェックが反転されることを確認。

一応loadTasks()やaddTask()などサーバーと通信している他のメソッドにも

error: (xhr, status, err) => {
  dispatch(toggleLoading(false));
  // 追加
  dispatch(tempErrorMessage('API Error'));

このように追加して、エラーメッセージが表示されるようにしておく。

次回はTodoを削除できるようにする。

Riot.js Redux Rails 8 / 8に続く