タスクの新規追加

2015/07/03

今回は、前回作ったタスクの新規追加フォームからタスクをデータベースに追加する機能を実装します。


まず、コンポーネント側から書き換えます。現在、renderCreateForm() のソースコードは次のようになっています。

  renderCreateForm(m) {
    m.formFor('new_task', m => {
      m.textField('title').sp();
      m.btn(`Add task #${ this.ds.tasks.length + 1 }`);
    });
  }

これを次のように変更します(2行挿入)。

  renderCreateForm(m) {
    m.formFor('new_task', m => {
      m.textField('title').sp();
      m.onclick(e =>
        this.ds.createTask(this.val('new_task.title')));
      m.btn(`Add task #${ this.ds.tasks.length + 1 }`);
    });
  }

この結果、タスクの新規追加フォームのボタンをクリックしたときに this.ds.createTask(this.val('new_task.title')) というコードが実行されることになります。

コンポーネントの val() メソッドはフォームフィールドの値を返します。引数には x.y という形式の文字列を渡します。x の部分がフォーム名で y の部分がフィールド名です。ここでは new_task という名前のフォームにある title フィールドの値を取得して、データストアの createTask() メソッドに渡しています。


次に、データストア側に createTask() メソッドを追加します。task_store.es6 を次のように書き換えてください。

class TaskStore extends Cape.DataStore {
  constructor() { ... }

  refresh() { ... }

  createTask(title) {
    $.ajax({
      type: 'POST',
      url: '/api/tasks',
      data: { task: { title: title } }
    }).done(data => {
      if (data === 'OK') this.refresh();
    });
  }

  toggleTask(task) { ... }
}

追加された createTask() メソッドは、引数としてタスクのタイトルを取ります。それを POST メソッドで /api/tasks に対して Ajax で送信し、返ってきたデータが OK であれば this.refresh() を実行します。this.refresh() は Rails アプリケーションの API にアクセスしてタスクのリストを取得し、コンポーネントを再描画します。


続いて、API を作ります。テキストエディタで app/controllers/api/tasks_controller.rb を開いて、次のように書き換えます(create メソッドを追加)。

class Api::TasksController < ApplicationController
  def index
    ...
  end

  def create
    if Task.create(task_params)
      render text: 'OK'
    else
      render text: 'NG'
    end
  end

  def update
    ...
  end

  private
  def task_params
    params.require(:task).permit(:title, :done)
  end
end

Task.create メソッドは tasks テーブルにレコードを追加します。

最後に config/routes.rb を書き換えて完成です(下から3行目に :create, を追加)。

Rails.application.routes.draw do
  root 'top#index'

  namespace :api do
    resources :tasks, only: [ :index, :create, :update ]
  end
end

ブラウザで動作確認をしましょう。新しいタスクのタイトルを書き入れて…

画面キャプチャ

ボタンをクリックすると…

画面キャプチャ

タスクが追加されました。


でも、ちょっと変ですね。タスク追加が済んだらフォームに記入したタイトルは消えてほしいところです。そのためには、コンポーネントの renderCreateForm() メソッドを次のように書き換えます。

  renderCreateForm(m) {
    m.formFor('new_task', m => {
      m.textField('title').sp();
      m.onclick(e =>
        this.ds.createTask(this.val('new_task.title', '')));
      m.btn(`Add task #${ this.ds.tasks.length + 1 }`);
    });
  }

変更点は下から4行目です。this.val('new_task.title')this.val('new_task.title', '') に変えました。

コンポーネントの val() メソッドは引数の個数が 1 個の場合、単にフィールドの値を返すだけですが、第 2 引数が与えられた場合、フィールドの値を第 2 引数の値にセットしてから、フィールドの元の値を返します。ここでは、タスクの新規追加フォームの title フィールドを空にしてから、そこに書かれていた文字列をデータストアの createTask メソッドに渡しています。

ブラウザで動作確認をしてください。タスク追加の直後、フォームの title フィールドの中が空になっていれば OK です。


もうひとつ UI の改善をしてみましょう。現状では、タイトルが空のままでもボタンをクリックできて、中身のないタスクが作られてしまいます。タイトルが空である間は、ボタンを無効化することにしましょう。

まず、効果が見えやすいようにスタイルシートの準備をします。app/assets/stylesheets/todo_list.scss を次のように書き換えてください(3行追加)。

#todo-list {
  label.completed span {
    color: #888;
    text-decoration: line-through;
  }
  button[disabled] {
    color: #888;
  }
}

そして、コンポーネントの renderCreateForm() メソッドを次のように変更します。

  renderCreateForm(m) {
    m.formFor('new_task', m => {
      m.onkeyup(e => this.refresh());
      m.textField('title').sp();
      m.attr({ disabled: this.val('new_task.title').trim() === '' });
      m.onclick(e =>
        this.ds.createTask(this.val('new_task.title', '')));
      m.btn(`Add task #${ this.ds.tasks.length + 1 }`);
    });
  }

2行追加しています。まず m.onkeyup(e => this.refresh()); を挿入して、タイトルフィールドの中身が書き換わるたびにコンポーネントの再描画を行っています。イベント keyup は、キーボードのキーが押されて上がった際に発生します。また、マウスを使って文字列が貼り付けられた時にも発生します。

もうひとつ挿入されたのは次のコードです。

      m.attr({ disabled: this.val('new_task.title').trim() === '' });

ちょっとややこしく見えますが、単純なことです。タイトルフィールドの中身を trim() で両端の空白を削ってから、それが空文字なら disabled 属性をボタンに設定しています。

ユーザーがタイトルフィールドの中身を書き換えるたびに、コンポーネント全体が再描画されます。中身が空ならボタンが無効化され、空でなければ有効化されます。

ブラウザをリロードした直後の画面では、次のようにボタンが無効化されています。

画面キャプチャ

しかし、タイトルフィールドに「あ」という 1 文字を書き入れると、

画面キャプチャ

ボタンが有効化されます。そして、「あ」を削除すれば再び無効化されます。

画面キャプチャ

この種の効果を jQuery で実現するのは意外に面倒です。仮想 DOM が活きる場面です。


次回は、タスクのタイトルを編集する機能を作ります。