65
65

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

モデルのバリデーションエラーはカスタムイベントで通知すると便利

Posted at

Backbone.js Tips And Patterns | Smashing Coding を読みました。

ほとんどは特に真新しくもない当たり前な内容なんですが、 "BROADCAST CUSTOM ERROR EVENT" はいいなと思ったので紹介します。

このパターンは Model で複数の属性のバリデーションを行いたい場面で使うものです。

RETURN AN ERROR OBJECT

まず、普通な validate の実装です。

Model
validate: function (attrs) {
  var errors = [];
  if (_.isUndefined(attrs.name)) {
    errors.push({
      attr: 'name',
      message: 'Name is required'
    });
  }
  if (errors.length) return errors;
}

まず Backbone の基本事項として下記を再確認してください:

  1. validate メソッドが真を返すと invalid イベントが発生する
  2. 返り値が invalid イベントのコールバックに渡される

で、上記のコードサンプルは errors という配列を作っています。なんでわざわざこんなことをしているのかというと、コールバックに複数の属性の情報を渡すには配列などを利用するしか無いわけです。そして View のコールバックでその配列の値をパースします。

このパターンは validate メソッドで複数の属性を処理するときによく使われます。前述のサイトでは "RETURN AN ERROR OBJECT" という名前で紹介されています。

この方法で一応複数属性のバリデーションはできるんですが、レンダラが肥大化してしてしまいがち、という問題があります。

View
initialize: function () {
  this.listenTo(this.model, 'invalid', this.renderMessages);
},
renderMessages: function (errors) {
  _.each(errors, function (error) {
    if (error.attr === 'name') {
      this.$('.name-message').html(error.message).show();
    }
  }, this);
}

ここでは renderMessages メソッドを定義しています。このメソッドでやるのがエラーメッセージをまとめてリストにして表示する、とかなら単純で大きな問題にはなりませんが、ここでやっているように属性毎にレンダリングする方法を変えたい、とかだと属性が増える度にどんどん肥大化していきます。

そのような問題に対して、個々の属性毎にレンダラを定義して、下記のようにリファクタリングすることもよく行います。

View
renderMessages: function (errors) {
  _.each(errors, function (error) {
    if (error.attr === 'name') {
      this.renderNameMessage(error.message);
    }
  }, this);
},
renderNameMessage: function (message) {
  this.$('.name-message').html(message).show();
}

BROADCAST CUSTOM ERROR EVENT

この個別レンダラに対応するカスタムイベントを発行するのが "BROADCAST CUSTOM ERROR EVENT" です。

Model
validate: function (attrs) {
  if (_.isUndefined(attrs.name)) {
    this.trigger('invalid:name', 'Name is required');
  }
  // 注意:バグがある。詳しくは後述
}

このように invalid:ATTRIBUTE となるようなイベントを発行します。対応する View では個別レンダラをコールバックに設定します。

View
initialize: function () {
  this.listenTo(this.model, 'invalid:name', this.renderNameMessage);
}

この方法のよいところは、 View で Model の validate メソッドの返り値の実装を知らなくてもよい点です。例えば今回の場合でいえば、 attrmessage というプロパティを持つオブジェクトの配列が返される、ということを View が知っていなければなりません。しかし、カスタムイベントでメッセージを渡す方式であれば、属性名の種類さえ知っていればよいので View と Model の結合度はより小さくなります。

2つを組み合わせるのがオススメ

ただ、引用しているブログ記事には重大なバグがあって、それは何かというと、 validate メソッドが何も返さないのでバリデーションに通ったとみなされてしまう、という点です。なので一つでもバリデーションに失敗したら true を返すようにする、などの修正が必要です。

ですが、オススメしたいのは "BROADCAST CUSTOM ERROR EVENT" と "RETURN AN ERROR OBJECT" を組み合わせる方式です。

Model
validate: function () {
  var errors = [];
  if (_.isUndefined(attrs.name)) {
    errors.push({
      attr: 'name',
      message: 'Name is required'
    });
    this.trigger('invalid:name', 'Name is required');
  }
  if (errors.length) return errors;
}

何故組み合わせるべきだと思うのか。それは changechange:ATTRIBUTE イベントの構造と似せることが可能だからです。

changechange:ATTRIBUTE イベントは属性が変更されたときに発生します。複数の属性が変更されたとき、属性毎に change:ATTRIBUTE イベントが発生した後に change イベントが発生します。個別対応したければ change:ATTRIBUTE を監視し、一括で問題無いなら change を使う、という使い分けがされます。

それと同じことが invalidinvalid:ATTRIBUTE でできるようになるわけです。インタフェースが似ているので使い勝手がよさそうです。

65
65
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
65
65

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?