HTML5 × CSS3 × jQueryを真面目に勉強 – #14 jQuery UI Widget(プラグイン)の作り方について詳しく | Developers.IO

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

そんな訳で、jQuery UI プラグイン(※以下、jQuery UI Widget) の作り方について学んだので、ここに書き記しておくとします。忘れっぽい自分のための備忘録として書いた内容なので、割りと基礎的な部分にフォーカスした入門編のような内容になっています。

ウィジェット作成のための前準備

当然ですが jQuery UI のプラグインなのだから、作成には jQuery エンジンだけでなくjQuery UI ライブラリが必要となります。

手順A | CDN で手軽にロードする

学習目的やちょっとしたテクニカル調査といった場合は、わざわざファイルをダウンロードするのも大げさなので、CDN (コンテンツ・デリバリ・ネットワーク)を利用させてもらうのが妥当です。

img-cdn

jQuery UI オフィシャルページのフッター部分に必要なURLがすべて記載されているので、これらをアナタが作成する HTML ファイルにコピペすれば準備完了です。

このような感じでHTML ファイルのヘッダに記述すればOK

<!DOCTYPE html>
<html>
<head>
<!-- <meta charset="UTF-8"> -->
<title>jQuery UI Widget Sample</title>
<link rel="stylesheet" href="//code.jquery.com/ui/1.10.0/themes/base/jquery-ui.css">
<script src="//code.jquery.com/jquery-1.9.0.js"></script>
<script src="//code.jquery.com/ui/1.10.0/jquery-ui.js"></script>
</head>

手順B | 必要なコンポーネントのみ選択してダウンロード

CDN を利用すれば実ファイルを手元に置くことなく URL を HTML 内で指定するだけで使用することができます。また、とりあえず jQuery UI コンポーネントフルセットをダウンロードしておけば、余計な考慮などをせずに間違いなく全ての機能を使用することができます。しかし本来なら必要のないコンポーネントも含めてダウンロードしてしまうのは、Web コンテンツ作成において必ずしもスマートな手法とは言えません。ページの読み込み速度を1ミリ秒でも速くするために、不要なコードは1バイトでも削るべきです。

jQuery UI のダウンロードページは必要な機能のみを選択して、それのみが実装されたファイルを生成することができます。

今回のところは $.widget のみが使用出来れば良いので、Core UI widgetの p にのみチェックをけてダウンロードします。

img-uicorewidget

バージョンは1.10.0を選択します。

img-uiversion

ダウンロードした zip ファイルを解凍すると、こんな感じになっています。jQuery UI が対応している jQuery の最新版もセットになっています。

img-jqueryui_src

参考までに、今回のように Core UI widget のみを選択した場合とフルセットでダウンロードした場合のファイルサイズは以下のようになりました。

jQuery UI - ファイルサイズ比較
Core UI Widget のみ フルセット
通常版 15 KB 433 KB
Minified版 7 KB 227 KB

ノーマルな jQuery プラグイン(※以下、ノーマルプラグイン)と jQuery UI Widget とでは記述ルールにかなりの違いがあります。簡単なコードで双方の構造を比較してみます。

ノーマルプラグインの書き方

;(function($) {
    //このPluginの名前 - (1)
    $.fn.myPlugin = function(options) {
        //要素をいったん変数に退避 - (2)
        var elements = this;

        // 渡されたオプションとデフォルトをマージ - (3)
        var opts = $.extend({}, $.fn.myPlugin.defaults, options);

        // 要素をひとつずつ処理 - (4)
        elements.each(function() {
            ・・・
        });

        // method chain用に要素を返す - (5)
        return this;
    };

    // プラグインのデフォルトオプション - (6)
    $.fn.myPlugin.defaults = {
        param: 'value',
        ・・・
    };
})(jQuery);

大まかな構造はこんな感じです。

  1. $.fn オブジェクトをプラグイン名(※カスタム jQuery メソッド)で拡張
  2. this にはプラグイン呼び出し時に指定された要素が格納されており、これを変数に退避しておく ※thisをそのまま使用しても構いません
  3. プラグイン内で定義したデフォルトのオプションと呼び出し時に渡されたオプション値をマージする
  4. 指定された要素全てに処理が適用されるように、each() を使って1つずつに同様の処理を行う
  5. method chain が途切れないように、最後にreturn this を書く
  6. プラグイン内にデフォルトオプションを定義する

ノーマルプラグインの作り方に関しては、以下の記事にて詳しく解説しています。

では jQuery UI Widget の方はどうでしょうか。

jQuery UI Widget の書き方

