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アプリの完成。

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

最終的に完成した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 Observable Rails 5 / 5に続く

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

最終的に完成したTodo App
https://github.com/atfeo/Riot_Observable_Rails

参考

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をチェック出来るようにする。

Riot Observable Rails 4 / 5に続く