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

更新履歴

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

jQuery 用にカスタマイズした myclass.js を使用して、プラグインの定義方法を考えてみます。

myclass.js による クラス定義

myclass.js を使用するとクラスベースな OOP な書き方ができます。

(function(target){
    target.Sample = MyClass.create({
        init: function(msg){
            this.msg = msg;
            return this;
        },
        method: function(msg){
            alert(msg||this.msg)
            return this;
        }
    })
})(window);
(function(){
    Sample('default').method().method('parameter');
})();

サンプルページ

継承もできます。

(function(target){
    target.Sample2 = MyClass.create(target.Sample,{ //第一引数に親クラスを指定
        init: function(node,msg){
            this.node=node;
            this.$super.init(msg); //親クラスのコンストラクタの実行
            return this;
        },
        setMsg: function(msg){
            this.node.innerHTML=msg||this.msg;
            return this;
        }
    })
})(window);
(function(){
    window.onload=function(){
        Sample2(document.getElementById('sample1'),'default').setMsg();
    }
})();

サンプルページ

myclass.js の詳細はこちらをどうぞ。
自分用class定義ライブラリmyclass.jsを作ってみる(1) (2) (3) (4)

myclass4jquery.js によるプラグイン定義

myclass.js を jQuery 用にカスタマイズした myclass4jquery.js でプラグイン定義してみます。jQuery.MyClass.create メソッドを使用し以下のよう定義します。

(function($j){
    $j.Sample = $j.fn.Sample = $j.MyClass.create({
        init: function(msg){
            this.msg = msg;
            return this;
        },
        method: function(msg){
            alert(msg||this.msg)
            return this;
        }
    })
})(jQuery)
jQuery(function($j){
    $j.Sample('none node').method();
    $j('#sample1,#sample2').Sample('has node').method();
})

サンプルページ

継承

myclass.js と同様、第一引数に親クラスを指定する事で継承も可能です。

(function($j){
    $j.Sample2 = $j.fn.Sample2 = $j.MyClass.create($j.Sample,{
        init: function(msg){
            this.$super.init(msg);
            return this;
        },
        setMsg: function(){
            this.$j().html(this.msg);
            return this;
        }
    })
})(jQuery)
jQuery(function($j){
    $j.Sample2('none node').method();
    $j('#sample1,#sample2').Sample2('has node').setMsg();
})

サンプルページ

jQueryDOMノードとプラグインオブジェクトの参照

myclass4jquery.js でプラグインオブジェクトを定義した場合、jQueryで取得したjQueryDOMオブジェクトと、プラグインオブジェクトが共存する事になります。これらのオブジェクトを参照する場合は自動生成される getter メソッドを使用します。
jQueryDOMオブジェクトを取得する場合は $j メソッド、プラグインオブジェクトを取得する場合は my メソッドを使用します。

jQuery(function($j){
    $j('#sample1,#sample2').Sample()
        .my().method('set blue')
        .$j().css('background','blue')
        .my().method('set red')
        .$j().css('background','red')
    ;
})

サンプルページ

前回 (jQueryプラグインの書き方を考えてみる(1)) のサンプルプログラムを myclass4jquery で書き換えてみます。
$j.Sample = $j.fn.Sample = $j.MyClass.create({
    init: function(opt){
        this.opt = $j.extend({
            max: 200,
            min: 100
        }, opt);
        return this.build();
    },
    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>')
        ;
        var out$j = my.$j().parent(), in$j = my.$j().find('> div');
        
        $j('<button>click!</button>').prependTo('body').click(function(){
            out$j.width() == my.opt.min ? my.max() : my.min();
        });
        in$j.click(function(){
            var idx = my.inFrame().index(this);
            return my.outFrame(idx).width() == my.opt.min ? my.max(idx) : my.min(idx);
        })
        out$j.each(function(idx){
            $j(this).css('margin-left', idx * my.opt.min);
        })
        my.crossBind({
            getter: {
                outFrame: out$j,
                inFrame: in$j
            },
            method: ['min', 'max']
        });
        return this.$j();
    },
    resize: function(size, idx){
        this.outFrame(idx).animate({
            width: size
        })
        this.inFrame(idx).animate({
            height: size - 40
        })
        return this;
    },
    min: function(idx){
        this.resize(this.opt.min, idx).resize(this.opt.max, idx).resize(this.opt.min, idx);
        return this;
    },
    max: function(idx){
        this.resize(this.opt.max, idx).resize(this.opt.min, idx).resize(this.opt.max, idx);
        return this;
    }
})
})(jQuery)

jQuery(function($j){
    $j('#sample1,#sample2').Sample()
        .inFrame().css('background', 'red')
        .outFrame().css({
            'background': 'blue',
            'padding': '10px'
        })
        .min()
    ;
})

サンプルページ

前回解決できなかった問題
    • inFrame() や outFrame() の getterメソッド実行後、min() や max() といったメソッドをメソッドチェーンで実行することができない
    • OOP的な記述にした影響で min,maxメソッドの記述が冗長化してしまう

これらが解決されてます。
これは内部的に生成される crossBind メソッドで解決してます。

my.crossBind({
    getter: {
        outFrame: out$j,
        inFrame: in$j
    },
    method: ['min', 'max']
});

