jQueryプラグインのオプションとHTML5 data-*属性でハマった時に幸せになれるかもしれないまとめ

html5 data parser

意外と要注意… なんて記事を書いておきながら、実際に HTML5 のカスタム・データ属性にオプションを指定する jQuery プラグインを作っていて、かなりハマってしまいました。

data-* 属性に書いた複雑なオプションを jQuery Data API で処理するには、JSON で指定しなくちゃダメなんですネ。ちゃんと ドキュメント にも書いてありました 😉 。

ということで、「もっと簡単に記述できるようにしたい!」を叶える 簡易な data-* 属性パーサー を作りましたので、ここに発表します。

ついでに data-* を使った jQuery オプション指定のユースケースを整理し、その実装方法をまとめてみたいと思います。

jQuery オプションと data-* のユースケース

プラグインの設計段階では、オプションの構造とその指定方法を想定しておくことが重要です。そこでまず、想定し得るケースを考えてみたいと思います。

まずは $('...') で集めた要素すべてに適用する、プラグイン起動時のオプション指定です。

$(function () {
	$('...').myplugin({
		propertyFoo: "foo",
		propertyBar: "bar"
	});
});

当たり前過ぎて解説の必要はありませんネ。

1つの data-* 属性に1つのオプションを指定するケース

style="..." でスタイルを指定すると最優先で適用されるのと同様、オプションが個々の HTML 要素ごとに指定できると嬉しい場合があるでしょう。

<div
	data-property-foo="foo"
	data-property-bar="bar"
></div>

この要素に指定したオプションは、デフォルト値や起動時の指定よりも優先されるよう、コーディングする必要があります。

プレフィックス付きでオプションを指定するケース

あまりに一般的過ぎるオプション名は、他のプラグインと衝突する可能性があります。特に配布予定のプラグインでは、data-* 属性にも「名前空間」の考え方を導入することが重要です。

<div
	data-myprefix-property-foo="foo"
	data-myprefix-property-bar="bar"
></div>

1つの data-* 属性に複数のオプションを指定するケース

前述のプレフィックス付きと類似の考え方です。参考情報1@cyokodog さんが警告している通り、jQuery Data API で処理するなら JSON で、即ちプロパティ名や値をダブルクォーテーションで囲んで記述しなければなりません。

<div
	data-myprefix='{
		"propertyFoo": "foo",
		"propertyBar": "bar"
	}'
></div>

今回作成した簡易パーサーはこの制限を取り払うことが可能で、次の様に指定できます。

<div
	data-myprefix="{
		propertyFoo: 'foo',
		propertyBar: 'bar'
	}"
></div>

多重化された複数のオプションを指定するケース

稀なケースだとは思いますが、オプションが多数になると、カテゴリ別の構造にした方が分かり易いかもしれません。

<div
	data-myprefix='{
		category1: {
			propertyFoo: "foo",
			propertyBar: "bar"
		},
		category2: {
			propertyHoge: "hoge",
			propertyFuga: "fuga"
		}
	}'
></div>

キッチリ JSON で記述すれば Data API でも処理可能ですが、簡易パーサーの利用がオススメなケースです。

もっと自由でシンプルに指定するケース

knockout.js のように、{...} で囲まない方が、カッコ良くて素敵です。

<div data-myprefix="propertyFoo: foo, propertyBar: bar"></div>

もはや Data API で単純に処理できません。簡易パーサーを使うべきケースです。

オプション処理のテンプレート

以下は jQuery プラグインの作り方の解説でよく見るコードのテンプレートです。本記事ではこのコードに沿って各ケースの実装を示すこととします。

;(function($, undefined) {

	$.fn.myplugin = function(options) {

		// 指定のオプションとデフォルトをマージする
		var settings = $.extend({}, $.fn.myplugin.defaults, options);

		// 要素を1つずつ処理するプラグインのメイン
		return this.each(function() {
			var $this = $(this);
	
			// 要素ごとに data-* 属性の値を取得し、
			// 設定済みのオプションとマージする
			...

			// 要素に対する処理
			...
		});
	};

	// オプションのデフォルト値
	$.fn.myplugin.defaults = {
		propertyFoo: "foo",
		propertyBar: "bar"
	};

})(jQuery);

