jQuery は非常に柔軟に DOM 要素の選択が出来る、世界中でもっとも成功した JavaScript ライブラリの1つです。本稿では、この jQuery の優れた特徴や特性をなるべく活かしたコードを記述するための方法について考えてみたいと思います。なお、本稿は jQuery についての基礎的な知識を持った方を対象とします。
§ 伝統的な例
実際にはほとんど見掛けないと思いますが、次の HTML と JavaScript のコードをスタート地点として考察してみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<ul id="openable-list"> <li> <header onclick="openContent('a')">タイトルA</header> <section id="content-a">Aの内容Aの内容Aの内容Aの内容Aの内容Aの内容Aの内容Aの内容。</section> </li> <li> <header onclick="openContent('b')">タイトルB</header> <section id="content-b">Bの内容Bの内容Bの内容Bの内容Bの内容Bの内容Bの内容Bの内容Bの内容Bの内容Bの内容。</section> </li> <li> <header onclick="openContent('c')">タイトルC</header> <section id="content-c">Cの内容Cの内容Cの内容Cの内容Cの内容。</section> </li> </ul> |
1 2 3 4 5 6 7 |
function openContent(target) { $('#content-a').slideUp(); $('#content-b').slideUp(); $('#content-c').slideUp(); $('#content-' + target).slideDown(); } |
[jsdo.it] http://jsdo.it/yuka2py/mljq-1
本稿の読者で、このコードの意味が分からない人はいないと思いますのでコードの説明は割愛します。このコードが驚くほどに jQuery の特徴を活かせていないことがとても印象的ですね。しかし、jQuery 登場以前には、こういったコードもよく見られました。ある意味、伝統的なコードと言えるかも知れません。
問題点として特に目につくのは、HTML と JavaScript の双方で要素の ID を指定する文字列が登場していることです。これでは、HTML と JavaScript は強烈に結びつき過ぎてしまって、HTML の僅かな変更でさえも、JavaScript への追加変更が必要です。改善しましょう。
§ 相対的に要素を取得する
コードを以下のように変更しました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<ul id="openable-list"> <li> <header onclick="openContent(this)">タイトルA</header> <section>Aの内容Aの内容Aの内容Aの内容Aの内容Aの内容Aの内容Aの内容。</section> </li> <li> <header onclick="openContent(this)">タイトルB</header> <section>Bの内容Bの内容Bの内容Bの内容Bの内容Bの内容Bの内容Bの内容Bの内容Bの内容Bの内容。</section> </li> <li> <header onclick="openContent(this)">タイトルC</header> <section>Cの内容Cの内容Cの内容Cの内容Cの内容。</section> </li> </ul> |
1 2 3 4 5 6 7 8 9 |
function openContent(handle) { var handle = $(handle); //handle から相対指定で、全ての section を取得して閉じる handle.closest('ul').find('section').slideUp(); //handle の次の要素(section)を開く handle.next().slideDown(); } |
[jsdo.it] http://jsdo.it/yuka2py/mljq-2
HTML から ID を消しました。代わりに、onclick=”openContent(this)” としてクリックされた要素自身を渡しています。
JavaScript 側では、クリックされた要素を handle という変数に受け取って、jQuery オブジェクトに変換してから、そこを起点に、closest() で直近の祖先の ul 要素を探し、次いで find() で子孫要素の section 要素全てを取得してから、slideUp() で開いている要素を閉じます。そして最後に、クリックされた handle の次の要素を next() で取得して、開いています。
jQuery ではこのように、多数の便利なメソッドを使って、DOM 内の要素を相対的かつ柔軟に取得することが出来ます。結果として、ID などによる指定がなくとも、この例ではイベントが発生した要素を起点として、動作に必要な要素を取得することが出来ます。
さて、この例では、HTML の onclick 属性を使って openContent() という JavaScript の関数をコールしていますが、これもやはり HTML と JavaScript の結びつきを強くしているでしょう。機能と外観はなるべく結びつきが弱い方が変更に強いです。もう少し改善しましょう。
§ JavaScriptでイベントをハンドルする
次の例を見てみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<ul id="openable-list"> <li> <header>タイトルA</header> <section>Aの内容Aの内容Aの内容Aの内容Aの内容Aの内容Aの内容Aの内容。</section> </li> <li> <header>タイトルB</header> <section>Bの内容Bの内容Bの内容Bの内容Bの内容Bの内容Bの内容Bの内容Bの内容Bの内容Bの内容。</section> </li> <li> <header>タイトルC</header> <section>Cの内容Cの内容Cの内容Cの内容Cの内容。</section> </li> </ul> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
function setupOpenableList(scope) { //全ての項目を取得 var items = $(scope).find('li'); //項目それぞれについて items.each(function() { var item = $(this); //クリックのイベントで、 item.find('header').on('click', function () { //全てのアイテムの section を閉じて items.find('section').slideUp(); //クリックされたアイテムの section を開く item.find('section').slideDown(); }); }); } //セットアップする setupOpenableList('#openable-list'); |
[jsdo.it] http://jsdo.it/yuka2py/mljq-3
HTML から onclick 属性が消えました。代わりに、JavaScript 側で、jQuery を使ってイベントのハンドルを設定しています。最後の行で、setupOpenableList(‘#openable-list’); と記述して、HTML 側の ID を指定していますが、これは JavaScript を適用する範囲のみを指定するものですので、最初の例示の ID 指定とは意図が違うので、まあ、こんなものだと思ってください。
JavaScript のコードを見てください。コメントも含めたので意味は読み取れるかと思います。ポイントは、3行目、5行目、8行目になると思います。
3行目で、与えられたスコープ内の項目を全て取得し、5行目でその要素を巡回処理し、8行目でクリックイベントをその要素にバインドしています。クリックされた時に行われる処理は、先ほどまでの例と結果的には同じです。全部閉じて、対象を開く。
このようにして jQuery では非常に簡単、かつ直感的な記述で、イベントを DOM の要素にバインドしていくことが出来ます。これにより、HTML 中にイベントハンドラを記述する必要が無くなるため、HTML と JavaScript の結びつきをさらに小さくすることが出来ました。
しかしこのコードはまだ問題があります。ちょっとバカらしい例ですが、「IE6 対応」と言われたらどうしましょうか? header や section 要素は普通は使えません。そんな理由、またその他の理由で、タグ名が変わると、このコードは途端に編集が必要になります。改善しましょう。
§ 機能のための CSS クラス
コードを以下のように変更してみました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<ul id="openable-list"> <li class="ol-item"> <header class="ol-handle">タイトルA</header> <section class="ol-content">Aの内容Aの内容Aの内容Aの内容Aの内容Aの内容Aの内容Aの内容。</section> </li> <li class="ol-item"> <header class="ol-handle">タイトルB</header> <section class="ol-content">Bの内容Bの内容Bの内容Bの内容Bの内容Bの内容Bの内容Bの内容Bの内容Bの内容Bの内容。</section> </li> <li class="ol-item"> <header class="ol-handle">タイトルC</header> <section class="ol-content">Cの内容Cの内容Cの内容Cの内容Cの内容。</section> </li> </ul> |
1 2 3 4 5 6 7 8 9 10 11 12 |
function setupOpenableList(scope) { var items = $(scope).find('.ol-item'); items.each(function() { var item = $(this); item.find('.ol-handle').on('click', function () { items.find('.ol-content').slideUp(); item.find('.ol-content').slideDown(); }); }); } setupOpenableList('#openable-list'); |
[jsdo.it] http://jsdo.it/yuka2py/mljq-4
ちょっと HTML がうるさくなりましたね。たくさんの CSS クラスを付与しました。そして JavaScript 側では、先ほどまでの例のようにタグ名ではなく、その CSS クラス名を対象に処理を行っています。その他の JavaScript の処理は、一つ前の例と同じです。これでタグ名が変わっても問題ありませんね!
さて、新しく付与された CSS クラスは、ページへのデザインの反映には使われませんので、ある程度自由な位置に指定できます。結果、例えばHTMLの構造が少し分かっても、例えば装飾の為の DIV のネストが増えても、概ね問題なく動作できるでしょう。かなり自由度が高まります。
これは誰でも思いつく単純な手法ですが、その効果は意外に大きいです。HTML 中に動作の為の CSS クラスを書くことは考え方によっては懸念があるかも知れませんが、これもひとつの有用なアプローチだと僕は思います。この手法を採用するときの注意点としては、「スクリプトの動作の為の CSS クラスには、絶対にスタイルを指定しない」という点です。時々誘惑に駆られそうになりますが、我慢すると幸せでいられます。
さて、でもこのコードですが、どうでしょう。まだもう少し、HTML の構造に自由度を与えることができるでしょう。このコードには、まだ親子関係という制約がありますね。この制約を解消できるでしょうか?
§ 考え方を変えてみる
では、次のコードを見てください。HTML は1つ前の例と同じなので、ここでは JavaScript のみ掲載します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
function setupOpenableList(scope) { scope = $(scope); var handles = scope.find('.ol-handle'); var contents = scope.find('.ol-content'); handles.each(function() { var handle = $(this); handle.on('click', function() { //全てのアイテムの .ol-content を閉じる contents.slideUp(); //順番を取得して、 var targetIndex = handles.index(handle); //順番から開くべき .ol-content を取得して、 var targetContent = $(contents.get(targetIndex)); //開く targetContent.slideDown(); }); }); } //セットアップする setupOpenableList('#openable-list'); |
[jsdo.it] http://jsdo.it/yuka2py/mljq-5
このコードは親子関係の制約を無くしたものです。コード内のコメントもあるように、このコードにおいて、クリックされた要素と、そして開かれるべき要素とを関連づけているのは「要素の順序」だけです。親子関係を持つ場合、必然的にそこには順序が生まれますが、よく考えると、順序だけあれば、それぞれの要素の対応を取る事ができることに注目して開発されたアイデアです。
結果、このプログラムは、これまでに見て来たような開閉リストとしての使い方だけではなく、離れた場所にある要素を開閉させることも出来るようになりました。HTML の構造には極力依存しないことで、利用方法は広がり、また HTML 作成でスクリプトを意識しなければいけないコストも、最大限小さくなったと思います。
このように jQuery の特徴や機能を活かす事によって、HTML と JavaScript の結びつきを極力減らした、柔軟性が高く再利用性の高いスクリプトを実現することができます。
§ jQueryなので…
最後に せっかくなので、jQuery のプラグインにしてみます。次のコードも HTML は先の例と一緒なので、JavaScript だけ掲載します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
//document.ready で実行させる jQuery(function($) { //jQuery プラグインにする $.fn.openableList = function (options) { //オプションの準備 options = $.extend(options, { handle: '.ol-handle', content: '.ol-content' }); //thisは自分自身 var handles = this.find(options.handle); var contents = this.find(options.content); handles.each(function() { var handle = $(this); handle.on('click', function() { contents.slideUp(); var targetIndex = handles.index(handle); var targetContent = $(contents.get(targetIndex)); targetContent.slideDown(); }); }); }; //プラグインを適用する(本当はHTML側に書く方が良いと思う) jQuery('#openable-list').openableList(); }); |
[jsdo.it] http://jsdo.it/yuka2py/mljq-6
この例では、要素を取得するセレクタも、外部から与えることが出来るようにしました。これによって、さらに様々な構造の HTML に対応できるようになりました。
以上ですぅ。何かのヒントになれば幸いです。