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
}
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
}
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');
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
}
});
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.
# Navigating between Pages
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';
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
});
}
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
orhistory.go()
or the user typing a new url in the location bar with thepushState()
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