準備
勉強用フォルダの下に「09」という名前で作業フォルダを作成して、以下のファイルを作成し、ライブプレビュー画面(ブラウザー)とコンソールを準備してください。
コード
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Chapter 09: 属性</title>
</head>
<body>
<p id="p_id" class="p_class">
属性にはエレメントのプロパティとして簡単にアクセスすることができます。
</p>
<p>
<a id="link" href="https://zenn.dev/ojk" target="_blank" rel="noopener">
<img src="https://github.com/ojklab.png" alt="画像" width="200">
</a>
</p>
<p class="p_class" style="color: firebrick; border: solid 1px navy">
style属性の扱いにはちょっとした決まり事があります。
</p>
<script src="script.js"></script>
</body>
</html>
'use strict';
console.log('Hello World!!');
属性へのアクセス
HTML 要素の属性には、エレメント(Element オブジェクト)のプロパティからアクセスすることができます。属性にアクセスするためのプロパティ名は、基本的には、属性名そのものです。
例えば、id 属性の値には エレメント.id
としてアクセスできます。
<p id="p_id" class="p_class">
属性にはエレメントのプロパティとして簡単にアクセスすることができます。
</p>
const p = document.getElementById('p_id');
console.log(`p要素のidは ${p.id} です`); // → p要素のidは p_id です
ただし、属性名とプロパティ名が異なるものも一部あります。
例えば、class 属性は className というプロパティ名になります。これは class というキーワードが JavaScript の予約語となっているからです。
console.log(p.class); // → undefined
console.log(p.className); // → p_class
とはいえ、代表的な属性でプロパティ名と属性名が異なるのは class くらいなので、まずは className だけ押さえておけばよいでしょう。
テキストノード
もう少し例を見ておきます。
<p>
<a id="link" href="https://zenn.dev/ojk" target="_blank" rel="noopener">
<img src="https://loremflickr.com/300/300" alt="画像">
</a>
</p>
上記の HTML コードは、Zenn の OJK のページにリンクを張った画像を表示するものです。
(このコードで使っているランダムで写真を取得できるサイトは読み込みに時間がかかるため、画像が表示されるまで少し待ちます)
// a要素とimg要素のエレメントを取得
const a = document.getElementById('link');
const img = a.firstElementChild;
// 属性値を書き換えることもできる
console.log(`元のリンクURL: ${a.href}`);
a.href = 'https://loremflickr.com/320/240/dog';
// つまり、JavaScript側で画像を差し替えることもできる
console.log(`元の画像ソース: ${img.src}`);
img.src = 'https://loremflickr.com/320/240/dog';
対応する JavaScript コードでは、HTML ドキュメントから取得した a 要素と img 要素の属性を書き換えています。
さて、このコードの続きに、a 要素の target 属性の値に応じて変化するテキストを画像の下に表示してみましょう。target 属性に「_blank」が設定されたときには「別のタブで開きます」と表示し、そうでないときは「同じタブで開きます」と表示します。
<p>
<a href="https://loremflickr.com/320/240/dog" target="_blank" rel="noopener">
<img src="https://loremflickr.com/320/240/dog">
</a>
<!-- 以下のbr要素とテキストをJavaScriptで追加したい -->
<!-- 1. 改行(<br>)を追加 -->
<!-- 2. 条件に応じて「別のタブで開きます」 or 「同じタブで開きます」というテキストを追加 -->
</p>
テキストの追加は p.textContent = '別のタブで開きます'
などと書きたくなりますが、p 要素は a 要素や img 要素の親要素なので、p 要素の textContent を書き換えるとその子要素である a 要素や img 要素も上書きしてしまいます。p 要素の textContent にはテキスト以外の子孫要素も含むからです。
そこで、テキストは テキストノード(Text オブジェクト) として用意し、エレメント(要素ノード)と同じように appendChild します(もちろん insertBefore でも OK)。ちょっと語弊がありますが、テキストノードは「テキスト情報だけを含むエレメントのようなもの」と思って扱えばよいでしょう。
テキストノードは createTextNode メソッド で生成します。
const テキストノード = document.createTextNode('テキスト');
前述の目的を果たすコードは、例えば次のように書くことができます。
// テキストノードを格納する変数(constではない)を用意
let textNode;
// a要素のtarget属性の値によってテキストノードの内容を場合分け
if (a.target == '_blank') {
textNode = document.createTextNode('別のタブで開きます');
} else {
textNode = document.createTextNode('同じタブで開きます');
}
// br要素を生成
const br = document.createElement('br');
// 親要素となるp要素(2つ目のp要素)を呼び出して、上記のノードを子要素として追加
const p = document.querySelectorAll('p')[1];
p.appendChild(br);
p.appendChild(textNode); // テキストノードもappendChildできる
HTML コードの target="_blank"
を target=""
に書き換えたりして動作を確認してみてください。今回の例のように、子要素に分断される形でテキストが入る場合にはこれまでの textContent では対処できなくなるので、テキストノードの概念を押さえておきましょう。
属性値の追加と削除
属性値の追加は、属性名プロパティへの代入によって追加できます。
// class属性が設定されていない2番目のp要素を取得
const p = document.querySelectorAll('p')[1];
console.log(p.className); // → 何も表示されない
p.className = 'new_class'; // class属性を追加
Chrome の開発者ツールの Element タブで class が追加されているか確認してみてください。
属性を削除するには、エレメント.removeAttribute メソッド を使います。その属性を持つ要素(エレメント)からメソッドを呼び出す必要があります。
const a = document.getElementById('link');
console.log(a.target); // → _blank
a.removeAttribute('target');
こちらも Chrome の開発者ツールの Element タブで target 属性が削除されているか確認してみてください。リンクをクリックしてみてもわかります。
設定済みの属性一覧の取得
ある要素に現在設定されてる属性の一覧は、そのエレメントの attributes プロパティ から取得することができます。
const a = document.getElementById('link');
const attrs = a.attributes; // 属性一覧を取得
// for-ofが使える
for (const attr of attrs) {
console.log(attr); // → id="link" → ...
}
attributes プロパティの値(中身)は NamedNodeMap オブジェクト といい、NodeList や HTMLCollection オブジェクトとも異なる「属性名と属性値のペアのリスト」です。
attributes の個々の値には、上記コードのように for-of 文でもアクセスできますし、普通のオブジェクトのようにアクセスすることもできます。
console.log(attrs.href); // → href="https://zenn.dev/ojk"
console.log(attrs['rel']); // → rel="noopener"
ここで attrs.href
は属性名と属性値のペアです。ここからさらに、name プロパティ と value プロパティ で属性名と属性値を取り出すことができます。value のほうは代入による書き換えも可能です。
console.log(attrs.href.name); // → href
console.log(attrs.href.value); // → "https://zenn.dev/ojk"
// value(属性値)のほうは書き換えも可能
attrs.href.value = 'https://loremflickr.com/320/240/dog';
オブジェクトの分割代入を使った for-of ループも便利です。
// 普通のfor-of文
for (const attr of attrs) {
console.log(attr.name + ':' + attr.value); // → id:"link" → ...
}
// オブジェクトの分割代入を使ったfor-of文
for (const { name, value } of attrs) {
console.log(name + ':' + value); // → id:"link" → ...
}
なお、NamedNodeMap オブジェクト(attributes プロパティ)からも removeNamedItem メソッド によって属性値の削除が可能です。
attrs.removeNamedItem('target');
style 属性の変更
以上のように、値を一つしか持たない属性の JavaScript での扱いは簡単です。しかし、スタイル属性は独特の値を持ちます。
例えば、コードの三つ目の p 要素は次のような style 属性を持ちます。
<p class="p_class" style="color: firebrick; border: solid 1px navy">
style属性の扱いにはちょっとした決まり事があります。
</p>
これは JavaScript ではどのように扱われるのでしょうか。ひとまずこの p 要素を呼び出して style プロパティを表示させてみましょう。
// p_classを持った要素の2番目を取得
const p = document.getElementsByClassName('p_class')[1];
console.log(p.style);
環境によって詳細は変わると思いますが、以下のような表示になったかと思います。
ごちゃごちゃと書かれていますが、左側の小さな ▶ をクリックして内容を見ると、どうやら HTML 側で明示的に設定していない CSS スタイルまですべて表示されているようです。
個々の CSS プロパティは style プロパティのプロパティとして保持されています(CSS の「プロパティ」と JavaScript オブジェクトの「プロパティ」が混乱しますね…)。何を言っているのかわからないかもしれませんが、要は JavaScript から CSS プロパティには「エレメント.style.CSS プロパティ名」としてアクセスできるということです。
console.log(p.style.color); // → firebrick
console.log(p.style.border); // → solid 1px navy
これでやっと、CSS スタイルが JavaScript から変更できます。
p.style.color = 'brown';
p.style.border = 'solid 5px gold';
HTML に記述されていない CSS スタイルを JavaScript から新たに設定することもできます。
p.style.padding = '10px 20px';
なお、class が className に なるとの同様で、CSS プロパティ名にも JavaScript の予約語と重複するものがあります。例えば、CSS プロパティの float は styleFloat になります。
キャメルケースとケバブケース
さて、実際にコードを打ち込みながら試していた人で、「background-color」などを設定しようとしてエラーが出てしまった人はいるでしょうか。もしくは、コードフォーマッタ Prettier が「background - color」とハイフンの左右に空白を入れてしまって気づいたかもしれません。
p.style.background-color = 'beige';
// → Error: Invalid left-hand side in assignment
プログラミング言語ではハイフン -
は減算演算子(つまり引き算)と解釈されてしまうので、ドット記法のプロパティ名の中では使えません。
その対策として、JavaScript では キャメルケース と ケバブケース という 2 種類の書き方が用意されています。
- backgroundColor … キャメルケース
- 'background-color' … ケバブケース
ケバブケースは単に引用符で囲んだだけの書き方です。引用符で囲めば文字列になるので、ハイフンは減算演算子と見做されません。ただし、引用符で囲んだ文字列はドット記法は使えないので、必然的にブラケット記法になります。
p.style['background-color'] = 'beige';
キャメルケースは「ハイフンの次の文字」を大文字にする記法です。
ラクダのコブのように見えるのでキャメルケースといいます。ちなみにケバブケースはハイフンで串刺しになっているからケバブだそうです。キャメルケースはブラケット記法でも使えますが、通常はドット記法を使います。
p.style.backgroundColor = 'beige';
プロパティ名を直接記述する場合はキャメルケースで記述するのが一般的で、プロパティ名を定数/変数に一旦格納する場合はケバブケース(ブラケット記法)が使われます。プロパティ名を定数/変数に代入するとブラケット記法になるのはオブジェクトの場合と同じです。
const widthArray = [
'border-top-width',
'border-left-width',
'border-bottom-width',
'border-right-width'
];
for (let i = 0; i < 4; i += 1) {
p.style[widthArray[i]] = i * 2 + 'px';
}
複数の値を持つ CSS プロパティ
border など、複数の値を持つ CSS プロパティについてもう少しだけみておきましょう。
CSS の border プロパティには solid 1px navy
といったように複数の値を空白を挟んで指定できます。それぞれの値は、border-style、border-width、border-color から個別にも設定できます。JavaScript でも個別設定ができることはすでにみたとおりです。
border プロパティで一括指定したときの個々のプロパティはどうなっているでしょうか。
const p = document.getElementById('p_id');
p.style.border = 'solid 1px navy';
console.log(`border-style: ${p.style.borderStyle}`); // → solid
console.log(`border-width: ${p.style.borderWidth}`); // → 1px
console.log(`border-color: ${p.style.borderColor}`); // → navy
一括指定すれば、個々のプロパティの値もちゃんと設定されています。
では、個々のプロパティを変更すると一括指定のプロパティにも反映されるでしょうか。上記のコードの続きです。
p.style.borderColor = 'firebrick';
console.log(p.style.border); // → solid 1px firebrick
反映されていますね。このあたりは普通の CSS での振る舞いと同じです。
class 属性によるスタイル設定
ここまで説明してきた style プロパティを使用したスタイル設定は style 属性を介したものでした。JavaScript による一時的なスタイル適用(警告のために文字を赤くするなど)なら style 属性でもよいのですが、そうでない場合は CSS ファイルに記述したいところです。
しかし、JavaScript から CSS ファイルは操作できません。
そのため、CSS ファイルに予め「class 毎に複数のスタイルをまとめて設定」しておき、JavaScript で(該当する HTML 要素の)class を切り替えることでスタイルを変化させます。
今回は CSS ファイルを用意せず、代わりに HTML ファイル内の style 要素にスタイルを記述します。CSS ファイルでも style 要素でも、class を介して JavaScript でスタイルを適用するという点は同じです。
コードの index.html の head 要素内に以下の style 要素を追加してください。
<style>
.p_class {
color: firebrick;
background-color: gold;
}
.myStyle {
color: lavender;
background-color: royalblue;
}
.baseStyle {
padding: 10px;
border: dotted 3px yellowgreen;
}
</style>
コードの id 属性が p_id である p 要素(以降は「p#p_id 要素」と表現します)には p_class クラスが最初から割り当てられています。
<p id="p_id" class="p_class">
属性にはエレメントのプロパティとして簡単にアクセスすることができます。
</p>
JavaScript で class を切り替えてみましょう。
const p = document.getElementById('p_id');
console.log(p.className); // → p_class
p.className = 'myStyle'; // myStyleクラスに切り替え
コードをコメントアウトして動作を確認してみましょう。Chrome の開発者モードの Element タブを確認するとよいと思います。baseStyle クラスの適用も試してみてください。
しかし、上記のやりかたでは複数の class を取り扱うのが面倒です。
例えば、「baseStyle + その他の class」という形にしたいとき、常に文字列でそれを表現しなくてはなりません。
// ダイアログからの入力によってスタイルを変える
const flag = window.prompt('A, B, or C');
if (flag == 'A') {
p.className = 'baseStyle p_class';
} else if (flag == 'B') {
p.className = 'baseStyle myStyle';
} else {
p.className = 'baseStyle';
}
もう少しスマートに書いたとしても以下のような感じでしょうか。やはり複数の要素を扱うときは配列(リスト)を使いたいところです。
p.className = 'baseStyle';
if (flag == 'A') {
p.className += ' p_class';
} else if (flag == 'B') {
p.className += ' myStyle';
}
そのための便利な機能はもちろん用意されていて、class を最初からリストで扱える classList プロパティ があります。ただし、nodeList などと同じく、配列ではなくて DOMTokenList オブジェクト というものですので、使えるメソッドは限られています。
classList プロパティとその add メソッド(classList.add メソッド)を使って、さきほどのコードを書き換えてみます。
const p = document.getElementById('p_id');
console.log(p.classList);
// → DOMTokenList ["p_class", value: "p_class"]
p.className = 'baseStyle'; // classNameプロパティも併用できる
const flag = window.prompt('A, B, or C');
if (flag == 'A') {
p.classList.add('p_class'); // p_classを追加
} else if (flag == 'B') {
p.classList.add('myStyle'); // myStyleを追加
}
classList
さきほどの classList プロパティの例ではあまりメリットを感じなかったかもしれません。ここでは JavaScript の文法学習も兼ねて、classList プロパティ(DOMTokenList オブジェクト)のもう少し便利な使い方を見ていきます。
例 1: もし p 要素に baseStyle クラスが設定されていなければ、baseStyle クラスを追加するプログラム
const p = document.getElementById('p_id');
// baseStyleクラスがclassListに含まれていなければ追加する
if (p.classList.contains('baseStyle') == false) {
p.classList.add('baseStyle');
}
このコードでは classList.contains メソッド を使用しています。contains メソッドは引数に class 名を受け取って、classList の中にその class が含まれていれば true を、含まれていなければ false を戻り値として返します。
興味のある知りたい人へ:プロパティとオブジェクト
ここで「classList.contains メソッド」と書いていますが、正確には「DOMTokenList オブジェクト.contains メソッド」です。classList というのは DOMTokenList オブジェクトにアクセスするための “プロパティ名” であって、メソッドを持っているのはオブジェクトのほうです。
attributes プロパティと NamedNodeMap オブジェクトの関係も同じです。
ただ、DOMTokenList オブジェクトといわれるよりも classList のほうがわかりやすいですし、NamedNodeMap オブジェクトといわれるよりも attributes といわれるほうがわかりやすいので、本書では便宜上、classList や attributes という言葉をオブジェクトであるかのように使用することもあります。
さて、サンプルコードでは if 文の条件式の中で、contains メソッドと false
を比較しています。この「false」には引用符が付いていませんが、定数/変数ではなく、Boolean 型 の値(リテラル)です。
Boolean 型は true(真)もしくは false(偽)の二値を持ちます(真偽値ともいわれます)。undefinded と同じく、引用符に囲わない値(キーワード)として覚えておいてください。
メソッドを条件式の中に記述するのは(このテキストでは)初登場かと思いますが、メソッド(関数)は戻り値に置き換わる…と頭の中で想像してみれば特に問題はないですね。しつこいですが、いつものイメージ図を示しておきます。
if (p.classList.contains('baseStyle') == false)
↓
if (戻り値 == false)
↓
if (false == false)
例 2: もし p_class クラスが設定されていたら、p_class クラスを削除して、baseStyle クラスと myStyle クラスを追加するプログラム
const p = document.getElementById('p_id');
if (p.classList.contains('p_class') == true) {
p.classList.remove('p_class'); // 削除
p.classList.add('baseStyle', 'myStyle'); // 追加
}
class の削除は classList.remove メソッド で行います。また、add や remove の引数には複数のクラス名を指定することができます。
class を 1 個削除して 1 個追加する(つまり交換する)なら、classList.replace メソッド で一行で書くこともできます。
p.classList.replace('削除するクラス', '追加するクラス');
const p = document.getElementById('p_id');
if (p.classList.contains('p_class') == true) {
// p.classList.remove('p_class');
// p.classList.add('baseStyle', 'myStyle');
p.classList.replace('p_class', 'baseStyle myStyle');
}
例 3: もし myStyle クラスが設定されていたら削除し、設定されていなかったら追加するプログラム
これは条件文を使って次のように記述できますが…
const p = document.getElementById('p_id');
if (p.classList.contains('myStyle') == true) {
p.classList.remove('myStyle');
} else {
p.classList.add('myStyle');
}
classList.toggle メソッド を使えば if 文も使わず 1 行で書けます。指定された class の有/無をトグルする、つまり、その class があれば削除し、無ければ追加します。
p.classList.toggle('myStyle'); // myStyleクラスがclassListにあれば削除し、無ければ追加する
true/false の省略
なお、if 文や三項演算子、for 文などの “条件式” における == true
や == false
は省略することができます。== true
は単純に省略、== false
は条件式の前に NOT 演算子「!」を付けます。
if (メソッド == true) ↔ if (メソッド)
if (メソッド == false) ↔ if (!メソッド)
したがって、前述の例題のコードは次のようにも書けます。
if (!p.classList.contains('baseStyle'))
慣れるまではわかりにくいのですが、世の中のコードでは省略されていることも多いので覚えておいてください。以下、詳細について説明します。
“条件式” は「式」というだけあって、計算されて結果(値)に置き換わります。そして、その値は常に Boolean 型になります。
つまり if 文は、その ( ) 内が true か false かで { } 内の処理を実行するか否かを決めています。だから以下のように Boolean 値を ( ) 内に直接書くこともできます。
if (true) {
console.log('絶対に実行される');
}
if (false) {
console.log('絶対に実行されない');
}
ということは、「戻り値が Boolean 型のメソッドや関数」であれば、いちいち true や false と比較しなくても、メソッドや関数の呼び出しだけ書いておけば true もしくは false に置き換わるので、問題なく if 文が機能します。
if (メソッドや関数の呼び出し)
↓
if (Boolean型の戻り値)
↓
if (true)
「== true」のときはメソッド呼び出しを書いておくだけでいいのですが、「== false」のときに条件式が成り立つようにするには 否定演算子「!」 を使う必要があります。否定演算子を Boolean 値の頭に付けることで、true と false を反転させることができます。
console.log(!true); // → false
console.log(!false); // → true
以上の知見を組み合わせると以下のような結論となります。
if (!p.classList.contains('baseStyle'))
↓
baseStyleが含まれなかった
↓
if (!false)
↓
if (true) {
p.classList.add('baseStyle'); // 含まれなかったので追加する
}