hiyoko-programingの日記

プログラミングを勉強したてのひよっ子。   エンジニア目指して勉強中。

JavaScriptで画面上の表示

JavaScriptで簡単な機能を実装

"ボタンをクリックしたらコンソール上に「Hello world」と出る"
簡単な機能を実装してみる。
https://tech-master.s3.amazonaws.com/uploads/curriculums//1e2931ca1a4af2b2b76be8e1b67bde78.gif

「ボタン」をJavaScript上で扱えるようにする

ボタンはHTMLの要素。HTMLの要素をJavaScript上で取得し操作するために、DOMという仕組みを利用する。

 DOM

DOMとはDocument Object Model(ドキュメントオブジェクトモデル)の略。HTMLを解析し、データを作成する仕組み。
ここで一度、HTMLやCSSがWEBページとして閲覧されるまでの流れを考えてみる。
HTMLで書かれたファイルは結局ただの文字情報なので、そのまま表示しても意味がない。HTMLを解析し、CSSJavascriptによる装飾を行ってから画面に映すという工程が必要なはずである。これを行っているのが、Google ChromeSafariといったブラウザ。ブラウザがHTMLやCSSJavaScriptを受け取ってユーザーに届けるまでには、以下のような手順を踏んでいる。

ブラウザの表示

HTMLは階層構造になっていることが特徴。
DOMによって解析されたHTMLは、階層構造のあるデータとなる。これを、DOMツリードキュメントツリーと呼ぶ。
DOMツリーはデータとして階層構造になっていることで、階層構造を扱うためのアルゴリズムを使い操作できる。

DOMとは

JavaScriptを使うと、DOMツリーを操作することができる。
HTMLの要素名や、「id、class」といった属性の情報を元にDOMツリーの一部を取得し、CSSを変更したり、要素を増やしたり、消したりできる。
するとそれがブラウザに反映され、描画も変わる。DOMツリーの一部のことを、ノードオブジェクトと呼ぶ。

 ノード

HTML1つ1つのタグが、DOMツリーの中ではノードと呼ばれる。

ノード

 DOMツリーの情報

  • index.htmlGoogle Chromeで開いて、検証ツールをみる。 検証ツールは右クリックで「検証」を選択するか、command + option + Iにより立ち上げることができる。 エレメントツリーを見ると、DOMツリーの情報を確認することができる。

https://tech-master.s3.amazonaws.com/uploads/curriculums//5bd457454fc4eb83789f1b796528acb0.png

エレメントツリーを確認したら、今度はボタンにあたるノードを取得する。

ノードの取得

JavaScriptを使ってHTML要素のid名やクラス名を指定することで、マッチするノードを取得できる。

 document.getElementById("id名");

documentは、開いているWebページのDOMツリーが入っているオブジェクト。documentに対していくつかのメソッドを利用することで、DOMツリーに含まれる要素を抽出して取得することができる。.getElementById("id名")はDOMツリーから特定の要素を取得するためのメソッドの1つ。引数に渡したidを持つ要素を取得する。

以下のように書くことで、マッチするidを持つノードを取得することができる。

【例】id名で取得したい時
1
document.getElementById("id名");

 document.getElementsByClassName("class名");

classを指定して取得する際はこちらを利用する。ここで気をつけたいのは、getElementsと複数形になっていること。
id名はhtml上に必ず一つしか存在しないのに対して、class名を指定するgetElementsByClassName("class名")の場合は、同じclassを持つ要素を全て取得することが可能。

以下のように書くことで、クラス名とマッチするデータを取得することができる。

【例】class名で取得したい時
1
document.getElementsByClassName("クラス名");

 document.querySelector("セレクタ名");

セレクタ名とは、CSSでスタイルを適用するために指定している要素。セレクタ名を指定してDOMを取得する場合、querySelectorメソッドを使用する。HTML上から、引数で指定したセレクタに合致するもののうち一番最初に見つかった要素1つを取得する。

【例】セレクタ名を取得したい時
1
document.querySelector("セレクタ名");

https://tech-master.s3.amazonaws.com/uploads/curriculums//450997dc81ee224ce3af1474052c26c5.png

どれを使用しても構わないが、今回はquerySelectorメソッドを使用して、ボタンのノードを取得する。

 コンソールでボタン要素を取得

まずは、コンソールを開く。
コンソールのショートカットキーは、command + option + Jで開ける。

