Single Page Application With No Framework
by Keith Rowles • 26/12/2023JavaScript
Summary
A single page application (spa) built with css, html and vanilla javascript - no frameworks used.
I wanted to better understand the fundamentals of what a SPA like React or Vue does under the hood.
This demo is based of a project from DCode.
I have included a link to his GitHub repo at the bottom of the page.
Tech
- html
- css
- javascript
- cPanel
- Express
Express Setup
On my local machine the application runs from an Express server.
const express = require('express');
const path = require('path');
const app = express();
// middleware
app.use(
'/static',
express.static(path.resolve(__dirname, 'frontend', 'static'))
);
app.get('/*', (req, res) => {
res.sendFile(path.resolve(__dirname, 'frontend', 'index.html'));
});
app.listen(process.env.PORT || 3000, () => console.log('Server running...'));
To publish… I had to make this work from a sub folder on my website because the url would point to the root directory.
HTML
<nav class="nav">
<a href="/code/spa/" class="nav__link" data-link>Dashboard</a>
<a href="/code/spa/posts" class="nav__link" data-link>Posts</a>
<a href="/code/spa/posts/1" class="nav__link" data-link>Post 1</a>
<a href="/code/spa/posts/2" class="nav__link" data-link>Post 2</a>
<a href="/code/spa/settings" class="nav__link" data-link>Settings</a>
</nav>
<div id="app"></div>
CSS
body {
--nav-width: 200px;
margin: 0 0 0 var(--nav-width);
font-family: 'Quicksand', sans-serif;
font-size: 18px;
}
.nav {
position: fixed;
top: 0;
left: 0;
width: var(--nav-width);
height: 100vh;
background: #222222;
}
.nav__link {
display: block;
padding: 12px 18px;
text-decoration: none;
color: #eeeeee;
font-weight: 500;
}
.nav__link:hover {
background: rgba(255, 255, 255, 0.05);
cursor: pointer;
}
#app {
margin: 2em;
line-height: 1.5;
font-weight: 500;
}
a {
color: #009579;
}
JS
.index.js
import Dashboard from './views/Dashboard.js';
import Posts from './views/Posts.js';
import Settings from './views/Settings.js';
import PostView from './views/PostView.js';
import { u } from './lib.js';
const pathToRegex = (path) =>
new RegExp('^' + path.replace(/\//g, '\\/').replace(/:\w+/g, '(.+)') + '$');
const getParams = (match) => {
const values = match.result.slice(1);
const keys = Array.from(match.route.path.matchAll(/:(\w+)/g)).map(
(result) => result[1]
);
return Object.fromEntries(
keys.map((key, i) => {
return [key, values[i]];
})
);
};
const navigateTo = (url) => {
history.pushState(null, null, url);
router();
};
const router = async () => {
const routes = [
{ path: u('/'), view: Dashboard },
{ path: u('/posts'), view: Posts },
{ path: u('/posts/:id'), view: PostView },
{ path: u('/settings'), view: Settings },
];
// Test each route for a potential match.
const potentialMatches = routes.map((route) => {
return {
route: route,
result: location.pathname.match(pathToRegex(route.path)),
};
});
let match = potentialMatches.find(
(potentialMatch) => potentialMatch.result !== null
);
if (!match) {
match = {
route: routes[0],
result: [location.pathname],
};
}
const view = new match.route.view(getParams(match));
document.querySelector('#app').innerHTML = await view.getHtml();
};
window.addEventListener('popstate', router);
document.addEventListener('DOMContentLoaded', () => {
document.body.addEventListener('click', (e) => {
if (e.target.matches('[data-link]')) {
e.preventDefault();
navigateTo(e.target.href);
}
});
router();
});
Demo
Open demo on my website.
Link to DemoRepo
View DCode’s GitHub Repo.
Link to GitHub Repo