jQuery ライクなプラグイン API の定義方法を考えてみる
jQuery UI の $.widget を使用しプラグイン定義を行うと、jQuery らしい一貫性のあるプラグイン API が使えるようになります(関連記事)但し「ui.core.js(13.6KB)が必須」「API メソッド実行の記述が冗長になる」といった条件がついてしまいます。
今回はこれらの問題を解決しつつ、jQuery UI のような柔軟性の高いプラグイン API を定義する方法について考えてみました。
jQuery UI と jQuery TOOLS のプラグイン API
jQuery UI では、プラグイン API のメソッドを実行するには、プラグインメソッドの引数に API メソッド名を指定するかたちで実行します。そのためメソッド実行の記述が少々冗長になります。
//jQuery オブジェクトが返えされる var dialogs = $('#sample1,#sample2').dialog(); //プラグインメソッドの引数に API メソッド名を指定する dialogs.dialog('close'); dialogs.dialog('open');
jQuery TOOLS の場合は、API オブジェクトを一旦取得した後、API オブジェクトのメソッドを実行します。
//api パラメータを指定することで API オブジェクトが取得できる var tooltip = $('#sample1,#sample2').tooltip({ api : true }); //API オブジェクトのメソッドを実行 tooltip.show(); tooltip.hide();
jQuery TOOLS の記述の方がすっきりした印象を受けますが、サンプルの様に複数要素に対してプラグインが実行されてた場合は、プラグイン API は、最後の要素にしか処理が適用されません。
プラグイン API を jQuery ライクな API にしてみる
API オブジェクトを jQuery でラップする
jQuery TOOLS の API オブジェクトは、prototype ベースで定義された関数オブジェクトをインスタンス化したもので、プラグインメソッドが実行されると、jQuery セレクタで指定された要素数分生成されます。api パラメータが true の場合は、最後に生成された API オブジェクトのみが返される仕様になっています。
//prototype ベースな実装定義 var tooltip = function(){} tooltip.prototype = { show : function(){}, hide : function(){} } $.fn.tooltip = function(opt){ var api; this.each(function(){ //要素数分 API オブジェクトを生成 api = new tooltip(); }); //最後の api オブジェクトが返却される return opt.api ? api : this; }
この処理を、要素毎に生成された API オブジェクト全てを配列に詰め、jQuery オブジェクト化したものを返すようにしてみます。
$.fn.tooltip = function(opt){ var api = []; this.each(function(){ //生成した API オブジェクトは配列に格納 api.push( new tooltip() ); }); //jQuery オブジェクトにして返却 return opt.api ? $(api) : this; }
jQuery セレクタには通常、セレクタ文字列や要素オブジェクトを指定しますが、配列を指定することもできます。こうすることで配列に格納された HTML 要素以外のオブジェクトに対しても jQuery の持つ各種メソッドを適用することができます。例えば、以下のように for 文を使用せず each メソッドで、個別の API オブジェクトを参照することができます。
//#sample1,#sample2 それぞれ専用の API オブジェクトがjQuery オブジェクトにラップされ返される var tooltip = $('#sample1,#sample2').tooltip({ api : true }); //each 内では個別の API オブジェクトを this で参照することができる tooltip.each(function(idx){ this.show(); this.hide(); });
ラッパーメソッドを動的に割り当てる
jQuery オブジェクトでラップされた API オブジェクトを返すようにしたため、each メソッドや添え字指定(あるいは get メソッド)の記述をしないと、API オブジェクトを参照できなくなってしまいました。
var tooltip = $('#sample1,#sample2').tooltip({ api : true }); alert(tooltip.show) // undefind //添え字指定もしくは get() メソッドで参照可能 tooltip[0].show(); tooltip.get(1).show();
これについては、API オブジェクトのメソッドに対するラッパーメソッドを、jQuery オブジェクトに割り当てることで解決してみます。下記関数にて割り当て処理を行います。
$.ex.api 関数
(function($){ $.ex = $.ex || {}; $.ex.api = function(api){ var api = $(api),api0 = api[0]; for(var name in api0) (function(name){ if($.isFunction( api0[name] )) api[ name ] = (/^get[^a-z]/.test(name)) ? function(){ return api0[name].apply(api0,arguments); } : function(){ var arg = arguments; api.each(function(idx){ var apix = api[idx]; apix[name].apply(apix,arg); }) return api; } })(name); return api; } })(jQuery);
プラグイン定義処理では、API オブジェクトを格納した配列を上記の関数に渡し、その返却値をそのまま返すようにします。
$.fn.tooltip = function(opt){ var api = []; this.each(function(){ api.push( new tooltip() ); }); //$.ex.api の返却値を返す return opt.api ? $.ex.api(api) : this; }
これにより、プラグイン API を jQuery ライクに使用することができるようになります。
//jQuery ベースな API オブジェクトが返される var tooltip = $('#sample1,#sample2').tooltip({ api : true }); //#sample1,#sample2 共に処理が適用される tooltip.show(); //メソッドチェーンも使用可 tooltip.show().hide(); //名前が getXXX の場合は 先頭 API オブジェクト(#sample1) のゲッターメソッドが実行される tooltip.getTip(); //添え字指定もしくは get() メソッドで、個別の API オブジェクトの参照が可能 tooltip[0].show(); tooltip.get(1).getTip(); //each メソッドでも個別 API オブジェクトの参照が可能 tooltip.each(function(idx){ this.show(); tooltip[idx].hide(); });
ラッパーメソッド割り当て関数 $.ex.api では、以下のような処理を行っています。
- 引数で受け取った配列で jQuery オブジェクトを生成
- 配列内の先頭の API オブジェクトより、API として定義されているメソッド名を順番に読み取る
- メソッド名が getXXX となっていた場合はゲッターメソッドと判断し、「配列内の先頭の API オブジェクトのメソッドのみを実行し、その処理結果を返却値として返すラッパーメソッド」を、生成した jQuery オブジェクトに割り当てる
- それ以外のメソッドの場合は、「配列内全ての API オブジェクトのメソッドを実行し、自身(this)を返却値として返すラッパーメソッド」を、生成した jQuery オブジェクトに割り当てる
- 生成した jQuery オブジェクトを返却値として返す
$.ex.api を使用してプラグインを定義してみる
プラグイン定義
(function($){ $.tooltip = function( idx , targets , option ){ var o = this, //パラメータの保持 c = o.config = $.extend({} , $.tooltip.defaults , option ); //要素の保持 c.targets = targets; c.target = targets.eq(idx); c.tip = $(c.tip).hide(); //イベントの割り当て if( c.bindEvent ){ c.target .mouseover(function(){o.tipShow()}) .mouseout(function(){o.tipHide()}); } } $.extend( $.tooltip.prototype,{ //ツールチップの表示 tipShow : function(){ var o = this,c = o.config; var pos = c.target.offset(); c.tip.html(c.target.attr('title')).show() .css({ 'position':'absolute', 'top':pos.top - c.tip.outerHeight() -20, 'left':pos.left - 20 }) return o; }, //ツールチップの非表示 tipHide : function(){ var o = this,c = o.config; c.tip.hide(); return o; }, //要素セットの取得 getTargets : function(){ return this.config.targets; }, //要素の取得 getTarget : function(){ return this.config.target; } }); $.extend( $.tooltip,{ defaults : { api : false, bindEvent : true } }); $.fn.tooltip = function( option ) { var targets = this,api = []; //要素単位にループ targets.each(function(idx) { var target = targets.eq(idx); //プラグインメソッド適用済み時は退避オブジェクトを取得、未適用の場合は新規に生成 var obj = target.data('tooltip') || new $.tooltip( idx , targets , option); //オブジェクトを配列に格納 api.push(obj); //オブジェクトを対応する要素に退避 target.data('tooltip',obj); }); //API リクエスト時は $.ex.api で加工した API オブジェクトを返す return option && option.api ? $.ex.api(api) : targets; }; })(jQuery);
HTML
<div id="demotip"></div> <div id="demo"> <img src="xxx.jpg" title="tooltip message"/> <img src="xxx.jpg" title="tooltip message"/> <img src="xxx.jpg" title="tooltip message"/> <img src="xxx.jpg" title="tooltip message"/> </div>
プラグインの実行
jQuery(function($){ $("#demo img[title]").tooltip({ tip : '#demotip' }) })
ツールチップ表示対象となる要素を jQuery セレクタで指定し、ツールチップ要素を tip パラメータで指定します。ツールチップには title 属性に記述された内容が表示されます。
プラグイン API を活用してみる
プラグイン API を使用し、他要素のイベントから「ツールチップの表示・非表示」「ツールチップ元画像のスタイル」を制御してみます。
HTML(追加コード分)
<span>表示したいツールチップ番号を選択してください</span> <select id="selectTip"> <option value="0">none</option> <option value="1">tooltip - 1</option> <option value="2">tooltip - 2</option> <option value="3">tooltip - 3</option> <option value="4">tooltip - 4</option> </select>
プラグインの実行
jQuery(function($){ //API オブジェクトの取得 var api = $("#demo img[title]").tooltip({ tip : '#demotip', bindEvent : false, api : true }) $('#selectTip').focus().change(function(){ var val = parseInt($(this).val()); //ツールチップを非表示にし、全ての画像を透過状態にする api.tipHide().getTargets().css('opacity', val ? 0.3 : 1); if( val ){ //選択されたツールチップを表示し、画像の透過を解除する api.get(val - 1).tipShow().getTarget().animate({'opacity':1}); } }) })
ex.api で定義されたラッパーメソッドのおかげで、each 等を使用しないシンプルな記述が可能になったかと思います。ex.api はソースも小さいので、個別のプラグイン内で定義しても良いかと思います。
ちなみに上記のツールチッププラグインでは、「data() メソッドによる API オブジェクトの退避」「プラグインメソッドの二重起動の制御」をしているため、以下のような任意のタイミングによる API オブジェクトの取得も可能です。
//#sample1,#sample2 に初期化処理が適用される $('#sample1,#sample2').tooltip(); ... //#sample3 にのみ初期化処理が適用され、#sample1,#sample3 の API オブジェクトが返される var api = $('#sample1,#sample3').tooltip({ api : true});