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

Use addEventListener() for callbacks

parent c60c76cf
Showing with 58 additions and 37 deletions
+58 -37
......@@ -18,11 +18,11 @@ Some examples of things webpages often do in JavaScript:
<script>
// We use the `id` property of the button to access it from JS
const button = document.getElementById('alert-button')
button.onclick = () => {
button.addEventListener('click', () => {
// You could put any other code you wanted here
// and it would run each time the button is clicked
alert('Button was clicked')
}
})
</script>
</body>
</html>
......@@ -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.
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.
Then `button.onclick = ...` tells the browser "this is the function I want to attach to the click action on `button`.
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`.
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 "click handler" function.
## How doesn't this work?
......@@ -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.
However, it has a more subtle problem.
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.
......@@ -79,14 +79,16 @@ In [this example](lock1.html), we make a combination lock with 4 digits and want
function makeLockDigit(changeCallback) {
// ... (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
const COMBO = [1, 2, 3, 4]
// Whether each digit is correct
const digitCorrect = [false, false, false, false]
for (let i = 0; i < COMBO.length; i++) {
for (let i = 0; i < 4; i++) {
makeLockDigit(value => {
// Digit i has changed, so store whether it is correct
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
```js
// The number of correct digits
let correctCount = 0
for (let i = 0; i < COMBO.length; i++) {
for (let i = 0; i < 4; i++) {
// Whether this digit was previously correct
let wasCorrect = false
makeLockDigit(value => {
......@@ -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.
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.
```js
// ... (the full code is at the link above)
......
......@@ -4,11 +4,11 @@
<script>
// We use the `id` property of the button to access it from JS
const button = document.getElementById('alert-button')
button.onclick = () => {
button.addEventListener('click', () => {
// You could put any other code you wanted here
// and it would run each time the button is clicked
alert('Button was clicked')
}
})
</script>
</body>
</html>
......@@ -16,7 +16,9 @@
// When the digit changes, call `changeCallback()`,
// passing the new value of the digit.
// 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
lock.appendChild(digit)
......@@ -26,7 +28,7 @@
const COMBO = [1, 2, 3, 4]
// Whether each digit is correct
const digitCorrect = [false, false, false, false]
for (let i = 0; i < COMBO.length; i++) {
for (let i = 0; i < 4; i++) {
makeLockDigit(value => {
// Digit i has changed, so store whether it is correct
digitCorrect[i] = (value === COMBO[i])
......
......@@ -16,7 +16,9 @@
// When the digit changes, call `changeCallback()`,
// passing the new value of the digit.
// 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
lock.appendChild(digit)
......@@ -26,7 +28,7 @@
const COMBO = [1, 2, 3, 4]
// The number of correct digits
let correctCount = 0
for (let i = 0; i < COMBO.length; i++) {
for (let i = 0; i < 4; i++) {
// Whether this digit was previously correct
let wasCorrect = false
makeLockDigit(value => {
......
......@@ -27,10 +27,10 @@
// Every time `<button id='add'>` is clicked,
// add the number in `<input id='number'>` to the sum
document.getElementById('add').onclick = () => {
document.getElementById('add').addEventListener('click', () => {
sum += Number(numberInput.value)
showSum()
}
})
</script>
</body>
</html>
......@@ -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".
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.
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.
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 @@
for (let col = 0; col < GRID_WIDTH; col++) {
const cell = document.createElement('td')
// Handles the click of a click-and-drag
cell.onmousedown = () => {
cell.addEventListener('mousedown', () => {
startRow = row
startCol = col
}
})
// Handles the release of a click-and-drag
cell.onmouseup = () => sendRectangle(row, col)
cell.addEventListener('mouseup', () => sendRectangle(row, col))
gridRow.append(cell)
}
grid.append(gridRow)
......@@ -84,22 +84,24 @@
// We need to run code that accesses HTML elements after
// the rest of the HTML is parsed (signaled by the `load` event)
window.onload = () => {
window.addEventListener('load', () => {
// Retrieve HTML elements
colorInput = document.getElementById('color')
grid = document.getElementById('grid-canvas')
colorInput.onchange = sendColor
colorInput.addEventListener('change', sendColor)
makeGrid()
// Create a WebSocket connection
socket = new WebSocket('ws://localhost')
// As soon as socket opens, send the initial color (black)
socket.onopen = sendColor
socket.addEventListener('open', sendColor)
// Handles `draw` messages from the server.
// `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>
</head>
<body>
......
......@@ -88,9 +88,9 @@ function sendColor() {
}
// When the socket opens, call `sendColor()`
socket.onopen = sendColor
socket.addEventListener('open', 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.
......@@ -104,11 +104,11 @@ let startCol
for (let row = 0; row < GRID_HEIGHT; row++) {
for (let col = 0; col < GRID_WIDTH; col++) {
const cell = document.createElement('td')
cell.onmousedown = () => {
cell.addEventListener('mousedown', () => {
startRow = row
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.
```js
// Handles `draw` messages from the server.
// `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
// extracting these fields from the message object
......
......@@ -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.
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
window.onload = () => {
window.addEventListener('load', () => {
const loginButton = document.getElementById('login')
// ...
}
})
```
## Requirements
......
......@@ -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 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.
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 and flagging are disabled when the game is over.
- 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:
- `element.appendChild(child)` adds `child` as the last child element of `element`
- `element.remove()` removes `element` from its parent
- `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.
- **Event handlers**
- `element.onclick` is the function to call when `element` gets 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()`.
- `element.oncontextmenu` is similar, but for right-clicks instead.
You can prevent the normal right-click menu from showing by returning `false` from the event handler.
- `element.addEventListener('click', handler)` registers the function `handler` to be called whenever `element` is clicked.
For example, `element.addEventListener('click', () => alert('clicked'))` would display the message "clicked" when `element` is clicked.
- `element.addEventListener('contextmenu', handler)` is similar, but for right-clicks instead.
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.
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