JavaScript Events
sourceJavaScript is considered to be and event driven, which is to say that your program waits for events to define its control flow; The program is waiting for events to react to.
Essentially we use JavaScript to channel and define the user experience as they interact with the user interface. We are waiting for the user to click on something or to fill out a form, submit a form mouse over a region, the return of a HTTP request is also an event.
The basic syntax for this mechanism in JavaScript looks like this:
object.eventListener(event, function);
We can attach these listener objects to elements in the DOM in order that a function be called when the prescribed event occurs.
myDiv.eventListener("click", doSomething);
function doSomething(ev) {
console.log(ev.type); // click
console.log(ev.target); // myDiv
}
When the object to which the event listener is attached is acted upon, the event listener will pass an object event into the registered function.
Working from the following html, we will explore some event listeners.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>events</title>
</head>
<body>
<p>
<button id="btn">Button</button>
</p>
<p>
<a href="http://google.com" id="link">Google</a>
</p>
<p>
<input type="text" id="text" />
</p>
<script src="events.js"></script>
</body>
</html>
... and our JavaScript, we can pass in either named functions or use an anonymous function, and that in order to get the link script to run we must first disable the effect of the anchor tag.
Notice how we can see which event listener is firing whilst using the text box, on entering a character, the input fires, on leaving the input box both the change and the blur events fire, change is for the change of state from input entry to no longer active, blur for the loss of focus.
let btn = document.getElementById("btn");
let lnk = document.getElementById("lnk");
let txt = document.getElementById("txt");
let res = document.getElementById("listener-example");
btn.addEventListener("click", buttonClicked);
function buttonClicked(ev) {
console.log(ev.type, ev.target, ev.currentTarget);
}
lnk.addEventListener("click", ev => {
ev.preventDefault(); // Stop the anchor link from being followed.
console.log(ev.type, ev.target, ev.currentTarget);
});
txt.addEventListener("input", ev => {
console.log(ev.type, ev.target, ev.target.value);
});
txt.addEventListener("change", ev => {
console.log(ev.target, ev.target.value);
});
txt.addEventListener("blur", ev => {
console.log(ev.target, ev.target.value);
});
As the code is currently written, if the script is declared in the head of the file, then it returns an error, non of the document elements exist yet and as such we are trying to put listeners onto null objects. We can resolve this by wrapping all of our listeners in a function. The document does not know about all of its elements and their id"s until it has finished loading, that in itself is also an event.
With this in mind, we can add an event listener to the document itself and call our wrapper script when the document load event fires.
document.addEventListener("DOMcontentLoaded", init);
Bubbling or Capture
sourceWhen an event is triggered inside of a nested element, the event bubbles up through all of the nested elements until it gets to the document body, from where it is caught by the enveloping browser or system. How the system treats the event depends upon a final parameter that we can provide after the callback function; We have the option also to provide a boolean value that defines how the event will be treated and in which order events will occur when there are multiple listeners reacting to the same event.
useCapture
The standard methods of functioning of events was decided upon by the w3c as the after combining the two very different methods that were in use when the standard was formulated, at the time both Microsoft and Netscape had browsers which both reacted differently to events.
Netscape used the term bubbling up to the DOM, which meant that the nested event was acted upon first and that each element in the nesting above the target node that also has a listener attached would act upon the event in order.
Microsoft used what they named as capturing the event, descending down into the nesting acting upon listeners that are closer to the document body first, triggering the target elements listener last.
The addEventListener can also take a third parameter, useCapture, a boolean which when set to true instructs the listener to act in accordance with the capture method.
By default the useCapture value is set to false, meaning that the events will bubble up though all of the nested elements in the Netscape way. This is used the majority of the time, so we do not often see this final parameter, as bubble is the most commonly used setting.
<div class="bubble" id="div1-a">
div one
<div id="div2-a">
div two
<div id="div3-a">
div three
<div id="div4-a">
div four
</div>
</div>
</div>
</div>
<script>
let res = document.getElementById("out-a");
let div1 = document.getElementById("div1-a");
let div2 = document.getElementById("div2-a");
let div3 = document.getElementById("div3-a");
let div4 = document.getElementById("div4-a");
let highlight = ev => {
// Add css gold class to the element.
console.log(ev.type, ev.target, ev.currentTarget);
let target = ev.currentTarget;
console.log(target);
target.className = "gold";
reset(target);
};
let reset = _element => {
setTimeout(() => {
_element.className = "";
}, 2000);
setTimeout(() => {
res.innerHTML = "";
}, 5000);
};
div2.addEventListener("click", ev => {
const target = ev.currentTarget;
console.log(target.attributes[0].value);
});
Each of the divs has a listener placed upon them, that will set the background colour to gold when triggered, div two has two listeners. So that we can see how the sequence that includes a completely separate listener functions.
[div1,div2,div3,div4].forEach(element => {
element.addEventListener("click", highlight);
});
</script>
Bubble Example
We can see in the output the order of the triggering of the listeners.
Capture Example
... and now with the capture variable set to true.
The capture model from Microsoft has several problematic properties which is the main reason that it is not recommendable to use them. A subject which is explained in more depth in this article.
Stop Propagation
The w3c have provided two functions to help give us some control over the flow of the triggering of listeners when using the bubble up method, by way of two functions, stopImmediatePropagation, and stopPropagation.
We will add the stopPropagation function to our highlight function, this will stop the propagation of its call to its parent elements. For our second div that has two listeners upon it we will place the stopImmediatePropagation which is require to stop both the propagation of the event and the triggering of the highlight function as a second listener upon the same element.
let res = document.getElementById("out-c");
let div1 = document.getElementById("div1-c");
let div2 = document.getElementById("div2-c");
let div3 = document.getElementById("div3-c");
let div4 = document.getElementById("div4-c");
let highlight = ev => {
// Stop propagation of highlight function event.
ev.stopPropagation();
console.log(ev.type, ev.target, ev.currentTarget);
let target = ev.currentTarget;
target.className = "gold";
reset(target);
};
let reset = _element => {
setTimeout(() => {
_element.className = "";
}, 2000);
setTimeout(() => {
res.innerHTML = "";
}, 5000);
};
div2.addEventListener("click", ev => {
// Stop propagation of message event as well as all other listeners.
ev.stopImmediatePropagation();
const target = ev.currentTarget;
console.log(target.attributes[0].value);
});
[div1,div2,div3,div4].forEach(element => {
element.addEventListener("click", highlight);
});
Event target and currentTarget
sourceLooking at the following simple code, we will investigate the
differences between event.target
and
event.currentTarget
, so as to better demonstrate what they
are.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>target and currentTarget</title>
<style>
</style>
</head>
<body>
<h1>event.target vs event.currentTarget</h1>
<main>
This text is inside the <main> element.
<div>This text is inside the <div> element.</div>
</main>
<script>
document.querySelector("main").addEventListener("click", clicked);
document.querySelector("div").addEventListener("click", clicked);
function clicked(ev) {
console.log("The click that was attached to ", ev.tagName,
"is currently at", ev.currentTarget.tagName);
}
</script>
</body>
</html>
Clicking upon the main text sends one single event, we see that both the target and the currentTarget are the same, when we click upon the div text, then two events are fired, one for the target and the second for its parent the main element, here we see that target always shows the targeted element and that currentTarget shows whichever element is currently being triggered by the event.
Built in JS handleEvent method
sourceEvent listeners also have a built in handleEvent
method,
this means that we can create a function upon an object that fires
when events set upon that object are triggered.
In the following code, we are adding the listeners to our object on
creation, using the this
key word to target the parent
object when the init function is called. The event that is being
passed into our handleEvent function will react to the
this
reference upon the listeners.
It is then possible to use a switch stament to handle different types of events in different ways.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>handleEvent</title>
<style>
h1 {
font-size: 1rem;
}
</style>
</head>
<body>
<h1>Built-in EventListener handleEvent method</h1>
<button id="btn">Click the button</button>
<script>
let obj = {
init: function() {
document.querySelector("#btn").addEventListener("click", this);
document.querySelector("#btn").addEventListener("focus", this);
document.querySelector("#btn").addEventListener("blur", this);
},
handleEvent: function(ev) {
switch(ev.type) {
case "click":
this.something(ev);
break;
case "focus":
this.something(ev);
break;
case "blur":
this.something(ev);
break;
case "explode":
break;
}
},
something: function(ev) {
// Gets called by click event list.
console.log("btn was", ev.type + "ed");
}
};
// Get things started.
obj.init();
</script>
</body>
</html>
Keyboard Events
sourceSetting up another event listener this time for the keyboard, we have
set them this time upon a text input element. There are three stages to
a keyboard event, keydown keyup
and
keypress
if all are added to listeners then a keypress will
trigger three events, one when the key is activated another when it is
disactivated and finaly when both the former have when both the former
have finished, this is the order that the events will fire in.
tagName
The event target obtained from currentTarget contains a property called tagName, this is the first html tag of the target element in full.
char
There are several different possibilites for recovering which
unicode character key was pressed, depending upon which browser and the
browsers age, so to retrieve the character we need to check all
three, ev.char ev.charCode ev.which
We can check all three using the || condition in one line
The char returned is the number of the byte, if we would like to
convert this to the actual character we can use the
String.fromCharCode
method.
If we want to also pick up keyboard input when we are not within the text input area, we need add a listener for the keyboard on the document body. For this example the listener is being put on the body of the document that is in an iframe and as such it is the body inside that frame which will activate the listener when focused.
let char = ev.char || ev.charCode || ev.which;
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Keyboard Events</title>
<script src="keyboard.js"></script>
<style>
p {
outline: 1px solid #333;
margin: 2rem;
}
input {
margin: 2rem;
}
input:focus {
background: gold;
}
</style>
</head>
<body>
<p>
<input type="text" id="txt" />
</p>
</body>
</html>
// events.js
//
// Keyboard events in the browser.
// ev.char || ev.charCode || ev.which
// keydown keyup keypress
let log = console.log
document.addEventListener("DOMContentLoaded", init);
function init() {
let txt = document.getElementById("txt");
txt.addEventListener("keydown", anyKey);
document.body.addEventListener("keydown", anyKey);
}
function anyKey(ev) {
log(ev.type, ev.target);
let target = ev.currentTarget;
let tag = target.tagName;
let char = ev.char || ev.charCode || ev.which;
let s = String.fromCharCode(char);
log(s, char, tag);
}
Comparing MouseEnter & MouseLeave with MouseOver & MouseOut
When dealing with mouse events, there are a few that are very similar. MouseEnter and MouseLeave, MouseOver and MouseOut. Both MouseEnter and MouseOver are triggered when you move your mouse over an object, MouseLeave and MouseOut are triggered when you leave that object. On the surface they look very similar. The following code shold help to clarify the differences between them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta type="viewport" content="width=device-width, initial-scale=1.0">
<title>MouseEnter ∧ MouseLeave vs MouseOver ∧ MouseOut</title>
<style>
h1 {
font-size: 1rem;
}
p {
padding: 1rem;
border: 2px solid black;
}
.enter {
color: white;
padding: 1rem;
background: purple;
}
.over {
color: black;
padding: 1rem;
background: lightgreen;
}
.blue {
background: lightblue;
color: black;
}
</style>
</head>
<body>
<h1>Comparing MouseEnter & MouseLeave<br>with MouseOver & MouseOut</h1>
<div class="enter">
<p>This area uses MouseEnter and MouseLeave.</p>
<p>The first paragraph.</p>
<p>The second paragraph.</p>
</div>
<div class="over">
<p>This area uses MouseOver and MouseOut.</p>
<p>The first paragraph</p>
<p>The second paragraph.</p>
</div>
<script>
let res = parent.window.document.getElementById("out-04");
document.querySelector(".enter p").addEventListener("mouseenter", entering);
document.querySelector(".enter p").addEventListener("mouseleave", leaving);
function entering(ev) {
ev.currentTarget.style.borderColor = "gold";
console.log("mouseenter p");
res.innerHTML += "<pre>mouseenter p</pre>";
res.scrollTop = res.scrollHeight;
}
function leaving(ev) {
ev.currentTarget.style.borderColor = "black";
console.log("mouseleave p");
res.innerHTML += "<pre>mouseleave p</pre>";
res.scrollTop = res.scrollHeight;
}
document.querySelector(".over p").addEventListener("mouseover", overing);
document.querySelector(".over p").addEventListener("mouseout", outing);
function overing(ev) {
ev.currentTarget.style.borderColor = "gold";
console.log("mouseover p");
res.innerHTML += "<pre>mouseover p</pre>";
res.scrollTop = res.scrollHeight;
}
function outing(ev) {
ev.currentTarget.style.borderColor = "black";
console.log("mouseout p");
res.innerHTML += "<pre>mouseout p</pre>";
res.scrollTop = res.scrollHeight;
}
// No differences so far ...
document.querySelector(".enter").addEventListener("mouseenter", function(ev) {
ev.currentTarget.classList.add("blue");
console.log("mouseenter div. Add blue.");
res.innerHTML += "<pre>mouseenter div. Add blue.</pre>";
res.scrollTop = res.scrollHeight;
});
document.querySelector(".enter").addEventListener("mouseleave", function(ev) {
ev.currentTarget.classList.remove("blue");
console.log("mouseleave div. Remove blue.");
res.innerHTML += "<pre>mouseleave div. Remove blue.</pre>";
setTimeout(function() {
res.innerHTML = "";
}, 6000);
res.scrollTop = res.scrollHeight;
});
document.querySelector(".over").addEventListener("mouseout", function(ev) {
ev.currentTarget.classList.remove("blue");
console.log("mouseout div. Remove blue.");
res.innerHTML += "<pre>mouseout div. Remove blue.</pre>";
setTimeout(function() {
res.innerHTML = "";
}, 6000);
res.scrollTop = res.scrollHeight;
});
document.querySelector(".over").addEventListener("mouseover", function(ev) {
ev.currentTarget.classList.add("blue");
console.log("mouseover div. Add blue.");
res.innerHTML += "<pre>mouseover div. Add blue.</pre>";
res.scrollTop = res.scrollHeight;
});
</script>
</body>
</html>
When we mouse over the .enter
div, first the div
mouseenter listener fires and the background turns blue, then when we
get to the paragraph its mouseenter listener fires and the border
changes to gold, moving out of the paragraph the mouseleave trigger
fires and the border reverts to black, then finally moving off of the
div the mouseleave fires on the div and the background reverts to its
original colour.
mouseenter div. Add blue. mouseenter p mouseleave p mouseenter div. Remove blue.
The difference between the two sets of listeners becomes apparent when
we mouse over the next .over
div, first the mouseover
listener fires and the background changes to blue, then as we mouseover
the paragraph the mouseout fires turning the div back to the original
colour before the mouseover on the paragraph fires and sets the border
to gold, next the mouseover from the div adds the blue background gain.
The same happens on leaving the paragraph and the div. The events are
removing and resting their actions on every state change.
mouseover div. Add blue. mouseout div. Remove blue. mouseover p mouseover div. Add blue. mouseout p mouseout div. Remove blue. mouseover div. Add blue. mouseout div. Remove blue.
We can see that mouse over and mouse out are using 8 events signals as the events are bubbling where as mouse enter and mouse leave are maintaining state and making half as many event calls, though both types of listeners appear to have the same effect, the mouse enter and mouse leave are a lot more efficient.
Creating and Dispatching Custom JS Events
sourceWe are by now familiar with some of the standard events, DOMContentLoaded, click, mouseover, mouseout, onsubmit. All of the standard events that come built in with the browser API's. Every now and then we need to have an event that does not really fit with the pattern of any of the existing events. There may be some process that we want to have happen when creating an object and something happens to it. This is a situation that can be resolved by creating a custom event.
Setting up a simple page that has some paragraphs added to it upon load, we can set a fuction that will remove a paragraph that is clicked upon by putting a listener upon the paragraph concerned; As we have already seen on many objects before now.
Now what we want to do is to create our own custom event, there are
two different ways that we can do this. One is with the command
new Event
passing in a string which will be associated
with the new event type. The second method is with new
CustomEvent
which requries the same string as a name, but we
can also add an object to it that has a property called
detail
, we will now explore how this can be used.
First we create an event saving it into a variable and giving it a name with its first string argument to define the name of the type. This name is then used when setting a listern upon an object, and finally the event variable is used, by sending it into a dispatch to trigger the event.
It is worth pointing out here that event listeners can be the cause of memory leaks in JavaScript as such it is a best practice to always remove listeners if and when they are no longer required; A persistant listener can keep an object alive so long as it is still attached to the DOM tree in some way, this needs to be paied attetion to when removing or destroying objets
more details on memory leaks
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta type="viewport" content="width=diveice-width, initial-scale=1.0">
<title>Custom Events</title>
<style>
h1 {
font-size: 1rem;
}
</style>
</head>
<body>
<h1>Creating Events</h1>
<main>
<!-- Add stuff here -->
</main>
<script>
let output = parent.window.document.getElementById("out-06");
parent.window.document.getElementById("btn-06").addEventListener("click", function() {
output.innerHTML = "";
birth();
});
// 1). let evt = new Event("explode");
// 2). let evt = new Event("explode", {detail:{speed:20, volume:40}});
let born = new Event("born");
let died = new CustomEvent("died", {detail:{time:Date.now())});
document.addEventListener("DOMContentLoaded", function() {
m = birth()
m.addEventListener("click", function(ev) {
removeParagraph(m, m.firstElementChild);
});
});
function birth() {
let m = document.querySelector("main");
addParagraph(m, "This is a new paragraph.");
addParagraph(m, "A new Star Wars movie is comming soon.");
return m
}
function addParagraph(parent, txt) {
let p = document.createElement("p");
p.textContent = txt;
// Set up and dispatch events.
p.addEventListener("born", wasBorn);
p.addEventListener("died", hasDied);
p.dispatchEvent(born);
// Add to screen.
parent.appendChild(p);
}
function removeParagraph(parent, p) {
// dispatch event
p.dispatchEvent(died);
// remove element from screen
parent.removeChild(p);
}
function wasBorn(ev) {
console.log(ev.type, ev.target);
output.innerHTML += `<pre>${ev.type} ${ev.target.innerText}</pre>`;
}
function hasDied(ev) {
console.log(ev.type, ev.target, ev.detail.time);
output.innerHTML += `<pre>${ev.type} ${ev.target.innerText} ${ev.detail.time}</pre>`;
// Remove the listeners
ev.target.removeEventListener("born", wasBorn);
ev.target.removeEventListener("died", hasDied);
}
</script>
</body>
</html>
When we click on a paragraph the died event the removeParagraph function is called in which the custom died event is dispatched.
Touch Events
sourceMany of the events that we have dealt with so far have been for
navigation with a mouse, however when writing applications for many
mobile devices such as tablets and phones, you do not have a mouse;
For these devices we have touch events. The events that we will be
working with are touchstart touchend touchcancel
and
touchmove
. Their are no events for swipe, swipeleft
swipeup/down/right rotate pinch zoom. Those do not exist, if you
would like to use some of those, you will have to write your own
script, using the events that we have, to piece that together.
One thing that is different with a mouse and with touch, is that we can have one, two, three, or four finger contact, whereas with a mouse there is always only one.
We can test the touch function in most browsers devtools, for at the very least an approximation of how the page will function.
To access the touch event we can call the property
Event.touches
this will give us all of the properties
that we might require to code our event reaction.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta type="viewport" content="width=device-width, initial-scale=1">
<title>Touch Events on Mobile Devices</title>
<style src="events/touch.css"> </style>
</head>
<body>
<div class="case">
<div class="screen">
<div class="menu">
<div class="square"></div><div class="square"></div><div class="square"></div><div class="square"></div><div class="square"></div><div class="square"></div><div class="square"></div>
</div>
<div class="text">
<h1>Touch Events</h1>
<p>This is a paragraph with some text, it is waiting to be touched, it can
only be activated on a touch capable device like a mobile phone or a
tablet. Clicking it with a mouse will not accomplish anything.</p>
</div>
<div class="footer">
<div class="rec chk1"></div><div class="rec chk1"></div><div class="rec chk1"></div><div class="rec chk1"></div><div class="rec chk1"></div><div class="rec chk1"></div><div class="rec chk1"></div><br>
<div class="rec chk2"></div><div class="rec chk2"></div><div class="rec chk2"></div><div class="rec chk2"></div><div class="rec chk2"></div><div class="rec chk2"></div><div class="rec chk2"></div>
</div>
</div>
</div>
<script>
// Touch events - touchstart, touchend, touchmove, touchcancel
// work on devices that are touch capable.
// No error on other devices because touchstart is just a name, like winlottery.
// The event will probably never happen on my laptop.
document.querySelector('p').addEventListener('touchstart', f);
document.querySelector('p').addEventListener('touchend', f);
function f(ev) {
console.log(ev.touches)
}
</script>
</body>
</html>
example
Planning Your DOM Event Targets
sourceHow do we decide what to attach our event listeners to? Next up, we will be looking at the different ways that we attach both listeners and information, in order that we can get at that information by way of the listener.
Often times, when dealing with data in html, elements within lists will have decorating element tags, such as in our example, part of the list-item content is within span tags, for reasons of styling. The problem that this can create is that if we were to attach the listener to the list item itself, if the user clicks on the piece of text within the span tags, then the span element will be the target of the event. If they click upon the number, the target will be the list item, the li; So you see that we have two different targets in one place.
In the script below we are setting listeners upon both the <li> and the <ul> tags. In understanding this we will need to be aware of the differenc between target and currentTarget. We must also be aware that the users can click either upon the number or upon the span. The span is an essential part of the styling which is very much a part of the user interface.
It is common practice, when the displayed information is split up by
styling, to inclued within the parent element, custom
data-*
attributes, this way we know that if we get to
the <li>
, reguardless of which part of the element
that they have clicked upon, the <li>
covers the
entire row and from within it we can access our data-*
attributes.
The use of
data-*
is a part of the HTML5 specification, we
can use it to store which ever data we would like, so long as the
attribute name begins with data and a hyphen.
We really have three options here, when it comes to where to put the
listener; We could put it on either of the span
, on the
<li>
or on the <ul>
. If we put it
on the &ls;ul>
we are being a little bit more
efficient. Regardless of where we put the listener, we have to be
able to travel through all of the html in list, understanding the
child parent relationship between these different elements. If the
listener is on the <ul>
, then we need a way of
getting to the <li>
, if the listener is on the
<li>
, then I have to deal with the fact that I could
click upon the <span>
, then we will have to check
which it was.
The event is picked up no matter where in the list that we click, our output tells us which element has registered the event.
Remembering that there are different ways of placing and treating an
even, and depending upon the situation, here we have removed the
listener from the <ul>
and use the listeners that
have been placed upon the list items. From where we are able to use
the data attributes.
<script>
let output = parent.window.document.getElementById("out-08");
parent.window.document.getElementById("btn-08").addEventListener("click", () => {
output.innerHTML = "";
});
let partyStarted = function() {
// add event listener to element(s) on the page.
//document.querySelector(".list-view").addEventListener("click", onClick);
// target will be ul
let lis = document.querySelectorAll(".list-item");
lis.forEach(li => {
li.addEventListener("click", onClick);
// target will be li
});
let spans = document.querySelectorAll(".list-item span");
}
let onClick = function(ev) {
// ev is the click event... but who called it?
alert("target " + ev.target + " and currentTarget " + ev.currentTarget);
output.innerHTML += `<p>target ${ev.target} and currentTarget ${ev.currentTarget}</p>`;
output.scrollTop = output.scrollHeight;
let id = ev.currentTarget.getAttribute("data-id");
let nm = ev.currentTarget.getAttribute("data-name");
let h2 = document.querySelector("h2");
h2.textContent = id + ": " + nm;
}
document.addEventListener("DOMContentLoaded", partyStarted);
</script>
Next we will add another function and demonstrate how we can achieve
the same task in another way, this time using the <ul>
listener.
Removing Event Listeners
sourceWe have looked in depth at adding event listeners, but it is also important to know that we should remove them too. The following code adds a post every second, we can remove from the displayed list by clicking on list elements. However, this will remove the element from the display but not from the computers memory, the fact that we have added a listener to the element means that it is still referenced by the main window object and as such is not destroyed by the garbage collector.
We must remove the event listener from the element before removing the element if we want the system to then free up the memory, not doing so in this type of situation in which things are repeatedly being created, is a memory leak.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta type="viewport" content="width=device-width, initial-scale=1">
<title>Removing Event Listeners</title>
<style>
h1 {
font-size: 1rem;
}
.gold {
color: gold;
}
</style>
</head>
<body>
<main>
<h1>Post Feed</h1>
<ul class="list-view">
<li class="list-item">first post</li>
<li class="list-item">second post</li>
</ul>
<script>
let partyStarted = function() {
// add listener to existing items.
document.querySelectorAll(".list-item").forEach(li => li.addEventListener("click", randomThing));
// add another list item every 2 seconds.
stop = setInterval(addItem, 1000);
}
let randomThing = function(ev) {
let li = ev.currentTarget;
li.className = 'gold';
console.log("randomThing");
setTimeout((function() {
// remove the li from the page after 1/2 second.
this.removeEventListener("click", randomThing);
this.parentElement.removeChild(this);
}).bind(li), 500);
li = null;
}
let addItem = function() {
let ul = document.querySelector(".list-view");
let li = document.createElement("li");
li.className = "list-item";
li.textContent = `Post number ${Date.now()}`;
<span style="color: red">li.addEventListener("click", randomThing);</span>
ul.appendChild(li);
}
document.addEventListener("DOMContentLoaded", partyStarted);
</script>
</main>
</body>
</html>
Currently if we remove elements as fast as they are being added, our memory use will still keep increasing. To remedy this we need to remove the event listener before removing the element.
<script>
let randomThing = function(ev) {
let li = ev.currentTarget;
li.className = 'gold';
console.log("randomThing");
setTimeout((function() {
// remove the li from the page after 1/2 second.
<span style="color: limegreen">this.removeEventListener("click", randomThing);</span>
this.parentElement.removeChild(this);
}).bind(li), 500);
li = null;
}
</script>
Right Click Menu's
source
Handling Global JS Errors
sourceWe could, when writing JavaScript to run in browsers, wrap our entire code in a try catch block. This is not a very elegant solution and it does not work for scripts that have been pulled in from other sources. With the inclusion of the import key word in ES6, we are loosing the ability to do this anyway, which is to say that it is not a good practice to adopt.
/* how not to handle global errors. */
try {
function a() {
referenceA.error.causing();
}
function b() {
referenceB.error.causing();
}
a();
} catch(err) {
console.error(err)
} */
There exists a method on the main window function to which we can set the function that is to be run upon receiving an error anywhere in the window. This function can have numerous arguments, all of which get passed in when an error occurs.
window.onerror()
window.onerror = function(msg, url, line, col, err) {
console.log("error:", msg, url, line, col, err);
}
notthere;
When we react to the error in this way, all of our data is output in the console log, however we also receive an error marked as an 'uncaught error' in the console as well, because we are still not doing anything with it, it has still not been dealt with. For this we can use the error event listener.
window.addEventListener("error", function(ev) {
console.log(ev.type);
});
event.preventDefault
We see the error type, but still we get the 'uncaught error' message, because we have still yet to do something about the error. We can prevent the default logging of the error to the console by requesting that in our code.
window.addEventListener("error", function(ev) {
console.log(ev.type);
ev.preventDefault();
console.warn(ev);
});
console.warn()
window.addEventListener("error", function(ev) {
console.log(ev.type);
ev.preventDefault();
console.warn(ev);
});
We can also add an onerror method to document elements and objects, here we will add an error to the image.
document.addEventListener("DOMContentLoaded", function() {
let img = document.getElementById("badImg");
img.onerror = function() {
console.warn("image did not load");
}
});
There is another situation in which we will not receive this
behaviour, it is when we try to load a script from an external
resource. The information generated by external scripts is hidden by
the browser for reasons of security. We can get around this by adding
the attribute crossorigin="anonymous"
to our script
tags. This tells the browser that it is not to send any information
about itself to the server when it request the script file. When this
attribute is set, if the corresponding server has its permissions set
to allow the same, then if errors occur in that script they can be
caught and dealt with in the browser, as we have been doing for our
local errors, including all the internal details. Without this set,
all you will receive in the case that there is and error, is a
generic 'script error'.
<script src="http://otherdomain.com/script.js" crossorigin="anonymous"></script>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta type="viewport" content="width=device-width, initial-scale=1">
<title>Window Error Event</title>
<style>
h1 {
font-size: 1rem;
}
</style>
</head>
<body>
<h1>Window Error Events & Handling</h1>
<p>The window object has an Error event that lets us handle any error that
happens on the page. Much better practice than wrapping your entire page
in a try catch block.</p>
<p><img src="notthere" id="badImg" alt="no image here" /></p>
<script src="http://otherdomain.com/script.js" crossorigin="anonymous"></script>
<script>
document.getElementById("btn-11").addEventListener("click", function() {
document.addEventListener("DOMContentLoaded", function() {
let img = document.getElementById("badImg");
img.onerror = function() {
console.warn("image did not load");
}
});
window.addEventListener("error", function(ev) {
console.log(ev.type);
console.warn(ev);
ev.preventDefault();
});
notthere;
}
</script>
</body>
</html>
Modifying Text As You Type
We have various different listeners for acting upon key presses, as
we have seen in the keyboard events
section. We will be looking at keydown keyup
and
keypress
which can be placed upon any element on the
page, along with input
which is explicitly for input
elements.
No matter what order the listeners are found in your code, they will
always fire in the same order. keydown keypress keyup
.
Neither keydown
or keyup
are able to tell
you what the charcode is, only keypress
has this data.
Only keyup happens after the key value has been recorded and as such
is able to update the key on the same iteration.
keydown
The keydown
event returns no charcode, as the char has
not yet been read, there is as yet still no input value, the input is
not converted into upper case until the next key is pressed.
keypress
The keypress
event contains the charcode data and value
of the key press thus the letter, but its value has still not been
stored. The letter pressed remains in lower case, the value displayed
in the input field is one step behind that which is made into upper
case.
keyup
The keyup
event happens after the key has been read, we
have no data about the charcode and the letter has been added to the
value property of the input object. Our input characters are changed
into upper case upon their entry.
input
The input
listener is explicitly for input fields, the
input
give us access to the full value and really is
the best choice for updating the value inside of an input text
element, we do not have the charcode but in many cases it is simply
not required.
TransitionEnd and AnimationEnd Events
sourceIn this next script we will use the transition end and animation end events to change the name of an animation, making the glow animation apply only during the transformation.
With the animation set upon the transition end, as have instructed the animation to run only once, once it has been used, it no longer effects any elements on the page. To resolve this issue we reset the animation to an empty string when it has finished its animation cycle. Now every time that the animation is placed upon a paragraph, it is triggered to run, and we have resolved the playing only once issue.
Scrolling pages with JavaScript and CSS
sourceThe following code sets out an array of paragraphs upon which we have a script that is set on the header, driving the underlying text to scroll.
console.group & console.groupEnd
The console object also has a method group, which allows us to simplify more complicated logging by grouping output messages together. We will be using a lot of comments here so this will come in handy.
scrollBy & scrollTo
The scrollBy and scrollTo functions are similar, scrollBy takes x/y values that will move the target object by those values from its current position. Where as scrollTo takes precise coordinate to which you can request an object be moved.
Angled Numbers
The numbered paragraph effect has been created by setting a data-*
attribute for each paragraph with the desired number, this number is
then picked up and applied using CSS by setting the
content
attribute using the attr(data-num)
designation. All have been rotated by 20deg's and every nth(odd)
element is rotated by negative 20deg's.
<p data-num="1">Some text ...</p>
p::before{
content: attr(data-num);
}
Intro to JavaScript Audio Effects on Webpages
sourceIn order to play audio with javascript on a web page, we can set a
<audio>
element on the page with no controles
attribute without which it is not displayed on the page, we can the
control this using javascript.
<audio id="a" src=""></audio>
<script>
let audio = document.getElementById("a");
</script>
Another option is to create an element instead. A few lines have been included here, that appeds the tag to the body and remove the controls, but we don't need this for the code to still work. They have been out to hilight this.
<script>
let audio = document.createElement("audio");
// audio.removeAttribute("controls");
// document.body.appendChild(audio);
</script>
What is the difference between these two different techniques? If we add the audio to the html before hand, we have got only one thing that is capable of playing audio, whereas using the second technique each event will get its own player.
It is not good to have multiple audio sources playing at the same time, you will overwhelm your users quickly if you do so, we also need to be careful about the volume as well.
audio.volume = 0.1;
We have to be very careful about sound on web pages, there should always be a way to disable it.
media events
Sometimes we will want to tie in our sounds with events, there exist also media events, which are all of the events that can occur with video and audio elements. There are a multitude all used in the same way as any other events with listeners.
https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement#eventsIf we would like to get a little more control over the playback of our sounds, we can track their state ourselves, this is only one of many approaches to doing this.
Using a variable called SOUNDS
, we can store the sound
elements whilst they are being played back, and set the same
data-file attribute on our newly created audio element so that we can
store and retrieve the playing audio's file name.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Audio Effects with JavaScript</title>
<style>
h1 {
font-size: 1.2rem;
}
p {
font-size: 1rem;
cursor: pointer;
}
p::after {
display: inline-block;
content: attr(data-file);
font-size: 1rem;
margin-left: 1rem;
color: rgba(0,0,0,0.5);
}
</style>
</head>
<header>
<h1>Audio Effects with JavaScript</h1>
</header>
<main>
<button id="btn-15">off</button>
<p data-file="clear-throat">click me to play a sound</p>
<p data-file="doorbell">mouseover to play a sound</p>
<p data-file="static">double-clik me to play a sound</p>
</main>
<audio id="a" src=""></audio>
<script>
document.addEventListener("DOMContentLoaded", init);
let btn = document.getElementById("btn-15")
btn.addEventListener("click", () => {
allowSound = !allowSound;
console.log("sound:", allowSound);
if (allowSound) {
btn.textContent = "off";
} else {
btn.textContent = "on";
for (let key in SOUNDS) {
if (SOUNDS[key]) {
SOUNDS[key].pause();
SOUNDS[key] = null;
}
}
}
});
const SOUNDS = {
"clear-throat":null,
"doorbell":null,
"static":null
};
let allowSound = true;
function init() {
let p1 = document.querySelector("p[data-file]");
let p2 = document.querySelector("p:nth-of-type(2)");
let p3 = document.querySelector("p:last-of-type");
p1.addEventListener("click", play);
p2.addEventListener("mouseover", play);
p3.addEventListener("dblclick", play);
}
function play(ev) {
let p = ev.currentTarget;
ev.preventDefault();
let fn = p.getAttribute("data-file");
let src = "media/" + fn + ".mp3";
if (SOUNDS[fn]) {
SOUNDS[fn].pause();
SOUNDS[fn] = null;
}
console.log(src);
//let audio = document.getElementById("a");
let audio = document.createElement("audio");
// audio.removeAttribute("controls");
// document.body.appendChild(audio);
audio.src = src;
audio.volume = 0.1;
if (allowSound) {
SOUNDS[fn] = audio;
audio.setAttribute("data-file", fn);
audio.play();
}
/*********************************************************************
Event list for <audio> and <video>
https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement
**********************************************************************/
// Listen for the event that ends sound
audio.addEventListener("playing", goAudio);
audio.addEventListener("ended", doneAudio);
}
function goAudio(ev) {
console.log(ev.target.src, "has started playing");
}
function doneAudio(ev) {
console.log(ev.target.src, "has stopped playing");
let fn = ev.target.getAttribute("data-file");
SOUNDS[fn] = null;
}
</script>
</html>
Capture DOM Changes with MutationObservers
sourceWith JaveScript we are able to make changes to practicaly any element in the DOM which is to say elements that are on the web page. An object has been added to JavaScript that is expressly for helping us to track our changes to DOM objects and our web pages.
For this we use the MutationObserver
into whos object
we pass the element or object that we would like to survail, along
with a config object that difines what it is about that object that
we wish to keep information on. We must define at the very least one
of childList attributes
or characterData
.
let config = {
attributes:true,
attributeOldValue:true,
attributeFilter: ["data-thing", "title", "style"],
childList: true,
subtree: true,
characterData: false,
characterDataOldValue: false
};
observer = new MutationObserver(mutated);
observer.observe(p, config);
attributes | Tracks the elements attributes for changes, removal of attributes, changes in their values or the addition of new attributes. |
attributeOldValue | Keep the old values of attributes after they have been changed. |
attributeFilter | For a more fine grained control over which attributes to watch. |
childList | Track all direct children of the element tag that is being observed. |
subtree | Track everything that is inside of the targeted tags tree. |
characterData | Track all instances of the characterData node abstract interface. (more general than just attributes) |
characterDataOldValue | Keep the old values of characterData nodes that have been modified. |
A mutation object will give us mutation records, each of which has an selection of properties.
- target - Element
- addNodes - NodeList
- removedNodes - NodeList
- oldValue
- attributeName
- attributeNamespace
- nextSibling - Element / textNode
- previousSibling - Element / textNode
- type "attributes" or "childList"
observer.disconnect
The observer disconnect method will instruct the observer to stop tracking changes.
observer.takeRecords
The takeRecords method will empty out the observer and give you an array of all of the changes that it contained, an array of everything that has happened to the observer.
HTML5 Screen Orientation API
sourceWe can detect which orientation the user's screen is by way of a few specific functions, for different types of browser and some key words. We can also add a listener to the widow for the case that it is a mobile device that chages orientation. This is pretty much all that we need to know for dealing with screen orientation.
<main>
<div id="output"></div>
</main>
<script>
let orn = getOrientation();
let out = document.getElementById("output");
out.textContent = orn;
function getOrientation() {
let _orn = screen.msOrientation ||
(screen.orientation || screen.mozOrientation).type;
switch(_orn) {
case "portrait-primary":
break;
case "portrait-secondary":
break;
case "landscape-primary":
console.log("This is the laptop/desktop version");
break;
case "landscape-secondary":
break;
case undefined:
// Not supported.
break;
default:
// Someting unknown.
}
return _orn;
}
// This listener will never fire on a laptop but on a mobile device it
// will.
window.addEventListener("orienttaionchange", ev => {
orn = getOrientation();
out.textContent = orn;
console.dir(ev);
});
</script>
Load VS DOMContentLoaded Event
sourceWe can see in the console.lot here that the load events always happens after the DOMContentLoaded event, this is because DOMContentLoaded only needs to wait for the html in the document, literally only the direct DOM content, where as load needs all related linked objects to load as well.
There is a very big difference between these different load events, especialy when a webpage is being accessed from a slow network, the choice of which to use can make a massive difference to when it is that your scripts are run.
clientX vs pageX vs screenX vs offsetX
sourceFine Tune your Touch Events with Passive mode
sourceWe have already seen that the object.addEventListener() function can take a third argument, a boolean that instructs the DOM which type of behaviour the listener should have, either bubbling or capture mode.
The need for being passive came about with the introduction of touch devices. When a user has touched a paragraph and their finger has still not yet moved, how do we know whether they are going to be interacting with that paragraph or whether they are going to swiping to scroll the page; Passive has to do with this. Instead of the boolean as the third parameter to the eventListener, what we now have is an options object.
There are three different properties that we can put into this options object, the first is a 'capture' boolean which is the same as our previous useCapture setting, the other options are 'once' and 'passive'.
Once is a really useful setting, it indicates that we only want the event to fire one time and then we would like the browser to remove it.
When using either a mobile device, or the mobile device emulation in
the devtools, we see along with our paragraph background change, a
touchstart event fired, with this we can touch and drag to scroll the
screen and perform other such mobile navigation touch screen tasks,
this is where we come to the passive setting, if we set passive to
'false' where the default is 'true', ev.preventDefault()
can be used within the events callback function to cancel this touch
start scrolling effect completely.
How to Build a Long Press Event
sourceWe are now going to build our own long press event, this is not something that natively exists in javascript just as there is no tap event either, long press is considered to be a gesture, as such we will see how event can be combined to create new events types.
DOM Structures with Event composedPath
sourceThe composedPath method returns the full path of the object that has been selected and returns that in an array, which allows you to interact with any of the elements up through the chain of nested elements to the document and then the window.
Controlling Copy & Paste with JS
sourceMonitor Events in Chrome Dev Tools
sourceInside the chromium console we have access to a method called monitorEvents(), we can add listeners to read output from in the console by calling this. Take the following example:
monitorEvents(document.body, "keydown");
DOM Element matches and closest methods
sourceHere are a couple of useful DOM methods, document.queryContent retrieves elemnts by thier class or type definition. object.matche() returns a boolean value that states whether or not that targed element matches the given classes and types. The target.closest() method returns the closeest element in the bubbeling that mathches the requested class or type.
HTML5 File and FileList Objects
sourceFile objects are built in to javascript and are a part of the web API
forms. Much of the time when we are dealing with forms we are
essentially dealing with text, however there is also an input type for
forms called file
, which allows the user to select files
from their operating system and to bring them into your web form.
By default the file selector will only allow the user to select one
file, to enable the selection of more, simply add the
multiple
attribute to the input field.
When calling scripts from buttons within a form, we need to use the preventDefault method on the event to stop the form from activating its action.
If we would like to specify which file types are permitted, we can use
the attribute, accept
, to define which file types
are expected.
If we would like to use our own design of button, rather than
the input with type=file, we must still have an input of type
file on the webpage somewhere, but it does not mean that we have
to show it, we can not use display none or visibility hidden. Or
screen readers will ignore it entirely, so we need to hide it
visually whilst keeping it as a part of the page. Grabbing that
element from within a script and then calling the
object.click()
method upon that object, has the
same effect as clicking upon the actual input button in the
browser.
Pasting Text into Multiple Input Fields
sourceWe have all seen the types of inputs that are used in security and for code entry in which there are multiple text input boxes one for each section of the code. How could we go about achieving the same type of behaviour as these multiple input fields?
The default behaviour when we have multiple fields is to only paste into the selected field and to require either mouse or tab to move into the next required text input area.
To be able to paste the entirety of a code into the boxes without requiring that the user perform all sorts of gymnastics is a desirable behaviour to provide.