Let's build a simple Javascript Router
Tags: javascript, router, frontend
Published: 2020-07-16 04:01:57

Can we create a **Single Page Application (SPA)** without server side modifications, frontend libraries / frameworks, and without a need to define routes? Yes and it's easy. Let me show you how I did it. (Also, there's a demo at the end that you can try out)
___
**Once completed, the router will be capable of:**
- fetching pages from the server
- navigating without triggering a reload
- storing pages to avoid resending the same request and retain DOM state

**Finally we run it with a single function that takes care of everything:**
```js
enableSpaNavigation()
```
*Don't worry about the compatibility. Browsers that don't support the router's features will be ignored thanks to our awesome* ***feature detection*** *that we're going to define as well*
___
## 1. Modify HTML
We need to tell the router which **`<a>`** tags should be prevented from causing a page reload, and instead fetch the page in the background by marking them like this: `class="interlink"`

Content of each web page you want to update also needs a container. I mark it like this: `id="app"`

```html
<div id="app">
    <a classname="interlink" href="./about">About Us</a>
    <!--rest of the page content comes here-->
</div>
```
___
## 2. Modify Javascript

### Define a state variable
```js
const pages = [];
```
Yes, that's all the state we're going to need

### 2. "Possess" the "interlinks"
Remember those **`<a>`** tags we marked? Now's the time to change their behavior. We do this by adding a `click` event listener on each. The listener stops them from reloading the page with `preventDefault` function, and calls `navigateTo` function passing  in the url...
```js
function possessInterlinks() {
    Array.from(document.getElementsByClassName('interlink')).forEach(link => {
        link.addEventListener('click', function (evt) {
            evt.preventDefault()
            navigateTo(evt.target.href)
        })
    })
}
```
### Navigation
this function updates the browser's history stack and the address bar with `window.history.pushState` method if necessary. It also fetches the page, if the page hasn't been previously stored; And it calls `possessInterlinks` if the links haven't been previoulsy 'possessed'.
```js
function navigateTo(url, isHistoryUpdated) {
    const targetPage = getStoredPage(new URL(url).pathname)
    if (!isHistoryUpdated) window.history.pushState({}, '', url)

    if (!targetPage.content)
        fetchPage(url).then(pageText => {
            targetPage.content = pageFromText(pageText)
            replacePageContent(targetPage.content)
            setTimeout(() => {
                possessInterlinks()
            }, 1)
        })
    else replacePageContent(targetPage.content)
}
```
### Page storage
Stores and accesses the pages from the `pages` state variable we declared earlier.
```js
function getStoredPage(pathname) {
    // returns the stored page, if it doesn't exist, creates one and returns it
    const page = pages.filter(p => p.pathname === pathname)[0]
    if (page) return page

    const newPage = {pathname}
    pages.push(newPage)
    return newPage
}

function storeCurrentPage() {
    getStoredPage(window.location.pathname).content = document.getElementById('app')
}
```
### Utility functions
```js
function fetchPage(url) {
    return fetch(url).then(res => res.text())
}
```
Converts the fetched page text into DOM and returns the new `#app` element.
```js
function pageFromText(pageText) {
    const div = document.createElement('div')
    div.innerHTML = pageText
    return div.querySelector('#app')
}
```
replaces the previous `#app` element with a new one.
```js
function replacePageContent(newContent) {
    document.body.replaceChild(newContent, document.querySelector('#app'))
}
```
### enableSpaNavigation
This function sets up the router. It calls `possessInterlinks` and takes care of  browser's navigation back / forward buttons.
```js
function enableSpaNavigation() {
    // feature detection: proceed if browser supports these APIs
    if (window.fetch && window.location && URL && window.history && window.history.pushState) {
        //store the page (optional)
        storeCurrentPage()

        // add 'click' event listeners to interlinks
        possessInterlinks()

        // handle browser's back / forward buttons
        window.addEventListener('popstate', evt => {
            const page = getStoredPage(location.pathname)
            if (page && page.content) {
                evt.preventDefault()
                navigateTo(evt.target.location, true)
            } else {
                window.location.reload()
            }
        })
    }
}
```

### Finally call `enableSpaNavigation`
we make sure the document is ready before calling `enableSpaNavigation`
```js
if (document.readyState !== 'loading') enableSpaNavigation()
else
    window.addEventListener('load', () => {
        enableSpaNavigation()
    })
```

That is all.

Here's the [demo](https://codesandbox.io/s/simple-router-demo-87vtc)
And here's the source in [github repository](https://github.com/filiBit/spa-navigator)

I'd like to know what you guys think of this.