The AJAX Saga

But first, more DOM stuff...

Modifying the page after page load

Text Operations

  • Each HTML tag contains either more HTML or a 'text node'
  • We can get text content like so:
var content = document.getElementById('whatever').textContent;

var $content = $('#whatever').text();
  • We can also set the content of the text node like so:
document.getElementById('whatever').textContent = "Hello World";

$('#whatever').text('Hello World');

HTML Operations

  • We can set the HTML content like so:
document.getElementById('whatever').innerHTML = "<ul class=\"messages\">"
 + "<li>Hello World</li>"
 + "</ul>";

$('#whatever')
    .html('<ul class="messages"><li>Hello World</li></ul>');
  • The setter methods listed so far DESTROY the content of the target and replace it with new content.
  • What if we don't want to destroy it, but simply add to it??
  • We can get the HTML content like so:
var content = document.getElementById('whatever').innerHTML;
var elementAndContent = document.getElementById('whatever').outerHTML;

var $content = $('#whatever').html();
var $elementAndContent = $('#whatever').prop('outerHTML'); // Beware SVGs

Creating Elements

  • Elements can be created either as a
    1. string
    2. element object
    3. using jQuery main function
// 1
document.getElementById('something').innerHTML = '<p>Hello</p>';

// 2
var newContent = document.createElement('a');
newContent.textContent = "Go to documents";
newContent.createAttribute('href', '....');

// 3
var $newPara = $('<p>Hi ' + name + '</p>');

Additive HTML Operations

  • There are various methods for adding content in and around HTML
var el = document.getElementById('whatever');
var parent = el.parentNode;

var newContent = document.createElement('p');
newContent.textContent = "After";

var newPreContent = document.createElement('p');
newPreContent.textContent = "Before";

el.appendChild(newContent); 
var firstLi = document.querySelectorAll('ul li')[0];
list.insertBefore(newPreContent, firstLi);

$('#whatever').append('<p>After</p>');
$('#whatever').prepend(newPreContent);

Additive HTML Operations

  • DOM manipulation is expensive! Try to do it as little as possible
  • You can create a document fragment and add all you HTML to that
  • When you append, the fragment melts away
var element  = document.getElementById('ul'); // assuming ul exists
var fragment = document.createDocumentFragment();
var browsers = ['Firefox', 'Chrome', 'Opera', 
    'Safari', 'Internet Explorer'];

browsers.forEach(function(browser) {
    var li = document.createElement('li');
    li.textContent = browser;
    fragment.appendChild(li);
});

element.appendChild(fragment);

el.append() vs el.appendChild()

var element  = document.getElementById('ul'); // assuming ul exists
var fragment = document.createDocumentFragment();
var browsers = ['Firefox', 'Chrome', 'Opera', 
    'Safari', 'Internet Explorer'];

browsers.forEach(function(browser) {
    var li = document.createElement('li');
    li.textContent = browser;
    fragment.appendChild(li);
});

element.appendChild(fragment);

WTF?

  • There are various methods for adding content in and around HTML
var para = document.createElement('p');
para.textContent = "Hello";
document.body.innerHTML = para;

When you try to pass an object to a property that expects a string, it calls that object's toString method and inserts the output

Add HTML around the node

var el = document.getElementById('whatever');

// There is also 'afterstart' and 'beforeend' to append and prepend
el.insertAdjacentHTML('beforestart', '<p>Before</p>');
el.insertAdjacentHTML('afterend', '<p>After</p>');

$('#whatever').before('<p>Before</p>');
$('#whatever').after('<p>After</p>');

Wrapping a node:

// Wrap an element
$("#target").wrap("<div class='new'></div>");

// Wrap a series of elements
$("li .inner").wrapAll("<div class='new' />");

// Wrap the contents of a selection of elements
$("#target").wrapInner("<div class='new'></div>");

Wrap can also take a function to construct layers of wrapping html

Other HTML functions

var el = document.getElementById('whatever');
var parent = el.parentNode;

parent.removeChild(el); // returns the removed node

var $el = $('#whatever');
var $parent = $el.parent();

var n = $parent.remove($el); // doesn't keep jQuery data on these nodes
var $m = $parent.detach($el); // does, so you can re-insert the node

Cloning a node:

var el = document.getElementById('whatever');
var clone = el.cloneNode(true); // true for deep cloning
document.getElementById('receiver').appendChild(clone);

var $clone = $('#whatever').clone();
$('#receiver').append($clone);

Removing a node:

Get/Set Values from an Input

// <input type="text" id="myTextBox"> 
var myInput = document.getElementById('myTextBox');
var value = myInput.value;
myInput.value = '10';

