IMG_5256
Development

Vanilla JavaScriptを書いてみよう その3(タブUIを実装)

さて前回はJavaScriptで取得したhtml要素のclass属性を簡単に追加や削除が行えるように Element というクラスの実装を紹介しました。

今回はそのElementクラスをクラスを使ってタブUIを操作できるクラスを実装してみたいと思います。

まず部品として以下の2点が必要かと思いますので、それぞれに対応するクラス名称をつけました。

  • タブボタン => TabNavItem
  • タブ内容ボックス => TabContent

TabNavItem

TabNavItemの仕様として以下の項目があげられます

  1. 生のhtml要素を渡して初期化出来る
  2. 選択中に切り替えられる
  3. 非選択中に切り替えられる
  4. 生のhtml要素と比較して自分自身かどうかをチェックできる

1. 生のhtml要素を渡して初期化出来る

これはコンストラクタですね。new した時の初期化処理ですが、引数で受け取った生のhtml要素を前回作成した Element オブジェクトにして保持しておきます。
後は選択中を示すclass名もここで定義しておきました。

function TabNavItem(el) {
  this.el = new Vanilla.Element(el);
  this.current_class_name = 'tab-nav__item--current';
}

2. 選択中に切り替えられる

自身のElementオブジェクトに対して、選択中のclass名を持っていなければ追加します。

TabNavItem.prototype.toCurrent = function() {
  if (this.isCurrent()) return;
  this.el.addClass(this.current_class_name);
};

3. 非選択中に切り替えられる

自身のElementオブジェクトに対して、選択中のclass名を削除します。

TabNavItem.prototype.toUncurrent = function() {
  this.el.removeClass(this.current_class_name);
};

4. 生のhtml要素と比較して自分自身かどうかをチェックできる

実はこれもタブUIを実装する上で結構重要な機能です。
これはElementオブジェクトの持っている同名のメソッドに処理を渡しています。

TabNavItem.prototype.isEqual = function(targetElement) {
  return this.el.isEqual(targetElement);
};

TabNavItemの実装は以上です。まとめたコードがこちらです。

var TabNavItem = (function() {
  function TabNavItem(el) {
    this.el = new Element(el);
    this.current_class_name = 'tab-nav__item--current';
  }

  TabNavItem.prototype.toCurrent = function() {
    if (this.isCurrent()) return;
    this.el.addClass(this.current_class_name);
  };

  TabNavItem.prototype.toUncurrent = function() {
    this.el.removeClass(this.current_class_name);
  };

  TabNavItem.prototype.isCurrent = function() {
    this.el.hasClass(this.current_class_name);
  };

  TabNavItem.prototype.isEqual = function(targetElement) {
    return this.el.isEqual(targetElement);
  };

  return TabNavItem;
})();

TabContent

TabContentは仕様として以下の項目があげられます。

  1. 生のhtml要素を受け取って初期化出来る
  2. 選択中に切り替えられる
  3. 非選択中に切り替えられる

1. 生のhtml要素を受け取って初期化出来る

TabNavItemとほぼ同じですが、TabContentの場合はhtml要素のid属性を label というプロパティに保持しておきました。

function TabContent(el) {
  this.el = new Vanilla.Element(el);
  this.current_class_name = 'tab-content--current';
  this.label = this.el.id;
}

2. 選択中に切り替えられる

自身のElementオブジェクトに対して、選択中のclass名を持っていなければ追加します。

TabContent.prototype.toCurrent = function() {
  this.el.addClass(this.current_class_name);
};

3. 非選択中に切り替えられる

自身のElementオブジェクトに対して、選択中のclass名を削除します。

TabContent.prototype.toUncurrent = function() {
  this.el.removeClass(this.current_class_name);
};

TabContentの実装は以上です。まとめたコードはこちら

TabContent = (function() {
  function TabContent(el) {
    this.el = new Element(el);
    this.current_class_name = 'tab-content--current';
    this.label = this.el.id;
  }

  TabContent.prototype.toCurrent = function() {
    this.el.addClass(this.current_class_name);
  };

  TabContent.prototype.toUncurrent = function() {
    this.el.removeClass(this.current_class_name);
  };

  return TabContent;
})();

イベントに紐付けて動かしてみる

最低限の役者が揃いましたので実際に動す為のコードを書いてみたいと思います。
まずはなにはともあれDOMが構築されたタイミングを取れるようにwindowのloadイベントにハンドラをあててからの作業とですね。

