Week 12 - Web APIs
You can find the full list of can be found on MDN here.
Web Storage API
Section titled “Web Storage API”The Web Storage API has two parts - localStorage
and sessionStorage
. They have the same methods and properties and work the exact same way. The only difference is that the sessionStorage
values are automatically deleted at the end of the user’s session. This means when they close the browser or the tab that contained the current domain as part of the tab’s current history.
From this point on I will be referring to localStorage
only but everything also applies to sessionStorage
. Both are a global property accessible from the window
object in the browser. They do not exist in NodeJS.
All the data stored in localStorage
gets stored as a JSON
string.
This means that only information that is capable of being a JSON string can be saved in localStorage
. No functions, no DOM elements, can be stored in localStorage
.
We will need to use the built-in JSON
methods to convert the data back and forth between a string and an object.
let myObject = { id: 123, title: 'The Big Bang Theory', seasons: 12, firstSeasonYear: 2007, stillPlaying: false,};//convert the object into a JSON string.let stringData = JSON.stringify(myObject);//convert the string back into an objectlet objectCopy = JSON.parse(stringData);
If you ever need to make a deep copy of an object, this is one way of doing it.
If the data that you want to convert to JSON is already a just a string, then you should not use the JSON.stringify()
method as it will add an extra set of quotation marks.
Every time we read or write a value from localStorage
we need to use a unique key. The key will be the unique name for that chunk of data. All keys need to be String values. An example key that you could use for an assignment would be abcd0001-com.example-userscores
. It has a username, a domain name, and a descriptive term for what the data is.
LocalStorage Methods
Section titled “LocalStorage Methods”The methods that we have for localStorage
are: getItem
, setItem
, removeItem
, clear
, key
and the property length
.
The length
property will tell you how many items have been saved in localStorage
for the current domain. You can create as many different keys as you want for your domain. Each key will have its own set of data.
JavaScript files use the Domain of the HTML page that loaded them as the domain context for things like cookies and localStorage.
let myData = []; //my global variable to hold the data, with default value.let storage = localStorage.getItem('abcd0001-com.example-userscores');//if there is no key matching this then we will get undefined as the value.if (storage) { //we got some data. Use JSON.parse to turn the string into an object myData = JSON.parse(storage);} else { //nothing matched. No data under that key yet. //Setting a default value is a good idea. localStorage.setItem('abcd0001-com.example-userscores', JSON.stringify(myData)); //don't forget the JSON.stringify() part.}
To remove a key and its value we would use the removeItem
method. It needs the key.
localStorage.removeItem('abcd0001-com.example-userscores');
To remove ALL keys and values for the current domain, use the clear
method.
localStorage.clear();
If you think that there may be lots of possible keys for the current domain and you want to search through them, you can use the key
method in conjunction with the length
property and a loop.
let len = localStorage.length;for(let i=0; i<len; i++){ console.log( `${localStorage.key(i)} is the name of the `${i}`th key.` );}
MDN Reference for LocalStorage
MDN Reference for SessionStorage
Session Storage
Section titled “Session Storage”Session Storage has the exact same methods and properties and abilities as localStorage
. The difference is that when the browser tab is closed or the browser is shut down then all the stored data is marked for deletion.
When it comes to the methods in sessionStorage
, they are the same as the localStorage
ones. Just replace local
with session
and they will all work.
Storage Events
Section titled “Storage Events”There is actually a storage event that you can listen for to trigger functionality on your page. If you have content on your page that is built based on content coming from Local or Session Storage you will want to know if that content has changed.
For the current window you are looking at you will know if it changes because it can only change if you actually call the localStorage.setItem
method.
However, it is possible that your user has multiple concurrent windows or tabs open for your website. If the user does something in Tab B
then you will want to know in Tab A
that the contents of localStorage
have been updated. This is why we have the storage event.
window.addEventListener('storage', (ev) => { //decide what you want to do WHEN something in local or session storage has been updated. ev.key; //the key of the data that was updated ev.oldValue; //the previous value for the key ev.newValue; //the latest value for the key ev.url; //the exact url of the tab/window that changed the storage ev.storageArea; //a Storage object representing the container for the key that was updated. // this last one is important because it points to Session or Local for the storage});
Storage events across tabs
Single Source of Truth
Section titled “Single Source of Truth”When working with data sources like localStorage and cookies, which are not directly accessible as a variable, we need to create what is known as a single source of truth
.
It is possible to have multiple functions in multiple scripts that are accessing the values in your cookies or localStorage within the context of the same page load. If one script updates localStorage
then we might not know that the change was made in another function that is sharing that data.
To avoid problems caused by this the best approach is to create your own globally accessible variable that will hold the data.
The cookie
and localStorage
values are the CACHED copy of the data. They only exist to give us context when the app is loaded the next time. We do NOT work from them.
When the page first loads, we read the value from the cookie
and/or localStorage
and put it into the global variable.
Any function that needs to access or change the information will make the change to that global variable.
Each time the global variable is altered you can call on the document.cookie
property or localStorage.setItem
method to update the cached
value. Remember we are only updating the cache for the next time the app loads.
The only time we ever read from the storage is when the app first loads - using the DOMContentLoaded
event as our trigger to fetch the cached data.
Geolocation
Section titled “Geolocation”The Geolocation
object in the browser sits inside of the window.navigator
object. It has three methods:
getCurrentPosition
A one time call to the current position.watchPosition
Similar to setInterval, it repeatedly asks for the position.clearWatch
stops the repeated calls towatchPosition
.
navigator.geolocation.getCurrentPosition(successCallback, failCallback, options);navigator.geolocation.watchPosition(successCallback, failCallback, options);
The two functions are the same except that getCurrentPosition
runs once and watchPosition
runs repeatedly. If you want to stop the watchPosition
method running then we call:
navigator.geolocation.clearWatch();
Both methods accept a success callback function and a failure callback function. The success function will be passed a position object. The failure callback will be passed a position error object. See the code below for examples.
MDN PositionError Object reference
The third parameter is the options object.
let options = { enableHighAccuracy: true, timeout: 5000, maximumAge: 0,};
click the “edit on codepen” link to open and test the code
Geolocation
Cache API
Section titled “Cache API”Added as part of HTML5, the Cache API
lets javascript developers intentionally save files, ( json, css, html, scripts, fonts, images, etc) in the browser to improve performance and reduce latency for repeated future visits.
The Cache API
is separate from the built-in browser cache that is controlled by the browser and HTTP Request and Response Headers.
When building a cache, the main reason you would want one is to be able to run your website when the network fails or the user turns off their wifi. We want our app to be able to run offline after the initial load.
While the Cache API
is primarily used from Service Workers for Progressive Web Apps, we can also use it directly from any webpage.
It is based on Promises, just like the Fetch API
.
Cache Method Reference
Section titled “Cache Method Reference”There are five main object types that you will be using with when working with the cache.
- A CacheStorage object called
caches
, which references all the cache objects for the current origin; - A Cache object that you will hold in a variable, which references a single cache;
- A Request object used as the reference to a single stored file;
- A Response object that is the container for each file in the cache;
- A Promise object which holds the result of nearly every cache method.
The caches
object can be used to get a reference to a single cache, get a list of all the cache names, or let you search for a file across all the caches.
caches.open('my-cache-name').then((ref) => { //ref is the reference to a single cache called 'my-cache-name'});caches.keys().then((names) => { //names is an array containing the names of all the origin caches.});caches.match('fileurl.jpg').then((response) => { //response is the Response object containing the file with the url 'fileurl.jpg'});
The cache reference object is returned by the caches.open
method. You have to call the open method to get this reference. Typically you will call this method once and store the reference in a global variable that can be accessed by other functions.
cacheRef.add(request).then(() => { //file saved});//fetch and add a response object to the cachecacheRef.addAll(requestArray).then(() => { //all files added});//fetch an array of urls and add them all to a cachecacheRef.put(request, response).then(() => { //success});//add or replace a response in the cache with the request as the keycacheRef.match(url).then((response) => { if (response) { //found in cache }});//search a single cache for a response with the matching urlcacheRef.keys().then((requests) => { //requests is an array of Request objects});//get a list of requests/urls in the single cache
Loading the Cache
Section titled “Loading the Cache”When you want to save files in the cache to use later.
let cacheName = 'myCache-v1'; //key name for our cachelet assets = ['/img/demo.jpg', '/img/other.jpg', '/img/joanne.png']; //the array of files to save in our cache
caches .open(cacheName) .then((cache) => { //the cache variable is a reference to the cache that you opened.
//these next three .add() calls are all asynchronous. //normally you would only do one of these calls inside this function. let urlString = '/img/1011-800x600.jpg?id=one'; cache.add(urlString); //cache.add() = fetch() + cache.put()
let url = new URL('http://127.0.0.1:5500/img/1011-800x600.jpg?id=two'); cache.add(url);
let req = new Request('/img/1011-800x600.jpg?id=three'); cache.add(req);
//cache.addAll(assets).then() is an alternative that lets you save an array of urls }) .catch((err) => { //the open method failed. });
In this example we are saving three copies of the same file (an image called 1011-800x600.jpg
) into our cache. This could have been three different files. This code is just to illustrate that changing the querystring value makes the browser view it as 3 different files.
If you want to have an array of file names you could call the cache.addAll
method and pass in the array. The current domain name will be prepended to all those strings when the requests are made, unless another protocol and domain are used at the start of the string.
If you actually did want to call the add()
method three times then you would need to put the calls in their own function wrapped in a then()
and the cache
reference would have to be a variable accessible to all three functions. OR you could just use the addAll()
method. OR you could make the one then()
function into an async
function so you could put await
in front of each
add()
call.
//example using async and awaitcaches .open(cacheName) .then(async (cache) => { //adding `async` in front of the function lets us use `await`.
let urlString = '/img/1011-800x600.jpg?id=one'; await cache.add(urlString); //cache.add() = fetch() + cache.put()
let url = new URL('http://127.0.0.1:5500/img/1011-800x600.jpg?id=two'); await cache.add(url);
let req = new Request('/img/1011-800x600.jpg?id=three'); await cache.add(req); }) .catch((err) => { //the open method failed. });
The cache
object also has fetch
and put
methods. The fetch
method will download the file. The put
method will save the file in the HTTP Response from the fetch
in the cache. The add
method does both the fetch
and put
steps together.
If you ever need to remove something from the cache then you can call the cache.delete()
method to remove the file. It takes a Request
, URL
or USVString
, which represent the file, as an argument. It returns a Promise that resolves when the file has been deleted.
Reading the Cache
Section titled “Reading the Cache”When you want to read a file from the cache then we use the cache.match()
method. It will return a Response
object, just like a fetch
call would. The difference is that it is looking at the internal browser cache for your domain under the key used to create the cache
object.
//the request argument can be a USVString, a URL object, or a Request object//the options argument is optional. It is an object with 3 propertieslet options = { ignoreSearch: true, //ignore the queryString ignoreMethod: true, //ignore the method - POST, GET, etc ignoreVary: false, //ignore if the response has a VARY header};cache .match(request, options) .then((response) => { // Do something with the response object... just like you would with fetch // if(! response.ok) throw new Error(response.statusText) // response.json() // response.blob() // response.text() // response.formData() }) .catch((err) => { console.warn(err.message); });
Reading a List of Files from the Cache
Section titled “Reading a List of Files from the Cache”Every cache
object has an asynchronous method called keys()
which will return a Promise that resolves to an array of Request objects. This is the list of all the files stored in the Cache.
//assume that we have this line already called in our code.const cacheRef = cache.open('MyCacheOfImages');
//with that cache Reference we can use keys() to get an array of Responses (Files) in the cachecacheRef.keys().then((keys) => { //keys is the array of Requests in the Cache. keys.forEach((request) => { //output the url of each file in the cache console.log(request.url); });});
The example above outputs the url for each file stored in the cache.
Remember that every file saved in the Cache is wrapped inside of a Response object. The Requests returned by the keys() method is like a variable name for each Response.
So, when we get the keys, we now have a url that we can use to retrieve the actual file from the cache.
We can now use the cacheRef.match(request)
method to retrieve each of the files, one at a time.
cacheRef.keys().then((keys) => { //keys is the array of Requests in the Cache. const responses = keys.map((request) => { //retrieve the response for each request return cacheRef.match(request); //OR cacheRef.match(request.url); });});// !!!! THIS CODE WILL FAIL !!!!
At first glance, the code above looks like it will do what we want. It loops through the array of keys (request) and calls the match()
method to retrieve a file.
The PROBLEM is that every match()
call returns a Promise. The responses
array will be an array of Promises and we cannot use the array until ALL of the promises are resolved.
The FIX is to wrap the keys.map()
method inside of Promise.all()
which will wait until all the promises in the returned array have been resolved. Only then will responses
be an array of actual
response objects.
See week 9 for notes about Promises and the Promise.all method.
Also, we will put the variable responses
into the next then()
method and return the result of Promise.all()
to that next then()
method.
cacheRef .keys() .then((keys) => { //keys is the array of Requests in the Cache. return Promise.all( //retrieve the response for each request keys.map((request) => cacheRef.match(request)) //skip the keyword return by making the arrow function a single line with no {} ); }) .then((responses) => { //here we have an array called responses, full of response objects //each response object contains a file from the cache. responses.forEach((response) => { const url = new URL(response.url); console.log(url.pathname, response.headers.get('content-type')); //output the path/filename plus type of each files from the cache }); });
The first Promise is created by the call to keys()
.
The next Promise is created by Promise.all()
.
Inside the Promise.all()
method we create an array of Promises by calling the array.map()
method on the array of keys/requests and calling cache.match()
on each of the requests.
You can think of the Promise.all()
part like this:
Promise.all([cacheRef.match('image.jpg'), cacheRef.match('pic.png'), cacheRef.match('index.html'), cacheRef.match('styles.css'), cacheRef.match('main.js')]);
Promise.all converts all the Promises created by each call to match()
into a single array wrapped in a single Promise.
Opening a List of Files from the Cache
Section titled “Opening a List of Files from the Cache”So, now we have an array of Response
objects created by a series of calls to cache.match()
wrapped inside a Promise.all()
.
If we want all the actual files instead of an Array of Response objects then we will repeat the same approach with Promise.all()
, except this time we need to use the Response.blob()
or Response.text()
, or Response.json()
methods.
For simplicities sake, let’s pretend that every file in our cache is an image. So, we will use the response.blob()
on each Response.
cacheRef .keys() .then((keys) => Promise.all( keys.map((request) => cacheRef.match(request)) )) .then((responses) => { return Promise.all(responses.map((response) => { return response.blob(); }); }) .then(blobs=>{ //blobs is an array of actual binary image data for each image from the cache });
The repeated call to response.blob()
inside the responses.map()
will create an array of Blobs (Binary Large OBjects). This is the binary data from each image file from the cache.
Now, in actuality, each blob
in the blobs
array is only a reference pointer to the location in memory where the image data is currently held.
If we want to use the blob
data on a webpage then we need to turn that memory location into a URL that we can pass to an img
src attribute. We do this with a URL.createObjectURL()
method.
cacheRef .keys() .then((keys) => Promise.all( keys.map((request) => cacheRef.match(request)) )) .then((responses) => Promise.all(responses.map((response) => response.blob())) .then(blobs=>{ blobs.forEach(blob => { //for each blob location in memory... const url = URL.createObjectURL(blob); //create a url that points to that memory location //and then add your images to the webpage as desired. const img = document.createElement('img'); img.src = url; document.body.append(img); }) });
For your final project you will be using the Cache API to save copies of files fetched from an image API.
Here are a couple videos talking about how to work with Blobs and Files in the browser. Many of the examples in the videos use the Cache for saving and retrieving the Files.
Files, Blobs, and ArrayBuffers
Files, Caches, Canvas, and Blobs
Saving Files in the Cache
Section titled “Saving Files in the Cache”Once we have a reference to an open cache, then we can actually start to save files inside of the cache. Remember that we can have multiple caches, so make sure you have the correct reference first.
With the reference to the cache there are two methods that we can use to save files inside a cache.
// cacheRef is our variable that references a cache.cacheRef.add(request);//returns a PromisecacheRef.put(request, response);//returns a Promise
The cache.add
method will take a Request
object, do a fetch call for that Request
and when the response returns successfully, add it to the cache. This method is for situations where you do not have the Response and its file yet.
The cache.put
method will take a Request
object and use that as the key to save the Response
object in the second parameter. This method works when you already have a Response/File or you are dynamically creating one.
Here are a few examples.
- Use
cache.add
to save a JSON file.
let url = 'https://jsonplaceholder.typicode.com/todos';let request = new Request(url, { method: 'GET' });cacheRef .add(request) .then(() => { //json file has been saved in the cache. }) .catch((err) => { //failed to fetch or save the file in the cache });
- Fetch a file and
cache.put
to save in Cache.
let url = 'https://jsonplaceholder.typicode.com/todos';let request = new Request(url, { method: 'GET' });fetch(request) .then((response) => { if (!response.ok) throw Error('fetch failed'); return cacheRef.put(request, response); }) .then(() => { //json file was fetched and saved in cache }) .catch((err) => { //fetch or cache failed });
- Create a file and
cache.put
to save in Cache
let data = [ { id: 123, name: 'Bob' }, { id: 456, name: 'Linda' },];let JSONString = JSON.stringify(data);let filename = 'bobsburger.json';let file = new File([JSONString], filename, { type: 'application/json' });let response = new Response(file, { headers: { 'content-type': file.type, 'content-length': file.size } });//create the response object to save in the cachelet url = new URL(`/data/${filename}`);let request = new Request(url, { method: 'GET' });//create the request file to use as the key in the cache
cacheRef .put(request, response) .then(() => { //successfully saved the file in the cache }) .catch((err) => { //failed to save the file in the cache });
Like all promises, we can use async await
or .then()
to handle them, as long as proper error handling is done for both.
When we save a file in the cache with either method, if the Request object is the same (think same method and URL), then the new file will overwrite the old version. We can overwrite a file as many times as we want.
Cache-Control Header
Section titled “Cache-Control Header”The Cache API
is not to be confused with the Cache-Control Header
that is part of the HTTP Response specification. The
Cache-Control Header is a header that is sent from the server or proxy-server to the client along with a requested file. It tells the browser how to handle the caching of the requested file and how long it should be considered valid before requesting a new copy of the file.
The name of the header is cache-control
and it can hold a variety of directives. Here are a few examples of the header showing some of the directives.
Cache-Control: max-age=604800Cache-Control: no-cacheCache-Control: max-age=604800, must-revalidateCache-Control: no-store
When working with files in the browser we can save them using the Cache API. The following video explains all the different ways we can use the Cache API, File objects, Request object, Response objects, and the HTML5 Canvas to read and write to files.
Working with files in the browser
History API
Section titled “History API”Any time that you are using a browser, the whole reason that you are able to use the back and forward buttons is because of the History API
. Your browser maintains an Array of visited pages for each and every tab.
Each entry in the history Array contains the full URL, including the querystring, plus the state
data. The state
data includes setting values that you created through JavaScript to save in the history array entry plus form data that was filled out if a form was submitted.
The History API
encompasses the history Array plus a collection of methods that let you move through that array, events for monitoring navigation and movement through the array, and methods for updating what is written in the location bar and entries in the history array.
The history
object is a top level object inside the window
object, just like document
and navigator
. Here are the basic property and methods for history
.
history.length; // the number of entries in the history array for the current tabhistory.go(-3); // navigate back three steps in the history arrayhistory.back(); // navigate back one stephistory.forward(); //navigate forward one step
The history.state
property gives you the value of the current entry’s state object.
If you want to change the value of state
, change or add an entry in the array, and change what is written in the location bar, then we use these two methods:
//both these methods will change what is written in the location bar//replaceState will replace the current history array entry with a new valuehistory.replaceState(state, title, url);//pushState will add a new entry to the history array. The new value will become the current entryhistory.pushState(state, title, url);//title is ignored in all browsers except Firefox//state is the data object saved in association with the history array entry
Notice that I did not say anything about navigating with these methods. They do not actually load anything. They add or change an entry in the history array. They change what is written in the location bar. They DO NOT navigate or load any page.
The events that we can use, in conjunction with the history api
, are hashchange
and popstate
.
The hashchange
event is fired when the fragment identifier of the URL has changed. The Event
object associated with hashchange
has an oldURL
and a newURL
property that you can use as reference.
Note that just calling history.pushState()
or history.replaceState()
won’t trigger a popstate
event. The popstate
event will be triggered by doing a browser action such as a click on the back
or forward
button (or calling history.back()
or history.forward()
in JavaScript).
Typing a new hash value in the locationbar will not trigger a popstate
event as the page has already loaded and a new hash value just makes the browser try to scroll to a matching id. It will, however, trigger a hashchange
event.
A Single Page App (SPA
) is the most common reason to use the History API
. If you are building a SPA
it means that you are building a website with only one HTML file. You use JavaScript to manage the History array, what is written in the location bar, the fetching and caching of new content, and the display of that content.
The general approach is to add click listeners to your anchor tags and when the user clicks them, you use ev.preventDefault()
to stop the browser from trying to load the new url. Instead, you can take the url and, with pushState()
you can update the History and the location bar. Then call whatever functions you want to add and remove the appropriate content.
If you wanted, you could actually use fetch()
to retrieve new HTML files and then display their content. Remember, that the Response
object has the text()
method as well as the json()
method. You can extract the text from an HTML file and turn it into a HTML String that can be queried just like a normal HTML file.
<nav> <a href="./index.html" class="navlink">Home</a> <a href="./stores.html" class="navlink">Stores</a> <a href="./products.html" class="navlink">Products</a></nav>
By using href
inside anchor tags it means that you could have static versions of those pages or routing set up on the server that would be loaded if JavaScript was disabled.
const APP = { init: () => { let nav = document.querySelector('nav'); nav.addEventListener('click', APP.handleNav); }, handleNav: (ev) => { ev.preventDefault(); //stop browser navigating let a = ev.target.closest('.navlink'); //get the link that was clicked if (!a) return; //exit function if no anchor was clicked history.pushState(a.href); //update history and location bar //decide what to do next if (a.href.contains('index.html')) { //home page... } else if (a.href.contains('stores.html')) { //stores page... } else if (a.href.contains('products.html')) { //products page... } },};
document.addEventListener('DOMContentLoaded', APP.init);
You can also add listeners in the init
function for popstate
and hashchange
. popstate
function handles the user clicking the back button or manually typing an address. The hashchange
function handles clicking on other links that only change the hash value in the url or the user manually typing a new hash value in the location bar.
The popstate
event and the hashchange
event will both have their listeners attached to the window
object.
window.addEventListener('popstate', handlePopNav);window.addEventListener('hashchange', handleHashNav);
popstate vs hashchange vs click vs load
Section titled “popstate vs hashchange vs click vs load”When you are trying to build a navigation system for your web app there are many different events that you can listen for. Understanding the difference between what triggers these different events is key.
Here is a list of what actions will trigger these events.
popstate
- moving between entries in the History array.
- loading a page by typing into the location bar
- clicking a link that loads a new URL
hashchange
- moving between History array entries where only the hash value has changed.
- user has clicked on an anchor where href is just a hash value.
- user types in the location bar and only changes the hash value.
click
- user clicks on an element on the page.
- this happens before any navigation would occur, so it is a chance to intercept and control the navigation.
load or DOMContentLoaded
- moving to a new entry in the History array, as long as the pathname, not just the hash value has changed.
- initial load of a webpage.
History API - part I
History API - part II
History pushState and replaceState
FullScreen API
Section titled “FullScreen API”While, making a video into a fullScreen object may seem like an obvious capability in a browser, did you know that you can actually make ANY HTML element into a fullScreen object.
Let’s say that we have this HTML:
<body> <main> <img src="https://picsum.photos/id/145/600/400" alt="Image 145 from Picsum.photos" /> </main></body>
Now, we want to toggle the <img>
element between it’s normal display and a fullScreen element.
document.addEventListener('fullscreenchange', (ev) => { console.log(ev); console.log('Something on the page has been made into a fullScreen object.');});
document.querySelector('main > img').addEventListener('dblclick', (ev) => { //toggle fullscreen on img let img = ev.target; console.log(document.fullscreenElement); //null or reference to the img
if (!document.fullscreenElement) { //attempt to make the image fullScreen if nothing else is currently fullScreen img.requestFullscreen().catch((err) => { //handle errors in trying to do this console.error('failed to make image fullscreen'); }); } else { //go back to the normal display. //setting document.fullscreenElement to null document.exitFullscreen(); }});
Determine if a Tab is Active with “pagevisibility”
Section titled “Determine if a Tab is Active with “pagevisibility””When a user is switching between tabs, what they are telling the browser, is that one tab is active and all other tabs are inactive. This is actually a pagevisibility
event that we can listen for.
Why would we care about this event? When the tab is put into the background:
- Maybe you want to pause our video or audio playback.
- Maybe you want to stop calling an API.
- Maybe you want to stop sending data via a websocket.
- Maybe you want to disable notifications.
The document
object can listen for the visibilitychange
event. It has a property called hidden
that returns a Boolean
which can tell us if the page is active or not. The document also has a visibilityState
property which returns either the string visible
or the string hidden
to answer the same question.
const audio = document.querySelector('audio');let playingOnHide = false;
document.addEventListener('visibilitychange', () => { if (document.hidden || document.visibilityState == 'hidden') { playingOnHide = !audio.paused; audio.pause(); } else { //document.hidden == false, document.visibilityState == 'visible' // Page became visible Resume playing if audio was "playing on hide" if (playingOnHide) { audio.play(); } }});
More HTML5 APIs
Section titled “More HTML5 APIs”At this point you might be saying something like,
“Holy Crap! That was a lot of code to get familiar with! I hope there aren’t many more HTML5 APIs.”
Well, there are actually ~120 HTML5 Web APIs.
If you include the ones discussed on this page though, you have actually already learned over 20 of these. Another ~20 are just experimental ones and a few more have been deprecated. At least another 40 are ones that you are unlikely to need unless you are working on a project that uses a very specific technology like the Payment Request API
or Picture-in-Picture API
or
MediaStream Recording API
or Web Audio API
or WebXR Device API
.
There is a new API coming called Navigation API
which is designed to replace the History API
.
Next semester you will be covering a handful more, and with that, you will have a very good grasp on what web developers need 80% of the time.
Here is the list of current Web APIs on the MDN site with documentation.
Multi-Page Scripts
Section titled “Multi-Page Scripts”When building websites that are made up of multiple HTML files, you must bear in mind that, every time the browser loads a webpage it is replacing an old one. When the new page loads, it will be parsed by the browser and it will load any attached JS files, CSS files, fonts, and images.
It is likely that these other files will be in the browser’s cache (not to be confused with the Cache API which is controlled by the developer). However, to run, they still need to be loaded into memory.
Any variables and functions that you had in your old JS file are being recreated.
Any values that were in your old variables ARE GONE.
This is why we need to use that DOMContentLoaded
event to call an init
method inside our namespace object. We need to run through the setup and add event listeners on every page load.
So, how does our script know which page has been loaded?
Page Specific Code
Section titled “Page Specific Code”There are many ways that you can figure out which page just loaded our script. You can look at the location
object to get the URL. But the simplest way to do it is to add a unique id
attribute to your <body>
element in each of your webpages.
<html> <head></head> <body id="home"></body></html>
Then you can use that in your code as the identifier for each page.
const APP = { init: () => { //any page has loaded APP.addListeners(); APP.pageSpecific(); }, pageSpecific: () => { //run code that is specific to each page let id = document.body.id; if (!id) return; //page is missing an id switch (id) { case 'home': //on the home page break; case 'other': //some other page break; case 'contact': //on the contact page break; default: //page that we don't recognize the id //or page with no specific code needed } }, addListeners: () => { //load any event listeners that are common to all pages //like hashchange, popstate, main nav clicks, etc },};
CSS and Style Implications
Section titled “CSS and Style Implications”If you are adding unique ids to each page’s <body>
tag then you can leverage that in your CSS.
A common use is to style current links or hide and show page specific elements.
#home .sidebar { display: none; /* on the home page hide the sidebar */}#products .footer { position: fixed; width: 100vw; bottom: 0; height: 6rem; /* make the footer fixed on the products page */}
A Single Page Application (SPA)
is what the name implies. A single HTML file that gets loaded to the browser and then the JavaScript is the Application logic responsible for showing and hiding and loading new content. We can use the History API
to manage the location bar and history array. We use the Fetch API
to get new content from the server. We can use the Cache API
and the Web Storage API
to save items, like images or JSON files, loaded from the server, which will improve performance in your app.
JAMStack
Section titled “JAMStack”The JAM
in JAMStack
refers to JavaScript, APIs, and Markup. Basically what we have been doing since week 5. A JAMStack
application can be a SPA
or it can be a multi-page web app. It uses all the same HTML5 APIs as a SPA site - history api
, fetch api
, cache api
, web storage api
and more.
The defining feature of a JAMStack
site is that all the files used to make the site are static ones. There is no server-side programming required, with the exception of the APIs. Generally, the APIs are built independently from the website. They can be hosted elsewhere and use any kind of server-side programming language.
You can even use Github Pages
to host and run a JAMStack website. The dynamic content will come from one or more APIs and be fed into your static website using client-side JavaScript.
Server-Side Programming, Servers, Hosting & Databases
Section titled “Server-Side Programming, Servers, Hosting & Databases”There are so many different programming languages in use today that it is virtually impossible to keep up with all of them. There are also many different kinds of web servers. Some webservers are able to support multiple programming languages, some support only one. On top of all that there are tons of options for different types of databases.
There are many kinds of hosting choices available to you and the one that you pick will be directly connected to which programming language and which kind of database you want to use.
Languages
Section titled “Languages”Here are a few of the server-side programming languages that you could use to build websites.
- NodeJS
- PHP
- Ruby
- ASP.Net C#
- Python
- Java J2EE
Most of those come with a variety of frameworks that can be used with the language to make development faster.
Next semester we will be leveraging your knowledge of JavaScript to create websites and APIs with NodeJS
.
Databases
Section titled “Databases”There are different types of Databases, each with its own strengths and weaknesses. Here are the main categories with a few examples, in brackets, of their implementations.
- Text Files (
txt
,csv
,xlsx
) - Relational Databases (
MySQL
,MariaDB
,PostgreSQL
,SQL Server
,Oracle
) - Document Databases (
MongoDB
,DynamoDB
,DocumentDB
) - Graph Databases (
Neo4j
) - Hierarchical Databases
Each type has its own learning curve and mental models that you need to figure out.
In our program we will be focused on Document Databases but will give you a brief introduction to Relational Databases too.
The database is where you save the data that you want to save with multiple users. It is where APIs get the information that they are sharing with webpages.
Window.open()
Section titled “Window.open()”If you ever need to open a new window or tab from JavaScript, you can do this with the window.open()
method. The decision to open a new tab or window depends on the settings in the user’s browser.
It is not something we can control with JS. There is also a window.close()
method.
When you use JavaScript to open a new window/tab you will be returned a reference to the newly created window
object. This means that you can control what is happening in the page that you opened
from the creating page script.
let url = 'https://www.mydomain.com';let features = 'menubar=yes,location=yes,resizable=yes,scrollbars=yes,status=no';let winRef = window.open(url, '', features);
The features parameter controls which elements will be visible if you are opening a new window.
If the domain for the page that you are opening is the same as the domain for the page that is running your script, then you will have full control over it. If the domain is different then there will be some security limitations to what you can control.
//to close the a window or tab that was opened by JS from within that pagewindow.close();
//to close the newly opened window from the script that opened itwinRef.close(); //winRef is the variable created in the script above
Window tab iframe open
Window Loading and Unloading
Section titled “Window Loading and Unloading”When you are loading and unloading pages there are a series of event that take place. They will always take place in the same order.
This concept applies to many of the events that you use in your code. A single user action results in a cascade of events. A user taps on theA
key on their keyboard -> at the very least there will
be a keydown
, keyup
, and keypress
event sequence. However, depending on what element had focus there could also be an input
event or a propertychange
event. These events all have a
predefined sequence that they have to follow.
When a page is loading these are the events that could be triggered in the order that they would occur.
window.readystatechange
window.DOMContentLoaded
window.load
window.pageshow
window.popstate
window.hashchange
When the user clicks on a link to navigate away from the page, these are the events that will be triggered, in the order that they will occur.
element.mousedown
element.focus
element.mouseup
element.click
window.pagehide
window.beforeunload
window.unload
And, if a page loses focus by a user switching tabs or applications.
window.visibilitychange
If you switch away from a page by moving to another tab or, on a mobile device, opening another app, then you will trigger the window.visibilitychange
event. Switching back to the tab will also
trigger the visibilitychange
event. The pagehide
event will not be triggered by leaving the tab or opening another app. The beforeunload
and unload
events are not as reliable for running code.
Usually, by the time those events happen the process of leaving is already underway. If you want to send a fetch()
call to upload recent data then the pagehide
or beforeunload
can be used. The
unload
event cannot be used for fetch
because the page will be gone before the message is sent. Even with beforeunload
you can send data but you will not receive a result before the page is
unloaded.
Something that might be of use to you at some point is the persisted
property, of the pageshow
and pagehide
events, which has a boolean value indicating whether or not the page was loaded from
the browser cache.
window.addEventListener('pageshow', (ev) => { if (ev.persisted) { //page was loaded from browser cache } else { //page was NOT loaded from browser cache }});
This might be useful if you decide you want to check for new versions of files or use your own files from the Cache API
.