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

2015/11/20

Blue eggs

前回、Cape.JS 1.3 の新機能である「部分コンポーネント」に関して概要を説明しました。

今回は、この仕組みを用いて「Cape.JS 入門」で作ってきた TODO アプリを書き換えていきます。


app/assets/javascripts/ ディレクトリに新規ファイル create_form.es6 を作成し、テキストエディタで次のような内容を書き込んでください。

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

部分コンポーネントのクラスは Cape.Partial クラスを継承し、必ず render() メソッドを持たなければなりません。まだ骨組みを作っただけです。

次に、app/assets/javascripts ディレクトリにある todo_list.es6 をテキストエディタで開いてください。現在、renderCreateForm() メソッドが次のように定義されています。

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

このメソッドの中身(m.formFor(m) { から }); まで)をコピーして、さきほど新規作成した create_form.es6render() メソッドの中身として貼り付けてください。すると、create_form.es6 のソースコードはこのようになりますね。

class CreateForm extends Cape.Partial {
  render(m) {
    m.formFor('new_task', m => {
      m.onkeyup(e => this.refresh());
      m.textField('title', { value: this.val('new_task.title') }).sp();
      m.attr({ disabled: this.val('new_task.title').trim() === '' });
      m.onclick(e =>
        this.agent.createTask(this.val('new_task.title', '')));
      m.btn(`Add task #${ this.agent.objects.length + 1 }`);
    });
  }
}

移設したコードの中では this.refresh() とか this.val() などのメソッドが呼び出されています。前回書いたことの繰り返しになりますが、これらのメソッドは親コンポーネントの同名のメソッドを呼び出します。

つまり、this.refresh() は部分コンポーネントを再描画するのではなく親コンポーネントを再描画します。親コンポーネントを再描画すれば、それが内包している部分コンポーネントも再描画されるので、結局はこの部分コンポーネント(CreateForm クラスのインスタンス)自体も再描画されることになります。

他方、this.agent はこの部分コンポーネント自体の agent 属性を指します。この属性はまだ定義されていないので、このままではエラーを引き起こします。この点は覚えておきましょう。

ここまでが書き換え作業の第一段階です。


第二段階へ進みます。todo_list.es6 に次のようなコードがあります。

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

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

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

new CreateForm(this) で部分コンポーネントを作り、それを親コンポーネントの createForm 属性にセットしています。部分コンポーネントのコンストラクタの引数には親コンポーネント自体を渡すことになっています。

さらに todo_list.es6 を書き換えます。現在 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.renderCreateForm(m);
  }

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

    else this.createForm.render(m);

属性 this.createForm には部分コンポーネントがセットされており、その render() メソッドを呼び出しています。部分コンポーネントの render() メソッドは、第 1 引数にマークアップビルダー(m)を取ります。


ソースコードの書き換え第三段階では、さきほど記憶にとどめておいた問題に対応します。

いま、CreateForm クラスの render() メソッドの中身はこうなっています。

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

2 箇所で this.agent という記述があり、ここでエラーが発生します。コピー元のソースコードでは、this.agent はリソースエージェント TaskCollectionAgent のインスタンスを指しています。この参照関係を維持するにはどうすればいいでしょうか。

部分コンポーネントの parent 属性には親コンポーネントのインスタンスがセットされています。とすれば、2 箇所の this.agentthis.parent.agent に書き換えてやればいいということになります。

しかし、前回紹介した技法を用いれば、もっとエレガントに書けます。create_form.es6 のソースコードを次のように書き換えてください。

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

  render(m) {
    (省略)
  }
}

CreateForm クラスのコンストラクタを上書きしています。先述の通り、部分コンポーネントのコンストラクタは第 1 引数として親コンポーネントを取りますので、親コンポーネントの agent 属性が参照するオブジェクトをこの部分コンポーネントの agent 属性にセットしていることになります。こうすれば 部分コンポーネントの render() メソッドを書き換える必要がありません。


最後に、todo_list.es6 から使われなくなった renderCreateForm() メソッドのコードを削除しましょう。

以上で、親コンポーネントである TodoList クラスからコードの一部を抽出して部分コンポーネント CreateForm クラスを作る作業は終了です。

結果として、todo_list.es6 のソースコードの行数は 95 から 85 に減少しました。

ブラウザをリロードして TODO アプリが従来通り動くことを確認してください。

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

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

から取得できます。

また、今回の変更内容は

  • 9914a00 Bump rails and sprockets-es6
  • 6acfaa5 Bump capejs and bootstrap
  • 85df966 Extract CreateForm partial component

で確認できます。


次回は、TodoList クラスからタスク編集フォームの描画とタスクの更新に関わる部分を部分コンポーネントとして抜き出します。