次章では、オプション処理にフォーカスし、各ケース毎の処理方法を見ていきます。

Data API を使ったオプションの処理

jQuery Data API による処理方法を、他サイト様の記事から2つ引用します。

data-* 属性をまとめて取得する方法

参考情報2 で紹介されている ケース 1.1 に対応する方法で、.data() を引数なしで呼び出します。

オプションのマークアップ
<div
	data-property-foo="hoge"
	data-property-bar="fuga"
></div>
オプションの処理
// 要素を1つずつ処理するプラグインのメイン部分
return this.each(function() {
	var $this = $(this);

	// data-* 属性の値をまとめて取得する
	var opts = $this.data();

	// 設定済みのオプションとマージする
	opts = $.extend({}, settings, opts);

	...
});
オプションの処理結果
opts = {
	propertyFoo: "hoge",
	propertyBar: "fuga"
};

このコードのポイントは、$this.data()data-* 属性をまとめて取得し、data- とハイフン - を削除、キャメルケース化した文字列をプロパティ名とする「オブジェクト」を返すという点です。

キャメルケース化のルールに則ってオプションを命名すれば、その処理も簡略化できるというワケです。

ただしこの方法のネガは、関係ない data-* 属性が混じる可能性があるという点です。特に配布予定のプラグインなら、次章のようにプレフィックスを付けるべきでしょう。

data-* 属性を1つずつ取得する方法

参考情報3 に詳しく解説されている方法で、ケース 1.2 に対応します。

オプションのマークアップ
<div
	data-myprefix-property-foo="hoge"
	data-myprefix-property-bar="fuga"
></div>
オプションの処理
// 要素を1つずつ処理するプラグインのメイン
return this.each(function() {
	var $this = $(this);

	// data-* 属性の値を1つずつ取得する
	var opts = {
		propertyFoo: $this.data('myprefix-property-foo'),
		propertyBar: $this.data('myprefix-property-bar')
	};

	// 設定済みのオプションとマージする
	opts = $.extend({}, settings, opts);

	...;
});
オプションの処理結果
opts = {
	propertyFoo: "hoge",
	propertyBar: "fuga"
};

ここでのポイントは、data-* 属性が指定されていなくてもエラーにならないことでしょう。存在しない属性を $this.data('...') で取得しようとすると undefined が返りますが、例えば { propertyFoo: undefined, propertyBar: "bar" } とはならず、{ propertyBar: "bar" } だけになります。

簡易パーサーを使ったオプションの処理

さてここからが本記事のハイライトです。

まずは具体例で確認してもらえるよう、オプションに応じて吹き出しの色が変わるサンプルを作成してみました。「JSON」という制限から解放されている様が見て取れると思います。

簡易パーサーの使い方

簡易パーサー は、$this.data('...')$this.parseData('...') に置き換えるだけで使えます。というより正確には、「指定の data-* 属性に書かれた JSON ライクな表記をオブジェクトに変換する」がその機能です。

もちろん ケース 1.1ケース 1.2 にも使えますが、本領を発揮するのは ケース 1.3 以降です。

オプションのマークアップ
<!-- JSON 形式 -->
<div data-myprefix='{"propertyFoo": "hoge", "propertyBar": "fuga"}'></div>

<!-- JavaScript オブジェクト形式 -->
<div data-myprefix="{propertyFoo: 'hoge', propertyBar: 'fuga'}"></div>

<!-- knockout.js 形式 -->
<div data-myprefix="propertyFoo: hoge, propertyBar: fuga"></div>
オプションの処理
// 要素を1つずつ処理するプラグインのメイン部分
return this.each(function() {
	var $this = $(this);

	// data-* 属性に書かれたオブジェクトを取得する
	var opts = $this.parseData('myprefix');

	// 設定済みのオプションとマージする
	opts = $.extend({}, settings, opts);

	...
});
オプションの処理結果
opts = {
	propertyFoo: "hoge",
	propertyBar: "fuga"
};

$.extend() のディープコピーについて

