729G87 Interaction Programming¶

Lecture 3¶

Johan Falkenjack, johan.falkenjack@liu.se¶

Lecture overview¶

  • Form related HTML elements
  • Adding some structure
  • Some useful JavaScript tricks
  • More on events
    • Clicking on nested elements - event bubbeling
    • Writing more general event handlers
  • Animating using anime.js

Form related HTML elements¶

HTML forms¶

  • Often used to enter data that is to be submitted to a server.
  • <form> element (parent) contains the form e.g. labels, text fields, checkboxes (children).
  • Form children have a name attribute that are used to identify them when the form is submitted.
  • Form children can also have an id which is used e.g. to identify them within the DOM.
  • Attributes name and id can be the same, but do not have to be the same.

Some form related HTML elements¶

  • <form> (parent)
  • <button> (can have content inside, e.g. image)
    • https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button
  • <datalist> (parent) + <option> (children) use for autocompleted text input
    • https://developer.mozilla.org/en-US/docs/Web/HTML/Element/datalist
  • <fieldset> grouping form elements + <legend> caption
    • https://developer.mozilla.org/en-US/docs/Web/HTML/Element/fieldset
    • https://developer.mozilla.org/en-US/docs/Web/HTML/Element/legend
  • <label> caption for form elements
    • https://developer.mozilla.org/en-US/docs/Web/HTML/Element/label
  • <select> (parent) + <option> (children) dropdown option
    • https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select
  • <textarea> multiple lines of text
    • https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea

Some form related HTML elements¶

<input type="inputtype">
  • where inputtype is one of e.g.
    • button, checkbox, color, date, file, hidden, number, password, radio, range, search, text, time
  • Not all input types can have children of their own, e.g.
<input type='button'>
  • https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input

Finding documentation¶

  • Each HTML element has a corresponding JavaScript WebAPI interface (a class for each element type).
  • E.g. <select> elements correspond to objects with the HTMLSelectElement interface.
    • https://developer.mozilla.org/en-US/docs/Web/API/HTMLSelectElement
  • There we can e.g. find out that events that HTMLSelectElement elements can fire are change and input.
  • We can also find out that HTMLSelectElement object have the property value that we can read to get the selected option.

Adding a little structure to your JavaScript code

Structure, examples¶

  • Run script when DOM has loaded
  • A init() function for initializing things

Run script when DOM has loaded (without defer)

<!DOCTYPE html>
<html lang="en">
    <head>
        <script src="script.js"></script>
        <title>Loaded example</title>
    </head>
    <body>
    </body>
</html>
document.addEventListener("DOMContentLoaded",
                          (event) => {
    // code to run when DOM has been loaded
});
  • Note: Using defer will only run the script after the DOM has been created, but before DOMContentLoaded fires.

Add an init function for startup code¶

  • It is good practice to define an init function that is executed when the script should "start".
    • Often this is when DOMContentLoaded fires.
// declare global variables if you need them

document.addEventListener("DOMContentLoaded", (event) => {
    init();
});

function init() {
    // collect all initialization code here
}
  • We might also set other other conditions for when init is called or simply call it from the last line of the script.

Some useful JavaScript tricks¶

Specify arguments for a function used as an event handler¶

  • An event handler function only receives an Event object as an argument. The Event object contains information about the event (more on this later).
function handleEvent(event) {
 // code for handling event
}
element.addEventListener('click', handleEvent);
  • If we want to pass specific arguments to our event handler function, we can wrap it using an anonymous function.
function handleOtherEvent(event, param1, param2 ...) {
    // code for handling event
}
element.addEventListener('click', (event) => {
    handleOtherEvent(event, "argument1", "argument2", ...)
});

Inline style¶

  • Inline styles are styles written using the HTML elements style attribute, e.g.
<p style="font-weight: bold">Hello!</p>
  • Read or set inline CSS property from JavaScript
element.style.PROPERTY = String
  • PROPERTY should be written in camelCase, e.g.
element.style.backgroundColor = 'lightblue'
element.style.display = 'none'
  • Remove inline CSS property
element.style.removeProperty(property-name : String)
  • property-name should be written in kebab-case, e.g.