コンソール上に以下のコードを実行。

1
document.querySelector("button");

https://tech-master.s3.amazonaws.com/uploads/curriculums//74bdac7c53de1bbf07ccc599c64c0564.gif

戻り値として、<button id="Button">ボタン</button>というHTMLタグのようなものが得られた、これがノード。

https://tech-master.s3.amazonaws.com/uploads/curriculums//cdf90a397e2815d89734bd0fbe42a370.png

querySelectorメソッドでは、複雑なセレクタも指定できる。

1
2
document.querySelector("button#Button2"); // idがButton2のbuttonタグ要素
document.querySelector("footer a.next"); // footerタグ要素の中の、クラスがnextのaタグ要素

これでquerySelectorメソッドを使って、ボタンをJavaScript上で取得することができた。

ここまでをおさらい。

DOMの取得とは、querySelectorメソッドなどを使用して、HTML要素をノードとして取得すること。
ノードを取得してからJavaScriptで操作することが多いので、この流れをしっかりおさえておく。
https://tech-master.s3.amazonaws.com/uploads/curriculums//5e3472555964f17f6f72c2af16fae326.png

「ボタンをクリックしたこと」を取得

取得のきっかけとなる操作についてJavaScriptでどのように検知するのか。
ボタンが「クリックされたこと」を取得するために、JavaScriptイベントと呼ばれる概念を使う。

 イベント

JavaScriptにおけるイベントとは、HTMLの要素に対して行われた処理要求のことをいう。例えば「ユーザーがブラウザ上のボタンをクリックした」「テキストフィールドでキー入力をした」「要素の上にマウスカーソルを乗せた」などがいずれもイベント。

また、イベントを起こすのはユーザーだけでなく、「ドキュメントの読み込みが終わった」などブラウザが発生させるものもある。
例えば以下の様に、Webサイト上のある箇所にマウスを乗せると、プルダウンが展開するような挙動がある。このうち、「マウスを乗せた」という動作がイベントに当たる。

 イベント駆動

JavaScriptはイベント駆動という概念に基づいて設計されている。
これは、「イベント」が発生したら、それをきっかけにコードが実行される仕組み。
JavaScriptはもともとブラウザに搭載するための言語として開発されており、ユーザーが行う様々な操作に対応できるイベント駆動の仕組みが適しているため。

イベントを取得するためには、先に取得したノードに対して処理を書く。

 addEventListener

addEventListenerメソッドはノードオブジェクトのメソッドで、以下のようにして実行する。

1
(ノードオブジェクト).addEventListener("イベント名", 関数);

上記のような記述により、この記述の読み込み以降で「ノードオブジェクト」に「イベント」が起きた時、「関数」を実行するようになる。
一つのイベントと一つの関数を紐付ける仕組みのことをイベントリスナと呼ぶ。

一つのイベントに複数の関数を紐付ける場合は、関数の数だけイベントリスナが存在する。

addEventListenerは、あるノードオブジェクトに対して、イベントリスナを追加するメソッド。

addEventListenerメソッドを用いて、ボタンがクリックされたらコンソールに適当な文字列を出力するようにしてみる。

 ボタンをクリックしたら、コンソールに「Hello world」と表示されるようにする

main.jsに以下のコードを追記。

main.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
let btn = document.querySelector("button");
// ボタンをノードオブジェクトとして取得し、変数btnに代入する

function printHello() {
  console.log("Hello world");
}
// printHello関数を定義

btn.addEventListener("click", printHello);
// ボタンのノードオブジェクトであるbtnに対して、
// clickイベントとprintHello関数を紐付ける仕組みであるイベントリスナを追加する

これにより、トップページにてボタンをクリックするとコンソールに「Hello world」と出力されることが期待できる。

しかし、ボタンを押しても反応せず、コンソールを開くと以下のエラーが出る。

1
Cannot read property 'addEventListener' of null

今回コードを記述したmain.jsは、index.htmlのheadタグ内に記述して読み込んでいる。


ブラウザは上から順に実行をするので、このJavaScriptのコードを読み込む時、まだhtmlファイルのheadタグ内までしか読み込まれておらず、bodyタグ内にあるbuttonタグは読み込まれていなかった。

https://tech-master.s3.amazonaws.com/uploads/curriculums//49f74e2aee9b5953995abaa0c98bc8c7.png

