Keith

Single Page Application With No Framework

by Keith Rowles • 26/12/2023JavaScript

Magnify glass on a blue background

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 Demo

Repo

View DCode’s GitHub Repo.

Link to GitHub Repo