getter には、ハッシュ形式で getter メソッド名と 参照したいオブジェクトを指定し、method には、メソッドチェーンの対象としたいメソッド名を 配列で指定します。以下のようなメソッドチェーンな記述が可能になります。

$j('#sample1,#sample2').Sample().inFrame().$j().outFrame().min().inFrame().my();
インデックス指定による jQueryDOMノードの再取得

jQueryを使用するとサンプルのように複数のDOMノードを容易に取得する事ができますが、取得後、n 番目のノードのみを再取得したくなる場合があります。そのような場合、

$j('div').filter(':eq(1)')		

のように記述すれば取得可能ですが、myclass4jquery.js でプラグイン定義した場合に同様の記述を行うと、前述の crossBind メソッドによって関連付けられたメソッドチェーンが使用できなくなってしまいます。この対策として、getter メソッドの引数に取得したいノードの番号( n )を指定する事で対象ノードが jQueryDOMノードとして再取得され、且つ、メソッドチェーンも継続して使用できるようになります。

jQuery(function($j){
    $j('#sample1,#sample2').Sample()
        .inFrame().css('background', 'red')
        .outFrame(0).css({
            'background': 'blue',
            'padding': '10px'
        }).min().max(1)
    ;
})

サンプルページ

myclass.js、myclass4jquery.js のソース

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

修正履歴
2008.9.9
:三世代以上の継承時、親オブジェクト内($super)で、先祖オブジェクト($super.$super)の参照ができなかったので修正。継承時、親オブジェクト内($super)で $j , my メソッドの参照ができなかったので修正。
2008.9.20
継承によるクラス定義の場合、動的に生成、割り当てされた getter や method の 子クラスへの伝播等に問題があったので修正。詳細は次のエントリにて。
myclass.js
(function(){
    window.MyClass = {
        create : function(c,e){
            var f = function(){
                var ins=MyClass.getMyInstance(f);
                if(ins.init)return ins.init.apply(ins,arguments);
                return ins;
            }
            f.__member__ = e||c;
            if(e)e.__c__=c;
            return f;
        },
        getMyInstance : function(f){
            var util = MyClass.MyUtil;
            if (f.__member__.__c__) {
                var ins = MyClass.getMyInstance(f.__member__.__c__)
                var $super = ins;
                ins = util.extend(
                        util.getInstance($super),util.getInstance(f.__member__,true)
                );
                ins.$super = $super;
                return ins;
            }
            return util.getInstance(f.__member__,true);
        }
    }
    window.MyClass.MyUtil = {
        extend : function(obj,ext){
            for(var i in ext)obj[i]=ext[i];
            return obj;
        },
        getDeepInstance : function(obj){
            if (obj.constructor === Object) {
                var f = function(){};      
                for(var i in obj)f.prototype[i]=MyClass.MyUtil.getDeepInstance(obj[i]);
                return new f();
            }
            else return obj;
        },
        getInstance : function(obj,deep){
            var f;
            (f = function(){}).prototype = obj;      
            obj = new f;
            if(deep)for(var i in obj)obj[i]=MyClass.MyUtil.getDeepInstance(obj[i]);
            return obj;
        }
    }
})();
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 n$j=this;
                    var ins=$j.MyClass.getMyInstance(f,n$j);
                    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,
                o.crossBind(ins,{
                    getter : {
                        $j : n$j
                    }
                })
            );
            return ins;
        },
        nestedBind : function(ins,getter){
            var o=this;
            $j.extend(ins,getter);
            ins.crossBind = function(opt){
                return o.crossBind(ins,opt);               
            }
            if(ins.$super)o.nestedBind(ins.$super,getter);
        },
        crossBind : function(ins,opt){
            var o=this;
            if(!ins.__getter__)ins.__getter__={};
            if (opt.getter) {
                for(var n in opt.getter)ins.__getter__[n]=o.getReGetter(n,opt.getter[n],ins);
                ins.__getter__.my=function(){return ins;}
            }
            if(opt.method){
                ins.__method__=ins.__method__||{};
                for (var i = 0; i < opt.method.length; i++) {
                    var name=opt.method[i];
                    ins.__method__[name] = o.getMethod(ins, ins[name])
                }
            }
            for (var n in ins.__getter__) {
                var obj=ins.__getter__[n]();
                $j.extend(obj, ins.__getter__);
                if (ins.__method__) {
                    $j.extend(obj, ins.__method__);
                }
            }
            return ins.__getter__;
        },
        getMethod : function(ins,f){
            return function(){
                return f.apply(ins,arguments);
            }
        },
        getReGetter : function(name,obj,ins){
            var o=this;
            return function(idx){
                var newObj = o.reget(obj,idx);
                $j.extend(newObj,ins.__getter__);
                if(ins.__method__)$j.extend(newObj,ins.__method__);
                return newObj;
            }
        },
        reget : function(obj,idx){
            if(!(obj instanceof $j))return obj;
            if(!obj.__allObj__)obj.__allObj__=obj;
            if(idx==undefined)return obj.__allObj__;
            var newObj=obj.__allObj__.filter(':eq('+idx+')')
            newObj.__allObj__=obj.__allObj__;
            return newObj;
        }
    }
})(jQuery);