そのため、querySelectorメソッドによりボタンのノードオブジェクトは取得できず、変数btnの中身はnull(何も無いという意味)となっていたの。
nullのプロパティとしてaddEventListenerメソッドが実行されるため、コンソールで「nullのaddEventLisenerは読み込めない」エラーが出た。

この問題を解決するために、上記の一連の処理を、「ページの読み込みが終わったら」main.jsの中身を実行するようにする。

 ページの読み込みをするwindow.onloadを使う

ページの読み込みは、以下2つの記述方法がある。

1
window.onload = function() { 処理 };
1
window.addEventListener('load', function() { 処理 });

「ページの読み込みが終わる」イベントは、windowオブジェクトのloadイベントに対応する。そこで、windowオブジェクトのloadイベントに対応する関数として上記の一連の処理を定義すれば良いと考えられる。なお、windowオブジェクトは、元から用意されている、ブラウザの情報を持つオブジェクト。

 ページの読み込みを先に行えるようにする

main.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
function printHelloWithButton() {
  let btn = document.querySelector("button");

  function printHello() {
    console.log("Hello world");
  }
  // 関数内で定義された関数は、関数の中でしか呼び出せない性質があるだけで、
  // 通常の関数同様に呼び出せる

  btn.addEventListener("click", printHello);
}
// 一連の処理をまとめた関数を作る

window.addEventListener("load", printHelloWithButton);

これにより、windowオブジェクトのloadイベントが起きたら、つまりページの読み込みが終わったら、printHelloWithButton関数が実行されることになる。
この時、ページは読み込まれているため、DOMオブジェクトとしてbuttonタグを取得することができ、イベントリスナを正常に追加することができ。

実際にトップページにアクセスし、ボタンをクリックすると、コンソール上に「Hello world」の出力がされますね。

https://tech-master.s3.amazonaws.com/uploads/curriculums//5223fb7251b9134809eef7d44f2f160b.gif

コードを見直す

まず、console.log一行のために関数を定義しているのは冗長であると考えられる。

わざわざ関数として定義しているのは、addEventListenerメソッドの第二引数として、関数を指定しなくてはならないからである。

そこで、addEventListenerメソッドの第二引数内で、関数を定義することを考える。

1
2
3
4
function func() {}
// 何もしないfunc関数

btn.addEventListener("click", func);

これはこのようにも書ける。

1
btn.addEventListener("click", function func() {});

addEventListenerの第二引数部分に、関数の定義部分を移動しただけ。

また、先程は関数を呼び出すために関数名をつけていたが、以下のように名前(上の例で言えばfunc)を省略することができる。名前の無い関数のことを無名関数と呼ぶ。

1
btn.addEventListener("click", function() {});

上の例では関数の中身は何もないが、処理がある場合(殆どの場合)は一般に改行を用いて以下のように書く。

main.js
1
2
3
btn.addEventListener("click", function() {
  // 処理
});

この書き方を用いて、main.jsの中身を書き換えてみる。

 コードが短くなるように、中身を書き換える

まず、window.addEventListenerの第二引数部分を書き換える。

main.js
1
2
3
4
5
6
7
8
9
window.addEventListener("load", function() {
  let btn = document.querySelector("button");

  function printHello() {
    console.log("Hello world");
  }

  btn.addEventListener("click", printHello);
});

次に、btn.addEventListenerの第二引数部分を書き換える。

main.js
1
2
3
4
5
6
7
window.addEventListener("load", function() {
  let btn = document.querySelector("button");

  btn.addEventListener("click", function() {
    console.log("Hello world");
  });
});

随分とすっきりした。このような書き方もよく使われる。

ここまでの内容を図でまとめ。

https://tech-master.s3.amazonaws.com/uploads/curriculums//3e33b8bc04ff85a4904481cb42c927e5.png

DOMツリーからノードを取得する
JavaScriptでやりたい処理を書く
イベント発火でHTML側で動かす

このイメージを掴むことができれば、応用してJavaScript側で色んな処理を書いて、画面に動きを持たせる実装を実現できるようになる。

DOMの置換

ボタン2を押したら、テキストが置換できるようにする。

https://tech-master.s3.amazonaws.com/uploads/curriculums//ac409d6fa16b9f933ff3d65b987ef1a8.gif

 innerHTML

innerHTMLを使用するとHTML要素の中身を書き換えることができる。

