JavaScript and the browser

Table of Contents

The DOM

  • Based on the received HTML document the browser builds up a model of the document structure, the Document Object Model (DOM)
  • The DOM has a tree structure and each node of the document is represented by an object
  • The browser renders the page based on the DOM and provides an API to read and modify the DOM with JS
  • The DOM is live, i.e., DOM modifications are immediately reflected on the page

The DOM Tree

<!doctype html>
<html>
  <head>
    <title>Todo App</title>
  </head>
  <body>
    <h1>Todo App</h1>
    <p>Don't forget anything!</p>
    <p>Please <a href="/login">login</a>.</p>
  </body>
</html>

the Tree Structure of the html above:

you can traverse the DOM Tree with:

// Root node of the dom tree (html)
const root = document.documentElement;

// The child elements of a node (only element nodes, no text nodes)
// -> HTMLCollection(2) [head, body]
root.children;

// head and body can also be accessed directly
const head = document.head;
const body = document.body;

// All children of a node (including text nodes)
// -> NodeList(7) [\#text, h1, \#text, p, \#text, p, \#text]
body.childNodes;

// Accessing children and node type
body.childNodes[0].nodeType === Node.TEXT_NODE;      // \#text
body.childNodes[1].nodeType === Node.ELEMENT_NODE;   // h1

body.firstChild;                                     // \#text
body.firstElementChild;                              // h1
// dito for last: lastChild, lastElementChild

Another example:

const h1 = document.body.children[0];

// Parent node
h1.parentNode;

// Siblings
h1.nextSibling;                                     // \#text
h1.nextElementSibling;                              // p
// dito for previous: previousSibling, previousElementSibling

// Example of recursively traversing the DOM
function findAnchors(node) {
  if (node.tagName === 'A') {
    console.log(node.textContent+": "+node.pathname);
  }
  if (node.nodeType === Node.ELEMENT_NODE) {
    for (let i = 0; i < node.children.length; i++) {
      findAnchors(node.children[i]);
    }
  }
}
findAnchors(document.body);

Finding Elements

It is often not a good idea to access a specific node by a fixed path since the document structure may change over time:

// Get the href attribute of the first anchor
document.body.childNodes[5].childNodes[1].href;

There are a number of functions to find DOM elements in a more sophisticated way. The functions can be called on the document or on any element node to reduce the search on the node’s descendants

// Get the href attribute of the first anchor
document.getElementsByTagName('a')[0].href;

more examples of finding elements:

// Get a HTMLCollection of nodes by tag name
document.getElementsByTagName('a');

// Get a HTMLCollection of nodes by class name
document.getElementsByClassName('external-link');

// A single node can be retrieved by id (only via document)
document.getElementById('logo');

// Elements can also be selected by using a CSS selector string
// A static NodeList is returned
document.querySelectorAll('p a.external-link');

// Like querySelectorAll but only the first matching node is returned
document.querySelector('#logo');

Element nodes

  • For different HTML elements (body, div, p, a, etc.) different objects with different properties and methods are created
  • For example, an anchor object has a href and pathname property, whereas a form object has a submit and reset method
  • w3schools provides a nice reference http://www.w3schools.com/jsref/default.asp
const a = document.querySelector('a');
console.log(a.href);
 // -> ’https://www.todo-app.com/login’
console.log(a.pathname);
 // -> ’/login’
const f = document.getElementById('myForm');
f.reset();
 // -> resets all fields of the form
f.submit();
 // -> submits the form

Manipulating the DOM

  • Almost everything in the DOM can be manipulated with JavaScript:
    • Single node properties can be changed
    • Nodes can be moved within the tree
    • Nodes can be removed 
    • New nodes can be added
const h1 = document.querySelector('h1');
// Change the color style property to ’#FF0000’
h1.style.color = '#FF0000';
// Change the display style property to ’none’
h1.style.display = 'none';
// Change the text of the h1
h1.childNodes[0].nodeValue = "Hello Earth!";
// Or more comfortable by using the textContent property (all children of h1
// are removed and and a new text node with the respective text is added)
h1.textContent = "Hello World!";

another example:

const myList = document.querySelector('ul');
// Nodes can be moved by simply re-inserting them at a different position
// -> The last node of the list is moved to the beginning of the list
myList.insertBefore(myList.lastElementChild, myList.firstElementChild);
// An element is removed by calling remove() on the node
// -> Removes the last element from the list
myList.lastElementChild.remove();
// New element and text nodes can be created. Newly created nodes must be
// added to the DOM tree explicitly. e.g. using appendChild()
// -> Creates a new list item and appends it to the end of the list
const li = document.createElement('li');
li.appendChild(document.createTextNode("New item"));
myList.appendChild(li);
// Nodes can also be created implicitly by using the innerHTML property
li.innerHTML = "An <b>important</b> item";