element.style.removeProperty('background-color")

Manipulate an element's classlist¶

  • Manually styling an element from JavaScript to reflect changes in state is usually avoided as it becomes very difficult to maintain.
  • The recommended approach is to create element classes (the ones you write in HTML and select in CSS) for different states.
  • E.g. in a system where we can click to select elements that then become active and get highlighted in the GUI:
element.classList.add("selected") // when an element is selected
element.classList.remove("selected") // when an element is deselected
  • Or we might use toggle to explicitly turn highlight on or off:
element.classList.toggle("highlight")
  • Or if we want to be explicit about both the active and inactive states we might use replace:
element.classList.replace("active", "inactive")

String interpolation: Template literals¶

  • Use backticks (`) instead of quotes
  • Expressions within ${} will be evaluated
  • Useful for creating strings!
element.style.width = `${width}px`
  • Compare with using f-strings in Python or Ada (Yes, gnat supports string interpolation since May 2024, welcome to the 1990s Ada!)
print(f"The sum is {5 + 5}")
Ada.Text_IO.Put_Line (f"The sum is {5 + 5}")
In [4]:
console.log(`The sum is ${5 + 5}`)
The sum is 10

Delayed execution¶

  • Run function func after waiting - Run function func after waiting delay milliseconds milliseconds
setTimeout(function[, delay])
  • Run function func after waiting delay milliseconds with arguments arg1, arg2, ...
setTimeout(func[, delay, arg1, arg2, ...])
  • Note that setTimeout does not pause execution, it will only delay its own argument (see example).

Delayed execution example

  • In js-settimeout.html:
<!DOCTYPE html>
<html lang="en">
    <head>
        <script src="js-settimeout.js"></script>
        <title>setTimeout example</title>
    </head>
    <body>
    </body>
</html>
  • In js-settimeout.js:
console.log("Before");
setTimeout(() => {
    console.log("Delayed");
}, 5000);
console.log("After");
  • js-settimeout.html

Nodelist.forEach()¶

  • (returned by e.g. querySelectorAll)
NodeList.forEach(func);
  • The function func is called once for each element in the NodeList object and the current element is passed as an argument to func.
document.querySelectorAll(selector).forEach((element) => {
    // do something with element, e.g. add event listeners
});

CSS selectors repetition - good to know if using e.g. querySelector()¶

selector result set
* all elements
TagName all elements of a specific type
.ClassName all elements with a specific class
#idName all elements with a specific id
TagA TagB all TagB elements that are descended from a TagA element
TagA > TagB all TagB elements that are direct children of a TagA element
TagA ~ TagB all TagB elements that are proceeded by a sibling TagA element
TagA + TagB all TagB elements that are immediately proceeded by a sibling TagA element
TagA:has(TagB) all TagA elements that has a TagB child element
TagA.Classname all TagA elements that also have the class Classname
TagA[attr] all TagA elements that have the attribute attr
TagA[attr=val] all TagA elements that have the attribute attr with the value val
TagA[attr^=val] all TagA elements that have the attribute attr with a value that starts with val
TagA[attr$=val] all TagA elements that have the attribute attr with a value that ends with val
TagA[attr*=val] all TagA elements that have the attribute attr with a value that contains val

More on event handling¶

Event handling in general¶

  • Observer design pattern.
  1. A subject has a set of observers and methods to add and remove observers.
  2. The state of the subject changes by its setState-method being called.
  3. At the end of the setState method, the subjects notifyObservers-method is invoked.
  4. The notifyObservers-method informs each of the subject's observers that a state change has taken place by calling their update-method.
  5. Each observer's update-method calls their subject's getState-method to get the subject's new state and change their behavior accordingly.

Observer pattern (simplified)¶

Adding an event handler as an event listener¶

  • We can use the element.addEventListener() method to add an event handler for a specific type of event
    • See https://developer.mozilla.org/en-US/docs/Web/Events#event_listing
  • We specify the event type and the listener function, e.g.
element.addEventListener("keydown", keyhandler)
  • Multiple listeners can be added to the same element.

Using .addEventListener()¶

// using an anonymous function
element.addEventListener('click', function(event) {
    alert("Hello World");
});

// using an arrow expression
element.addEventListener('click',(event) => {
    alert("Hello World");
});

// using a function object
function hello(event) {
    alert("Hello World!");
}
element.addEventListener('click', hello);

Removing an event listener¶

  • You can remove an event handler from an element using
element.removeEventListener(event, handler)
  • Needs reference to handler - i.e. does not work if you set an anonymous function as the handler when adding it.
  • Why removing listeners?
    • In order to remove or change what happens when the event occurs depending on current state.

Example of event listeners using id:s


Box size sliders using id


https://codepen.io/jodyfoo/pen/poWzOJR

Writing more general event handlers


Using event.target and DOM structure

Event objects¶

  • Event handler functions recieve an Event object that contains information about the event.
  • The kind of event decides what event type is sent
  • All events have
    • Event.target: reference to origin of event
  • MouseEvents have e.g.
    • MouseEvent.screenX, MouseEvent.screenY: X and Y coordinates of the mouse
  • See https://developer.mozilla.org/en-US/docs/Web/API/Event#interfaces_based_on_event

Example using structure relative to event.target¶

  • The element that was the target of the event is referenced from the event object that is sent to the event listener.
  • Using an arrow function expression to create an anonymous listener function:
element.addEventListener('click', (event) => {
    event.target.innerHTML = "I am the target.";
});
  • Using a named function as event listener instead:
function changeTargetText(event) {
    event.target.innerHTML = "I am the target.";
}

element.addEventListener('click', changeTargetText);

Element relatives¶

  • Get the first element from a selector result:
let myElement = document.querySelector(".box p");
  • Get an element's relatives:
myElement.firstElementChild;
myElement.lastElementChild;
myElement.parentElement;
myElement.nextElementSibling;
myElement.previousElementSibling;

Use query on element¶

  • Both .querySelector() and .querySelectorAll() can be called from an element.
  • Example: We get the first element with the class box that is a descendant of an element with the class left:
let element = document.querySelector('.left .box')
  • Then we find the closest descendant of that element that is a paragraph (a p element):
element.querySelector('p')

Find closest ancestor matching selector¶

element.closest(selector)
  • Example: We get the first paragraph that is a descendant of the element with the id left-panel:
let element = document.querySelector("#left-panel p");
  • Then we find the losest ancestor of that paragraph with the class header
element.closest('.header')

Example: Navigate DOM from event.target


https://codepen.io/jodyfoo/pen/YzrKOgP

Clicking on nested elements


Event Bubbling

Scenario¶

  • If we have a nested structure, which element should we add our event listener to?

Event flow: capture, target, bubble¶

  • capture phase: event object propagates from the top (window) down to the target
  • target phase (at-target): event object arrives at target
  • bubble phase: event object propagates up the hierarchy ending at the window

Event flow: capture and bubbling¶

Listening for events¶

  • Listening handlers are notified during the bubble phase by default
  • Using options to .addEventListener() we can e.g.
    • listen during capture
    • only listen once
  • Handlers can also invoke the Event.stopPropagation() to e.g. prevent handlers on ancestors from activating or to conserve resources (e.g. in a game with a lot of clicks)

Demo, event bubbling¶

  • https://www.ida.liu.se/~729G87/course-material/lectures/examples/card-bubble/card.html
  • Colors
    • body: peachpuff
    • .card: orange
    • .background: pink
    • .description: lightgreen
    • p: lightblue
  • What to look at:
    • how events bubble
    • mouseover/mouseout - event bubbles up
    • mouseenter/mouseleave - event does not bubble (best practice; less resource intensive)

Alternative to adding listeners to each child: adding a single event listener on a ancestor¶

  • Listen for clicks on document that bubble up.
document.addEventListener('click', (event) => {
    // element.matches(selector) returns true if element matches the selector
    if (event.target.matches('button')) {
        // code for button
    } else if (event.target.matches(".circle")) {
        // code for circle
    } else if (event.target.matches(".box")) {
        // code for box
    }
});
  • Use this only when it makes sense. Don't use this as your default solution.
  • Unnecessary resource use if it manages lots of elements compared to only the appropriate smaller handler being called when needed.

Example: ancestor event listener + creating elements


https://codepen.io/jodyfoo/pen/ZEXzmaW

JS Trick: data-* attributes¶

  • We can add data-* attributes in our HTML code, e.g
<div class="card" data-date-of-birth="Nov 1" data-name="Cleo">...</div>
  • And then access all of them via the element.dataset property (both bracket and .-syntax are possible)
let card = document.querySelector(".card");
console.log(card.dataset["dateOfBirth"]);
console.log(card.dataset.dateOfBirth);
console.log(card.dataset["name"]);
console.log(card.dataset.name);
  • Note: Keys are subject to dash-style to camelCase conversion.
  • Use-case: Storing information for various use in scripts.

Creating & removing elements using JavaScript¶

Creating elements¶

  • Create new elements using
let newElement = document.createElement(elementName : String)
  • Add it to the DOM using e.g.
// add newElement as last child of parentElement:
parentElement.append(newElement) 
// insert newElement before siblingElement:
siblingElement.before(newElement)
  • Remove an element
element.remove()

Animations with anime.js¶

About anime.js¶

  • https://animejs.com
  • "Anime.js (/ˈæn.ə.meɪ/) is a lightweight JavaScript animation library with a simple, yet powerful API."
  • Open source, well documented + many examples available.
  • Can animate
    • CSS properties
    • SVG
    • DOM attributes
    • JavaScript Objects
  • More flexible than CSS animations.

Setup¶

  • Download minified js-file (anime.min.js) from
    • https://animejs.com
  • Link to it before your JavaScript code.

Create an animation¶

  • anime(options) returns an animation object and executes the animation
  • Where options is a JavaScript object containing e.g.
    • targets - element(s) to animate
    • properties - what to animate
    • animation parameters - how the animation is played
    • easing - rate of change of a parameter over time
  • options can also contain e.g.
    • animation parameters (direction, loop, autoplay)
    • duration

Object data type¶

  • Objects have properties.
  • Properties are accessed using dot notation: object.property
  • Properties can also be accessed using square bracket notation:
    • object["property name"]
  • Objects can also have functions that can behave as methods.
  • Functions in objects are called using dot notation: object.function()
  • Similar to both instances in general and dictionaries in particular in Python.
  • Similar to records or tagged types in Ada.

Object example¶

  • Here we use an Object literal to explicitly describe what properties the Object should have.
In [ ]:
let image = {
    
    filename: "cat1.jpg",
    
    sayHello: function() {
        console.log("Hello!");
    }
    
}

console.log(image.filename);
image.sayHello();

Animation targets¶

  • CSS selector (String)
  • DOM Node or NodeList
  • Example of targets (incomplete options)
anime( {targets: '.box' } )
anime( {targets: element })
anime( {targets: elements })
anime( {targets: document.querySelector('#left-panel') })

Animating CSS properties¶

  • Animate change of the style property top. The .box elements start with top: -58vh
anime({
 targets: '.box',
 top: '0', // animate top CSS property
 color: '#F0F', // animate color CSS property
 duration: 5000
});
  • See: https://animejs.com/documentation/#cssProperties
  • Example: https://codepen.io/jodyfoo/pen/LYzPgzG

Animate when clicked¶

  • We can define functions that apply specific animations that we can apply to any element later:
function fly(element) {
    anime({
        targets: element,
        top: '-58vh',
        duration: 2000
    });
}

// Apply `fly` animation to `.box` elements on click
document.querySelectorAll(".box").forEach((box) => {
    box.addEventListener('click', (event) => {
        fly(event.target);
    });
});
  • Example: https://codepen.io/jodyfoo/pen/WNZeaJg

Animating transforms¶

  • Especially powerful when combined with CSS transformations like translate, scale, rotate, etc.
anime({
    targets: '.box',
    translateX: '80vmin', // move along x-axis
    rotate: '1turn',
    duration: 5000 // duration is in milliseconds
});
  • https://animejs.com/documentation/#CSStransforms
  • https://developer.mozilla.org/en-US/docs/Web/CSS/transform
  • Example: https://codepen.io/jodyfoo/pen/eYGOPLq

Animation parameters - configure how the animation will play¶

anime({
    targets: '.box',
    translateX: '20vw',
    rotate: '1turn',
    duration: 2000,
    direction: 'alternate', // 0% -> 100% -> 0%
    loop: true // loop animation
});
  • https://animejs.com/documentation/#direction
  • https://animejs.com/documentation/#loop
  • Example: https://codepen.io/jodyfoo/pen/xxXKyNO

Easings - rate of change over time¶

  • Easings affect the rate of change in the animation.
  • E.g. start slow and accelerate, or start slow, accelerate and also slow down before end.
anime({
    targets: '.box',
    translateX: '31vw',
    duration: 2000,
    easing: 'linear',
    //easing: 'easeInCubic',
    //easing: 'easeInOutSine',
    //easing: 'easeInOutBack',
    direction: 'alternate',
    loop: true
});
  • https://animejs.com/documentation/#linearEasing
  • https://easings.net/
  • Example: https://codepen.io/jodyfoo/pen/KKXPGOL

Stopping animation¶

  • remove animation completely from targets
anime.remove(targets)
  • pause/resume animation
// `animation` is the animation object returned by `anime()`
animation.pause()
animation.play()
  • run when animation is done
    • set complete option to a callback function or use
anime({...}).finished.then(callback)

Doing something when the animation is done¶

  • Use complete option
anime({
    ...
    complete: callback
}
  • Or use JavaScript "promise" feature
anime({...}).finished.then(callback)
  • Promise using reference to the animation
let animation = anime({...});
animation.finished.then(callback);
  • Note: callback is a function

Click and hide¶

function hide(element) {
    anime({
        targets: element,
        opacity: 0,
        duration: 2000,
    }).finished.then(() => {
        element.classList.add("hidden");
    });
}
document.querySelectorAll(".box").forEach((box) => {
    box.addEventListener('click', (event) => {
        hide(event.target);
    });
});
  • Example using complete property: https://codepen.io/jodyfoo/pen/mdBbzzB
  • Example using promise: https://codepen.io/jodyfoo/pen/abLoRxN