main.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
window.addEventListener("load", function() {
  let btn = document.querySelector("button");
  btn.addEventListener("click", function() {
    console.log("Hello world");
  });
 // テキストの要素を取得し、変数で定義
 let btn2 = document.querySelector("#Button2");
 let changeText = document.querySelector("p");
 // ボタン2をクリックしたらテキストが置換される
 btn2.addEventListener("click", function() {
   changeText.innerHTML = '変更されました';
 });
});

ボタン2をクリックしたら、テキストが置換されることを確かめる。

DOMの追加

「ボタンをクリックするとクラス要素が追加される機能」を実装。
https://tech-master.s3.amazonaws.com/uploads/curriculums//0efb4516642821e717c28ca5c4b7c737.gif

 classList.add

「クラス追加」ボタンが押されたら、cssの背景色が赤のredクラスが追加されるようにclassList.addを利用する。

main.js
1
2
3
4
5
6
7
// Button3を取得して、変数で定義
let btn3 = document.querySelector("#Button3");

// クラス追加を押したらredクラスが追加される
btn3.addEventListener("click", function() {
  changeText.classList.add("red");
});

これで、redクラスが追加された。
console.logを使ってデバッグする。

main.js
1
2
3
4
5
6
7
8
// Button3を取得して、変数で定義
let btn3 = document.querySelector("#Button3");

// クラス追加を押したらredクラスが追加される
btn3.addEventListener("click", function() {
  changeText.classList.add("red");
  console.log(changeText.classList); // ここに追加
});

これで、クラス追加ボタンを押した際に、コンソール上にDOMTokenList ["red", value: "red"]と出ていれば成功。

console.logJavaScriptがちゃんと動いているかどうか、デバッグする際によく使う。

DOMの削除

すでにあるクラスを削除する実装。
「クラス削除ボタン」を押すと、青背景に設定しているblueクラスが削除されるようにする。
https://tech-master.s3.amazonaws.com/uploads/curriculums//ac00fcf0a398b18ef8f2f75464983683.gif

 classList.remove

「クラス削除」ボタンが押されたら、cssの背景色が青のblueクラスを削除するclassList.removeを利用する。

main.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// Button4を取得して、変数で定義
let btn4 = document.querySelector("#Button4");

// div要素を取得して、変数で定義
let obj = document.querySelector("div");

// クラス削除を押したらblueクラスが削除される
btn4.addEventListener("click", function() {
  obj.classList.remove("blue");
});

blueクラスが削除されて、青背景がなくなるのを確認。

最終的に、main.jsの中身は以下のようになる。

main.js
 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
31
32
33
34
window.addEventListener("load", function() {
  let btn = document.querySelector("button#Button");

  btn.addEventListener("click", function() {
    console.log("Hello world");
  });

  // テキストの要素を取得し、変数で定義
  let btn2 = document.querySelector("button#Button2");
  let changeText = document.querySelector("p");

  // ボタン2をクリックしたらテキストが置換される
  btn2.addEventListener("click", function() {
    changeText.innerHTML = '変更されました';
  });

  // Button3を取得して、変数で定義
  let btn3 = document.querySelector("#Button3");

  // クラス追加を押したらredクラスが追加される
  btn3.addEventListener("click", function() {
    changeText.classList.add("red");
  });

  // Button4を取得して、変数で定義
  let btn4 = document.querySelector("#Button4");
  // div要素を取得して、変数で定義
  let obj = document.querySelector("div");

  // クラス削除を押したらblueクラスが削除される
  btn4.addEventListener("click", function() {
    obj.classList.remove("blue");
  });
});

メニュータブの切り替え実装

メニュータブの切り替えができる実装を考える。

https://tech-master.s3.amazonaws.com/uploads/curriculums//2e61205feb7f9ae3f8650c6f4a129b95.gif

projectsディレクトリにgit clone

ターミナル
1

ダウンロードしたindex.htmlのbodyタグ内を確認。

index.html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<div class="container">
  <ul class="menu">
    <li><a href="#" id="about" class="menu_item active">Rails</a></li>
    <li><a href="#" id="service" class="menu_item">Javascript</a></li>
    <li><a href="#" id="contact" class="menu_item">Ruby</a></li>
  </ul>
  <ul class="contents">
    <li class="content show">
      about about about about about about about about about about about about about about about about about about about about about about about about about about about about about about about about about about about about about about about
    </li>
    <li class="content">
      service service service service service service service service service service service service service service service service service service service service service service service service service service
    </li>
    <li class="content">
      contact contact contact contact contact contact contact contact contact contact contact contact contact contact contact contact contact contact contact contact contact contact contact
    </li>
  </ul>