Animation

  • The setTimeout function waits a given number of milliseconds and then calls a function
  • setTimeout can be used to implement an animation
// The background color shall change between red and blue every second
const ball = document.querySelector('#ball');
let toggle = false;
// The animation function
function animate() {
ball.style.backgroundColor = toggle ? '#f00' : '#00f';
toggle = !toggle;
// Keep the animation runnning
setTimeout(animate, 1000);
}
// Start the animation
setTimeout(animate, 1000);
  • setInterval is similar to setTimeout but calls the function every interval and not only once
  • clearInterval and clearTimeout clear a timer set with setInterval and setTimeout respectively
const ball = document.querySelector('#ball');
let toggle = false;
function animate() {
ball.style.backgroundColor = toggle ? '#f00' : '#00f';
toggle = !toggle;
}
// Start the animation
const id = setInterval(animate, 1000);
// Stop the animation after 10s
setTimeout(function(){
clearInterval(id);
}, 10000);
  • For smooth animations use requestAnimationFrame
  • The scheduled animation function is called when the browser is ready to repaint the screen (at a rate of about 60 times per second but only when the window/tab is active)
  • By using requestAnimationFrame the browser can optimize concurrent animations
const ball = document.querySelector('#ball');
let angle = Math.PI / 2;
function animate(time, lastTime) {
if (lastTime) {
// Compute the next position based on the elapsed time
angle += (time - lastTime) * 0.001;
}
ball.style.left = (Math.cos(angle) * 200) + 'px';
requestAnimationFrame(function(newTime) { animate(newTime, time); });
}
requestAnimationFrame(animate);

Event handling

  • Normally, a GUI must not only provide data to the user but also react to user input 
  • A user interacts with the GUI by key strokes, mouse clicks, mouse moves, etc.
  • There are also implicit user interactions like window scrolling, window resizing, etc.
  • To react programmatically to user input, one could constantly read the state of the input device or adopt a polling mechanism
  • A better approach is a system that actively notifies registered listeners, also called handlers, about user inputs by firing events
  • Event handlers are registered for a certain event type using addEventListener and removed using removeEventListener
  • Event handlers are always registered in a context and are only called for events in that context
  • Event handlers can be registered on the window or on any DOM element
  • Multiple handlers can be registered for the same event and the same handler can be registered for multiple events
// An event handler can be registerd for a certain event type
window.addEventListener('click', function() {
console.log("That's amazing!");
});
window.addEventListener('resize', function() {
console.log("Window has been resized");
});
// Event handlers can be registerd on DOM elements to confine the context
const myButton = docuemnt.querySelector('#my-button');
myButton.addEventListener('click', function(){
console.log('What a surprise!');
});
// Event handlers can also be removed again
function once() {
console.log('Done.');
window.removeEventListener('click', once);
}
window.addEventListener('click', once);

Event Object

  • Most events are propagated through the DOM tree, thus event handlers registered on an ancestor nodes are called too
  • Default propagation behaviour is bubbling (the more specific handler, the earlier it is called)
  • Handlers can also be registered in the capturing phase (before the bubbling phase)
  • The propagation of an event can be stopped by any handler by calling stopPropagation on the event object

Event Propagation

// Registering a click event handler on the body and a button
document.querySelector('body').addEventListener('click', function(e){
console.log("Handler on body (bubbling)");
});
document.querySelector('button').addEventListener('click', function(e){
console.log("Handler on button (bubbling)");
});
// Clicking on the button results in the following output:
// -> Handler on button (bubbling)
// -> Handler on body (bubbling)
// Register an event handler in the capturing phase
document.querySelector('body').addEventListener('click', function(e){
console.log("Handler on body (capturing)");
}, true);
// Clicking now on the button results in the following output:
// -> Handler on body (capturing)
// -> Handler on button (bubbling)
// -> Handler on body (bubbling)
// The propagation of an event can be stopped by an event handler
document.querySelector('button').addEventListener('click', function(e){
console.log("Handler on button (bubbling)");
e.stopPropagation();
});
// Clicking on the button results in the following ouput:
// -> Handler on body (capturing)
// -> Handler on button (bubbling)

Default action

  • Some events have associated default actions:
    • clicking on a link loads the link target
    • clicking on a submit button submits the form
    • pressing the down arrow scrolls the page down
  • Event handlers are called before the default behaviour takes place
  • If the default action is unwanted, then the event handler can call preventDefault on the event object
// The default action can be prevented by a handler
document.querySelector('a').addEventListener('click', function(e) {
e.preventDefault();
alert("Sorry, but we don't want you to leave!");
});

