Backbone.jsの基本

細かいことはサンプルコードで、ということで……

backbone.js v1.0.0

function l(s) {console.log(s)};
function a(s) {alert(s)};
l('*****start*****');

l('*****Backbone.Model*****');
var Todo = Backbone.Model.extend({
    defaults: {
        title: ''
        ,completed: false
    }
    ,initialize: function() {
        l('*** model initialize***');
        this.on('change', function(){l('model has changed');});
        this.on('change:title', function(){l('title has changed');});
        
        this.on('invalid', function(){l(this.validationError)});
    }
    ,validate: function(attrs) {
        if(!_.isBoolean(attrs.completed)) {
            return 'completedはブーリャンでお願いしますね!';
        }
    }
});

var todo = new Todo({title: 'sample'});
l(JSON.stringify(todo));
l(todo.get('title'));
l(todo.attributes.title);
l(todo.hasChanged());//false
l(todo.hasChanged('title'));//false
l(todo.hasChanged('completed'));//false

todo.set('title', 'changed title');
l(todo.get('title'));
l(todo.hasChanged());//true
l(todo.hasChanged('title'));//true
l(todo.hasChanged('completed'));//false

todo.set('completed', 'hoge');
todo.save();// invalid


l('*****Backbone.View*****');
var TodoView = Backbone.View.extend({
    id: 'sample_view'
    ,className: 'sample_class1 sample_class2'
    ,tagName: 'li'
    ,template: _.template('example')
    ,events: {
        'dblclick label': 'edit'
        ,'keypress .edit': 'updateOnEnter'
        ,'blur .edit': 'close'
    }
    ,render: function() {
        this.$el.html(this.template(this.model.toJSON()));
        this.input = this.$('.edit');
        return this;
    }
    ,edit: function(){}
    ,updateOnEnter: function(){}
    ,close: function(){}
});

var todoView = new TodoView();
l(todoView.el);
l(todoView.$el);

var button = $('<button></button>');
todoView.el = button;
l(todoView.el);//jQueryオブジェクトになっちゃってる
l(todoView.$el);//前から変わってない

todoView.setElement(button);
l(todoView.el);//期待通り
l(todoView.$el);//期待通り

todoView.el = '<input/>';
l(todoView.el);//期待通り
l(todoView.$el);//前から変わってない

l('*****Backbone.Collection*****');
(function(){
    var collection = new Backbone.Collection;
    collection.add([{id:1, name:'dog'},{id:2, name:'cat'}]);
    l(JSON.stringify(collection));
    
    collection.add([{id:1, name:'dogdog'}], {merge:true});
    l(JSON.stringify(collection));//id:1のモデルのnameがマージされてdogdogになる
    l(collection.get(1));//上でマージしたやつ
    l(collection.get(1).idAttribute);//id
})();

var TodoList = Backbone.Collection.extend({
    model:Todo
});
var todoList = new TodoList();
todoList.on('add', function(model){l(model.cid + ' added to collection');});
todoList.on('remove', function(model){l(model.cid + ' removed from collection');});
todoList.on('change', function(model){l(model.cid + ' changed in collection');});
todoList.on('change:title', function(model){l(model.cid + ' title changed in collection');});
todoList.on('reset', function(models,options){
    l('collection reset');
    l(models);
    _.each(options.previousModels, function(m){
        l('  ' + m.cid + ' removed');
    });
    _.each(models.models, function(m){//models.modelsってなんだ……
        l('  ' + m.cid + ' added');
    });
});

var todo = new Todo({title: 'sample'});
l(todo.id);//undefined
l(todo.cid);//c1とか
l(todo.idAttribute);//id
todoList.add([todo]);
l(JSON.stringify(todoList));
//l(list.get(0).get('title'));//idがないからidでgetはできない
l(todoList.get(todo.cid).get('title'));//sample

todoList.add([{id: 0, title: 'sample'}]); //別にnew Todo()しなくても属性値だけでaddできちゃう
l(todoList.get(0).get('completed'));//Todoとしてaddされているのでちゃんとfalseと出力される

//一旦Collectionにaddしたならmodel単体で操作してもCollection側のイベントハンドラは有効
var temp = todoList.get(0);
temp.set('title', 'タイトル変えてみるわ');//Todo,TodoList両方でchangeイベントがハンドリングされる
todoList.remove(0);