一点、ケース 1.4 では、オプションのマージ方法に注意が必要です。

例えばオプションのデフォルト設定とマークアップが次の通りだとします。

// オプションのデフォルト値
$.fn.myplugin.defaults = {
	category: {
		propertyFoo: "foo",
		propertyBar: "bar"
	}
};
<div data-myprefix='{category: {propertyFoo: "hoge"}'></div>

このとき $.extend() には、オブジェクトを再帰的に辿ってコピーしてくれる「ディープ」なコピーとして機能させるために、第1引数に true を指定します。

// 設定済みのオプションとマージする(ディープコピー)
opts = $.extend(true, {}, settings, opts);

単純なコピーでは category が置き換わっちゃいますが、「ディープ」なコピーでは次のようにちゃんと2つがマージされます。

opts = {
	category: {
		propertyFoo: "hoge",
		propertyBar: "bar"
	}
};

これは簡易パーサーに限ったハナシではないので、覚えておくと良いでしょう 🙂 。

変換の原理と安全性

さて一般に文字列をオブジェクトに変換するなら、参考情報4 で検討されているように eval() が便利ですが、セキュリティ上の問題回避が厄介です。

そこで問題を単純化するため、簡易パーサーの構成を次の2つとしました。

  • JSON ライクな文字列を正規の JSON に変換するユーティリティー関数 _jsonize()
  • JSON をオブジェクトに変換する .parseData()

特に後者は $.parseJSON() を利用していて、入力が正規の JSON かどうかが 次のコード で検査されます。

// Make sure the incoming data is actual JSON
// Logic borrowed from http://json.org/json2.js
if ( rvalidchars.test( data.replace( rvalidescape, "@" )
	.replace( rvalidtokens, "]" )
	.replace( rvalidbraces, "")) ) {

	return ( new Function( "return " + data ) )();
}

eval() ではなく Function() が使われていますが、検査ロジックに誤りがあれば、危険に曝されることに代わりありません。

変数 rvalid* は、JSON 要素 を検査する正規表現で、Douglas Crockford の json2.js の検査ロジックが流用されています。

このロジックはなかなか賢くて、マッチした正規の JSON 要素を他の文字に置き換えていって、最後に @] だけが残るなら「正しい」と判定されます。

つまりは Douglas Crockford の検査ロジックと同じ安全性で、簡易パーサーも機能するというワケです。eval() を使ったパターンと比較した _jsonize() の原理モデルを jsdo.it に置きましたので、興味があればチェックしてみて下さいネ。

まとめ

(読み返してみると)ユースケースというほどハッキリした使いどころを示せていませんし、汎用解と特殊解を一気に詰め込んだために、分かりにくい記事となってしまいました。が、オプション設計の全体像から書いてみたかったので、こうなりました。まぁ、許して下さい 😉 。

jQuery Data API には色々と注意事項があるのは確かですが、私はユースケースをキッチリ定めてやれば、とても便利な関数だと思っています。

そのユースケースの1つに、今回作成した簡易パーサー tokkonopapa/jQuery-parseData を加えてもらえればウレシイですし、問題点や改善点を挙げてもらえれば、もっと喜びます 🙂 。

参考情報

  1. この警告にもっと早く気付いていれば…

  2. HTML5 × CSS3 × jQueryを真面目に勉強 – #8 jQueryプラグインの作り方について詳しく | Developers.IO
    • 初心者から中級者までカバーできる内容にまとめられています。

  3. HTML5 のカスタムデータ属性を jQuery プラグインのオプションとして使う – terkel.jp
    • Jquery Data API のユースケースが明快に示されています。

  4. CYOKOLOG — JSON 文字列が設定された独自データ属性を jQuery で参照した時の変換ルール
    • JSON モドキを含めて、それなりに処理する方法が整理されています。

  5. jQueryでHTML5の独自データ属性(data Attributes)を扱う | webOpixel
    • jQuery Data API の色々な使い方です。

  6. markdalgleish/jquery-html5data
    • 特定のプレフィックスが付いた data-* だけを処理するプラグインです。こういうアプローチもアリかと思います。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

海外からの投稿は受け付けていません