</div>

activeクラスshowクラスが付いているものが、cssdisplay: block;で表示できるようにし、付いていないものはdisplay: none;で表示できないようにしている。
タブメニューをクリックした時に、対応するクラスにactiveクラスshowクラスを追加することで実装することができる。

https://tech-master.s3.amazonaws.com/uploads/curriculums//4f0f3e4d6ced50f1ca9d31f6b6aa96c3.gif

コンソール上でクラスの移動が確認できる。
今までの知識でクラスの削除とクラスの追加については書けるはずなので、main.jsの中身を穴埋め形式で埋めて、コードを完成させる。

main.js
 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
31
32
window.addEventListener("load", function() {
  // タブのDOMを取得し、変数で定義
  let tabs = document.getElementsByClassName("menu_item");
  // tabsを配列に変換する
  tabsAry = Array.prototype.slice.call(tabs);

  // クラスの切り替えをtabSwitch関数で定義
  function tabSwitch() {
    // 全てのactiveクラスのうち、最初の要素を削除("[0]は、最初の要素の意味")
    document.getElementsByClassName("active")[0].classList.remove("active");
    // クリックしたタブにactiveクラスを追加
    // ②`this.`の後に、classListを使用してactiveクラスを追加しよう
    this.

    // コンテンツの全てのshowクラスのうち、最初の要素を削除
    // ③`document.getElementsByClassName('show')[0].`の後に、showクラスを削除しよう
    document.getElementsByClassName('show')[0].

    // 何番目の要素がクリックされたかを、配列tabsから要素番号を取得
    const index = tabsAry.indexOf(this);

    // クリックしたcontentクラスにshowクラスを追加する
    // ④`document.getElementsByClassName("content")[index].`の後に、showクラスを追加しよう
    document.getElementsByClassName("content")[index].
  }

  // タブメニューの中でクリックイベントが発生した場所を探し、下で定義したtabSwitch関数を呼び出す
  tabsAry.forEach(function(value) {
    // ①`value.`の後に、イベントリスナーでクリックイベントが発生した時に、tabSwitch関数を呼び出す処理を書きましょう。
    value.
  });
});

 thisについて

関数(tabSwitch)の中でthisを使うと、イベントの発生元となった要素を取得することができる。

解答・解説

模範解答

main.js
 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
window.addEventListener("load", function() {
  // タブのDOMを取得し、変数で定義
  let tabs = document.getElementsByClassName("menu_item");
  // tabsを配列に変換する
  tabsAry = Array.prototype.slice.call(tabs);

  // クラスの切り替えをtabSwitch関数で定義
  function tabSwitch() {
    // 全てのactiveクラスのうち、最初の要素を削除("[0]は、最初の要素の意味")
    document.getElementsByClassName("active")[0].classList.remove("active");

    // クリックしたタブにactiveクラスを追加
    this.classList.add("active");

    // コンテンツの全てのshowクラスのうち、最初の要素を削除
    document.getElementsByClassName("show")[0].classList.remove("show");

    // 何番目の要素がクリックされたかを、配列tabsから要素番号を取得
    const index = tabsAry.indexOf(this);

    // クリックしたcontentクラスにshowクラスを追加する
    document.getElementsByClassName("content")[index].classList.add("show");
  }

  // タブメニューの中でクリックイベントが発生した場所を探し、下で定義したtabSwitch関数を呼び出す
  tabsAry.forEach(function(value) {
    value.addEventListener("click", tabSwitch);
  });
});

 Array.prototype.slice.call()

Array.prototype.slice.call()は引数にとったオブジェクトを配列に変換してくれる。

 forEach()

配列に対してよく使われる繰り返し処理。

1
配列.forEach( コールバック関数 )

コールバック関数とは、配列の各要素に対して行う処理のこと。

1
2
3
配列.forEach( function(value, index) {
  // 配列のデータに対しての処理、valueは値、indexは要素番号
} )

第二引数を省略した場合(indexを定義しない)、配列の各要素(value)だけ取り出せる。

 indexOf()

inexOf()は配列に対してだけ使い、DOMを引数にとって一致した要素番号を戻す。