今回のお題は、ボタンをすべて押したらつぎの処理に進む、というものです。組み立て方はいろいろ考えられましょう。ここで試すのは、配列とビット演算、それに Promise
オブジェクトを用いたやり方です。そのとき、ECMAScript 2015(ES6)以降の新しめの構文も織り交ぜてゆきます。
querySelectorAll()でボタンのリストを得る
<button>
要素は3つ、つぎのようにid
属性を定めた<div>
要素に入れます。
<div id="buttons">
<button type="button">button 1</button>
<button type="button">button 2</button>
<button type="button">button 3</button>
</div>
3つの<button>
要素は、querySelectorAll()
メソッドで得られます。戻り値は、NodeList
オブジェクトです。配列のように角かっこ[]
にインデックスを与えた構文で要素が取り出せます。また、要素数を調べるのはNodeList.length
プロパティです。
Living StandardにはNodeList.prototype.forEach()
メソッドが備わっています。このメソッドで3つのボタンにクリックイベントのリスナー関数を定めるのが次のコードです。ボタンを押すと、コンソールにそれぞれのインデックス番号が示されます。
function init() {
const buttons = [...document.querySelectorAll('#buttons button')];
buttons.forEach((button, id) => {
button.addEventListener('click', (event) => {
console.log(id);
});
});
}
document.addEventListener('DOMContentLoaded', init);
もっとも、NodeList
オブジェクトはArray
クラスと同じメソッドを備えているわけではありません。たとえば、ECMAScript 5.1のArray.every()
メソッドが使いたいとき、これまでの構文ではFunction.call()
メソッドを呼び出さなければなりませんでした。
const buttons = document.querySelectorAll('#buttons button');
Array.prototype.every.call(buttons, function(button, id) {
// 要素ごとの処理
});
ArrayのようなオブジェクトにArray.every()メソッドを使う
3つのボタンがすべて押されたら処理をするお題に進みましょう。そのとき使いたいのがArray.every()
メソッドです。引数のコールバック関数がすべてtrue
を返したときのみ、メソッドはtrue
を返します(他の場合はfalse
)。
ECMAScript 2015(ES6)に備わったArray.from()
メソッドを使えば、Array
のような(array-like)オブジェクトをArray
に変えられます。さらにお手軽なのがスプレッド構文です。配列をカンマ区切りの引数として扱えるので、Array
リテラルの角かっこ[]
に入れれば新たな配列がつくれます。
ボタンがクリックされたら、リスナー関数で自分のオブジェクトのプロパティ(clicked)に値true
を与えることにしましょう。すると、ボタンの配列(buttons)に対してArray.every()
メソッドを呼び出して、すべてクリックされたかどうか調べられます。
function init() {
const buttons = [...document.querySelectorAll('#buttons button')];
buttons.forEach((button, id) => {
button.addEventListener('click', (event) => {
button.clicked = true;
const allDone = buttons.every((button) => button.clicked);
if (allDone) {
console.log('completed');
}
});
});
}
ビット演算でフラグを扱う
ボタンを押したかどうかは2値です。これを0/1
で扱うことにすると、ビット演算が使えます(「2進数・16進数とビット演算」参照)。はじめの値は2進数00000
として、各桁をボタンに割り振るのです(2進数数値構文はECMAScript 2015で採り入れられました)。クリックされたらその桁の値を1
にすれば、0b111
つまり10進数7
になったとき、ボタンがすべて押されたことになります。
ビット演算を用いると、前項のコードはつぎのように書き替えられます。実は、forEach()
メソッドは、ECMAScript 2015では配列のようなオブジェクトにも備わっているのです。また、べき乗演算子**
はECMAScript 2016で加わりました。なお、ビット演算の結果がわかるように、押したボタンと3つのボタンの2進数の値をコンソールに示しています。
function init() {
// const buttons = [...document.querySelectorAll('#buttons button')];
const buttons = document.querySelectorAll('#buttons button');
let allDone = 0;
buttons.forEach((button, id) => {
// button.id = id;
button.addEventListener('click', (event) => {
// button.clicked = true;
// const allDone = buttons.every((button) => button.clicked;
allDone |= 1 << id; // button.id;
console.log('button', (1 << id).toString(2)); // 押したボタンの値
console.log('allDone', allDone.toString(2)); // 3つのボタンの値
if (allDone >= 2 ** buttons.length - 1) {
console.log('completed');
}
});
});
}
jsdo.itにサンプルを掲げました。
サンプル
Promise.all()メソッドを使う
ECMAScript 2015に備わったPromiseは非同期の処理を扱うためのオブジェクトです。処理が成功したか、失敗したかによるコールバックがそれぞれ定められます。Promiseオブジェクトの基本的な使い方については「ES6: Promiseオブジェクトを使う」をお読みください。
click
イベントのリスナー関数が定められたボタンひとつをPromiseで扱うコードはつぎのとおりです。
const button = document.querySelector('#buttons button');
const buttonPromise = new Promise((resolve) =>
button.addEventListener('click', () => resolve())
);
buttonPromise.then(() => console.log('clicked'));
さらに、Promise.all()
メソッドを使えば、複数のPromise
がすべて完了したとき、Promise.prototype.then()
メソッドを呼び出せます。つまり、3つのボタンのPromise
オブジェクトをこのメソッドに渡せば、すべてのボタンが押されたときの処理が定められます。
function init() {
const buttonPromises = [...document.querySelectorAll('#buttons button')]
.map((button) =>
new Promise((resolve) =>
button.addEventListener('click', () => resolve())
)
);
Promise.all(buttonPromises)
.then(() => {
console.log('completed');
});
}
なお、3つのボタンを押せばPromise
がすべて完了しますので、それ以降のクリックはコールバックを呼び出しません。
[追記: 2018年10月1日] culageさんのコメントにもとづいて「Promise.all()メソッドを使う」を加えました。