var $myInput = $('#myTextBox');
var $value = $myInput.val();
$myInput.val('10');

When using forms:

  1. Bind listeners to the form's 'submit' event
  2. e.preventDefault() in the listener, to stop the form submitting
  3. Get the data from the form attributes (serializing):
    1. vanilla JavaScript
    2. jQuery: serialize (or serializeArray - diff is format returned)

Get/Set Attributes

// <input type="text" placeholder="Enter name" name="name" id="name">
var button = document.getElementById('toggler');
var $jqButton = $('#jq-toggler');
var input = document.getElementById('name');
var $input = $(input);

button.addEventListener('click', function(e) {
  var isDisabled = input.getAttribute('disabled');
  alert(!!isDisabled);
  if (!!isDisabled) {
    input.removeAttribute('disabled');
  } else {
    input.setAttribute('disabled', 'disabled');
  }
});

$jqButton.on('click', function($e) {
  var isDisabled = $input.prop('disabled');
  alert(!!isDisabled);
  if (!!isDisabled) {
    $input.prop('disabled', false);
  } else {
    $input.prop('disabled', true);
  }
});

Beware attr() vs prop() in jQuery

Attaching Data to Nodes

// <button id="buy" data-price="1.24">Buy</button>
var $buyButton = $('#buy');
var buyBtn = document.getElementById('buy');

// GETTER
var price =$buyButton.data('price');
var price = buyBtn.dataset.price
console.log(price); // "1.24"


// SETTER
$buyButton.data('price', '6.50');
buyBtn.dataset.price = '6.50';
console.log($buyButton.data('price')); // "6.50"

A Problem with HTML Insertion

Order of occurance:

  1. HTML Parsed
  2. CSS Bound
  3. DOM Created (DOMContentLoaded)
  4. JavaScript event listeners bound
  5. Ajax calls
  6. HTML inserted into the page
  7. But how do you bind event listeners to that HTML?

Delegated Events

<ul class="carsList"> <!-- Here when JS was bound -->
    <li>Ferarri <button class="delete">&times;</button></li> <!-- Templated in later -->
    <li>Bugatti <button class="delete">&times;</button></li><!-- Templated in later -->
</ul>
// Get the parent DIV, add click listener...
document.querySelectorAll(".carsList")[0].addEventListener("click", function(e) {
    // e.target was the clicked element (unless <i>, then check parent too!
    if (e.target && e.target.matches("button.delete")) {
        // Delete button was clicked, so do something...
    }
});
// Get the parent UL, add click listener...
$(".carsList").on("click", '.delete', function(e) {
    // binds on page load to existing <ul>
    // Second parameter selects the internal element (i.e. the delete buttons in the <li>s
    // 'this' will be bound to the target button
});

jQuery

Templating

Manufacturing HTML

Bad:

var person = { name: 'James', age: 39, height: 1.75 };

// Normal JS
var profileFrag = document.createElement('DIV');
profileFrag.className = "profile";
profileFrag.innerHTML = "<dl><dt>Name:</dt><dd>" + person.name + 
"</dd><dt>Age:</dt><dd>" + person.age + "</dd><dt>Height:</dt><dd>" +
person.height + "</dd></dl>";
document.getElementById("target").innerHTML = profileFrag;

// jQuery
$('#target').html('<div class="profile"><dl><dt>Name:</dt><dd>' + 
person.name + '</dd><dt>Age:</dt><dd>' + person.age + '</dd><dt>Height:</dt><dd>' +
person.height + '</dd></dl></div>');
  • Not easy to read
  • Not easy to maintain
  • Purpose is not always clear

What is Templating?

Good:

<div class="entry">
  <h3>{{title}}</h3>
  <div class="body">
    <p>{{detailText}}</p>
  </div>
</div>

It is cleaner if I can use the above, providing context 

(i.e. the values that go in the 'holes')

Warning: ES6 Template Strings are designed to replace this technology!

Different Templating Libraries

Handlebars Demo

Handlebars Guide

Task

  • Create a collection of your favourite <somethings>
  • Use jQuery/vanilla JavaScript and Handlebars to insert a list of them into the page

AJAX

Ramifications of being able to edit the page after load

  • This means that we don't have to send all the data in one go.
  • This means that we can get data, turn it into appropriate HTML and insert it into the page
  • But how?
  • How do we call servers for information after the page has loaded??

AJAX

What is it?

  • AJAX is a way to call a server to get data (inc. HTML)
  • AJAX stands for 'Asynchronous JavaScript and XML'
    • 'Asynchronous' we already know
    • JavaScript - well, duh!
    • XML? XML is a transport language, the same as JSON. In this context it often replaced by JSON, but at the time it was the way that it was done.
      • XML looks like this:
