2013/07/04

Derbyでサンプルアプリを改造してマルチユーザ同時編集可能な付箋アプリっぽくしてみる

DerbyのサンプルのTODOアプリ自体は既にマルチユーザで同時編集なわけなので、今回はそれをjQuery UIを利用して付箋のように見せるようにして、その付箋配置を共有できるようにしてみました。

なお、このサンプルアプリはGitHubで公開しておりますので、詳細はそちらを参照ください。

derby initで作成されたテンプレートプロジェクトのソースファイルを修正していきます。

なお、Derbyの導入やプロジェクト作成は以前の記事「リアルタイムデータ同期が可能なNode用MVCフレームワークのDerbyを導入する」を参照してください。

views/app/list.html

まず、前回の記事を参考にして、jQueryとjQuery UIをCDNから読み込むようにします。

<Scripts:>
  <script src="http://ajax.googleapis.com/ajax/libs/jquery/2.0.2/jquery.min.js"></script>
  <script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.10.3/jquery-ui.min.js"></script>

続いて、表を表示させるかわりに付箋を表示させるようにするために、tableタグがあった部分をすべて消して、次のようにします。

    <div class="span8 offset1">
      <div class="items-div">{#each _page.items}<app:item>{/} </div>
    </div>

これは、_page.itemsの各要素をそれぞれapp:itemコンポーネントでテンプレート展開することを意味します。

なお、app:itemはDerbyのコンポーネントです。 そのitemコンポーネントの定義は次のようになっており、これをファイル末尾に追加します。

<item:>
  <div style="left:{.pos.x}px; top:{.pos.y}px" x-bind="mousedown: list.drag">
    <input value="{.name}" />
    <button class="btn" x-bind="click: list.remove">x</button>
    <textarea>{.note}</textarea>
  </div>

先ほどの{#each _page.items}もそうですが、{...}はDerbyのModel-View bindingによって値が自動的に埋められます。

ドット . で始まっているのは、モデル_page.itemsが示す配列の要素にスコープが移っているからです。

次のコードは実際のDerbyのコードではありませんが、このようなオブジェクトに適用するものだと思えばよいです。

_page.items = [
    { name:'本を買う', note:'MongoDB', pos:{x:10, y:20} },
    { name:'勉強する', note:'jQuery', pos:{x:110, y:120} }
];

また、x-bindはDerbyのDOM event bindingです。

見て想像が付くと思いますがdivのmousedownのイベント発生時にlist.dragのハンドラ、ボタンのclicklist.dragのハンドラが呼ばれるようにして、付箋の移動と削除ができるようにしています。

lib/app/index.js

まず、付箋の移動を処理するlist.dragのハンドラを追加しましょう。

末尾に次のコードを追加します。

app.fn('list.drag', function(e, el) {
  var id = e.get('.id');
  make_draggable(this.model, id, $(el));
});

function make_draggable(model, id, obj) {
    obj.draggable({
        stop:function(ev, ui) {
            var pos = ui.position;
            model.set('items.' + id + '.pos', {x:pos.left, y:pos.top});
        }
    });
}

ハンドラが引数に要素elを取るので、それをdraggableにしているだけです。

そして、ドラッグ終了時に、その座標変更をモデルにセットしなおすようにします。

Derbyでは、先ほど紹介したModel-View bindingによって、モデルの変更でビューが自動で切り変わるので、ビュー変更についてのコードは不要です。

あとは細かい修正です。付箋の追加時に初期位置を設定したり、削除が正しくできるようにしたりしています。

 app.fn('list.add', function(e, el) {
   var newItem = this.model.del('_page.newItem');
   if (!newItem) return;
   newItem.userId = this.model.get('_session.userId');
+  newItem.pos = {x:450, y:100};
   this.model.add('items', newItem);
 });


 app.fn('list.remove', function(e) {
!  var id = e.get('.id');
   this.model.del('items.' + id);
 });

styles/app/list.styl

付箋ぽい概観を指定するために、次のコードをスタイルに追加するだけです。

.items-div div
  background: white;
  position fixed;
  width: 250px;
  height: 150px;
  padding: 0.5em;
  border: 1px solid #aaaaaa;

.items-div div input
  border: none;

.items-div div textarea
  width: 90%;
  height: 70%;

これで、mongod, redisが実行していることを確認して、npm startすれば最初に紹介したムービーのように付箋が操作できるようになっているはずです。

Derbyの各種機能の説明

ここからは、今回のソース修正などで利用しているDerbyの各種機能について説明していきます。

モデル-ビュー バインディング

Model-View bindingMeteorにもある、モデルの値変更によってページの状態を自動的に更新する仕組みです。

公式のドキュメントには次のように書かれています。(意訳注意)

モデル-ビュー バインディングは動的なインタラクションをページに付加するアプローチとして最近よく使われている方法のひとつである。 このような記述的な文法を利用することによって、アプリケーション内に頻出するエラーしやすいDOM操作のコード量を劇的に減らすことができる。 Derbyのバインディングシステムを利用するなら、なんらかのDOMコードを書く必要がほとんどないようにすべきである。

Derbyテンプレートではバインディングを利用するには、 {{ ではなく、{ を用いる。もし { 自体を表示させたいときはHTMLエンティティ &#123; を用いること。

割り付けられたテンプレートタグは、通常のタグのように最初のレンタリング時に値を出力する。 さらに、モデルが変更したときはすぐにビューを更新するように、バインディングを作成する。 もしバインディングがフォームのINPUTのようにユーザインタラクションによる変更のある要素に対して用いられていたときは、 Derbyはその値の変更にともなって自動的にモデルを更新する。

単に値を取るだけでなく、ヘルパ関数を利用して値を変更させることもできます。

DOMイベントバインディング

属性x-bindは任意のHTML要素に用いることができ、これはひとつ以上のDOMイベントを、名前で指定されたひとつのコントローラ関数に割り付けることができる。 割り付けた関数はアプリ上でエクスポートさせる必要がある。 割り付けた関数には、独自のイベントオブジェクト、属性x-bindが指定された要素、イベントバブルを継続させることができる関数next()が渡される。

ブラウザはターゲット上でDOMイベントを排出し、その親ノードのそれぞれで ハンドラe.stopPropogation()が呼ばれないかぎり、ドキュメントルートへ向かってイベントを受け渡していく。

Derbyはイベントバブリングをよりrouteっぽくした (訳注 多分Expressのrouteのこと)。 ターゲット要素や最初に割り付けられた親要素のハンドラ関数が呼ばれるとそこでイベントバブリングは停止する。 ハンドラで関数next()を呼び出すことで、イベントバブリングを継続させることができる。

コンポーネント

コンポーネントはHandlebarsのpartialsに似ているが、それよりも強力である。 partialsのように、利用している親コンテキストのスコープを継承できることに加えて、Derbyのコンポーネントは、ノード属性や子要素を引数として与えることができる。 可読性とより効率のよいテンプレート集約のために、個々のテンプレートをシンプルにし、それぞれの重要な要素ごとにコンポーネントを使うのがベストである。

どのDerbyテンプレートもコンポーネントとして利用でき、それらは特別な名前空間の付いたカスタムHTMLタグとしてインクルードされる。 アプリ内で定義されたコンポーネントは、すべてapp名前空間からアクセスされる。

スタイルシート

StyleSheetsはデフォルトではStylusを利用します。

ちなみにDerbyではソースやスタイルシートは変更を検知したら自動的に実行中のサーバに変更が反映されますので、サーバを再起動したりする必要がありません。

おわりに

なお、今回はapp.readyは利用しませんでした。

ちなみに、ちょっと前に試したコードでは、次のような感じでdiv要素にdraggableを付けるようにしていたのですけれど、0.5.0からは追加したアイテムに対しては動作しなくなってしまいました。

function make_all_draggable(model) {
    $('.items-div div').each(function(i) {
        var id = model.get('_page.items.' + i + '.id');
        make_draggable(model, id, $(this));
    });
}

app.ready(function(model){
    make_all_draggable(model);

    model.on('insert', '_page.items', function (index, value, passed) {
        make_all_draggable(model);
    });
});

Derbyのソースコードを見ていませんが、作られたDOMノードが存在しつづける保証がなさそうなのでうまくいかないのかもしれません。

というか、Bootstrap (derby-ui-boot) のようにコンポーネントライブラリ化したらいいような気がします。

関連リンク

0 件のコメント:

コメントを投稿

注: コメントを投稿できるのは、このブログのメンバーだけです。