;(function($) {
    // このウィジェットの名前(名前空間つき) ※1
    $.widget('ui.myPlugin', {

        //ウィジェットのデフォルトオプション
        options: {
            key1 : 'value'
        },

        // 一番最初に呼ばれる内部関数
        _create: function() {
            // セットアップ処理
            ・・・
        },

        // _create()後に呼ばれる内部関数
        _init: function() {
            ・・・
        },
        
        // オプションパラメータ変更時に呼ばれる内部関数
        _setOption: function(key, value) {
            // 変更したパラメータに応じて任意の処理を行う
            switch(key) {
                case 'key1':
                break;
            }
            $._super('_setOption', this, arguments);
        },
        
        // インスタンスを破棄
        destroy: function() {
            ・・・
            $._super('destroy', this, arguments);
        }
    });
})(jQuery);

もはや別物と言ってよいくらいにノーマルプラグインとは異なった構造化がなされています。

では実際にサンプルコードを書きながら一つずつ解説していきます。

まずは最小規模なモノからはじめます。

ウィジェットの定義

// プラグインの定義
$.widget('ui.captionator', {
	// オプション、メソッドの定義
});

jQuery UI Widget は $.widget() というファクトリーメソッドを用いて作成します。第一引数に作成したいウィジェット名を名前空間付きで渡します。名前空間を付けないとエラーとなって作成できません。そして第二引数にウィジェットが持つオプションやメソッドといった機能を定義したオブジェクトを渡します。

おっと匿名関数でウィジェット全体をラップするのを忘れずに!

;(function($) {
	// プラグインの定義
	$.widget('ui.captionator', {
		// オプション、メソッドの定義
	});
})(jQuery);

ノーマルプラグインと同様に jQuery UI Widget においても全体を匿名関数でラップして堅牢な作りにします。1行目冒頭のセミコロンも忘れずに。

初期化メソッド

jQuery UI Widget には _create_init という2種類の初期化メソッドが予め用意されており、これらはウィジェットのインスタンス生成時に1回ずつ自動で呼び出されます。

jquery.captionator1.js

;(function($) {
	// プラグインの定義
	$.widget('ui.captionator', {
		// 初期化処理として一番最初に呼び出される
		_create: function() {
			var self = this,	// ウィジェットインスタンスを変数に待避 - (※1)
				element = self.element;	// 要素を変数に待避 - (※2)
			alert(element.attr('alt'));
		},
		// _create の次に呼び出される
		_init: function() {
			var self = this,
				element = self.element;
			alert(element.attr('title'));
		}
	});
})(jQuery);

まず最初に this を変数に待避していますが(※1)、$.widget() 第二引数の中での this はウィジェットオブジェクト自身となります。ノーマルプラグインの場合だと this には指定された要素が格納されていましたが、 jQuery UI Widget においては this.element に格納されています。

$('#hoge').myPlugin();
・・・
_create: function() {
	this.element;	// => $('#hoge')
},

では jQuery セレクタで複数要素を選択した場合は?

ノーマルプラグインでは選択された要素は全て this に配列形式で格納されており、each() を使ってそれらに対して1つずつ処理を適用していくというフローとなっていました。

jQuery UI Widget において要素が複数選択されていた場合は、一つ一つの要素に対して _create と _init が実行され、 this.element には処理対象となる要素のみが格納されます。

$('#hoge, #foo').myPlugin();
・・・
_create: function() {
	this.element;	// 一回目は$('#hoge')、二回目は$('#foo')が格納される
},

このあたりは自前でループ処理を書くといった複数要素に対する配慮を気にしなくて良い作りになっており、作り手はより機能の作り込みに取り組むことが出来る優れた構造と言えます。

メソッドチェーンについて

ノーマルプラグインではメソッドチェーンを保つために、コードの最後にreturn this; を記述してjQueryオブジェクトを返すというセオリーが有りました。

一方 jQuery UI Widget ではこの辺りを内部的に処理してくれるので、return this を明記する必要はありません。

sample1.html

<!DOCTYPE html>
<html>
<head>
<!-- <meta charset="UTF-8"> -->
<title>#1 | jQuery UI Widget</title>
・・・
<script src="js/jquery.captionator1.js"></script>
<script>
$(function() {
	$('img').captionator();
	$('img').captionator().fadeOut(); // メソッドチェーンも正常に動きます
});
</script>
</head>
<body>
・・・
	<div class="center">
		<img src="../common/img/myLogo.jpg" height="251" width="251" alt="wakamsha logo." title="本名は山田直樹です。" />
	</div>

</body>
</html>