<!-- Typical (simple) XML -->
<person>
  <name>James</name>
  <age>38</age>
</person>

Look familiar?? That's because HTML is a subset of XML. i.e. It's XML that we've agreed as a way to represent web page elements.

These days we use JSON

  • JSON stands for 'JavaScript Object Notation'
  • It is a transport language for javascript entities
var tutors = [
 {name: 'frank', age: 23, isQualified: false},
 {name: 'tom', age: 37, isQualified: true}
];

var tutorData = JSON.stringify(tutors);

console.log(tutorData); 
// [{"name": "frank", "age": 23, "isQualified": false}, ...etc.];

Rules for JSON

  • https://www.json.org/
  • All keys are in double quotes ("")
  • All strings are in double quotes ("")
  • No object methods!
  • Other datatypes are fine as they are
  • Cannot have circular references
    • e.g. ingredient -> meal -> recipe -> ingredient

Transforming Data

Javascript to JSON

var person = {name: 'fred'};
var personJSON = JSON.stringify(person);

// personJSON: "{"name": "fred"}"
var personJSON = '{"name": "fred"}';
var person = JSON.parse(personJSON);

// person: {name: 'fred'}

JSON to JavaScript

Stages of a Call

readyState

Value State Description
0 UNSENT Client has been created. open() not called yet.
1 OPENED open() has been called.
2 HEADERS_RECEIVED send() has been called, and headers and status are available.
3 LOADING Downloading; responseText holds partial data.
4 DONE The operation is complete.

Normal Syntax

var xmlhttp = new XMLHttpRequest();

xmlhttp.onreadystatechange = function() {
  console.log('state changed', xmlhttp.readyState);
    if (xmlhttp.readyState === XMLHttpRequest.DONE ) {
       if (xmlhttp.status === 200) {
         console.log('response: ', xmlhttp.responseText);
         console.log('parsed: ', JSON.parse(xmlhttp.responseText));
       }
       else if (xmlhttp.status === 404) {
          alert('The resource was not found');
       }
       else {
           alert('Error: Call to server failed with code: ' + xmlhttp.status);
       }
    }
};

xmlhttp.open("GET", "https://jsonplaceholder.typicode.com/users/1", true);
xmlhttp.send();

Note that you have to parse the response before using it JSON.parse(response);

jQuery Syntax

$.ajax({
    method: 'GET', //optional. GET (default), POST, PUT, DELETE, etc.
    context: document.body, //optional. Scopes the response to a part of your page
    data: {}, // <-- body of the request goes here
    url: '//api.github.com/users/jmsherry'
}).done(function(response){
    console.log(response);
}).fail(function(error){ //just to show you how to error catch
  console.error(error);
}).always(function(){ //this happens after EVERY ajax call
  console.info("ajax call made");
});

jQuery takes care of all of that, including parsing the JSON response.

fetch Syntax

fetch('http://example.com/cats.json', {
    body: JSON.stringify(data), // must match 'Content-Type' header
    cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached
    credentials: 'same-origin', // include, same-origin, *omit
    headers: {
      'user-agent': 'Mozilla/4.0 MDN Example',
      'content-type': 'application/json'
    },
    method: 'POST', // *GET, POST, PUT, DELETE, etc.
    mode: 'cors', // no-cors, cors, *same-origin
    redirect: 'follow', // *manual, follow, error
    referrer: 'no-referrer', // *client, no-referrer
  })
  .then(function(response) {
    return response.json();
  })
  .then(function(response) {
    console.log(response);
  })
  .catch(function(error) {
    console.error(error);
  });

axios Syntax

  axios.post('/user', {
    firstName: 'Fred',
    lastName: 'Flintstone'
  })
  .then(function (response) {
    console.log(response);
  })
  .catch(function (error) {
    console.log(error);
  });

Can be used client and server side

TASK

Let's combine these two technologies (templating and AJAX):

  1. Make an ajax GET request to https://jsonplaceholder.typicode.com/users
  2. Get the data back and insert it into the page so that there is a list of users shown, with their contact details and a section for their company

 

Maybe you can try it with your github account: call https://api.github.com/users/jmsherry

Homework Task:

  1. Use your api server from week 4
  2. Have it send a webpage which
    1. has a list of your friends (or cars, or whatever)
    2. has an add/update form in the page
      1. For add, the fields will be blank
      2. For update they will have values to be updated
  3. Make it so you can operate your API via the page
  4. Deploy to Heroku [optional, if time permits]