今回のお題は、ボタンをすべて押したらつぎの処理に進む、というものです。組み立て方はいろいろ考えられましょう。ここで試すのは、配列とビット演算、それに 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()メソッドを使う」を加えました。
