jQueryプラグインの書き方を考えてみる(3)

更新履歴

2010-01-21
プラグイン定義方法については、約1年後に再考した下記エントリの方をお勧めします。

前回「jQueryプラグインの書き方を考えてみる(2)」の続きです。

前回作成した myclass4jquery でいろいろ試したところ、不具合やら改良したい点がいろいろでてきたので修正してみました。

crossBind メソッド の method パラメータ

crossBind メソッド の method パラメータの指定が配列形式でしたが何か不恰好な感じがしたので、ハッシュ形式で指定するように変更しました。
また、前回の例題では method パラメータで指定した min() や max() といったメソッドに対しても、実行の都度、アニメーション対象要素のインデックス番号を指定してましたが、やはりこれも不恰好な気がしたので、getter メソッドで取得された要素に対してのみメソッドが適用されるように変更しました。

$j('#sample1,#sample2').Sample()
.inFrame(0).max() // #sample1 のみが拡大される
.inFrame(1).min() // #sample2 のみが縮小される
.inFrame().max()  // #sample1 , #sample2 の両方が拡大される

継承ありクラスの不具合

$super.init のコール等で親クラス内の crossBind により生成された getter や method が子クラス内に伝播されてこない。
$super.init のコール等で親クラス内で my() メソッドが実行された場合、親クラスの実体が取得される必要があるが、子クラスの実体が取得されてしまう。
等、不具合がありましたのでロジックを組みなおしました。

各種メソッドの追加、修正

前述を含む改善を図るため myclass4jquery に汎用メソッドの追加、修正を行いました。

idx() メソッド

現在選択されてる要素のインデックス番号を返します。

$j('#sample1,#sample2').Sample()
.inFrame(1).idx() // 1
idxEach メソッド

jQuery の each メソッド相当のものですが、選択要素のインデックス番号を受け取ることができます。
each メソッドを使った場合

var my=$j('#sample1,#sample2').Sample();
my.inFrame(1).each(function(idx,node){
    alert(idx); // 0
})

idxEach メソッドを使った場合

$j('#sample1,#sample2').Sample()
.inFrame(1).idxEach(function(idx,node,n$j){
    alert(idx); // 1
})
getter メソッドの改良

getter メソッドの引数は inFrame(1) のようにインデックス番号のみとしてましたが、通常の DOM ノードを渡した場合、対応するインデックス番号の jQuery DOMノードを返すようにしました。

var my=$j('#sample1,#sample2').Sample();
my.inFrame(1).click(function(){
    alert(my.inFrame(this)==my.inFrame(1)) // true
})

前回のサンプルプログラムの修正

前述の改良を活用し、前回のサンプルプログラムを改良版 myclass4jquery に対応させてみます

jQuery(function($j){
    $j.Sample = $j.fn.Sample = $j.MyClass.create({
        init: function(opt){
            this.opt = $j.extend({
                max: 200,
                min: 100
            }, opt);
            this.build();
            return this;
        },
        build: function(){
            var my = this;
    
            //要素の生成とメソッドのバインド
            my.$j()
                .wrap('<div style="border:solid 10px #fafafa;margin:10px;"></div>')
                .css('border', 'solid 10px #eee').html('click!')
                .wrapInner('<div style="border:solid 10px #ddd;background:#ccc"></div>')
            ;
            my.crossBind({
                getter: {
                    btn : $j('<button>click!</button>').prependTo('body'),
                    outFrame: my.$j().parent().each(function(idx){
                        $j(this).css('margin-left', idx * my.opt.min);
                    }),
                    inFrame: my.$j().find('> div')
                },
                method:my.method
            });
    
            //イベントのバインド
            my.inFrame().click(function(){
                my.inFrame(this).inFrameClick()
            
            });
            my.btn().click(function(){
                my.btn(this).btnClick()
            });
            return this;
        },
        nextResize : function(idx){
            this.outFrame(idx).width() == this.opt.min ? this.outFrame(idx).max() : this.outFrame(idx).min();
        },
        method : {
            btnClick : function(){
                this.my().nextResize();
                return this;
            },
            inFrameClick : function(){
                this.my().nextResize(this.idx());
                return this;
            },
            resize: function(size){
                this.outFrame(this.idx()).animate({
                    width: size
                })
                this.inFrame(this.idx()).animate({
                    height: size - 40
                })
                return this;
            },
            min: function(){
                var p=this.my().opt;
                this.resize(p.min).resize(p.max).resize(p.min);
                return this;
            },
            max: function(idx){
                var p=this.my().opt;
                this.resize(p.max).resize(p.min).resize(p.max);
                return this;
            }
        }
    })
})
jQuery(function($j){
    $j('#sample1,#sample2').Sample()
        .inFrame().css('background', 'red')
        .min()
        .outFrame(1).css({
            'background': 'blue',
            'padding': '10px'
        })
        .max()
    ;
})