オプションを渡すことで、ウィジェットの利便性を何倍にも高めることができます。当然ウィジェット側にもオプションのデフォルト値を定義するわけですが、ノーマルプラグインと違って $.widget() の第二引数内に options という名前で定義し、その中に必要なパラメータを設定します。

jquery.captionator2.js

;(function($) {
	// プラグインの定義
	$.widget('ui.captionator', {
		options: {
			backgroundColor: '#fff'
		},
		// 初期化処理として一番最初に呼び出される
		_create: function() {
			var self = this,	// ウィジェットインスタンスを変数に待避 - (※1)
				element = self.element,	// 要素を変数に待避 - (※2)
				opts = self.options;	// マージ済みのオプションを変数に待避 - (※3)

			element.addClass('polaroid')
				.css('backgroundColor', opts.backgroundColor);
		}
	});
})(jQuery);

デフォルトオプションは上記のようにして定義します。ノーマルプラグインの場合だと、ユーザーが指定したオプションとデフォルトの値をマージする処理も自前で記述する必要がありましたが、 jQuery UI Widget ではコレに関しても内部で自動的に処理してくれます。

// オプションを指定
$('img').captionator({ backgroundColor: '#aeee00' });

// 指定しない
$('img').captionator();

また、オプションの値は以下の式で取得することができます。

$('img').captionator('option', 'backgroundColor'); // => #aeee00

パブリックメソッドとプライベートメソッド

jQuery UI Widget にはウィジェット内部からのみアクセス可能なプライベートメソッドと、外部からもアクセス可能なパブリックメソッドが用意されています。

ここまでに出てきた _create と _init にはどちらもメソッド名の最初に_(アンダースコア)が付けられているのでプライベートメソッドとして定義され、付いていないモノに対してはパブリックメソッドとして定義されます。

以下はイメージ画像にキャプションを自動で生成して付与するというウィジェットのコードです。

jquery.captionator3.js

;(function($) {
	// プラグインの定義
	$.widget('ui.captionator', {
		// デフォルト値
		options: {
			location: 'bottom',
			color: '#fff',
			backgroundColor: '#000'
		},
		_captionClassName: 'ui-caption',

		// 初期化処理として一番最初に呼び出される
		_create: function() {
			var self = this,	// ウィジェットインスタンスを変数に待避 - (※1)
				element = self.element,	// 要素を変数に待避 - (※2)
				opts = self.options;	// マージ済みのオプションを変数に待避 - (※3)

			element.addClass('polaroid').wrap('<div class="ui-caption-container"/>');
			var container = element.parent().width(element.width()),
				cap = $('<span/>').text(element.attr('alt'))
					.addClass(this._captionClassName)
					.css({
						backgroundColor: opts.backgroundColor,
						color: opts.color,
						width: element.width()
					}).insertAfter(element),
				capWidth = element.width() - parseInt(cap.css('padding')) * 2,
				capHeight = cap.outerHeight();
			cap.css({
				width: capWidth,
				top: self._setTopCoordinate(opts.location, element, capHeight),
				left: (element.outerWidth() - element.width()) / 2
			});
		},
		
		// キャプションのtop値を算出
		_setTopCoordinate: function(value, element, capHeight) {
			var offset = (element.outerHeight() - element.height()) / 2;
			return (value === 'top') ? 0 : element.outerHeight() - offset - capHeight;
		},

		// キャプションのパラメータ値を取得
		getCaptionAttr: function(keys) {
			var vals = [],
				cap = this.element.next();
			for (var i=0, len=keys.length; i<len; i++) {
				vals.push($(cap).css(keys[i]));
			}
			return vals;
		}
	});
})(jQuery);

_setTopCoordinate() はプライベート、getCaptionAttr() はパブリックとしてそれぞれ定義しています。パブリックメソッドを外部から呼び出すには以下のように記述します。

sample3.html (※JS部分のみ)

$(function() {
	$('img').captionator({
		backgroundColor: '#aeee00'
	});

	$('#getCapParamBtn').click(function(e) {
		var keys = new Array('width', 'height', 'backgroundColor', 'color'),
			vals = $('img').captionator('getCaptionAttr', keys),
			$valueList = $('#valueList');
		for (var i=0, len=vals.length; i<len; i++) {
			$valueList.append($('<li/>').text(vals[i]));
		}
	});
});

jQuery UI Widget でインスタンス化した後、第一引数にパブリックメソッド名、第二引数以降にそのメソッドに必要な引数を指定することで、パブリックメソッドを呼び出すことができます。また、コレに関しても自動的にjQUeryオブジェクトを返してくれるので、メソッドチェーンは保たれます。

