LoginSignup
0
1

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

Last updated at Posted at 2018-09-11

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

サンプル

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

0
1
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1