todoList.reset([{id: 0, title: 'dog'}, {title: 'cat'}]);//完全に中身を作り直す
todoList.set([{id:0, title: 'dogdog'}, {title: 'mouse'}]);//add,remove,changeを駆使する
todoList.reset();//resetイベントのみ発生

Backbone.Model

RubyActiveRecordみたいにhasChangedで変更有無を調べられるのはよかった。setter(model.set(attr, val))でセットしないと変更されたと見なされないのも同じ。

バリデーションはイベントだけが提供されていて、Railsのsexy validationみたいなのはない。

Backbone.View

HTML要素1つに紐付くっぽい。そのdom elementを一から作るなら、

var v = Backbone.View.extend({
    id: 'sample_view'
    ,className: 'sample_class1 sample_class2'
    ,tagName: 'li'
});

と言った調子で表現できる。

既に存在する要素に対しては

var v = Backbone.View.extend({
    el: '#sample_view'
});
var v = Backbone.View.extend({});
v.setElement($('<button></button>'));

といった調子。newしたあとにv.elに直接代入するのはよくないっぽいので上記2つ以外はやらない方がたぶんいい。

setElement()で要素を設定したらel$el両方が自動セットされる。

Backbone.Collection.addでmergeするところがちょっとよくわからなかったんです

var collection = new Backbone.Collection;
collection.add([{id:1, name:'dog'},{id:2, name:'cat'}]);
l(JSON.stringify(collection));

collection.add([{id:1, name:'dogdog'}], {merge:true});
l(JSON.stringify(collection));

これで

[{"id":1,"name":"dog"},{"id":2,"name":"cat"}]
[{"id":1,"name":"dogdog"},{"id":2,"name":"cat"}] 

となぜidをキーにマージされるのかわからなかったけど、これはつまり、addに渡したオブジェクトはもうBackbone.Model扱いを受けるので、そのidはdom elementのidと同義になるから、だからこのid属性が同じならdom上でも同じだからマージしちゃうわけですねたぶん。

追記:違った・・・
Backbone.jsのモデルはすべて

  • オブジェクトごとに自動付与されるcid
  • オブジェクトを識別するid
  • オブジェクトを識別する属性を指定するidAttribute
    • これのデフォルトがidで、もし対応するテーブルのPKがid_userならそう書き換えて使う。

を持っているんですって。

あーそうかそうか、Backbone.Viewのidとごっちゃにしてしまったのか。失礼しました。

jQuery Mobileでポップアップの中にcolumntoggleなtableを表示したいんです

普通に書けば普通にできる。私は普通ではなかったのでハマった……

ただしカラム選択用ボタンはダメ。1画面1ポップアップとかそんな感じの制限があるっぽく、カラム選択ポップアップを表示すると元のtableを表示していたポップアップの方が消える。data-dismissible="false"にしても消える。しばらくデバッガで処理追ったけど、やめた。

カラム選択ボタンには.ui-table-columntoggle-btn {display: none;}で退場願った。

カラム優先順位data-priorityは1から6までしかないので、間違って7、8…と書くと「あれなんでこのカラム消えねーの・・・なんでや・・・」ってなる。

tableはtable-layout:fixedにしてtdにword-wrap:break-wordにしないと、改行ポイントのない長い英数字があったらそのカラムの幅が長くなってしまう。

参考

そんなことより

JQM1.3.2だと画面幅狭めて一度消えたカラムが、幅を戻しても戻らないんですね-。公式デモでも確かめられてしまう。JSFiddleでjQuery1.9と併用できるJQM1.3.0b1だとそんなことないんですけどねー。なんで1.3.2でそんなことになったんですかねー。

jQuery Mobileのラジオボタンを選択解除できるようにしたいんです

やり方によるが、「二度押し」で解除させることができなくてハマった。

「もうお馴染みのラジオボタン」は1度選択したら最後解除できない、というのは利用者としてもおおむね常識だと思う。むしろ選択解除できるようにしたいものはプルダウンリストを使う。

スマートフォンだと、プルダウンで表現するとせっかくのタッチデバイスなのにいちいちツータッチ必要で面倒臭い、ラジオボタンにしてワンタッチにしたいという欲求が……ないですかね。
ところがラジオボタンだと選択解除ができないという。

