DOM events Most useful DOM events click click or tap on an element dblclick two clicks on the same element within a short time, rarely used contextmenu mouse right-clicks on an element or special contextmenu keyboard key pressed mouseover , mouseout mouse cursor comes over / leaves an element mousedown , mouseup mouse button is pressed / released over an element mousemove mouse is moved keydown , keyup keyboard key is pressed and released submit visitor submits a form focus visitor focuses on an element, e.g. input, textarea DOMContentLoaded the HTML is loaded and processed, DOM is fully built transitionend CSS-animation finishes Take a closer look at DOM events . Event handler in HTML-attribute Event handler - a function that reacts on events
<input value="Click me" onclick="alert('Click!')" type="button"></input>
<input type="button" oNcLiCk="countRabbits()" value="Count rabbits!"></input>
inside onclick we use single quotes, because the attribute itself is in double quotes HTML attribute names are not case-sensitive, so ONCLICK works as well as onClick and onCLICK… But usually attributes are lowercased
// !!! NOT working, coz value should be a string
document.body.setAttribute('onclick', function() { alert(1) })
element.on
<input id="elem" type="button" value="Click me">
elem.onclick = function() { alert('Thank you') }
or...
const sayThanks = () => alert('Thanks!')
elem.onclick = sayThanks // no parentheses
Some handlers work only with addEventListener , for ex DOMContentLoaded event elem.onclick = null remove a handler As there’s only one onclick property, we can’t assign more than one event handler Adding a new handler with JavaScript overwrites the existing handler DOM-property case matters addEventListener
element.addEventListener(event, handler, [options])
event event name, e.g. "click" handler handler function options additional optional object with properties
element.addEventListener('click', func, {
capture: false,
once: false
passive: false
})
capture: true catch an event on the capturing phase once: true fires an event only ones passive: true tells the browser that the handler is not going to cancel scrolling, not going to call preventDefault() Then browser scrolls immediately providing a maximally fluent experience For some browsers (Firefox, Chrome), passive is true by default for touchstart and touchmove events removeEventListener To remove a handler we should pass exactly the same function as was assigned if we don’t store the function in a variable, then we can’t remove it To remove the handler, removeEventListener needs to have identical parameters
function handler() { alert( 'Thanks!' ) }
input.addEventListener("click", handler)
input.removeEventListener("click", handler)
el.addEventListener("event", fn, true)
el.removeEventListener("event", fn, true)
Will not work, because technically they are different function objects
el.addEventListener( "click" , () => alert('Thanks!'))
el.removeEventListener( "click", () => alert('Thanks!'))
Multiple handlers Multiple event handlers can be achieved by multiple addEventListeners.
function handler1() { alert('Thanks!') }
function handler2() { alert('Thanks again!') }
elem.onclick = () => alert("Hello")
elem.addEventListener("click", handler1) // Thanks!
elem.addEventListener("click", handler2) // Thanks again!
this The value of this inside a handler is the element, which has the handler on it.
<button onclick="alert(this.innerHTML)">Click me</button> // Click me
Event object when an event happens, event object is created and passes it as an argument to the handler event.type event type, for ex. click event.currentTarget el where the handler is, same as this unless arrow func event.target el that initiated the event event.clientX window-relative coordinates of the cursor, for pointer events event.eventPhase current phase (capturing=1, target=2, bubbling=3) There are more event properties, depending on the event type
elem.onclick = function(event) {
event.type // Event type, here it’s "click".
event.currentTarget // el where the handler is // same as 'this' unless arrow func
event.target, // el that initiated the event
event.clientX // window-relative coordinates of the cursor, for pointer events
event.clientY
event.eventPhase // current phase (capturing=1, target=2, bubbling=3)
}
The event object is also available in HTML handlers
<input type="button" onclick="alert(event.type)" value="Event type">
object with handleEvent can assign not just a function, but an object as an event handler when an event occurs, its handleEvent method is called
const obj = {
handleEvent(event) {
alert(event.type + " at " + event.currentTarget)
}
}
document.addEventListener('click', obj)
// we can use class
class Menu {
handleEvent(event) {
switch(event.type) {
case 'mousedown':
elem.innerHTML = "Mouse button pressed"
break
case 'mouseup':
elem.innerHTML += "...and released."
break
}
}
}
let menu = new Menu()
elem.addEventListener('mousedown', menu)
elem.addEventListener('mouseup', menu)
// or even like that
class Menu {
handleEvent(event) {
// mousedown -> onMousedown
let method = 'on' + event.type[0].toUpperCase() + event.type.slice(1);
this[method](event);
}
onMousedown() { elem.innerHTML = "Mouse button pressed" }
onMouseup() { elem.innerHTML += "...and released." }
}
let menu = new Menu()
elem.addEventListener('mousedown', menu)
elem.addEventListener('mouseup', menu)
Bubbling When an event happens on an element, it first runs the handlers on it, then on its parent, then all the way up on other ancestors span --> div --> body --> html --> document --> window some events reach 'window', not all Almost all events bubble focus event does not bubble
<form onclick="alert('form')">FORM
<div onclick="alert('div')">DIV
<p onclick="alert('p')">P</p>
</div>
</form>
Click on <p> triggers 3 alerts due to bubbling bubbling happens upwards till the document object if we have a single handler form.onclick, then it can “catch” all clicks inside the form e.target element that initiated the event The most deeply nested element that caused the event is called a target element handler on a parent el can get info where it actually happened e.currentTarget element from where cursor exited or where it is entered depends on the event for mouseenter , mouseover , dragenter - element exited from for mouseleave , mouseout , dragleave - element entered into e.relatedTarget el where the handler is same as this e.stopPropagation() stop bubbling Don’t stop it w/o a real need we forever deny access to information about events for any outer code document.addEventListener('click', fn) will not work on document level for stopped bubble events, it might be useful
<body onclick="alert(`the bubbling doesn't reach here`)">
<button onclick="event.stopPropagation()">Click me</button>
</body>
e.stopImmediatePropagation() stop the bubbling and prevent handlers on the current element After it no other handlers execute If an el has multiple event handlers on a single event, then even if one of them stops the bubbling, the other ones still execute but on the current element all other handlers will run Capturing Event propagation has 3 phases Capturing - event goes down to the element Target – event reached the target element Bubbling – event bubbles up from the element Capturing phase is invisible to us normally rarely used in real code Capturing path example Window -> Document -> <html> -> <body> -> <table> -> <tbody> -> <tr> -> <td> Capturing path example Window -> Document -> <html> -> <body> -> <table> -> <tbody> -> <tr> -> <td>
el.addEventListener('click', fn, { capture: true })
Or..
el.addEventListener('click', fn, true)
if false (default), then the handler is set on the bubbling phase 2nd phase (“target phase”: the event reached the element) is not handled separately handlers on both capturing and bubbling phases trigger at target phase Event delegation event delegation - a single handler on their common ancestor in the handler we get event.target to see where the event actually happened and handle it event must bubble
document.querySelector('#main').addEventListener('click', (e) => {
if (!e.target.closest('#main selector')) return
func()
})
Highlight cells on click...
table.onclick = function(e) {
let td = e.target.closest('td') // we may click on some tag inside td, so we find the nearest td
if (!td) return // check if was inside any <td>
if (!table.contains(td)) return // check if it is out table
highlight(td) // do action
}
Delegation via custom attributes...
<div id="menu">
<button data-action="save">Save</button>
<button data-action="load">Load</button>
<button data-action="search">Search</button>
</div>
class Menu {
constructor(elem) {
this._elem = elem
elem.onclick = this.onClick.bind(this)
}
save() { alert('saving') }
load() { alert('loading') }
search() { alert('searching') }
onClick(event) {
let action = event.target.dataset.action
if (action) this[action]()
}
}
new Menu(menu)
Another example... increase value on click
Counter: <input type="button" value="1" data-counter>
One more counter: <input type="button" value="2" data-counter>
document.addEventListener('click', function(e) {
if (e.target.dataset.counter != undefined) e.target.value++
})
e.preventDefault() stops default browser actions some events flow one into another, if we prevent the first event, there will be no second mousedown on an <input> field leads to focusing in it, and the focus event If we prevent the mousedown event, there’s no focus. The focusing is still possible with Tab key, But not with the mouse click any more e.defaultPrevented true if the default action was prevented sometimes better to use than stopping bubbling by event.stopPropagation() we can use event.defaultPrevented instead, to signal other event handlers that the event was handled solution would be to check in the document handler if the default action was prevented
innerDiv.oncontextmenu = function(e) {
e.preventDefault()
alert("innerDiv context menu")
}
div.oncontextmenu = function(e) {
if (e.defaultPrevented) return
e.preventDefault()
alert("div context menu")
}
outterDiv.oncontextmenu = function(e) {
if (e.defaultPrevented) return
e.preventDefault()
alert("outterDiv context menu")
}
return false stops default browser actions prevents the event from propagating (or “bubbling up”) the DOM. Stops callback execution and returns immediately when called
<ul id="menu" class="menu">
<li><a href="/html">HTML</a></li>
<li><a href="/javascript">JavaScript</a></li>
<li><a href="/css">CSS</a></li>
</ul>
menu.onclick = function(event) {
if (event.target.nodeName != 'A') return
let href = event.target.getAttribute('href')
alert( href ) // ...can be loading from the server, UI generation etc
return false // prevent browser action (don't go to the URL)
}
Custom events
const event = new Event(type, [options]);
create Event objects bubbles: true event bubbles, false by default cancelable: true “default action” may be prevented, false by default elem.dispatchEvent(event) trigger event on an element handlers will react on it as if it were a regular browser event return false if event is cancelable and any handlers which received event called preventDefault()
const event = new Event("my-event")
const event = new Event("click", { bubbles: true, cancelable: true })
elem.dispatchEvent(event)
programmatic event trigger
let event = new Event("click")
document.querySelector('button').dispatchEvent(event)
dispatchEvent is processed immediately, synchronous
document.addEventListener('hi', () => alert('hi'))
let textarea = document.querySelector('textarea')
textarea.addEventListener('click', function () {
alert(1)
textarea.dispatchEvent(new CustomEvent("hi", { bubbles: true }))
alert(2)
})
// 1 --> hi --> 2
isTrusted
event.isTrusted
true for events that come from real user false for script-generated events Built-in event classes
new MouseEvent("click")
const event = new MouseEvent("click", {
bubbles: true,
cancelable: true,
clientX: 100,
clientY: 100
})
event.clientX // 100
use it instead of new Event The right constructor allows to specify standard properties for that type of event generic Event constructor does not allow that we can work around that by assigning directly event.clientX=100 List of some classes for UI Events... new UIEvent() new FocusEvent() new MouseEvent() new WheelEvent() new KeyboardEvent() others… Custom event
const helloEvent = new Event("hello", { bubbles: true })
Better way...
const helloEvent = new CustomEvent("hello", {
bubbles: true,
detail: { name: "John" }
})
document.querySelector('button').dispatchEvent(helloEvent) // Hello from BUTTON
document.addEventListener("hello", function(e) {
alert("Hello from " + e.detail.name + ' ' + e.target.tagName)
})
We should use addEventListener for our custom events new CustomEvent allows to add detail property preventDefault for custom events new custom events have no default browser action but a code that dispatches such event may have own plans what to do after triggering the event return false if event is cancelable and any handlers which received event called preventDefault()
const textarea = document.querySelector('textarea')
textarea.addEventListener('hide', isPrevented)
textarea.addEventListener('click', hide)
function isPrevented(e) {
if (confirm("Call preventDefault?")) e.preventDefault()
}
function hide() {
const event = new CustomEvent("hide", { cancelable: true })
if (!textarea.dispatchEvent(event)) {
alert('The action was prevented by a handler')
return
}
textarea.hidden = true
}