A new structure for my Single Page Application
This article chronicles a small part of my journey towards creating a full website that can be rendered in two parallel frameworks and design systems (React + Material UI; Angular + Clarity), plus a “vanilla” implementation with all components and business logic written “by hand”.
← Previous article: HTML templates in Webpack
→ Next article: Using TDD to build a “click counter button” in TypeScript
I started this project with the goal of creating a website that could be rendered in parallel in React, Angular, and “Vanilla” JavaScript. I also wanted to start from a blank canvas. Several steps later, and I have what is on the surface a barebones website, but with most of the behind-the-scenes tooling I need to build fully-fledged web apps. Admittedly, I’ve yet to implement any styling in my site, but that’s coming soon! First, I wanted to refactor my project’s structure from having a “website feel” — a base template plus different views — to an Single Page Application (SPA) format, in which the UI attaches itself to a single DOM element, and manages all interactivity therein.
Technically, my site already is a SPA, because it’s all rendered on a single page — there’s not loading required when changing views. However, I do not think the behind-the-scenes structure, with content arranged in a series of “views”, would scale very well. The implementation outlined in this article is pretty basic, but it should set the sage for future development and exploration.
Before I dive into describing the refactor from site to app, there are two decisions I made for which I want to provide context, as they’ll set the direction for this and future articles.
First decision: I initially set out to build my site using three, not two, design systems. I was going to use Material, Clarity, and Carbon. I initially thought that Carbon would be a good pairing for a Vanilla implementation; however, on closer inspection, I found it was a React-first system, and did not have as comprehensive nor user-friendly HTML+CSS support as I initially thought. Not bad things, just not what I sought.
I did a bit of shopping around for a replacement design system, but didn’t find anything that caught my eye and curiosity. I also realized that building an app without a design system in place would be a valuable experience, especially when also working without a framework.
So, moving forward, my goal is to build a site than can be rendered in parallel in React + Material UI, Angular + Clarity, and as a fully homegrown app, written in Typescript, HTML, and Sass (coming soon!).
Second decision: When I put in place the first content for my site, I wrote about organizing it into views, rather than pages. While the logic for that decision still holds, I’ve decided to adopt a different structure. I am adapting a pattern from the Google Android Application Fundamentals, and will build my app around activities rather than views.
I’m doing this, in part, because I like the pattern’s user-first and interaction-focused nature. Every component in my app should be dedicated to enabling the user to do something — even if that thing is reading some text. Enforcing this requirement will focus my thinking and, I believe, lead to a smoother and more elegant UX.
I also like this pattern because I think it will link the end product, the UI, to the code in a way that “features” or “views” don’t. Truth be told, a “feature” links a code more closely to product planning than it does the user experience. Yes, we develop features for our users, but why? We deliver a feature so the user can do something. I want to code with the user in mind… even if this particular project is purely for my own benefit.
Finally, I’m doing this specifically because I don’t know how well it will work. This is an experiment. If it works well, I think I will have found a compelling architectural pattern for web applications — and let’s be honest, Google’s a pretty good authority when it comes to web apps. And if it doesn’t work well, then I’ll hopefully come to understand why it doesn’t work, and will gain some insight into what makes other more common patterns, such as views, features, work well. So, to a certain extent, adopting this pattern is taking a gamble. But it’s a gamble with very little risk, because pretty much any outcome results in valuable learning
To implement the new activities based structure, and to implement a proper mount point for my app, I started from the view level and worked my way up:
views to activities → refactor navigation → mount function → project config
I chose this approach to I could maintain functionality at each stopping point. Once I had converted my views to activities, I adjusted the navigation to suppor the new rendering method, and so on. This helped keep my thinking (and my code!) organized, and helped me catch errors along the way. Few things are worse in development than taking a leap of faith on a big chunk of code, and then having to retrace your steps to find out where things started to go wrong. Better to be incremental whenever possible.
In converting to activities, I settled upon a few development guidelines:
- I created
src/activities
, into which each activity will get its own directory. - Each activity directory will be named in the format,
<verb>-<object>
, such asread-about-simon
,navigate-site
, and so on. I debated going with<object>-<verb>
, which has its advantages, but I decided to take the easier to read, and activity-focused, approach. - Each activity will have a
mount___()
function, which will adhere to the following type:MountActivityFn = (mountPoint: HTMLElement, ...args: any[]) => HTMLElement
. The first parameter will be the element on which to mount theactivity component
....args: any[]
allows for arbitrary arguments required for the activity in question. And every function will return itsmountPoint
, with the component mounted. This last part may not prove necessary, but it does help with unit testing, and may prove a useful tool to have, down the line. - Each activity directory will have an
index.ts
file, which will export only the “public API” necessary to use the component. In the basic example, this is simply themount___()
function, but could also be internal types, utility functions, etc.
Here’s the mount function for my read-about-site
activity:
This is a pretty straightforward implementation; in future articles, I’ll almost certainly develop mount functions that are more involved.
Once the views were complete, refactoring the navigation proved fairly straightforward. I updated my NavItem
type to the following:
type NavItem = {
label: string;
mountFn: MountActivityFn;
};
Since the mountFn
would be provided its mountPoint
, each navItem
no longer requires a target-id
. I also switched from a tuple to an object, for clearer code and more flexible desctructuring, when constructing my navLinks
.
My mountNavigateSite
function is again pretty basic. Since it’s mounting multiple nodes rather than HTML, I needed to use a different implementation to attach it to the DOM. And since the navigation menu requires a target in which to display its content, I pass that in to the mount function, so it can be passed into the component:
With all my activities prepared, it was time to create the central mounting mechanism for my overall app. Because I plan to implement multiple apps (in parallel frameworks), I went ahead and built this one in src/apps/vanilla
.
The overall structure of my vanilla app
is as follows:
layout.html
defines the layout for the app, including specifying the top-level mounting points, identified by uniqueid
attributes.mountVanillaApp()
takes 3 parameters:appId
,container
, andinitFn
. The function creates a<div>
withid=appId
, inserts thelayout
template therein, and attaches it to thecontainer
.- With the framework in place,
mountVanillApp()
calls theinitFn
, found ininit.ts
, which handles any initialization logic, such as loading initial content.
With the activity refactor and the app creation phases complete, there was one small refactor remaining. Since the app is mounted as the top level element under document.body
, and since it injects the core layout, there’s (currently) no need for a static public/index.html
. I removed that file, and removed the line within webpack.config.js
where I told HtmlWebpackPlugin
to look for it. Now, HtmlWebpackPlugin
will automatically create an index.html
that includes an empty body tag and all the JavaScript bundles, and the rest will be handled by mountVanillaApp
!
It’s been a long road (and arguably a longer article!) to this point, but I’m very happy with where I am. I’ve created a solid foundation, and the architectural patterns that should allow me to really start building out functionality. My likely next steps are:
- Build a simple but interactive
activity component
, to confirm my component approach works. - Implement Sass styling and build an actual layout, so this stops looking like a text-only site!