サンプルページ

イベントのバインドのところがもう少しスリムにできそうですが、前回のロジックに比べればだいぶ見通しが良くなったかと思います。

myclass4jquery.js のソース

以下のようなつくりになっています。

(function($j){
    $j.MyClass = {
        create : MyClass.create({
            init : function(c,e){
                var o=this;
                var f = MyClass.create(c,e);
                var r = function(){
                    var ins=$j.MyClass.getMyInstance(f,this);
                    return ins.init.apply(ins,arguments);
                };
                r.__member__ = f.__member__;
                return r;
            }
        }),
        getMyInstance : function(f,n$j){
            var o=this;
            var _init = f.__member__.init;
            f.__member__.init=function(){return this};
            var ins=f.apply(n$j,arguments);
            if(_init)ins.init = f.__member__.init=_init;
            o.nestedBind(ins,n$j);
            return ins;
        },
        nestedBind : function(ins,n$j){
            var o=this;
            if (ins.$super) {
                o.nestedBind(ins.$super,n$j);
                ins.__getter__=MyClass.MyUtil.getInstance(ins.$super.__getter__);
            }
            ins.my=function(){return ins}
            ins.crossBind = function(opt){return o.crossBind(ins,opt)}
            o.crossBind(ins,{getter:{$j:n$j},method:o.comnMethod})
            return ins;
        },
        crossBind : function(ins,opt){
            var o=this;
            var g = ins.__getter__=ins.__getter__||{}
            var m = ins.__method__=ins.__method__||{}
            if(opt.getter)$j.extend(g, opt.getter);
            if(opt.method)$j.extend(m, opt.method);
            for(var i in g)ins[i]=o.getReGetter(i,g[i],ins);
            $j.extend(ins,m);
            return ins;
        },
        getReGetter : function(name,obj,ins){
            var o=this;
            var g=ins.__getter__;
            var m=ins.__method__;
            return function(idx){
                var newObj = o.reget(obj,idx);
                newObj.my=ins.my;
                for(var j in g)newObj[j]=o.getReGetter(j,g[j],ins);
                $j.extend(newObj,m);
                return newObj;
            }
        },
        reget : function(obj,idx){
            if(!(obj instanceof $j))return obj;
            if(!obj.__allObj__)obj.__allObj__=obj;
            if(typeof idx == 'object')idx = obj.__allObj__.index(idx);
            if(idx >= obj.__allObj__.size())idx=obj.__allObj__._idx;
            obj.__allObj__._idx=idx;
            if(idx==undefined)return obj.__allObj__;
            var newObj=obj.__allObj__.filter(':eq('+idx+')')
            newObj.__allObj__=obj.__allObj__;
            return newObj;
        },
        comnMethod : {
            idx : function(){
                return this.__allObj__?this.__allObj__._idx:undefined;
            },
            idxEach : function(f){
                var o=this,cIdx=o.idx();
                return o.each(function(idx,node){
                    f(cIdx==undefined?idx:cIdx,node,o);
                })
            }
        }
    }
})(jQuery);