AJAX

  • AJAX (Asynchronous JavaScript and XML) is used to create faster and more interactive web applications
  • AJAX enables JavaScript to load data from the server without reloading the page
  • On the protocol level, AJAX requests are just a normal HTTP requests
  • The XMLHttpRequest (XHR) object is the interface for AJAX requests
  • Despite the term XML in AJAX and XHR, any type of data can be loaded from the server
  • ES6 introduced the new Promise based Fetch API
// Create a new XMLHttpRequest object
const xhr = new XMLHttpRequest();
// Specify the type of request: method and url
xhr.open('GET', '/ajax/time');
// Register a callback to be notified about state changes
xhr.onreadystatechange = function() {
  // Check ready state: 4 means request completed
  if (this.readyState === 4) {
    // Check HTTP status: 200 OK
    if (this.status === 200) {
      // Update DOM with the response text
      document.getElementById('time').innerHTML = this.responseText;
    } else {
      // Error handling...
      console.log("Something failed!");
    }
  }
};
// Send the request
xhr.send();

AJAX and JSON

  • The property responseText of the XMLHttpRequest object contains the HTTP response body as a string
  • If the response is an XML document, the data can be accessed directly by the responseXML property
  • If the response is a JSON document, the JSON string must be parsed manually
// Retrieving a JSON document
const xhr = new XMLHttpRequest();
xhr.open('GET', '/ajax/data.json');
xhr.onloadend = function() {
  if (this.status === 200) {
    // Parse the responseText into an object
    const obj = JSON.parse(this.responseText);
    console.log(obj.name+" is "+obj.age+" years old.");
  } else {
    console.log("Error...");
  }
};
xhr.send();

Same-Origin Policy

  • The same-origin policy is a browser security feature that restricts how documents and scripts from one origin can interact with resources from another origin
  • Two URLs have the same origin if the protocol, host, and port are the same
  • Generally, embedding a cross-origin resource using tags such as img, link, and script is permitted
  • Accessing a cross-origin resource using an AJAX request is blocked by default
  • Using CORS headers, a web server can explicitly allow certain hosts to access its resources

jQuery

”jQuery is a fast, small, and feature-rich JavaScript library” / jquery.com

  • jQuery simplifies
    • element selection
    • DOM traversal/manipulation
    • event handling
    • animation
    • AJAX requests
  • Provides an easy-to-use API
  • Is tested on all major browsers
  • Was initially released in 2006 and used by 80% of the top 100k websites in 2022 (builtwith.com)

jQuery usage

jQuery can be downloaded and included by a single script tag:
<script src="js/jquery.js"></script>
jQuery can also be loaded directly from jQuery or a CDN:
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script src="https://unpkg.com/jquery"></script>

Example:

// On document ready...
$(function() {
  // Set the css property color of each div to red
  $('div').css('color', 'red');

  // Load some content for each selected element
  $('.content').each(function(){
    $(this).load('/ajax/content', {id: this.id});
  });

  // Register a click event handler. On each click a paragraph is added
  // to the article element
  $('#add-content').click(function() {
    $('article').append($("<p>More content...</p>"));
  });

  // Make an AJAX request and insert the received HTML into the DOM
  $.get('/ajax/test.html', function(data) {
    $('#result').html(data);
  });
});

There is no magic!

  • $ is an alias for jQuery, which is a function and thus an object
  • $(...) is equal to jQuery(...) and is just a function call
  • $.abc() calls the jQuery’s static utility function abc
// From jQuery’s source code: jQuery is a function and $ an alias
var jQuery = function(slector, context) {
// ...
};
window.jQuery = window.$ = jQuery;

The jQuery Object

  • A jQuery object references one or more DOM nodes
  • jQuery objects have methods to manipulate and to access these nodes
  • Manipulations are applied to each DOM node referenced by the object
  • jQuery object methods are chainable

$('div')
.css('background-color', '#f00')
.addClass('whatever')
.click(function(){ console.log("Clicked!"); })
.first()
.fadeOut(400);

jQuery and AJAX

  • jQuery has great support for AJAX
  • Supported data types are XML, HTML, Script, JSON, JSONP, and TEXT
  • Besides the basic $.ajax() function, there are a number of convenient shorthand functions:
    • $.get()
    • $.post()
    • $.getJSON()
    • .load()
  • Provides simplified callback handling (success/error)
  • In addition, the jqXHR object implements the Promise interface
// AJAX example with success and error callback
$.ajax({
  url: "/ajax/time",
  type: "GET",
  dataType: "text",
  success: function(data){ $("#time").html(data); },
  error: function(jqXHR, status, msg){ $("#error").html("Error! "+msg); }
});

// GET example with success callback
$.get("/ajax/time", function(data){ $("#time").html(data); });

// Load example
$("#time").load("/ajax/time");

// POST example with success callback and JSON response
$.post(
  "/ajax/get_person_data",
  $('#form').serialize(),    // request data
  function(person) { $('#person').html(person.name+" is "+person.age); },
  "json");