Edited at

JavaScript: ボタンをすべて押したらつぎに進む ー ECMAScriptの新しめの構文を織り交ぜて

今回のお題は、ボタンをすべて押したらつぎの処理に進む、というものです。組み立て方はいろいろ考えられましょう。ここで試すのは、配列とビット演算、それに 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進数0b000として、各桁をボタンに割り振るのです(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にサンプルを掲げました。


サンプル

three_buttons_.png

>> judo.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()メソッドを使う」を加えました。