※補足その1 | プライベートメソッドを外部から呼び出す裏ワザ

以下のような記述でプライベートメソッドを外部から呼び出すという裏ワザがかつてありましたが、この方法は jQuery UI 1.10.0 以降使えなくなりました。

$('img').data('captionator')._setTopCoordinate();

裏ワザとはいえ、外部から呼び出せてしまってはプライベートメソッドの意味がないですし、そもそもなぜ今まで呼び出せていたのかが不思議なくらいです

destroy メソッド

破壊という名の通り内部で保持されたインスタンスが破棄され、パブリックメソッドといったウィジェットの API が全て使用できなくなります。

sample4.html (※JS部分のみ)

$(function() {
	$('img').captionator({
		backgroundColor: '#aeee00'
	});

	$('#getCapParamBtn').click(function(e) {
		var keys = new Array('width', 'height', 'backgroundColor', 'color'),
			vals = $('img').captionator('getCaptionAttr', keys),
			$valueList = $('#valueList');
		for (var i=0, len=vals.length; i<len; i++) {
			$valueList.append($('<li/>').text(vals[i]));
		}
	});

	$('#destroyBtn').click(function(e) {
		e.preventDefault();
		$('img').captionator('destroy');
	});
});

先の sample #3 にdestroy を呼び出す処理を追加しただけのものです。#getCapParamBtn をクリックする度にキャプションのスタイルを取得するというパブリックメソッドを呼び出す処理がありますが、 destroy メソッドを呼び出して実行すると、この処理が一切呼び出されなくなります。

disable と enable

disable メソッドを呼び出すと、 jQuery UI Widget の実行対象となった要素に以下の2つのクラスが追加されます。

  1. "名前空間"-"ウィジェット名"-"disabled"
  2. "名前空間"-"ウィジェット名"-"state-disabled"

enable はこれら2つのクラスを要素から排除します。あくまでもクラスを付け外しするだけであって、APIが一時的に無効化されるといったことにはなりません。そのため以下のサンプルでは disable 時のクラスが付与されているかを評価してから処理を行うと記述しています。

sample5.html (※JS部分のみ)

$(function() {
	$('img').captionator({
		backgroundColor: '#aeee00'
	});

	$('#getCapParamBtn').click(function(e) {
		var disabledClassName = $('img').captionator('getDisabledClassName');
		if ($('img').hasClass(disabledClassName)) {
			console.log(disabledClassName);
			return false;
		}
		var keys = new Array('width', 'height', 'backgroundColor', 'color'),
			vals = $('img').captionator('getCaptionAttr', keys),
			$valueList = $('#valueList');
		for (var i=0, len=vals.length; i<len; i++) {
			$valueList.append($('<li/>').text(vals[i]));
		}
	});

	$('#destroyBtn').click(function(e) {
		e.preventDefault();
		$('img').captionator('destroy');
	});

	$('.btn-condition').click(function(e) {
		e.preventDefault();
		$('img').captionator($(this).text().toLowerCase());
	});
});

※補足その2 | いろいろなプロパティ

名前空間というのが再び出て来ましたが、jQuery UI Widget にはオプションや名前空間をはじめとした色々なプロパティが参照できます。

$.widget('ui.captionator', {
	・・・
	this.namespace;			// => ui 名前空間
	this.widgetName;		// => captionator ウィジェット名
	this.widgetEventPrefix;		// => captionator イベント名に付く接頭辞
});

オプション値を更新する

インスタンス化した要素に対し、個々のオプション値が新たに渡される(※更新)と、内部で _setOption メソッドが呼び出されます。

jquery.captionator6.js

;(function($) {
	// プラグインの定義
	$.widget('ui.captionator', {
		// デフォルト値
		options: {
			location: 'bottom',
			color: '#fff',
			backgroundColor: '#000'
		},
		
		・・・・・・

		// オプションの値が変更されると呼び出される
		_setOption: function(key, value) {
			var element = this.element,
				cap = element.next();
			cap.css(key, value);
			this._super('_setOption', this, arguments);
		}
	});
})(jQuery);

メソッドには変更されたオプションの key と value が渡され、実装者はコレを使って任意の処理を記述することができます。

イベントを定義する

イベントに関する機能も随分とお手軽に操作できるように作りこまれています。まずイベントの発火方法ですが、発火させたい箇所に以下の一行を記述します。

this._trigger(callbackName, [event], [uiObject]);

引数は以下の3つです。

