かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

JavaScript jQueryを使わずに要素選択するやつのメモ。

雰囲気で使ってたから、ちょうどいい機会だしと思い簡単に調べてみたメモ

document.getElementById

  • id名で要素を選択して取得する
  • idは#idNameではなく、idName
  • Elementを返す
  • idはHTML上ではユニーク(な筈)仮にidが重複して存在している場合は最初にマッチした要素が返る
  • 存在しないidで取得できない場合はnullが返る
  • documentオブジェクトのみで利用可能
  • プリミティブだからたぶん高速

HTML

<div id="app">
  <h1 id="title"></h1>
</div>
<div id="main" class="main1"></div>
<div id="main" class="main2"></div>

JavaScript

const app = document.getElementById('app');
// => <div id="app"></div>
document.getElementById('main');
// => <div id="main" class="main1"></div>
document.getElementById('nowhere_guy');
// => null
app.getElementById('title');
// => Uncaught TypeError

getElementsByClassName

  • クラス名で要素を選択し取得する
  • クラス名の指定に.不要
  • HTMLCollectionを返す
    • .lengthで要素数を返す
    • 配列のようにインデックスを指定すると該当要素にアクセスできる。e.g. document.getElementsByClassName(names)[0]なら見つかった要素の1つめ
    • 素数外のインデックスを指定するとundefined (エラーにはならない)
  • 該当する要素が存在しない場合はHTMLCollection []を返す。
    • length0
    • インデックスで要素にアクセスするとundefined
  • "A B"のようにスペース区切りで複数のクラス名を渡した場合は、AかつBのクラスを持つものを取得
  • document以外でも使用できる。
    rootElem.getElementsByClassName(names)親要素rootElemの子要素から該当する要素を探す
  • 配列のようにインデックスで要素にアクセスできるけどforEachなどのメソッドを持っていないので、ループで回す場合はforを使うか、Array.fromで配列に変換してループで回す

HTML

<div id="app">
  <h1 id="title">Get Element</h1>
  <p class="text red">text1</p>
  <p class="text blue">text2</p>
</div>
<p class="small text red">text3</p>

JavaScript

const texts = document.geteElementsByClassName('text');
// => HTMLCollection(3) [p.text.red, p.text.blue, p.small.text.red]
console.log( texts.length );
// => 3
console.log( texts[0] );
// => <p class="text red">text1</p>
console.log(texts[100]);
// => undefined

const notFoundClass = document.getElementsByClassName('notfound');
console.log( notFoundClass );
// => HTMLCollection []
console.log( notFoundClass.length );
// => 0
console.log( notFoundClass[0] );
// => undefined

const app = documents.getElementById('app');
app.getElementsByClassName('text');
// => HTMLCollection(2) [p.text.red, p.text.blue]

const red_text = document.getElementsByClassName('text red');
// => HTMLCollection(2) [p.text.red, p.small.text.red]

red_text.forEach((elm) => console.log(elm));
// => getelement.html:42 Uncaught TypeError

for(let i=0, l=red_text.length; i<l; i+=1) {
  console.log( red_text[i].textContent );
}
// => text1
// => text3

const red_text_arg = Array.from( red_text );
red_text_arg.forEach((elm) => console.log(elm.textContent));
// => text1
// => text3

querySelector

document.querySelector(selectors);

  • #idName, .className, input[type="text"]のようなCSSセレクターで取得する要素を指定する
  • Elementを返す
  • 要素が存在しない場合はnullを返す
  • 複数の要素が該当する場合は最初に見つかった要素を返す
  • targetElem.querySelector()とした場合は、targetElemの子要素から要素を探す

HTML

<div id="app">
  <h1 class="title">Title</h1>
  <p class="text red">text1</p>
  <p class="text blue">text2</p>
  <div class="content">
    <p class="text blue">text3</p>
  </div>
</div>

JavaScript