jQuery Mobileでラジオボタンを利用すると、こうラジオボタンというより押しボタン式選択装置のような見た目になりますよね。で利用者としては「これ二度押ししたら選択解除できるんじゃね」と思いそう……ではないですかね。いやそうしてあげたい。

取り得る道として

  1. 上述通り二度押しで選択解除
  2. クリアボタン
  3. ラジオボタンやめて排他選択チェックボックス

くらいが思いつきます。

排他チェックボックスは昔何かで作った気がしますが、選択解除できるラジオボタンより排他選択なチェックボックスの方がより異常だと思うので、これはやりたくない。
※追記:そんなことない気が後からしてきた

クリアボタンなんて置きたくない。画面狭いし。二度押し選択解除でいいでしょ(結論ありき)、と思ってやってみました。

でもこれがなかなかできませんでした。

クリアボタン式なら楽です。

$("#clear_button").on("click", function(){
  $("#radio input:radio:checked").prop("checked", false).checkboxradio("refresh");
});

これでイケます。色々調べてもだいたいこの書き方が載っています。

ところが二度押しで解除させようとするとダメ。どうもバブリングしたイベントのせいでうまく選択解除されてくれない様子だったけど、stopPropagetion()してもだめ。
なんでや。。

どうするのかというと、

    $radio.prop("checked", false).checkboxradio("refresh");
    event.preventDefault();
    event.stopPropagation();

preventDefault()もやる。片方ではダメ。両方書かないと、せっかく$radio.prop("checked", false)しても後からcheckedにされてしまう。

するってーと

ボタンを押すと選択したラジオのvalueをalertします。

参考

jQueryMobileのliのカウントバブルを他のタグで使いたいんです

li内でui-li-countクラスを付加したspanを書けばカウントバブルになる。この例は探せばいくらでも出てくる。

ただ、それ以外のタグの例となるとほとんど無い。StackOverflowで検索しても、要するにあり合わせのクラスを指定して何とかする例しか見つからなかった。

tdタグ内にカウントバブルを表示させるに当たってはこう書いた。

<td><span class="ui-btn-up-b ui-btn-corner-all">3</span></td>
td span {
    padding: .2em .5em;
}

するってーと

jQuery Mobileのポップアップ内の選択結果を親画面に反映したいんです

よくあるやつ。よく見るのはwindow.openerがどうたらっていう書き方。

jQuery Mobileのポップアップウィジェットは同一html内に書いてあるものを擬似的にポップアップさすだけなのでwindow.openerもくそもない。
なのでごりごり書くしかないと思われる。

<div data-role="page">
    <div data-role="header">header</div>
    <!-- 親画面 -->
    <div data-role="content" id="parent">
        <a href="#sample_pop" data-role="button" data-rel="popup" data-position-to="window">popup</a>
    </div>
    <!-- ポップアップ -->
    <div data-role="popup" id="sample_pop">
        <fieldset data-role="controlgroup">
            <legend>select</legend>
            <input type="checkbox" name="item1" id="item1" value="1" />
            <label for="item1">1st</label>
            <input type="checkbox" name="item2" id="item2" value="2" />
            <label for="item2">2nd</label>
            <input type="checkbox" name="item3" id="item3" value="3" />
            <label for="item3">3rd</label>
        </fieldset>

        <a href="#" data-role="button" data-rel="back" data-inline="true">cancel</a>
        <a href="#" data-role="button" data-rel="back" data-inline="true" id="submit">reflect</a>

    </div>
    <div data-role="footer">footer</div>
</div>
$(document).ready(function () {
    $("#submit").on("click", function () {
        // 子画面選択結果反映用のdivを作る
        if (!$("#selection").get(0)) {
            $("#parent").append("<div id='selection'></div>");
        }
        
        // チェックしたチェックボックスのラベルを親画面に反映
        $("#selection").text($("#sample_pop input[type='checkbox']:checked").map(function () {
            return $("label[for='" + $(this).attr("id") + "']").text();
        }).get().join(","));
    });
});

するとこんな感じ。

なんか画面リフレッシュされちゃいますねー。。なんでや。。面倒なので放っときます。。

参考