addEvent(window, 'load', function() {
  // 各部品のhtml要素を取得
  var tabNav = document.querySelector('.tab-nav');
  var tabNavItemElements = var tabNav.querySelectorAll('.tab-nav__item');
  var tabContentElements = document.querySelector('.tab-contents').querySelectorAll('.tab-content');

  // 格納用の配列を初期化
  var tabNavItems = [];
  var tabContents = [];

  // TabNavItem と TabContent のインスタンスを生成
  for (var i in tabNavItemElements) {
    // 配列のプロパティ length を取り出してしまわないようにチェック
    if (typeof tabNavItemElements[i] === 'object') {
      tabNavItems.push(new TabNavItem(tabNavItemElements[i]));
    }
    if (typeof tabContentElements[i] === 'object') {
      tabContents.push(new TabContent(tabContentElements[i]));
    }
  }

  // タブボタンのクリックイベントを一気に登録する
  addEvent(tabNav, 'click', function(e) {
    // ループ中で使う変数を用意
    var tabNavItem = null;
    var tabContent = null;

    // クリックしたタブボタンのhref属性を取得
    var label = e.srcElement.href.replace(/^.+#/, '');

    // クリックしたタブボタンのli要素を取得
    var clickedItem = e.srcElement.parentNode;

    // 全TabNavItemを回しながら選択状態を切り替えていく
    for (var i in tabNavItems) {
      tabNavItem = tabNavItems[i];
      if (tabNavItem.isEqual(clickedItem)) {
        tabNavItem.toCurrent();
      } else {
        tabNavItem.toUncurrent();
      }

      // TabContentの数も一緒なので同じループで選択状態を切り替えていく
      tabContent = tabContents[i];
      if (tabContent.label === label) {
        tabContent.toCurrent();
      } else {
        tabContent.toUncurrent();
      }
    }
  });
});

以上でタブUIの全体の実装が完了です。

実際に動作するサンプルがこちらです。

ポイントは部品ごとにクラス化しておくこと、イベントハンドリングは全てのボタンに割り当てるのではなく親要素にあてておく、見た目の切り替えはcssで書いてjsはclass属性の付け替えなどにとどめておくこと等です。

標準
IMG_5256
Development

Vanilla JavaScriptを書いてみよう その2(タブUIを実装)

前回はイベントハンドリングとjQueryの様にクラス名でhtml要素を検索出来るquerySelectorを紹介しました。

今回はjQuery等の等のライブラリを一切使わずにタブUIを実装して見るための下準備をご紹介したいと思います。

デザイン

デザインは以下の様なもので、タブのボタンとその内容の文章が3つずつ存在し、選択されていないタブをクリックすると内容と共に選択中の表示に切り替わります。

スクリーンショット 2014-02-24 12.23.24

 

各タブに .tab-nav__item–current というクラスが付いていると選択中の表示になり、内容部分はデフォルトで display: none; に設定されているので .tab-content–current というクラスが付いているもののみ display: block; で可視化するという仕様です。

見た目の制御はcssで行うようにしていますので、JavaScriptからhtml要素のclass属性を操作できる必要があります。

クラスを定義してみる

class属性は element.className というプロパティを使って操作できますので、専用のクラス(ここで言うクラスはインスタンスを生成するためのオブジェクトという意味)を用意しておきましょう。

ところがJavaScriptにはクラスを定義する構文は用意されていません。ではどうやるのかというと、関数オブジェクトに備わっているprototypeという仕組みを利用することでクラス定義をエミュレート出来ます。
具体的には以下のように記述します。

var Person = (function() {
  function Person(name) {
    this.name = name;
  }

  Person.prototype.greet = function() {
    alert('Hello ! My name is ' + this.name);
  }

  return Person;
)();

Personクラスは初期化時に name という変数を受け取り自身の name プロパティに格納します。そして greet というメソッドを持っていて、nameプロパティを連結した挨拶文をアラートする動作をします。

このPersonクラスからインスタンスをインスタンスを生成するには new Person(‘name’) という形で初期化関数を実行します。

var yamagata = new Person('yamagata');

greetメソッドの実行はこうです

yamagata.greet();

Elementクラス

そして今回のために実装したElementクラスはこちらです。

var Element = (function() {
  function Element(el) {
    this.el = el;
    this.id = this.el.id;
  }

  Element.prototype.classes = function() {
    return this.el.className.split(/\s+/);
  };

  Element.prototype.removeClass = function(targetClass) {
    var classes = this.classes();
    for (var i in classes) {
      if (classes[i] === targetClass) {
        classes.splice(i, 1);
      }
    }
    this.el.className = classes.join(' ');
  };

  Element.prototype.addClass = function(targetClass) {
    var classes = this.classes();
    for (var i in classes) {
      if (classes[i] === targetClass) {
        return
      }
    }
    classes.push(targetClass);
    this.el.className = classes.join(' ');
  };

  Element.prototype.hasClass = function(targetClass) {
    var classes = this.classes();
    var exist = false;
    for (var i in classes) {
      if (classes[i] === targetClass) {
        exist = true;
      }
      if (exist) break;
    }
    return exist;
  };

  Element.prototype.isEqual = function(targetElement) {
    return this.el === targetElement;
  };

  return Element;
})();

class属性を操作する場合、複数のclassを扱うことを考慮しなければいけないので、スペースで分割した配列を返す classes メソッドを実装しました。
addClass と removeClass はその配列に対して操作を行う仕組みです。

配列に要素を追加する場合は、末尾に追加する push というメソッドがメソッドが便利です。
また、配列から要素を削除する場合は、spliceというメソッドが便利です。これは削除したい要素のインデックスとそのインデックスの位置から何個の要素を削除するかという個数を指定します。今回は常に1つの要素を消すように 2番目の引数には 1 を渡しています。

var fruits = ['apple', 'banana', 'melon'];
fruits.splice(1, 1); // => bananaが削除される

また特定のclass属性を持っているかチェックをする hasClass と html要素自体が一致しているかをチェックする isEqual も実装しておきました。

このElementクラスを使用するためにはhtml要素を渡して new しなければいけませんので、以下のようにしてタブのボタンをElement化します。

var tabNavItems = [];
var tabNavItemElements = document.querySelectorAll('.tab-nav__item');
for (var i in tabNavItemElements) {
  if (typeof tabNavItemELements[i] === 'object') {
    tabNavItems.push(new Element(tabNavItemElements[i]));
  }
}

.tab-nav__item が付いているhtml要素を全て取得し、1つずつnew Elementしているのですが、typeofで取り出した要素が確実にhtml要素であることを確認しています。
これはfor文では配列のプロパティである length (中身は整数)も取り出してしまうためです。

まとめ

class属性を操作する準備は整いました、普段jQueryで行っている処理もわりと簡単に作れてロジックを書く勉強にもなるのでおすすめです。一度くらい車輪の再発明をしてみるのもいいと思います。
次回はクリックイベントを監視してどのようにタブの切替を行うのかをご紹介します。

標準
IMG_5256
Development

Vanilla JavaScriptを書いてみよう その1

ブラウザ内でアニメーションをさせたり、ユーザーの操作に反応して何かを行う場合必要になるのがJavaScriptです。

最近でこそJavaScriptは便利なライブラリやプリプロセッサが充実してるので、JavaScriptを使ったプログラミングは簡単に始められて楽しいものですが、一昔前まではブラウザ間の差異に悩まされめんどくさいという印象が強かったのでは無いかと思います。

そこで登場したのがjQueryでした。便利なセレクターやメソッドチェーンで簡潔にわかりやすく、そしてクロスブラウザで動作するスクリプトを書けるとあって、これ以外に選択肢を考えることすら無くなったのではないかと思います。

ところで昔からweb制作ではIE対応に悩まされてきた方が多いと思いますが、時代は流れ悪名高いIE6や7の対応はほぼ無くなって2014年現在ではIE8以上としている場合が殆どではないでしょうか。IE8ですら最近は切り捨てられることも増えてきているようです。

さらに最近のブラウザのJavaScriptは進化していて、今までjQueryの領域だと認識されていた部分にも素のJavaScriptが追い付いてきているようです。タイトルにも書きましたが、Vanilla JavaScriptというのは新しいライブラリやフレームワーク等ではなく、素で真っ白な状態のJavaScriptという意味です。

Vanilla JavaScriptだと何がいいのか?jQuery等の読み込みが無くなるのでhttpリクエストをひとつ減らせます。ただ最近ではサイトの公開時にJavaScriptをファイル1つにまとめてを圧縮しておくことも多くなったのでそこまで大きなメリットのようには思えません。ただスマートフォンやタブレットが普及しサイトの半数近くがそれらのモバイルデバイスであり、PCにおいてもモダンブラウザが多数を占めているなら簡単なものはVanilla JavaScriptで書くことで読み込みや実行速度を高めることに一役買うでしょう。

またjQueryの内部ではなにが行われているのか?全ての処理がjQueryの独自実装で動いているのか?そんなことはありません。実はブラウザに同様の機能がある場合はそちらを使うように設計されています。ですのでアニメーションなどは行わずにシンプルなDOM操作のみの場合はjQueryの旨味を十分に発揮させていないことがあるのです。

今回はそんなVanilla JavaScriptではコードをどのように記述していくのかを紹介したいと思います。

イベントの登録を行ってみよう

まずはなにはなくともブラウザ上で動くJavaScriptといえばイベントハンドリングが出来ないと話になりません。なぜならhtmlが読み込まれDOMが構築された後でないと満足にページ内の操作が行えないからです。

body要素にonload属性を追加して関数を実行したり、windowオブジェクトのonloadプロパティに関数を代入する方法でも確実にDOM構築後にスクリプトを実行することが可能ですが、せっかくならjQueryで普通にやっていることを置き換える形で試してみたいと思います。

<script type="text/javascript">
   function start() {
     ...
   }
 </script>
 <body onload="start()">
   ...
 </body>

これでも動くし

<script type="text/javascript">
   window.onload = function() {
     ...
   }
 </script>

これでも動くけど…

<script type="text/javascript">
   jQuery(function($) {
     ...
   });
 </script>

せっかくならこれに置き換わる形で書こう!

JavaScriptでイベント登録を行う関数といえばaddEventListenerがありますが、残念ながらこれはIE8では動作しません。ではどうするか?IE8以前にはattachEventという関数があります。これをif文で使い分けるようにするaddEvent関数を作って対応しましょう。

function addEvent(obj, event_name, handler) {
   if (obj.addEventListener) {
     // addEventListenerが使える場合
     obj.addEventListener(event_name, handler, false);
   } else if (obj.attachEvent) {
     // attachEventが使える場合
     obj.attachEvent('on' + event_name, handler);
   }
 }

次にこれを使ってDOM構築が完了したイベントにハンドラを登録するには以下のようにします。

addEvent(window, 'load', function() {
   alert('Hello');
 });

上記のように書けばwindowのloadイベントに対して何度イベントハンドラを登録したとしても先の処理を上書きすること無く全てが実行されます。

addEvent(window, 'load', function() {
   alert('Hello');
 });
 addEvent(window, 'load', function() {
  alert('World');
 });

HelloにつづいてWorldもアラートされるはずです。

jQueryのようにHTML要素を取得できるquerySelector、querySelectorAllを使ってみよう

イベントハンドリングの壁は超えられたので次にhtml要素を取得してみましょう。

jQueryの大きな特徴としてcssのように使える便利なセレクターがあります。これはページ内にあるDOMノードをJavaScriptから見つけるために欠かせない手順ですが、従来のJavaScriptではあまり開発者にやさしくないものでした。

しかしモダンなブラウザはもちろんIE8以降で使える関数でquerySelectorとquerySelectorAllという2つがあります。前者はセレクタに該当する最初の要素を取得し、後者はセレクタに該当する全ての要素を取得して配列で返してくれます。

<h1>Hello</h1>

このような要素があったとして

addEvent(window, 'load', function() {
  var h1 = document.querySelector('h1');
  alert(h1.innerText);
 });

というスクリプトを書くとh1要素の中身のテキストがアラートされます。 ちなみにquerySelectorを使った場合はh1要素が複数あった場合最初の要素のみ取得されるので

<h1>Hello</h1>
<h1>World</h1>

この場合もHelloのみがアラートされます。

querySelectorAllを使った場合は要素は配列に格納されて返されますので

addEvent(window, 'load', function() {
  var h1s = document.querySelectorAll('h1');
  for (var i in h1s) {
    alert(h1s[i].innerText);
  }
});

と書くとHelloにつづいてWorldもアラートされます。

さらにこれらの関数の便利な点はDOMツリーのスコープが適用されるというところです。上記の例ではdocumentオブジェクトというwindowオブジェクトに続く2番めに大きいオブジェクトですので要素の検索はページ全体に渡ります。しかし2つ目のdiv要素の中にあるh2のテキストのみをアラートしたい場合はどうすればよいでしょうか。しかもh2にはclassもidも降ってないとしたら検索は更に困難になります。

そのような場合にquerySelectorのスコープを活用すると便利です。

<h2>zero</h2>
<div>
  <h2>first</h2>
</div>
<div>
  <h2>second</h2>
</div>

このような要素があるとして

addEvent(window, 'load', function() {
  var h2 = document.querySelectorAll('div')[1].querySelector('h2');
  alert(h2.innerText);
});

このようにすればsecondとアラートされます。
まず最初にdocumentの持っているスコープから2番目のdiv要素を見つけ、そのdiv要素の中にあるh2を見つけたという流れです。querySelectorAllで見つかった要素は配列に格納されているので2番目の要素は[1]で取り出せるというわけですね。

まとめ

いかがでしたでしょうか?このエントリーで書いたJavaScriptはIE8でも問題なく動きます。ちょっとした処理ならjQuery無しで書いてみようかな?という気が少しでも持てれば幸いです。
若干jQuery使用時に比べると簡潔ではありませんが、それでも工夫次第(例えばメソッドチェーンを多用しすぎないとか、別の処理は行を分ける等)で綺麗に書くことも可能だと思います。なによりライブラリに頼らないことでよりJavaScriptに対する理解が深まるのはいいことですよね。

次回では取得した要素に対して変更を行い、実践的な内容に挑戦してみたいと思います。

標準