JAVASCRIPT / THE BROWSER: DOM, EVENTS, AND STORAGE
Keyboard, mouse, and form events in practice
Wire keydown, mouse, and form events to real controls, read the right property off each event, and know why input, change, and submit fire when they do.
What you will learn
- Read e.key for the character produced and e.code for the physical key pressed.
- Use input for each keystroke, change on commit, and submit on the form element.
- Call preventDefault() in a submit handler, then read fields with new FormData(form).
- Expect keydown to run before the field's value has been updated.
Understanding Keyboard, mouse, and form events in practice
Browser input events arrive in two layers. Device events (keydown, keyup, mousedown, mouseup, mousemove) describe what the hardware did: this key went down, that button was pressed at these coordinates. Semantic events (input, change, click, submit) describe what the interaction meant, and they fire no matter how the user got there: a clipboard paste, autofill, dropped text, speech input, or Enter pressed in a text field. The working rule is to listen at the semantic layer and drop to the device layer only when you genuinely need the key or the button.
The keyboard layer has a fixed order for a printable keystroke: keydown fires, the default action inserts the character, input fires, then keyup. Reading the field's value inside keydown therefore shows the text as it was before the keystroke, and preventDefault() in keydown is what stops the character from being inserted at all, since by keyup the edit has already happened. Each keydown carries e.key, the character the layout produces ('a', 'A', 'Enter', 'ArrowLeft'), and e.code, the physical key position ('KeyA', 'Enter'), alongside e.ctrlKey, e.metaKey, e.shiftKey, e.altKey and e.repeat for auto-repeat while a key is held.
Mouse events carry two different button fields: e.button names the button that caused this event (0 primary, 1 middle, 2 secondary), while e.buttons is a bitmask of the buttons held down during it, which is why buttons is 0 in a mouseup. click only ever comes from the primary button; a right-click surfaces as contextmenu instead. Forms work the same way one level up: submit fires on the form, not on the button, because submission is a form-level action that can start from a click on a submit button, Enter in a text field, or form.requestSubmit(). Binding click to the button covers only the first case, so bind submit to the form, call e.preventDefault() to cancel the navigation, and read the named controls with new FormData(form).
placeholder
const form = document.createElement('form');
form.innerHTML = '<input name="city"><button type="submit">Go</button>';
document.body.append(form);
const city = form.elements.city;
city.addEventListener('keydown', (e) => {
console.log('keydown key=' + e.key + ' code=' + e.code + ' value="' + city.value + '"');
});
city.addEventListener('input', () => {
console.log('input value="' + city.value + '"');
});
form.addEventListener('submit', (e) => {
e.preventDefault();
console.log('submit city=' + new FormData(form).get('city') + ' reload prevented=' + e.defaultPrevented);
});
// Replay one real keystroke in the order the browser produces it.
city.dispatchEvent(new KeyboardEvent('keydown', { key: 'O', code: 'KeyO', shiftKey: true }));
city.value = 'O';
city.dispatchEvent(new Event('input'));
form.dispatchEvent(new Event('submit', { cancelable: true }));Browsers report input twice, as low-level device events describing what was pressed and high-level semantic events describing what it meant, and correct code usually listens to the semantic layer.
Worked examples
Enter to send, Shift+Enter for a newline
Separates two meanings of the same physical key and uses e.code for a layout-independent shortcut.
const box = document.createElement('textarea');
document.body.append(box);
box.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
console.log('send prevented=' + e.defaultPrevented);
} else if (e.key === 'Enter') {
console.log('newline kept');
}
if ((e.ctrlKey || e.metaKey) && e.code === 'KeyS') {
e.preventDefault();
console.log('save prevented=' + e.defaultPrevented);
}
});
const press = (init) =>
box.dispatchEvent(new KeyboardEvent('keydown', { cancelable: true, ...init }));
press({ key: 'Enter', code: 'Enter' });
press({ key: 'Enter', code: 'Enter', shiftKey: true });
press({ key: 's', code: 'KeyS', ctrlKey: true });Example explained
Line 1cancelable: true is required here, because preventDefault() on a non-cancelable event does nothing and defaultPrevented would stay false.
Line 2e.key === 'Enter' && !e.shiftKey splits send from newline, and preventDefault() is what keeps the textarea from inserting the line break.
Line 3e.code === 'KeyS' matches the physical key, so the shortcut stays in the same spot on a layout where that key produces a different e.key.
Line 4The synthetic keydown runs the handler but types nothing: dispatched key events never edit a field's value.
Which button, and which buttons are held
Shows the difference between e.button and e.buttons, and where right-clicks actually land.
const pad = document.createElement('div');
pad.textContent = 'pad';
document.body.append(pad);
const show = (e) =>
console.log(e.type.padEnd(12) + 'button=' + e.button + ' buttons=' + e.buttons);
['mousedown', 'mouseup', 'click', 'contextmenu'].forEach((t) =>
pad.addEventListener(t, show)
);
pad.dispatchEvent(new MouseEvent('mousedown', { button: 0, buttons: 1 }));
pad.dispatchEvent(new MouseEvent('mouseup', { button: 0, buttons: 0 }));
pad.dispatchEvent(new MouseEvent('click', { button: 0, buttons: 0 }));
pad.dispatchEvent(new MouseEvent('mousedown', { button: 2, buttons: 2 }));
pad.dispatchEvent(new MouseEvent('contextmenu', { button: 2, buttons: 2 }));Example explained
Line 1e.button identifies the single button responsible for this event: 0 primary, 1 middle, 2 secondary.
Line 2e.buttons is a bitmask of what is held during the event, so it reads 1 while the primary button is down and 0 once it is released.
Line 3The secondary button produces no click event, which is why right-click handling belongs on contextmenu, where preventDefault() also suppresses the native menu.
Line 4A real click only happens when mousedown and mouseup land on the same element, so dragging off the element cancels it.
input versus change, and programmatic value writes
Demonstrates that assigning value fires nothing, and that a checkbox reports state through checked.
const q = document.createElement('input');
q.name = 'q';
const agree = document.createElement('input');
agree.type = 'checkbox';
agree.name = 'agree';
document.body.append(q, agree);
q.addEventListener('input', () => console.log('input -> "' + q.value + '"'));
q.addEventListener('change', () => console.log('change -> "' + q.value + '"'));
agree.addEventListener('change', (e) => console.log('checkbox checked=' + e.target.checked));
q.value = 'pizza';
console.log('after assignment value=' + q.value);
q.dispatchEvent(new Event('input'));
q.dispatchEvent(new Event('change'));
agree.checked = true;
agree.dispatchEvent(new Event('change'));Example explained
Line 1Assigning q.value updates the DOM but fires no event, because these events report user interaction rather than property writes.
Line 2input reports the value after every edit, so it is the hook for live search, counters, and validation as you type.
Line 3change on a text field waits for the value to be committed by blur or Enter, so it fires once per completed edit.
Line 4A checkbox keeps its state in checked, not value, so read e.target.checked in the handler.
Important notes
focus and blur do not bubble, so a single handler on a container has to use focusin and focusout instead.
Events you build with new Event() are untrusted: isTrusted is false, cancelable defaults to false so preventDefault() silently does nothing, and a dispatched keydown never inserts text, which is why the examples set value by hand.
Common mistakes
Reading input.value inside keydown: the value is still the pre-keystroke text, so the field always looks one character behind and a search box silently queries stale input.
Attaching click to the submit button instead of submit to the form: pressing Enter in a text field or calling requestSubmit() skips the handler entirely and the page submits normally.
Forgetting e.preventDefault() in a submit handler: the browser navigates, the console clears, and it looks as though the handler never ran.
Try it yourself
Change, predict, then run
On a blank page, build a form with one text input and log its trimmed value on every input event, clear the field when e.key === 'Escape' in keydown, and log the submitted FormData value on submit without letting the page reload.
Open the JavaScript workspaceCheck your understanding
A text input has a keydown listener that logs input.value. The user types c, a, t one key at a time. What appears in the console?
- "", "c", "ca"
- "c", "ca", "cat"
- "cat", "cat", "cat"
- Nothing, because keydown does not fire on text inputs
Show answer
keydown runs before the default action inserts the character, so the field still holds the text from before that keystroke, giving "", "c", "ca". The tempting "c", "ca", "cat" is what the input event (or keyup) reports, since those run after the value has been updated.