Web Apps

14.2 Project

# Final Project

# Hash vs QueryString vs State

Three different sources that can be used to provide data to your script to dynamically control the contents of your HTML file are: the URL hash value; the URL querystring; and the state property of the History object which accesses the data stored in the History array entry associated with the current page.

Reading values from hash

To get the hash value we read the hash value of the location object.

The contents of the hash string must start with a # but you can add whatever other information you need. If the information you are storing has multiple parts then you need to have a separator between each part. You could use a colon :, a hyphen -, a pipe character |, or a forward slash like you see in URL file and folder paths /. It really is up to you which character you use.

You would then split() the hash string on that character to get an array of parts. The first part is usually the #. We would place a separator right after the # to make sure.

Using the example of a hash value that could be:

  • #/ the base page with no user data
  • #/users show a list of users
  • #/users/34 show the details of user 34
  • #/users/add show the form to add a new user

We would take the hash and split it, then ignore the first value in the array. Based on the length, size and datatype of the other values we can determine what to do.

This is a good opportunity to use destructuring to discard the first value.

let hash = location.hash;
let [, type, action] = hash.split('/');
if (type && type === 'users') {
  //users section
  if (action) {
    if (typeof action === 'number') {
      let userid = action;
      //get the details for userid
    } else if (action === 'add') {
      //show the form to add a new user
    } else {
      //unknown or future command like delete
    }
  } else {
    //show the list of users
  }
} else {
  //base page with no user data
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

Reading values from QueryStrings

The process for getting data out of the QueryString is very much like the one for hash strings. The difference with the querystring is that the separators are predetermined and each part comes as a name-value pair.

The best approach is to create a URLSearchParams object with the search string from the location object.

Using the same functionality as the hash examples:

  • `` the base page with no user data. no querystring
  • ?users show a list of users
  • ?users=34 show the details of user 34
  • ?users=add show the form to add a new user
let params = new URLSearchParams(location.search);
if (location.search && params.has('users')) {
  if (params.get('users') === 'add') {
    //show the form to add a new user
  } else if (isNaN(params.get('users')) == false) {
    let userid = +params.get('users');
    //show the details for a specific user
  } else {
    //unknown or future command like delete
  }
} else {
  //base page with no user data
}
1
2
3
4
5
6
7
8
9
10
11
12
13

Reading values from State

The third approach is to save data in the state property of each History array entry. We can do this with the history.pushState() and history.replaceState() methods. This makes it ideal for Single Page Applications (SPA).

The value that we put into state can be pretty much anything that you would put into an object literal except for HTML references or functions.

Again, using the same example as above, we could add this data along with the index.html url as history entries like this:

let state = null;
let state1 = { users: null };
let state2 = { users: 34 };
let state3 = { users: 'add' };

history.pushState(state, null, './index.html');
history.pushState(state1, null, './index.html');
history.pushState(state2, null, './index.html');
history.pushState(state3, null, './index.html');
1
2
3
4
5
6
7
8
9

Once the data is there, we can read that data from inside of either the DOMContentLoaded or popstate event handlers by checking the value of the history.state property.

window.addEventListener('popstate', (ev) => {
  let state = history.state;
  if (state && 'users' in state) {
    if (state.users === null) {
      //show all users
    } else if (state.users === 'add') {
      //show the form to add a new user
    } else if (typeof state.users === 'number') {
      let userid = state.users;
      //show the details for a specific user
    } else {
      //unknown or future command like delete
    }
  } else {
    //base page
  }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

The downside of the history.state approach is that you cannot pass data directly to history.state when you are using location.href or a form or an anchor tag to navigate to another page. You can only put the data into history.state through the pushState() or replaceState() methods.

To navigate from one page to another we can let a user click an <a> tag or submit a form and then not ev.preventDefault().

Alternatively, in the JavaScript we can change the current url and add a new history array entry at the same time by using the Location object, like this:

location.href = '//www.example.com/index.html';
1

If you are building a Single Page Application (SPA) then you are usually using CSS to hide and show <div> or <section> elements on the page to hide and reveal content. Typically this is also combined with a fetch() call to get new data to display on the page.

Another less common approach is to use a fetch() call to retrieve a new HTML file. The user would click on a navigation element of some kind to call a function that fetches the new HTML. Once the HTML has been retrieved, a DOMParser is used to extract the content from the new HTML file. Then any desired part of the content can be loaded into the current webpage body.

function init() {
  let anchor = document.getElementById('navLink');
  anchor.addEventListener('click', getPageTwo);
}

function getPageTwo(ev) {
  ev.preventDefault();
  let url = ev.target.href; //extract the url from the anchor tag that was clicked
  fetch(url)
    .then((response) => {
      if (!response.ok) throw new NetworkError('Navigation error', response);
      return response.text(); //extract the String from the file with all the HTML
    })
    .then((htmlStr) => {
      let parser = new DOMParser();
      let doc = parser.parseFromString(htmlStr, 'text/html');
      //doc is the HTML DOM for the fetched file
      let newMain = doc.querySelector('main').clone(true);
      //get the <main> from the fetched doc and copy it
      //find the current <main> element to be replaced
      document.querySelector('main').remove();
      //add the new main element to the body
      document.body.append(newMain);
    })
    .catch((err) => {
      //handle errors
    });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# Events popstate and DOMContentLoaded

The events that we will listen for during navigation in our app are popstate and DOMContentLoaded. The popstate event occurs each time the user actually navigates to a new history entry. This could mean clicking the browser back or forward button. It could also be the user clicking an anchor tag or submitting a form and the default behaviour is not prevented in your script with ev.preventDefault(). It could also be triggered if the user manually types a new hash value for a url into the location bar of the browser. Basically, we are looking for the browser to move to a new entry in the history array.

Do NOT confuse navigating with location.href or history.go() or the user typing a new url in the location bar with the pushState() method that changes what is written in the location bar and creates a new History array entry. pushState() is not actually moving to a new entry, just creating one.

The DOMContentLoaded event is the one we use to run a function on every initial load of a page.

# What to do this week

TODO Things to do before next week.

  • Read all the content from Modules 14.1, 14.2.
  • Work on your Final Project
Last Updated: 12/4/2022, 2:30:39 PM