Location, navigation, and history
Location
The Location object is represented by the location
property on the Window and Document object, and it represents the current URL of the document displayed in the window:
window.location
is the same asdocument.location
document.URL
is the URL string- provides an API for loading new documents in the window
href
property andtoString()
method return the URLhash
returns the fragment identifiersearch
returns the part of the URL that starts with a?
- the query string
URL
objects have a searchParams
property that is a parsed representation of the search
property
- Location object does not have the
searchParams
property - you need to make a URL object out of the location object and then you can parse the query parameters:
// http://127.0.0.1:5500/index.html?q=test
let url = new URL(window.location); // URL object
let query = url.searchParams.get("q"); // test
Loading new documents
You can change the document that the browser loads by assigning window.location
a new URL string or fragment:
window.location = 'http://www.google.com';
- absolutewindow.location = 'headings.html';
- relativelocation = '#section1';
- fragmentreplace()
method takes a string URL and loads a new page, and then also replaces the calling document in the browser’s history.- Use case: if the user’s browser doesn’ not support features on your JS page, you can use
replace('https://static-site.com')
to replace it with a static version.
- Use case: if the user’s browser doesn’ not support features on your JS page, you can use
reload()
method makes the document reload
Browsing history
The History object represents the history for the window:
- comes from early days of the web when everything happened on the server
- modern web pages load content dynamically, so they are not always loading pages from the server
- these applications manage history - really viewing previous app states - with hashchange events or
pushState()
- browsing history is a list of documents and document states
- length is stored in
list
property - scripts are not allowed access to history URLs, for security
- history object methods:
back()
: go to previous page in historyforward()
: go to previous page in historygo()
: takes a positive or negative integer to jump that many pages forward or backward
- child window elements like
<iframe>
navigate forwards or backwards with the main window
history.back()
history.forward()
history.go(-3) // press back button 3x
history.go(0) // reload current page
hashchange Events
hashchange events use the location.hash
property:
location.hash
takes an arbitrary fragment identifier - just don’t pick a name of an existing element ID so there is no chance it will scroll to the fragment- create a unique fragment identifier to for each state of your application to let the user use browsing history
- setting the property updates the URL and adds an entry into the browser history
- when you set the fragment explicitly or when it changes, a ‘hashchange’ event is fired on the Window object to notify you that the user is using browsing history
- This method is hacky
Steps:
- Define a fragment - encode the state information that your app needs to render the page into a short string of text
- Write a function that convers page state into a string
- Write a functino that parses the string to re-create the page state it represents
- Write
window.onhashchange
orwindow.addEventListener('hashchange', ()=>{})
that reads the fragment inlocation.hash
and converts that string into a representation of that state - When the user initiates a new state, don’t render it directly - encode the state as a string and set
location.hash
to that string. This triggers the hashchange event and the handler will display the new state
pushState()
history.pushState()
uses a ‘popstate’ event to track history:
adds an object that represents the state to the browser history
when the user clicks forward or back, it fires a ‘popstate’ event with a copy of the saved state object and the app recreates the state
Also saves a URL for each state, which means users can bookmark states
Object arguments:
- First arg: the object - supports Map, Set, Date, and typed arrays with ArrayBuffers - it is serialized with the structured clone algorithm, which is more robust than
JSON.stringify()
- The event object for the
popstate
event has astate
property that contains a copy of the object that you pass
- The event object for the
- Second arg: string that was supposed to be title for state, but its not generally supported, so pass empty string
- Third arg: optional, a URL displayed in location bar immediately and if the user returns to this state with browser back or forward button
- users can bookmark, but you have to restore the state by parsing the URL
replaceState()
Takes same args as pushState()
but replaces current history state instead of adding a new state to the browser history:
- when you load an app that uses
pushState()
, you should callreplaceState()
at the start to define the initial app state
Example program
This is a number guessing program that demonstrates pushState()
and replaceState()
:
JS
class GameState {
// factory function
static newGame() {
let s = new GameState();
s.secret = s.randomInt(0, 100); // 0 < n < 100
s.low = 0; // guesses must be greater than this
s.high = 100; // guesses must be lower than this
s.numGuesses = 0; // num of guesses already made
s.guess = null; // what the last guess was
return s;
}
// recreate GameState object based on plain object that we get from
// popstate event
static fromStateObject(stateObject) {
let s = new GameState();
for (let key of Object.keys(stateObject)) {
s[key] = stateObject[key];
}
return s;
}
// encode the state of any game as a URL.
toURL() {
let url = new URL(window.location);
url.searchParams.set("l", this.low);
url.searchParams.set("h", this.high);
url.searchParams.set("n", this.numGuesses);
url.searchParams.set("g", this.guess);
return url.href;
}
// factory func that creates a new GameState obj and initializes it
// with a URL. returns null if there are errors in the parameters
static fromURL(url) {
let s = new GameState();
let params = new URL(url).searchParams;
s.low = parseInt(params.get('l'));
s.high = parseInt(params.get('h'));
s.numGuesses = parseInt(params.get('n'));
s.guess = parseInt(params.get('g'));
// return null if URL is missing params
if (isNaN(s.low) || isNaN(s.high) ||
isNaN(s.numGuesses) || isNaN(s.guess)) {
return null;
}
// pick new secret number when you restore the game from URL
s.secret = s.randomInt(s.low, s.high);
return s;
}
// return integer n, min < n < max
randomInt(min, max) {
return min + Math.ceil(Math.random() * (max - min - 1));
}
// modify the document to display current state of the game
render() {
let heading = document.querySelector('#heading');
let range = document.querySelector('#range');
let input = document.querySelector('#input');
let playagain = document.querySelector('#playagain');
// update the document heading and title
heading.textContent = document.title = `I'm thinking of a number between ${this.low} and ${this.high}.`;
// update the visual range of numbers
range.style.marginLeft = `${this.low}%`;
range.style.width = `${this.high - this.low}%`;
// make sure the input field is empty and focused
input.value = "";
input.focus();
// display feedback based on user's last guess
if (this.guess === null) {
input.placeholder = 'Type your guess and hit Enter';
} else if (this.guess < this.secret) {
input.placehoder = `${this.guess} is too low. Guess again.`;
} else if (this.guess > this.secret) {
input.placehoder = `${this.guess} is too high. Guess again.`;
} else {
input.placeholder = document.title = `${this.guess} is correct!`;
heading.textContent = `You win in ${this.numGuesses} guesses!`;
playagain.hidden = false;
}
}
// Update the state of the game based on what the user guessed.
updateForGuess(guess) {
// If it is a number and is in the right range
if ((guess > this.low) && (guess < this.high)) {
// update state obj based on guess
if (guess < this.secret) this.low = guess;
else if (guess > this.secret) this.high = guess;
this.guess = guess;
this.numGuesses++;
return true;
} else {
// invalid guess: notify user but don't update state
alert(`Please enter a number greater than ${this.low} and less than ${this.high}`);
return false;
}
}
}
// Initialize, update, save, and render the state obj when appropriate
// either load existig game from URL or start new game
let gamestate = GameState.fromURL(window.location) || GameState.newGame();
// save initial state with replaceState
history.replaceState(gamestate, gamestate.toURL());
// display initial state
gamestate.render();
// after user guess, update state then save new state to browser history
// and render new state
document.querySelector('#input').onchange = (event) => {
if (gamestate.updateForGuess(parseInt(event.target.value))) {
history.pushState(gamestate, "", gamestate.toURL());
}
gamestate.render();
};
// if users goes back or forward in history, you get popstate event
// on window obj w a copy of the state obj saved in pushState.
// When this happens, render the new state
window.onpopstate = event => {
gamestate = GameState.fromStateObject(event.state);
gamestate.render();
};
HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script defer src="pushState.js"></script>
<title>Location and history</title>
<style>
body {
height: 250px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-evenly;
}
#heading {
font: bold 36px sans-serif;
margin: 0;
}
#container {
border: solid black 1px;
height: 1em;
width: 80%;
}
#range {
background-color: green;
margin-left: 0%;
height: 1em;
width: 100%;
}
#input {
display: block;
font-size: 24px;
width: 60%;
padding: 5px;
}
#playagain {
font-size: 24px;
padding: 10px;
border-radius: 5px;
}
</style>
</head>
<body>
<h1 id="heading">I'm thinking of a number...</h1>
<!-- visual representatino of the numbers that are not yet ruled out -->
<div class="container">
<div id="range"></div>
</div>
<!-- where user enters guess -->
<input type="text" name="name" id="input">
<!-- button that reloads with no search string. Hidden until game ends -->
<button id="playagain" hidden onclick="location.search='';">Play Again</button>
</body>
</html>