callbackName
発火させるイベントの名前です。好きなのを付けられます。
event
イベントオブジェクトです。 イベントタイプの値はwidgetEventPrefix とイベント名を全て小文字にして結合したものになります。
uiObject
発火対象となるオブジェクトを格納します。

イベントが発火されると、options 内の callbackName に対応する関数が呼び出されます。

jquery.captionator7.js

;(function($) {
	// プラグインの定義
	$.widget('ui.captionator', {
		// デフォルト値
		options: {
			location: 'bottom',
			color: '#fff',
			backgroundColor: '#000',
			changed: function(e, cap) {
				console.log(e, cap)
			}
		},

		・・・・・・

		// オプションの値が変更されると呼び出される
		_setOption: function(key, value) {
			var element = this.element,
				cap = element.next();
			cap.css(key, value);
			this._super('_setOption', this, arguments);

			// オプションの値変更後にイベントを発火
			this._trigger('changed', event, cap);
		}
	});
})(jQuery);

呼び出される関数はオプション内に設定されているので、呼び出し側からここで実行する処理を更に設定することができます。

sample7.html (※JS部分のみ)

$(function() {
	$('img').captionator({
		backgroundColor: '#aeee00',
		changed: function(e, cap) {
			alert(e.type);
		}
	});

	・・・・・・
});

ゼロから大規模なウィジェットを作ったり、要件の仕様に適した必要最低限の機能のみを持つ軽量なウィジェットを作るのもアリですが、jQuery UI には既に数多くのウィジェットが標準コンポーネントとして用意されています。これらを拡張して独自のUIコンポーネントを作ることで、より少ないコストで開発を進めることが出来ます。

jquery.customDialog.js

;(function($) {
	// プラグインの定義 - オーバーライド
	$.widget('ui.customDialog', $.ui.dialog, {
		// デフォルトのオプションをオーバーライド
		options: {
			width: 500,
			height: 250,
			autoOpen: false
		},
		// createメソッドをオーバーライド
		_create: function() {
			// 継承元の_createメソッドを呼び出す
			this._super('_create', this, arguments);

			// コンテンツを入れ替える
			this.element.find('p').text('Hi! My name is Wakamsha. I am a Web depeloper. My favourite languages are HTML5, CSS3, JavaScript etc...');
		},

		// openメソッドをオーバーライド
		open: function() {
			// 継承元のopenメソッドを呼び出す
			this._super('open', this, arguments);
			// 非表示にしてフェードイン表示させる
			this.element.hide().slideDown(1000);
		},
		
		// closeメソッドをオーバーライド
		close: function() {
			if (confirm('Is it closing time?')) {
				this._super('close', this, arguments);
			}
		}
	});
})(jQuery);

$.widget() の第二引数に機能オブジェクトでなく継承したいウィジェットを指定します。以降はオーバーライドしたいメソッドを定義して任意の機能を記述していくだけですが、ただ単に自分のコードだけを書いていては継承元にあったオリジナルの処理が実行されないままになってしまいます。そこで13行目30行目にあるように _suer() を使って継承元のメソッドを呼び出すようにします。コレを忘れてしまうと正常に動作しなくなるので必ずつけるようにします。

※補足3 | バージョンによって異なる呼び出し方

上記のサンプルでは継承元のメソッドを呼び出すのに _super() を使っていましたが、これは jQuery UI 1.9 から実装された機能です。したがって 1.8x までのバージョンを使う必要がある場合は、以下のように書かなくてはなりません。

// 継承元の_createメソッドを呼び出す
$.ui.dialog.prototype._create.apply(this, arguments);	// ※1.8までの書き方
this._super('_create', this, arguments);		// 1.9以降の書き方

おわりに

とりあえず思いつくままに jQuery UI Widget の基本的な内容を書き連ねてきました。これで全て網羅できたかどうか定かではありませんが、余程のことがない限りは当記事の内容を元に作ることは可能かと思います。

絶対に分からせますから。ホントに呆れるほど分かるほど分からせますから。 (※このブログを読むことによって)(※メソッドを) 間違うことは良いことだ!人間、間違わなかったら進歩がないでしょ!JSのチカラを上げていくことが目的だから・・・、プラグインを出すことに、こだわっちゃイカン。オブジェクト指向とは実態を分かることなんです。何をやっているのかを分かれば良いんです。正確に (※英文のリファレンスを) 読んでいくんです。必ずそのメッセージは・・・、届くッ・・・、ようにッ・・・、書かれています!基礎の基礎がコワいってことを、きょう何度も言っておきます。じゃ、いつやるか・・・。 今でしょ。

参考サイト