Skip to content
Ayumu Sato edited this page Dec 6, 2013 · 1 revision

Phalanxは、Viewの役割を分割することに注力したライブラリですが、薄いラッパーとしてまとめているため、フルスタック系の機能は備えていません。

###Phalanx.Layout

Layoutは、WebMVCにおけるControllerに似た振る舞いをもったViewの単位です。Viewを継承しているため、Viewとしての基本機能もすべて備えています。次に挙げるregionsを利用するほどでもない簡単なページであれば、Layoutのみで完結することも可能です。

####regions

Layoutには、regionsと呼ばれるViewの管理領域が存在します。HTML側で、管理領域の対象となる要素に、idを割り当てます。

<body>
  <header id="js-reg-header">
    <!-- HEADER -->
  </header>
  <div id="js-reg-content">
    <!-- MAIN CONTENT -->
  </div>
  <footer id="js-reg-footer">
    <!-- FOOTER -->
  </footer>
</body>

クラスの宣言時にregionsプロパティに、名前とセレクタのペアで管理領域を定義します。Layoutをnewしたあと、それぞれの管理領域にViewをassignすることで、Layout内に表示すべき要素を配置していきます。

var AcmeLayout = Phalanx.Layout.extend({
  regions: {
    header : '#js-reg-header',
    content: '#js-reg-content',
    footer : '#js-reg-footer'
  },
  onChange: function(regionName, newView, oldView) {
    // Called when regions view was changed.
  }
});

var layout = new AcmeLayout({el: 'body'});

layout.assign('header',  new HeaderView());
layout.assign('content', new ContentView());
layout.assign('footer',  new FooterView());

Layoutでは、管理領域(region)にViewを割り当てる(assign)する際、$.ajax()Backbone.Model.fetch()などで取得したデータオブジェクトを、各Viewに渡すことで表示データについても管理を行います。

LayoutとViewの関係は言い換えれば、MainViewとSubViewのような関係と考えても良いでしょう。

###Phalanx.View

一般的なViewの拡張です。componentslistenersという宣言用のプロパティをもちますが、これらは後述するPhalanx.Componentと関連する機能であるため、次項で説明します。

Viewの中にある特定の要素を取得する際のショートハンドとして、uiプロパティによる宣言が用意されています。

<section id="js-list">
  <div data-ui="list">
    <div>Item 1</div>
    <div>Item 2</div>
    <div>Item 3</div>
  </div>
</section>

data-ui="list"という属性で、listという識別子を与えます。

var ListView = Phalanx.View.extend({
  ui {
    list: null
  },
  onAfterRender: function() {
    var ul = this.ui.list;   // DOMElement
    var $ul = this.$ui.list; // jQuery|Zepto
  }
});
var listView = new ListView({el: '#js-list'});

JS側でもuiプロパティにlistを宣言しておくことで、this.ui.listまたはthis.$ui.listとアクセスできるようになります。

本来、lookupUiメソッドは、Backbone.setElement相当の処理が行われる際に自動で実行されていますが、JavaScriptでテンプレーティングを行ったあとは、別途lookupUiを実行して要素を拾い直す必要があります。

###Phalanx.Component

ComponentはViewのような振る舞いを持ちますが、やや特殊な単位として扱われます。実体としてもComponentは独自にクラス定義されており、Backbone.Viewを継承していないため、似ているだけで別のものです。

目安として下記のような要素をComponentとして扱います。

  • ページ内に不特定多数しうる要素
  • ユーザーアクションによって通信処理などを伴い、ステートの変化によって表示状態も変わる要素
  • ViewModelのような振る舞いを持ちうる要素

Componentのクラス定義においては、eventsを利用して、Component内のイベントを定義することができます。ただし、ここでのイベントは、Componentを管理するView(View側の記述は後述)のeventsとマージされて、View.elの委譲イベントとして管理されます。そのため、ひとつのViewで複数のComponentを管理する際、指定されたセレクタがView自体のeventsや他のComponentのeventsと衝突すると、正常に動作しません。

var LikeBtnComponent = Phalanx.Component.extend({
  // Events are delegated to the View.
  events: {
    'click .js-like': 'doLike'
  },
  // Elements with the data-ui are auto stored when create component instance.
  ui: {
    count: null
  },
  // Increment like count when POST completed.
  doLike: function() {
    $.post('/api/like', {id: this.id}, function() {
      this.$ui.count.text(parseInt(this.$ui.count, 10) + 1);
    }.bind(this));
  }
});

Componentを利用するViewでは次のように定義します。ここでは、componentslikeBtnという名前でLikeBtnComponentクラスを指定しています。

var AcmeListView = Phalanx.View.extend({
  // Specified name here, will specify as data-component attr in the HTML.
  components: {
    'likeBtn': LikeBtnComponent
  }
});
var listView = new AcmeListView({el: '#js-list'});

HTMLは次のようになります。

<section id="js-list">
  <ul>
    <li>
      <a>screen_name</a>
      <p>Lorem ipsum dolor sit amet, consectetur adipisici…</p>

      <!-- Events that have been delegated to occur, creation of component is delayed. -->
      <div data-component="likeBtn">      
        <button class="js-like"><button>
        <span data-ui="count">3</span>
      </div>
    </li>
    ...
    ..
    .
  </ul>
</section>

Viewのcomponentsに指定したコンポ—メント名(エイリアス)をdata-component属性として、要素に指定します。それによって指定された要素が、Componentとして管理されるようになります。

Componentは遅延してインスタンスが生成されます。上記の例では、click .js-likeが最初に発生したときに初めて、その要素のComponentが生成されます。他のViewから生成済みまたは生成前のComponentのインスタンスにアクセスする用途は想定されていません。ViewとはComponentからtriggerされるイベントによってメッセージングするようにします。

なお、例のHTMLのようにリストアイテムにおけるいいねボタンのように、ひとつのViewの中で繰り返し出現する要素をComponentで管理した場合は、要素ごとに個別のComponentインスタンスが生成されるため、要素とComponentインスタンスは常に1:1の関係になります。

イベントメッセージングは、Componentがtriggerしたものを、Viewがlistenersプロパティの宣言で受け取ります。

var ReadMoreBtnComponent = Phalanx.Component.extend({
  events: {
    'click [data-ui="btn"]': 'onClickBtn'
  },
  onClickBtn: function(evt) {
    var href  = evt.currentTarget.getAttribute('href');

    $.get(href, function(resp) {
      this.trigger('success', resp);
    }.bind(this));
  }
});

ここではReadMoreBtnComponentが、もっと見るAPIへのリクエストに成功した際に、successイベントを発信するようなイメージで実装されています。

これを、View側では次のようにlistenersプロパティに'{イベント名} {コンポーネントエイリアス}': '{リスナーメソッド名}'を指定することで、任意のComponentが発したイベントを、View側で受け取れるようになります。内部的には、view.listenTo(component, 'event', view.listenerMethodが処理されるので、view.stopListening()を手動で実行した場合はイベントのリスニングが止まる点に注意してください。

var ListView = Phalanx.View.extend({
  components: {
    'moreBtn': ReadMoreBtnComponent
  },
  listeners: {
    'success moreBtn': 'renderMore'
  },
  ui {
    list null
  }
  renderMore: function(html) {
    this.$ui.list.append(html);
  }
});

var listView = new ListView({el: '#js-list'});

Componentの操作結果がComponent要素の外側に干渉する場合は、属しているViewにイベントを通じて処理を委譲するように実装すべきです。

なお、Componentはeventsのほか、el$elui$uiについてViewと同様の設定を行うことができます。

Clone this wiki locally