部分コンポーネント(3)

2015/11/21

Nested boxes

前回に引き続き、「部分コンポーネント」による TODO アプリのリファクタリングを行っていきます。


まず、app/assets/javascripts/ ディレクトリに新規ファイル update_form.es6 を作成し、テキストエディタで次のような内容を書き込みます。

class UpdateForm extends Cape.Partial {
  render(m) {
  }
}

そして、todo_list.es6 から renderUpdateForm メソッドの中身をコピーして、update_form.es6render() メソッドの中身として貼り付けます。

class UpdateForm extends Cape.Partial {
  render(m) {
    m.formFor('task', m => {
      m.onkeyup(e => this.refresh());
      m.textField('title').sp();
      m.attr({ disabled: this.val('task.title').trim() === '' });
      m.btn('Update', { onclick: e => this.updateTask() });
      m.btn('Cancel', { onclick: e => this.reset() });
    });
  }
}

todo_list.es6 から renderUpdateForm メソッド自体は不要ですので削除します。


現在、TodoList コンポーネントの init() メソッドはこのように定義されています。

  init() {
    this.agent = new TaskCollectionAgent(this);
    this.createForm = new CreateForm(this);
    this.editingTask = null;
    this.agent.refresh();
  }

これを次のように書き換えてください(4 行目を挿入)。

  init() {
    this.agent = new TaskCollectionAgent(this);
    this.createForm = new CreateForm(this);
    this.updateForm = new UpdateForm(this);
    this.editingTask = null;
    this.agent.refresh();
  }

また、TodoList#render() メソッドは現状こうなっています。

  render(m) {
    m.ul(m => {
      this.agent.objects.forEach((task, index) => {
        m.li(m => {
          this.renderTask(m, task);
          this.renderButtons(m, task, index);
        });
      });
    });
    if (this.editingTask) this.renderUpdateForm(m);
    else this.createForm.render(m);
  }

下から 3 行目を次のように書き換えてください。

    if (this.editingTask) this.updateForm.render(m);

ここまでの変更により、TodoList コンポーネントの renderUpdateForm() メソッドを部分コンポーネントに抜き出したことになります。


さて、TodoList コンポーネントに残っているメソッドのうち、以下のメソッドはタスクの編集フォームと強く結びついています。

  • editTask()
  • reset()
  • updateTask()

これらを部分コンポーネント UpdateForm へ移設してください。結果として、UpdateForm クラスのソースコードはこうなります。

class UpdateForm extends Cape.Partial {
  render(m) {
    m.formFor('task', m => {
      m.onkeyup(e => this.refresh());
      m.textField('title').sp();
      m.attr({ disabled: this.val('task.title').trim() === '' });
      m.btn('Update', { onclick: e => this.updateTask() });
      m.btn('Cancel', { onclick: e => this.reset() });
    });
  }

  editTask(task) {
    if (this.editingTask === task) {
      this.reset();
    }
    else {
      if (this.editingTask) this.editingTask.modifying = false;
      task.modifying = true;
      this.reset();
      this.editingTask = task;
      this.val('task.title', task.title);
      this.refresh();
    }
  }

  reset() {
    if (this.editingTask) this.editingTask.modifying = false;
    this.editingTask = null;
    this.val('task.title', '');
    this.refresh();
  }

  updateTask() {
    var task = this.editingTask;
    task.modifying = false;
    this.editingTask = null;
    this.agent.updateTask(task, this.val('task.title', ''));
  }
}

移設したコードを注意深く観察してください。未定義の属性が 2 個存在します。agenteditingTask です。

そこで、CreateForm クラスの場合と同様にコンストラクタをオーバーライドします。

class UpdateForm extends Cape.Partial {
  constructor(parent) {
    super(parent);
    this.agent = parent.agent;
    this.editingTask = null;
  }

  render(m) {
  (以下省略)

agent 属性については親コンポーネントと共有します。「編集中のタスク」を保持するための editingTask 属性は親コンポーネントにあるよりも、UpdateForm クラスのものとした方がいいでしょう。


TodoList クラスに残ったメソッドに必要な修正を行います。

まずは、init() メソッド。

  init() {
    this.agent = new TaskCollectionAgent(this);
    this.createForm = new CreateForm(this);
    this.updateForm = new UpdateForm(this);
    this.agent.refresh();
  }

4 行目の下にあった this.editingTask = null; を削除しました。

次に、render() メソッド。

  render(m) {
    m.ul(m => {
      this.agent.objects.forEach((task, index) => {
        m.li(m => {
          this.renderTask(m, task);
          this.renderButtons(m, task, index);
        });
      });
    });
    if (this.updateForm.editingTask) this.updateForm.render(m);
    else this.createForm.render(m);
  }

下から 3 行目の if 文の条件式を this.editingTask から this.updateForm.editingTask に変更しました。

最後に、renderButtons() メソッドを変更します。

  renderButtons(m, task, index) {
    m.onclick(e => this.updateForm.editTask(task));
    m.span('Edit', { class: 'button' });
    m.onclick(e => {
      if (confirm('Are you sure you want to delete this task?'))
        this.agent.destroyTask(task);
    });
    m.span('Delete', { class: 'button' });
    if (index === 0) m.class('disabled');
    else m.onclick(e => this.agent.patch('move_higher', task.id));
    m.span({ class: 'button' }, m => m.fa('arrow-circle-up'));
    if (index === this.agent.objects.length - 1) m.class('disabled');
    else m.onclick(e => this.agent.patch('move_lower', task.id));
    m.span({ class: 'button' }, m => m.fa('arrow-circle-down'));
  }

変更したのは 2 行目です。this.editTask(task)this.updateForm.editTask(task) に変更しました。

以上でいったん書き換え終了です。ブラウザで動作確認してください。

なお、書き換え後のソースコードは

https://github.com/oiax/capejs-tut2-app-source/archive/lecture-03.zip

から取得できます。

また、今回の変更内容は

  • 51ec1d1 Extract UpdateForm partial component

で確認できます。


3 回に続いた「部分コンポーネント」に関するチュートリアルはこれで終了です。

次回からしばらくの間は、Cape.JS のルーティングについて書く予定です。お楽しみに。