The projects we have been demonstrating and working with so far have alle been of the form where selecting a menu item induced requesting a new page from the server. Working with APIs we are edging towards one page solutions, where user interaction means fetching data from the server in order to fit it into the page already on ther users screen. The keyword is AJaX. You have worked with it before.
In this chapter we shall work at providing data to AJaX requests and fitting them into some of the pages of the Node.js/Express application we're building. We may even have data that we can supply publicly to others building their own applications.
In short we shall create an API, and we shall look at being consumers of that API. For demonstration we shall take the context of the world database that we have been working with for a while. Let us start with the menu.
assa0/views/layout.pug
doctype html
html
head
title= title
link(rel='icon' type='image/svg+xml' href='favicon.svg')
link(rel='stylesheet', href='/stylesheets/style.css')
script(type='module' src='/javascripts/page.js')
body
header
nav
ul
li
a(href="/") Home
li
a(href="/worldview") World View
main
block content
assa0/views/worldview.pug
extends layout
block content
h1= title
h2= subtitle
main
div#continents
button#gcont Get the Continents
div#contdata
div#countries
div#countdata
p Countries will be here
div#cities
div#citydata
p Kilroy will be here
assa0/routers/index.js
const express = require('express');
const router = express.Router();
const modContinent = require("../models/handleContinents");
const modCountry = require("../models/handleCountries");
/* GET home page. */
router.get('/', function(req, res, next) {
res.render('index', {
title: 'Fragments of the World',
subtitle: 'Playing with the World'
});
});
router.get('/worldview', async function(req, res, next) {
res.render('worldview', {
title: 'Fragments of the World',
subtitle: 'Start by Choosing a Continent'
});
});
router.get('/continents', async function(req, res, next) {
let continents = await modContinent.getContinents({}, {sort: {name: 1}});
res.json(continents);
});
router.get('/countries/:cont', async function(req, res, next) {
let countries = await modCountry.getCountries({continent: req.params.cont}, {sort: {name: 1}});
res.json(countries);
});
module.exports = router;
assa0/public/javascripts/page.js
"use strict";
import {$} from "./modules/nQuery.js";
import {Ajax} from "./modules/Ajax.js";
/*
* Event handler for button - create ajax object and get data
*/
const getContinents = function(ev) {
let req = Object.create(Ajax);
req.init();
req.getFile("/continents", showContinents);
};
const getCountries = function(ev) {
let req = Object.create(Ajax);
req.init();
req.getFile(`/countries/${ev.target.value}`, showCountries);
};
/*
* callback function for the above AJaX
*/
const showContinents = function(e) {
/*
* here you put the ajax response onto your page DOM
*/
console.log(e.target.getResponseHeader("Content-Type"));
let element = $("contdata");
while (element.firstChild) {
element.removeChild(element.firstChild);
}
let div = document.createElement("div");
let h3 = document.createElement('h3');
let txt = document.createTextNode('The Continents');
h3.appendChild(txt);
div.appendChild(h3);
let continents = JSON.parse(e.target.responseText);
let sel = document.createElement('select');
sel.setAttribute('id', 'chooseContinent');
sel.addEventListener('change', getCountries);
continents.forEach(function(continent) {
let opt = document.createElement('option');
let opttext = document.createTextNode(continent.name);
opt.appendChild(opttext);
sel.appendChild(opt);
});
div.appendChild(sel);
$("contdata").appendChild(div);
}
const showCountries = function (e) {
/*
* here you put the ajax response onto your page DOM
*/
console.log(e.target.getResponseHeader("Content-Type"));
let element = $("countdata");
while (element.firstChild) {
element.removeChild(element.firstChild);
}
let div = document.createElement("div");
let h3 = document.createElement('h3');
let txt = document.createTextNode('The Countries');
h3.appendChild(txt);
div.appendChild(h3);
let countries = JSON.parse(e.target.responseText);
let sel = document.createElement('select');
sel.setAttribute('id', 'chooseCountry');
// sel.addEventListener('change', getCountries);
countries.forEach(function(country) {
let opt = document.createElement('option');
let opttext = document.createTextNode(country.name);
opt.appendChild(opttext);
sel.appendChild(opt);
});
div.appendChild(sel);
$("countdata").appendChild(div);
};
/*
* Listen to the get films button
*/
const showStarter = function () {
$('gcont').addEventListener('click', getContinents);
}
window.addEventListener("load", showStarter); // kick off JS
assa0/public/javascripts/modules/Ajax.js
'use strict';
/*
* AJaX object
* @author NML
* @Date Nov 2019
*/
let Ajax = {
init() {
this.ajaxobj = false;
try {
this.ajaxobj = new XMLHttpRequest();
} catch(err) {
window.alert(err.message + " Get yourself a browser ;)");
}
},
/*
* method: getFile
* @param filename: url of wanted file
* @param callback: function to handle response
*/
getFile(url, callback) {
try {
this.ajaxobj.addEventListener('load', function(ev) {
callback(ev);
});
this.ajaxobj.open("GET", url);
this.ajaxobj.send("");
} catch(err) {
window.alert(err.message + 'WTF');
}
}
}
export {Ajax};
assa0/public/javascripts/modules/nQuery.js
'use strict';
/**
* nQuery, *the* JS Framework
*/
const $ = function (foo) {
return document.getElementById(foo);
}
const deg2rad = function(deg) {
return deg * Math.PI / 180;
}
const rad2deg = function(rad) {
return rad * 180 / Math.PI;
}
const roll = function(foo, bar = 0) {
return Math.floor(Math.random() * foo + 1) + bar;
}
const randomColor = function() {
let hexDigits = '0123456789abcdef';
let rrggbb = '#';
for (let i = 0; i < 6; i++) {
rrggbb += hexDigits[Math.floor(Math.random() * 16)];
}
return rrggbb;
}
/*
* kudos Brian Suda, https://24ways.org/2010/calculating-color-contrast/
* yiq
* @param: toWhat => '#rrggbb'
*/
const contrastColor = function(toWhat) {
toWhat = toWhat.substr(1);
let r = parseInt(toWhat.substr(0,2),16);
let g = parseInt(toWhat.substr(2,2),16);
let b = parseInt(toWhat.substr(4,2),16);
let yiq = ((r*299)+(g*587)+(b*114))/1000;
return (yiq >= 128) ? 'black' : 'white';
}
export {$, deg2rad, rad2deg, roll, randomColor, contrastColor};
The application internal use of an API method of providing data for the frontend is a matter of style, and perhaps of performance. In a highly requested service it should ease the burden of the server to provide data only to the frontend instead of both providing data, and formatting the pages for the user. Today's fashion seems to indicate ssingle page applications ie using APIs.
Public APIs will be dealt with when we turn to data integration, and the security aspects of who is allowed and who not, will be dealt with when we discuss security as a topic of it's own.