const app = document.querySelector('#app');
// => <div id="app"></div>
app.querySelector('.content .text');
// => <p class="text blue">text3</p>
document.querySelector('#notFoundID');
// => null
app.querySelector('.text.blue');
// => <p class="text blue">text2</p>

querySelectorAll

  • 渡されたCSSセレクターにマッチする要素を集合(NodeList)として取得する
  • Array-likeなNodeListオブジェクトを返す
    • .lengthで要素数を返す
    • 配列のようにインデックスを指定すると該当要素にアクセスできる。
    • 素数外のインデックスを指定するとundefined (エラーにはならない)
  • 該当する要素が存在しない場合はNodeList []を返す。
    • length0
    • インデックスで要素にアクセスするとundefined
  • targetElem.querySelector()とした場合は、targetElemの子要素から要素を探す
  • getElementsByClassNameで取得できるHTMLCollectionとは違いNodeListforEachメソッドを持っているので、そのままループ処理をすることができる。
    for...of...でもループさせることは出来る。
    • ※ 当初はforEachメソッドも持っていなかったが、使えるようになった
    • map, filter, reduce, someなどの配列のメソッドは持っていないのでTypeErrorになる

HTML

<div id="app">
  <h1 class="title">Title</h1>
  <p class="text red">text1</p>
  <p class="text blue">text2</p>
  <div class="content">
    <p class="text blue">text3</p>
  </div>
</div>
<footer>
  <p class="small text red">text4</p>
</footer>

JavaScript

const textNodeList = document.querySelectorAll('.text');
// => NodeList(4) [p.text.red, p.text.blue, p.text.blue, p.small.text.red]
console.log( notFoundNodeList.length );
// => 4
console.log( textNodeList[0] );
// => <p class="text red">text1</p>
console.log( textNodeList[100] );
// => undefined

const notFoundNodeList = document.querySelectorAll('.notFoundClass');
// => NodeList []
console.log( notFoundNodeList.length );
// => 0
console.log( notFoundNodeList[0] );
// => undefined

const app = document.querySelector('#app');
const appTextNodes = app.querySelectorAll('.text.blue');
appTextNodes.forEach((elm) => console.log(elm));
// => <p class="text blue">text2</p>
// => <p class="text blue">text3</p>
for(let val of appTextNodes) {
  console.log(val);
}
// => <p class="text blue">text2</p>
// => <p class="text blue">text3</p>

appTextNodes.map((elm, i) => elm.innerHTML = `text-${i}`);
// => Uncaught TypeError

mapなどを使いたい場合は[...obj]Array.from()で配列化してやればOK

const appText = app.querySelectorAll('.text.blue');
[...appText].map((elm, i) => elem.innerHTML = `text-${i}`);
// => <p class="text blue">text-0</p>
// => <p class="text blue">text-1</p>

Array.from(appTextNodes).map((elm, i) => {
  elm.classList.replace('blue', 'green');
  console.log(elm);
});
// => <p class="text green">text-0</p>
// => <p class="text green">text-1</p>

 

感想とポエム

