Commit 40f238c6 authored by Caleb C. Sander's avatar Caleb C. Sander
Browse files

Use addEventListener() for callbacks

parent c60c76cf
No related merge requests found
Showing with 58 additions and 37 deletions
+58 -37
...@@ -18,11 +18,11 @@ Some examples of things webpages often do in JavaScript: ...@@ -18,11 +18,11 @@ Some examples of things webpages often do in JavaScript:
<script> <script>
// We use the `id` property of the button to access it from JS // We use the `id` property of the button to access it from JS
const button = document.getElementById('alert-button') const button = document.getElementById('alert-button')
button.onclick = () => { button.addEventListener('click', () => {
// You could put any other code you wanted here // You could put any other code you wanted here
// and it would run each time the button is clicked // and it would run each time the button is clicked
alert('Button was clicked') alert('Button was clicked')
} })
</script> </script>
</body> </body>
</html> </html>
...@@ -36,8 +36,8 @@ Each time the button is clicked, it displays a message saying that the button wa ...@@ -36,8 +36,8 @@ Each time the button is clicked, it displays a message saying that the button wa
Let's tease apart the code responsible for making button clicks display the message. Let's tease apart the code responsible for making button clicks display the message.
First, note that `() => alert('Button was clicked')` is a *function* in JS. First, note that `() => alert('Button was clicked')` is a *function* in JS.
When this function is called, it causes the message `Button was clicked` to appear. When this function is called, it causes the message `Button was clicked` to appear.
Then `button.onclick = ...` tells the browser "this is the function I want to attach to the click action on `button`. Then `button.addEventListener('click', ...)` tells the browser "I want the click action on `button` to call this function".
When a user interacts with the webpage, the browser figures out whether the button was clicked, and if so, it runs the function stored in `button.onclick`. When a user interacts with the webpage, the browser figures out whether the button was clicked, and if so, it runs the "click handler" function.
## How doesn't this work? ## How doesn't this work?
...@@ -65,7 +65,7 @@ So why doesn't JS use an interface like one of these? ...@@ -65,7 +65,7 @@ So why doesn't JS use an interface like one of these?
2. This approach solves the issue of not being able to respond to multiple events: we could just add an `if (button2.wasClicked())` inside the loop. 2. This approach solves the issue of not being able to respond to multiple events: we could just add an `if (button2.wasClicked())` inside the loop.
However, it has a more subtle problem. However, it has a more subtle problem.
The loop may run billions of times before the user actually clicks the button. The loop may run billions of times before the user actually clicks the button.
Since our program can't actually do anything until the user finally clicks the button, this is an incredible waste of the processor's time. Since our program won't actually do anything until the user finally clicks the button, this is a huge waste of the processor's time.
We see that the "event handler" approach used by JS has two main benefits: it can easily set up handlers for thousands of events at once, and no JS code runs while we wait for an event to occur. We see that the "event handler" approach used by JS has two main benefits: it can easily set up handlers for thousands of events at once, and no JS code runs while we wait for an event to occur.
...@@ -79,14 +79,16 @@ In [this example](lock1.html), we make a combination lock with 4 digits and want ...@@ -79,14 +79,16 @@ In [this example](lock1.html), we make a combination lock with 4 digits and want
function makeLockDigit(changeCallback) { function makeLockDigit(changeCallback) {
// ... (the full code is at the link above) // ... (the full code is at the link above)
digit.onchange = () => changeCallback(Number(digit.value)) digit.addEventListener('change', () => {
changeCallback(Number(digit.value))
})
} }
// The super secret combination // The super secret combination
const COMBO = [1, 2, 3, 4] const COMBO = [1, 2, 3, 4]
// Whether each digit is correct // Whether each digit is correct
const digitCorrect = [false, false, false, false] const digitCorrect = [false, false, false, false]
for (let i = 0; i < COMBO.length; i++) { for (let i = 0; i < 4; i++) {
makeLockDigit(value => { makeLockDigit(value => {
// Digit i has changed, so store whether it is correct // Digit i has changed, so store whether it is correct
digitCorrect[i] = (value === COMBO[i]) digitCorrect[i] = (value === COMBO[i])
...@@ -105,7 +107,7 @@ There are other ways to store the state of the digits; for example, we could ins ...@@ -105,7 +107,7 @@ There are other ways to store the state of the digits; for example, we could ins
```js ```js
// The number of correct digits // The number of correct digits
let correctCount = 0 let correctCount = 0
for (let i = 0; i < COMBO.length; i++) { for (let i = 0; i < 4; i++) {
// Whether this digit was previously correct // Whether this digit was previously correct
let wasCorrect = false let wasCorrect = false
makeLockDigit(value => { makeLockDigit(value => {
...@@ -159,6 +161,7 @@ In general, this requires us to create the next asynchronous event *inside* the ...@@ -159,6 +161,7 @@ In general, this requires us to create the next asynchronous event *inside* the
As a concrete example, let's simulate a "random walk" on a grid. As a concrete example, let's simulate a "random walk" on a grid.
At each step, we randomly move left, right, up, or down, and we count the total number of times we have visited each grid square. At each step, we randomly move left, right, up, or down, and we count the total number of times we have visited each grid square.
To run an asynchronous action after another one finshes, we *nest it inside the previous callback*.
Here is a [first attempt](walk1.html) that only moves 3 times. Here is a [first attempt](walk1.html) that only moves 3 times.
```js ```js
// ... (the full code is at the link above) // ... (the full code is at the link above)
......
...@@ -4,11 +4,11 @@ ...@@ -4,11 +4,11 @@
<script> <script>
// We use the `id` property of the button to access it from JS // We use the `id` property of the button to access it from JS
const button = document.getElementById('alert-button') const button = document.getElementById('alert-button')
button.onclick = () => { button.addEventListener('click', () => {
// You could put any other code you wanted here // You could put any other code you wanted here
// and it would run each time the button is clicked // and it would run each time the button is clicked
alert('Button was clicked') alert('Button was clicked')
} })
</script> </script>
</body> </body>
</html> </html>
...@@ -16,7 +16,9 @@ ...@@ -16,7 +16,9 @@
// When the digit changes, call `changeCallback()`, // When the digit changes, call `changeCallback()`,
// passing the new value of the digit. // passing the new value of the digit.
// Inputs' values are strings, so we convert them to numbers. // Inputs' values are strings, so we convert them to numbers.
digit.onchange = () => changeCallback(Number(digit.value)) digit.addEventListener('change', () => {
changeCallback(Number(digit.value))
})
// Add the digit to the lock // Add the digit to the lock
lock.appendChild(digit) lock.appendChild(digit)
...@@ -26,7 +28,7 @@ ...@@ -26,7 +28,7 @@
const COMBO = [1, 2, 3, 4] const COMBO = [1, 2, 3, 4]
// Whether each digit is correct // Whether each digit is correct
const digitCorrect = [false, false, false, false] const digitCorrect = [false, false, false, false]
for (let i = 0; i < COMBO.length; i++) { for (let i = 0; i < 4; i++) {
makeLockDigit(value => { makeLockDigit(value => {
// Digit i has changed, so store whether it is correct // Digit i has changed, so store whether it is correct
digitCorrect[i] = (value === COMBO[i]) digitCorrect[i] = (value === COMBO[i])
......
...@@ -16,7 +16,9 @@ ...@@ -16,7 +16,9 @@
// When the digit changes, call `changeCallback()`, // When the digit changes, call `changeCallback()`,
// passing the new value of the digit. // passing the new value of the digit.
// Inputs' values are strings, so we convert them to numbers. // Inputs' values are strings, so we convert them to numbers.
digit.onchange = () => changeCallback(Number(digit.value)) digit.addEventListener('change', () => {
changeCallback(Number(digit.value))
})
// Add the digit to the lock // Add the digit to the lock
lock.appendChild(digit) lock.appendChild(digit)
...@@ -26,7 +28,7 @@ ...@@ -26,7 +28,7 @@
const COMBO = [1, 2, 3, 4] const COMBO = [1, 2, 3, 4]
// The number of correct digits // The number of correct digits
let correctCount = 0 let correctCount = 0
for (let i = 0; i < COMBO.length; i++) { for (let i = 0; i < 4; i++) {
// Whether this digit was previously correct // Whether this digit was previously correct
let wasCorrect = false let wasCorrect = false
makeLockDigit(value => { makeLockDigit(value => {
......
...@@ -27,10 +27,10 @@ ...@@ -27,10 +27,10 @@
// Every time `<button id='add'>` is clicked, // Every time `<button id='add'>` is clicked,
// add the number in `<input id='number'>` to the sum // add the number in `<input id='number'>` to the sum
document.getElementById('add').onclick = () => { document.getElementById('add').addEventListener('click', () => {
sum += Number(numberInput.value) sum += Number(numberInput.value)
showSum() showSum()
} })
</script> </script>
</body> </body>
</html> </html>
...@@ -31,6 +31,7 @@ There are two main reasons: ...@@ -31,6 +31,7 @@ There are two main reasons:
In Node.js, an asynchronous task that produces chunks of data is represented as a "readable stream". In Node.js, an asynchronous task that produces chunks of data is represented as a "readable stream".
To process a readable stream, we need to specify the callback function that will be used to process each chunk of data. To process a readable stream, we need to specify the callback function that will be used to process each chunk of data.
We can also register a callback to run when the stream finishes emitting data. We can also register a callback to run when the stream finishes emitting data.
In Node.js, the `.on()` method for "event emitters" works similarly to the `.addEventListener()` method for DOM elements in the browser.
As we saw above, reading a file is a prime example of a stream. As we saw above, reading a file is a prime example of a stream.
Indeed, the `fs` module provides a function [`fs.createReadStream()`](https://nodejs.org/api/fs.html#fs_fs_createreadstream_path_options) to make a readable stream for a given file. Indeed, the `fs` module provides a function [`fs.createReadStream()`](https://nodejs.org/api/fs.html#fs_fs_createreadstream_path_options) to make a readable stream for a given file.
......
...@@ -57,12 +57,12 @@ ...@@ -57,12 +57,12 @@
for (let col = 0; col < GRID_WIDTH; col++) { for (let col = 0; col < GRID_WIDTH; col++) {
const cell = document.createElement('td') const cell = document.createElement('td')
// Handles the click of a click-and-drag // Handles the click of a click-and-drag
cell.onmousedown = () => { cell.addEventListener('mousedown', () => {
startRow = row startRow = row
startCol = col startCol = col
} })
// Handles the release of a click-and-drag // Handles the release of a click-and-drag
cell.onmouseup = () => sendRectangle(row, col) cell.addEventListener('mouseup', () => sendRectangle(row, col))
gridRow.append(cell) gridRow.append(cell)
} }
grid.append(gridRow) grid.append(gridRow)
...@@ -84,22 +84,24 @@ ...@@ -84,22 +84,24 @@
// We need to run code that accesses HTML elements after // We need to run code that accesses HTML elements after
// the rest of the HTML is parsed (signaled by the `load` event) // the rest of the HTML is parsed (signaled by the `load` event)
window.onload = () => { window.addEventListener('load', () => {
// Retrieve HTML elements // Retrieve HTML elements
colorInput = document.getElementById('color') colorInput = document.getElementById('color')
grid = document.getElementById('grid-canvas') grid = document.getElementById('grid-canvas')
colorInput.onchange = sendColor colorInput.addEventListener('change', sendColor)
makeGrid() makeGrid()
// Create a WebSocket connection // Create a WebSocket connection
socket = new WebSocket('ws://localhost') socket = new WebSocket('ws://localhost')
// As soon as socket opens, send the initial color (black) // As soon as socket opens, send the initial color (black)
socket.onopen = sendColor socket.addEventListener('open', sendColor)
// Handles `draw` messages from the server. // Handles `draw` messages from the server.
// `event.data` is the JSON string received. // `event.data` is the JSON string received.
socket.onmessage = event => draw(JSON.parse(event.data)) socket.addEventListener('message', event => {
} draw(JSON.parse(event.data))
})
})
</script> </script>
</head> </head>
<body> <body>
......
...@@ -88,9 +88,9 @@ function sendColor() { ...@@ -88,9 +88,9 @@ function sendColor() {
} }
// When the socket opens, call `sendColor()` // When the socket opens, call `sendColor()`
socket.onopen = sendColor socket.addEventListener('open', sendColor)
// When `colorInput`'s value changes, call `sendColor()` // When `colorInput`'s value changes, call `sendColor()`
colorInput.onchange = sendColor colorInput.addEventListener('change', sendColor)
``` ```
Rectangles can be drawn by clicking and dragging, and this should send a `draw` message. Rectangles can be drawn by clicking and dragging, and this should send a `draw` message.
...@@ -104,11 +104,11 @@ let startCol ...@@ -104,11 +104,11 @@ let startCol
for (let row = 0; row < GRID_HEIGHT; row++) { for (let row = 0; row < GRID_HEIGHT; row++) {
for (let col = 0; col < GRID_WIDTH; col++) { for (let col = 0; col < GRID_WIDTH; col++) {
const cell = document.createElement('td') const cell = document.createElement('td')
cell.onmousedown = () => { cell.addEventListener('mousedown', () => {
startRow = row startRow = row
startCol = col startCol = col
} })
cell.onmouseup = () => sendRectangle(row, col) cell.addEventListener('mouseup', () => sendRectangle(row, col))
} }
} }
...@@ -131,7 +131,9 @@ This requires setting the `message` event handler on the socket. ...@@ -131,7 +131,9 @@ This requires setting the `message` event handler on the socket.
```js ```js
// Handles `draw` messages from the server. // Handles `draw` messages from the server.
// `event.data` is the JSON string received. // `event.data` is the JSON string received.
socket.onmessage = event => draw(JSON.parse(event.data)) socket.addEventListener('message', event => {
draw(JSON.parse(event.data))
})
// The curly braces around the arguments are shorthand for // The curly braces around the arguments are shorthand for
// extracting these fields from the message object // extracting these fields from the message object
......
...@@ -34,12 +34,12 @@ There is a small bit of additional CSS in `style.css`. ...@@ -34,12 +34,12 @@ There is a small bit of additional CSS in `style.css`.
One change is that `client.js` runs *before* the elements are defined, which is typical for webpages. One change is that `client.js` runs *before* the elements are defined, which is typical for webpages.
This means, for example, that `document.getElementById('login')` won't work until the rest of the page is loaded. This means, for example, that `document.getElementById('login')` won't work until the rest of the page is loaded.
Any code that depends on page elements should be put inside the `window.onload` callback, e.g.: Any code that depends on page elements should run as a handler for the window's `load` event, e.g.:
```js ```js
window.onload = () => { window.addEventListener('load', () => {
const loginButton = document.getElementById('login') const loginButton = document.getElementById('login')
// ... // ...
} })
``` ```
## Requirements ## Requirements
......
...@@ -42,6 +42,7 @@ Your game should have the following features, which are shown in the video linke ...@@ -42,6 +42,7 @@ Your game should have the following features, which are shown in the video linke
- Clicking on a square without a mine displays the number of mines in the 8 neighboring squares (up, down, left, right, and diagonally) - Clicking on a square without a mine displays the number of mines in the 8 neighboring squares (up, down, left, right, and diagonally)
- Clicking on a square with 0 neighboring mines automatically reveals the numbers in the neighboring squares. - Clicking on a square with 0 neighboring mines automatically reveals the numbers in the neighboring squares.
You should add a small time delay before revealing the neighbors to simulate a ripple effect across large regions of 0s. You should add a small time delay before revealing the neighbors to simulate a ripple effect across large regions of 0s.
To verify that your ripple visits the squares efficiently, make sure it looks smooth even on large grids, e.g. 30 x 30 with 10 mines!
- Clicking on a square with a mine ends the game and displays a loss message (you can use `alert()` for this). - Clicking on a square with a mine ends the game and displays a loss message (you can use `alert()` for this).
Clicking and flagging are disabled when the game is over. Clicking and flagging are disabled when the game is over.
- Revealing all the non-mine squares ends the game and displays a win message - Revealing all the non-mine squares ends the game and displays a win message
...@@ -76,12 +77,20 @@ Here is a minimal subset of APIs you will likely find useful for this project: ...@@ -76,12 +77,20 @@ Here is a minimal subset of APIs you will likely find useful for this project:
- `element.appendChild(child)` adds `child` as the last child element of `element` - `element.appendChild(child)` adds `child` as the last child element of `element`
- `element.remove()` removes `element` from its parent - `element.remove()` removes `element` from its parent
- `element.classList` represents the set of classes that `element` has. - `element.classList` represents the set of classes that `element` has.
(In HTML, classes are strings used to identify groups of elements in JavaScript or CSS.
Each element can belong to any number of classes.)
`classList.add(className)`, `classList.remove(className)`, `classList.toggle(className)`, and `classList.contains(className)` do what you expect. `classList.add(className)`, `classList.remove(className)`, `classList.toggle(className)`, and `classList.contains(className)` do what you expect.
- **Event handlers** - **Event handlers**
- `element.onclick` is the function to call when `element` gets clicked. - `element.addEventListener('click', handler)` registers the function `handler` to be called whenever `element` is clicked.
You can set a click handler by setting `element.onclick` to a handler function, and you can also simulate a click by calling `element.onclick()`. For example, `element.addEventListener('click', () => alert('clicked'))` would display the message "clicked" when `element` is clicked.
- `element.oncontextmenu` is similar, but for right-clicks instead. - `element.addEventListener('contextmenu', handler)` is similar, but for right-clicks instead.
You can prevent the normal right-click menu from showing by returning `false` from the event handler. You can prevent the normal right-click menu from showing by calling `event.preventDefault()`:
```js
element.addEventListener('contextmenu', event => {
event.preventDefault()
// Handler code...
})
```
It is also possible to attach data to HTML elements using DOM APIs, but they are tricky to use. It is also possible to attach data to HTML elements using DOM APIs, but they are tricky to use.
Instead, it is simpler to store the state of the grid using JavaScript objects and arrays. Instead, it is simpler to store the state of the grid using JavaScript objects and arrays.
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment