historify.js is a simple set of functions that facilitate history management for single page applications.
The module works by keeping an artificial copy of the history, and making sure that the browser’s history and the artificial history are as in sync as possible.
The default variables are set at the beginning of the module:
export let history = []
export let navigatePostprocessor = null
export let excludeHashes = false
In order for historify to work, it needs to be set up and it needs to listen
to popstate events (which will indicate a change of path).
This is done through the function historifySetup()
, which is passed a
navigatePostprocessor function called every time the browser location changes.
export function historifySetup (_navigatePostprocessor = null, _excludeHashes = false) {
navigatePostprocessor = _navigatePostprocessor
excludeHashes = _excludeHashes
window.addEventListener('popstate', e => popStateCallback(window.location, e))
popStateCallback(window.location, null)
}
Since changing the location with pushState
and replaceState
doesn’t trigger
popstate
, the module will emit its own artificial one. The state
is marked as { artificial: true }
– this will have implication later
when the history is manipulated
function emitArtificialPopstate (state) {
state = { ...state, artificial: true }
const e = new PopStateEvent('popstate', { state })
window.dispatchEvent(e)
}
It’s time to implement the actual navigation methods.
go() will go to a URL by using pushState() and emitting the artificial
popstate` event.
export function go (path, state = {}) {
window.history.pushState(state, '', path)
emitArtificialPopstate(state)
}
The teleport()
method is similar to go()
, except that it doesn’t affect the
history. For the browser’s history, this is a natural consequence of using
replaceState()
. For the module itself, the last entry in the history is
modified, and the artificual popstate
event is generated with the
property noHistory
set to true
, which will prevent the module from
adding the entry in the history
export function teleport (path, state = {}) {
history[history.length - 1] = path
state = { ...state, noHistory: true }
window.history.replaceState(state, '', path)
emitArtificialPopstate(state)
}
The back button works as expected: if there are entries in the history,
the browser’s back()
method is called, and the artificial history is then
shortened by 1.
Note that there is no need to emit an artificial popstate
event, since
history.back()
will emit a real one
export function back (state = {}) {
if (history.length > 1) {
history.pop()
window.history.back()
}
}
The history is made available to the external world
export function getHistory () {
return history
}
When the location changes, the popstate
event is emitted. This might happen
either organically, coming from the browser, or artificially by calling
emitArtificialPopstate()
.
This is where the new URL is actually added to the history (unless the
noHistory
property is set in the event, as it happens in teleport()
).
If an event isn’t marked as artificial
, it means that the user has clicked
on one of the forward/back buttons on the browser, or has clicked on
a link.
If the event is not artificial, then the module will make sure that the artificial history is properly in sync with the browser’s history, by truncating the artificial history to the latest matching entry.
async function popStateCallback (location, e) {
const path = decodeURIComponent(location.pathname) + (excludeHashes ? '' : location.hash)
/* Push the new page in the artificial history */
/* (unless noHistory was in the popstate state) */
if (!e || !e.state || !e.state.noHistory) history.push(path)
/* For PURE browser popstate event (different to the artificial ones emitted with */
/* status set as { artificial: true }, reset the artificial history to this page */
if (e && e.type === 'popstate' && (!e.state || !e.state.artificial)) {
/* if (e && e.type === 'popstate' && e.state && !e.state.artificial) { */
const where = history.lastIndexOf(path)
if (where !== -1) history = history.slice(0, where + 1)
}
if (navigatePostprocessor) await navigatePostprocessor(path, e)
}