querySelector, querySelectorAllを使えばjQueryと同じ感覚でHTMLの要素を取得できるようになっているので所謂「脱jQuery」しやすい環境かなという印象です。
querySelectorjQueryのようにCSSセレクターで要素を取得だったので、複数の要素も取得できそうなイメージを持っていました。実際には最初にマッチした要素1つしか取得できないのでjQuery感覚で覚えてしまっているとハマりどころになりそうです。(複数の要素取得するつもりで、querySelector(.foo).forEach()ってやっちゃうとエラーになりますし)
後はクラス名で要素を取得しようとした時にナウいquerySelectorAllで取得できるNodeListオブジェクトはforEachできるけど、伝統的なgetElementsClassNameで取得できるHTMLCollectionオブジェクトはforEachしたらエラーで死ぬってのもちょっと紛らわしいんで統一感欲しかった感はあります。(querySelector系使っていけってメッセージなのかもしれないけど…

そして、jQueryとは違い要素が存在していないと、その後のメソッドでエラーになり処理そのものが止まってしまうので、取得した要素が絶対に在る事が担保できる状態で取得した要素を使う必要があるでしょう。
その点jQueryは良くも悪くもjQueryオブジェクトでラップしているので要素が本当に存在しているかどうかを厳密にチェックせずとも雑に使って大丈夫ですし、取得する要素が単数でも複数でもあまり意識する必要が無い。という部分は気にかける部分が減るという意味で利点だと思います。(VDOMのフレームワークで実際のDOM操作を気にかけなくて済むのも、気にかける部分が減るという意味で同じでしょう)

なもんでブラウザのAPIが揃ってきたから、この案件の対応ブラウザの条件ならjQueryは不要ってのは理解できるのだけれど、
jQueryは重いから不要とか、jQueryは辛いからもう使うべきじゃないって、jQuery悪の枢軸みないなワードを目にすることもありますが、前者は他のライブラリ使ったりwebpackしても、それらの容量あるんだし気になるような大きさじゃなくない?って思うし、後者はたしかに状態管理の為にDOMにデータ紐づけて管理するのは辛いと思うけど、それjQueryそのものじゃなくてDOMに直接イベント付けたりして状態管理する手法が辛いって事じゃない?ってチョット過激なポジショントークのように感じてます。jQuery使ってたらJavaScript覚えられないってのもフレームワークのお作法の範囲で使っていたら、深い部分を理解する必要がないので同じことでしょう。

状態管理するまでもないよう動きつけるだけで、後方互換も重要視されるような"ほうむぺぃじ"製作とかならDOM操作のPolyfillとしてjQueryは別に悪くない選択肢にまだまだ使える場面があり得るよねーって印象で、結局は関わっている案件や作りたいもののに依るんじゃないかなーって感想。

定量的に考えることが得意なはずのエンジニア界隈でライブラリや言語やエディタの話になると宗教問題みたいな論調が幅を利かすのチョット謎。やっぱ思い入れ? (IEの6, 7, 8, 9, 10対応みたいなバージョンがたくさん生きてた時代jQueryには大変お世話になったので悪の枢軸みたいに言われるのチョットなんだなかーって....これは私の思い入れ


[参考]

セレクター…

JavaScript classListでクラス属性を操作するゾ

Element.classList

classListは配列のように扱えるDOMTokenListオブジェクト

// HTML: <div id="main" class="c1 c2 c3"></div>
const main = document.getElementById('main');
const classList = main.classList;
console.log( classList );
// => DOMTokenList(3) ["c1", "c2", "c3", value: "c1 c2 c3"]

Element.classList.lengthでDOMに付いているクラスの数が返される

main.classList.length; // => 3

配列のようにElement.classList[0]でアクセスするとインデックス番号のクラス名が取得できる

main.classList[0]; // => "c1"
// 存在しないインデックスにアクセスすると undefined
main.classList[100]; // => undefined

Element.classList.valueはクラス属性の値が文字列で取得できる

main.classList.value; // => "c1 c2 c3"
main.classList.value === main.className; // => true

element.classNameで得られる文字列と等しい

classList.value を直接変更するとクラス名が置き換えられる
// HTML: <div id="main" class="c1 c2 c3"></div>
const main = getElementById('main');
main.classList.value = "foo bar";
console.log( main.classList );
// => DOMTokenList(2) ["foo", "bar", value: "foo bar"]
console.log( main.className );
// => "foo bar"
console.log( main )
// => <div id="main" class="foo bar"></div>

同様にclassNameに直接クラス名の値を代入して変更すると、classListの内容変更される
またclassListはオブジェクトなので = で変数に取っておいても元のオブジェクトが変更されると、変数にも変更が反映される反映される

const _classList = main.classList;
main.className = "pizza hotdog";
console.log( main.classList );
// => DOMTokenList(2) ["pizza", "hotdog", value: "pizza hotdog"]
main.classList === _classList; // => true

method

  • add( className ): クラスを追加
  • remove( className ): クラスを削除
  • toggle( className ): クラスがあれば削除・無ければ追加
  • contains( className ): クラス名の有無を true / false で返す
  • replace( oldClass, newClass ): oldClassをnewClassで置き換え

classList.add

add( String [, String [, ...]] )
クラスの追加、,区切りで複数のクラスを一度に追加することもできる
既に存在するクラスは無視される
返り値はundefinedなのでメソッドチェーンはできない

// HTML <div id="main"></div>
const main = document.getElementById('main');
main.classList.add('bold');
// => <div id="main" class="bold"></div>
main.classList.add('text-info', 'small');
// => <div id="main" class="bold text-info small"></div>
console.log( main.classList.add('foo') ); // undefined

,区切りでクラス名を追加できるので、クラス名の配列をスプレッド演算子で展開して渡して追加とかもできる

// HTML <div id="main"></div>
const main = document.getElementById('main');
const classArg = ['pizza', 'hotdog', 'gohan'];
main.classList.add(...classArg);
// => <div id="main" class="pizza hotdog gohan"></div>

classList.remove

remove( String [, String [, ...]] )
クラスの削除、,区切りで複数のクラスを一度に削除もできる
存在しないクラスを削除しようとした場合もエラーにはならない
返り値はundefinedなのでメソッドチェーンはできない

// HTML <div id="main" class="bold text-info small"></div>
const main = document.getElementById('main');
main.classList.remove('text-info');
// => <div id="main" class="bold small"></div>
main.classList.remove('bold', 'small');
// => <div id="main"></div>
main.classList.remove('text-warn');
// => エラーにはならない

classList.contains

contains( String )
クラスが存在しているかどうかをtrue / falseで返す
jQueryで言うところの$elm.hasClass()

// HTML <div id="main" class="error"></div>
const main = document.getElementById('main');
main.classList.contains('error'); // => true
main.classList.contains('success'); // => false

add, removeのように複数のクラス名を渡してもエラーにはならないが、第一引数以外は無視され第一引数での有無の判定結果が返る

main.classList.contains('error', 'foo');
// => 第一引数の error はあるので true
main.classList.contains('foo', 'error');
// => 第一引数の foo はないので false

classList.toggle

toggle( String [, force] )
クラスが存在していれば削除(remove)してfalseを返す・クラスがなければ追加(add)してtrueを返す
第二引数にtrueを渡すと常に追加(add)、falseを渡すと常に削除(remove)
jQuery$elm.toggleClass(name, switch)と同じ感じ

// HTML: <div id="main"></main>
const main = document.getElementById('main');
main.classList.toggle('is_show'); // true
// => <div id="main" class="is_show"></main>
main.classList.toggle('is_show'); // false
// => <div id="main"></main>

main.classList.toggle('required', true); // true
// => <div id="main" class="required"></main>
main.classList.toggle('bold', false); // false
// => <div id="main" class="required"></main>

第二引数はforceなので、add, removeのように複数のクラス名をtoggleさせることはできない

// HTML: <div id="main" class="foo"></main>
const main = document.getElementById('main');
main.classList.toggle('bar', 'foo');
// => <div id="main" class="foo bar"></main>
// * main.classList.toggle('bar', true); と解釈される

スペースが入っているクラス名だとエラーになる

// HTML: <div id="main" class="foo"></main>
const main = document.getElementById('main');
main.classList.toggle("foo bar");
// => Error 
// Uncaught DOMException: Failed to execute 'toggle' on 'DOMTokenList': The token provided ('foo bar') contains HTML space characters, which are not valid in tokens.

\t, \nでも同様にエラーになったが、\s, \d, =, &のようなクラス名としてアレな値が入っていてもエラーにはならなかったので、渡された値がクラス名として成立しているかどうかはチェックしていないっぽい

classList.replace

replace( oldClass, newClass )
第一引数のクラスを、第二引数のクラスに置き換える
置き換えられるoldClassがDOMに無ければ無視される
置き換えるクラス(newClass)が既に存在する場合、置き換えられるoldClassが消えるだけ

// HTML: <div id="main" class="pizza hotdog gohan"></main>
const main = document.getElementById('main');
main.classList.replace('gohan', 'beef');
// main.className => "pizza hotdog beef"
main.classList.replace('fish', 'chiken');
// main.className => "pizza hotdog beef"
main.classList.replace('hotdog', 'pizza');
// main.className => "pizza beef" 

classList.toggleと同様にスペースの入っている値を渡すとエラーになる

// HTML: <div id="main" class="pizza beef"></main>
const main = document.getElementById('main');
main.classList.replace('pizza beef', 'okome');
// => Error
// Uncaught DOMException: Failed to execute 'replace' on 'DOMTokenList': The token provided ('pizza beef') contains HTML space characters, which are not valid in tokens.

// 置き換える側も同様
main.classList.replace('pizza', 'okome misosoup');
// => Error
// Uncaught DOMException: Failed to execute 'replace' on 'DOMTokenList': The token provided ('okome misosoup') contains HTML space characters, which are not valid in tokens.

正規表現はそもそも受け付けていないのでNG

main.classList.replace(\pizza\, 'misosoup');
// => Error
// Uncaught SyntaxError: Invalid or unexpected token

Element.classList - Web API | MDN ここではclassList.replaceSafari未実装と表示表示されているけど、Can I useではグリーンになっていて、Safari 12.0.3 では問題なく動作していたので使えるっぽい

内部的にclassName.replace(oldClass, newClass)で置き換えているのかと思ったのだけれど、スペースを含むとエラーになるのでどうやら違うっぽい。内部でイテレーター回すとかしてるならclassName.replace()の方が高速な気がする...

// HTML: <div id="main" class="pizza beef"></main>
const main = document.getElementById('main');
 main.className = main.className.replace('pizza beef', 'okome misosoup');
// => <div id="main" class="okome misosoup"></main>

classList.item

item( index )
引数で渡したインデックスのクラス名を返す
存在しないインデックスを指定するとnullが返る

// HTML: <div id="main" class="pizza hotdog"></div>
const main = document.getElementById('main');
console.log( main.classList.item(1) ); // => "hotdog"
console.log( main.classList.item(-1) ); // => null

classList[n]で配列ようにアクセスするのとほぼ同じだが、clasList[n] で存在しないインデックスにアクセスした場合undefinedが返るので挙動が異なるので注意が必要

main.classList.item(100) === main.classList[100]; // => false
console.log( main.classList.item(100) ); // => null
console.log( main.classList[100] ); // => undefined

実装状況

classList (DOMTokenList)
Can I use... Support tables for HTML5, CSS3, etc

IEのサポートをしなくて良いなら問題なく使えそうです

感想

jQueryで行っていたクラス名の操作がほぼ同じ感覚でできるって印象です。(jQuery感覚でメソッドチェーンにするとエラーになりますが...
classList.replaceでのクラス名の置き換えはjQueryだと$elm.add().remove()のように追加・削除をメソッドチェーンしなければならなかったのが単一メソッドで書けるので良いです。 jQueryでの複数クラスの追加・削除はclassNameみたいにスペース区切りの1つの文字列を渡さなければならないので、classListでの操作の方が楽かもしれません。

e. g.

jQuery

$('#main').addClass('bold text-info');
$('#main').removeClass('bold text-info');

Element.className

const main = document.getElementById('main');
main.className += " bold text-info";
const classArg = main.className.split(' ').filter((val) => {
  return val !== 'bold' && val !== 'text-info';
});
main.className = classArg.join(' ');

 
ジャパニランドだとまだまだ使われてる、IEはサポートしてないメソッドも多いので、IEをサポート不要で同じDOMでその後jQueryオブジェクトを使う必然性がなければクラス属性の操作はclassListで事足りそうです。

jQueryとの比較で感想を書いているのは、Vue.jsとかReactのプロジェクトだとクラス名の操作はフレームワークで行うでしょうからclassListの出番はjQuery使ってるwebサイト制作くらいな気がしているから…


[参考]

リスト:超絶技巧練習曲

リスト:超絶技巧練習曲

  • アーティスト: ボレット(ホルヘ),リスト
  • 出版社/メーカー: ユニバーサル ミュージック クラシック
  • 発売日: 2009/05/20
  • メディア: CD
  • この商品を含むブログを見る

JavaScript クッキー操作できるjs-cookie使ってみた

インストール

# 1. ダウンロードして使う

GitHub からスクリプトをダウンロードしてスクリプトタグで読み込む

<script src="/path/to/js.cookie.js"></script>
# 2. CDNを使う
<script src="https://cdn.jsdelivr.net/npm/js-cookie@2/src/js.cookie.min.js"></script>
# 3. NPMでインストール

npm

$ npm install js-cookie --save

yarn

$ yarn add js-cookie

読み込み

import Cookies from 'js-cookie';

使い方

Cookieを保存

Cookies.set('cookie_name', 'value')

Cookies.set('idol', '星宮いちご');

期限付きのCookieを保存
Cookies.set('cookie_name', 'value', { expires: 1 })
オプションにexpiresキーワードで保存。期限のexpiresの単位は「日数
e.g. 1週間なら { expires: 7 } 12時間なら { expires: 1/2 }

Cookies.set('one_day_idol', '紫吹蘭', {expires: 1});

パスを指定してCookieを保存
Cookies.set('cookie_name', 'value', { path: '' }) オプションにpathキーワードでパスを渡す。パスの指定があると、そのパスは以下でのみCookieが有効になる。{path: ''}とすると現在のページ配下で有効

Cookies.set('current_idol', '霧矢あおい', {path: ''});
Cookieを取得

Cookies.get('cookie_name')

const idol = Cookies.get('idol');
console.log(idol); // => 星宮いちご

Cookieが存在しない場合はundefinedを返す

const idol = Cookies.get('legend_idol');
console.log(idol); //=> undefined

Cookie名を指定しないCookies.get() で保存されているCookie{name, value}形式のオブジェクトで取得できる ※ 取得できるものだけ

Cookies.get();
// => {idol: '星宮いちご', ,current_idol: '霧矢あおい', one_day_idol: '紫吹蘭'}
Cookieの削除

Cookie.remove('cookie_name')

Cookies.set('idle', '星宮いちご');
Cookies.remove('idle');
console.log( Cookies.get('idle') ); // => undefined

パス付きのCookieはパスが合ってないと削除できない。※ 有効なパスに居るればパス指定なしでも削除できるっぽい

Cookies.set('current_idol', '霧矢あおい', { path: '/cool' });
Cookies.remove('current_idol');
console.log( Cookies.get('current_idol') ); // => 霧矢あおい
Cookies.remove('current_idol', { path: '/cool' });
console.log( Cookies.get('current_idol') ); // => undefined

 
簡単にCookieを操作することができました!

Cookieの有効期限をあまり短い期間で指定することって無いのかもだけど、js-cookieは日数がデフォルトなので1日以下にしたい時、1時間なら1/24 = 0.0416666....、30分なら1/( 24 * 2 ) = 0.0208333...という具合に値が少数になってしまいます。
直接値を指定するなら近い数字にすれば良いと思いますが、小数点を含む計算式だと誤差(IEEE754)が出る可能性もありますので注意が必要かなーって思いました。
Floot値にもなるから個人的には、殆どの場合intで指定できるPHPsetcookieみたいに秒数指定の方が良かったのではないか説。


[参考]

クッキー柄のクッション…思ったより無かった…