A few months ago, I started looking for a new job within JavaScript ecosystem, and I started interviewing with many different companies. Most of them were conducting remote interviews and they often asked me to complete many different kinds of programming assignments.
One of such assignment consisted in developing (surprise, surprise) a simple Todo List single-page application but without using any third-party library. Besides providing basic functionalities like adding/removing/completing todo items etc., you would get bonus points for things like support for data pagination, simple data persistence, and support for filtering todo items.
I found this quite a refreshing challenge, but I soon realized how tedious it was to operate on the DOM “in the old way” and how messy things could get really quickly.
I really, really wanted to use even something minimal like Mithril to carry out the assignment! And started wondering how hard it would be to implement a quick-and-dirty Virtual DOM to manage changes more efficiently and also a simple HyperScript implementation to speed-up the creation of HTML elements…
After a few minutes, I found myself deep in creating a reasonably efficient (virtual) DOM diffing and rendering algorithm and soon enough I was able to use my little microframework to create my Todo List. It was very basic and had quite a few bugs in it, but it did the job alright, and I quickly completed the assignement and sent it back to the company for review.
The reviewer seemed very happy about it but also started to point out that things weren’t quite right. He wanted me to improve the efficiency of the DOM diffing algorithm, he pointed out a few little bugs here and there… and told me to improve things. We kept doing back and forth with my code until he was (reasonably) satisfied with the result.
I then continued the interview process (one of the toughest but by far the most rewarding I’ve ever been through) and I got offered the job. Sadly I ended up not taking it for a few reasons that are well beyond the scope of this article, but the point is that I was left with the code of that assignment sitting in an abandoned repository…
…until a few weeks ago, when I decided to do something with it.
I remember using h3()
as the HyperScript construtor as a sort of pun on the fact that typically you’d use h()
but I wanted to be more personal to me so I decided to use the first two letters of the name of web site for it… Therefore, because what I lack in creativity I have in ego, I decided to create a new microframework based on it, and call it H3.
Why oh why another microframework you ask? For the same reason why I developed a web server, a NoSQL store, a Markdown processor, a static site generator, and even a programming language: BECAUSE I CAN.
I also wanted to be able to fully understand every single line of the UI framework I was using, and also be able to fully control it — something few people can honestly say about mainstream frameworks.
Most likely, I am gonna get a huge amount of very constructive critiques on Hacker News which I will treasure greatly, and hopefully some help on how to make my little creation actually useful to someone besides myself.
Well, here you have it, my very own microframework. It’s just seven methods and two read-only properties, go try it out if you are brave enough:
Oddly enough, the actual JavaScript source code (app.js file) of the site above (without H3 or the two third-party libraries I used for markdown processing and syntax highlighting) is under 100 lines, and it should give you a taste of how it feels like to create a SPA with H3:
import h3 from "./h3.js";
import marked from "./vendor/marked.js";
import Prism from "./vendor/prism.js";
const labels = {
overview: "Overview",
"quick-start": "Quick Start",
"key-concepts": "Key Concepts",
tutorial: "Tutorial",
api: "API",
about: "About",
};
const pages = {};
const fetchPage = async (pages, id, md) => {
if (!pages[id]) {
const response = await fetch(md);
const text = await response.text();
pages[id] = marked(text);
h3.redraw();
}
};
const Page = () => {
const id = h3.route.path.slice(1);
const ids = Object.keys(labels);
const md = ids.includes(id) ? `md/${id}.md` : `md/overview.md`;
fetchPage(pages, id, md);
return h3("div.page", [
Header,
h3("div.row", [
h3("input#drawer-control.drawer", { type: "checkbox" }),
Navigation(id, ids),
Content(pages[id]),
Footer,
]),
]);
};
const Header = () => {
return h3("header.row.sticky", [
h3("a.logo.col-sm-1", { href: "#/" }, [
h3("img", { alt: "H3", src: "images/h3.svg" }),
]),
h3("div.version.col-sm.col-md", [
h3("div.version-number", "v0.1.0"),
h3("div.version-label", "“Audacious Andorian“"),
]),
h3("label.drawer-toggle.button.col-sm-last", { for: "drawer-control" }),
]);
};
const Footer = () => {
return h3("footer", [h3("div", "© 2020 Fabio Cevasco")]);
};
const Navigation = (id, ids) => {
const menu = ids.map((p) =>
h3(`a${p === id ? ".active" : ""}`, { href: `#/${p}` }, labels[p])
);
return h3("nav#navigation.col-md-3", [
h3("label.drawer-close", { for: "drawer-control" }),
...menu,
]);
};
const Content = (html) => {
const content = html
? h3("div.content", { $html: html })
: h3("div.spinner-container", h3("span.spinner"));
return h3("main.col-sm-12.col-md-9", [
h3("div.card.fluid", h3("div.section", content)),
]);
};
h3.init(Page);
h3.on("$redraw", () => Prism.highlightAll());
Simple enough. Note that it’s all ES6, and no, there are no plans to provide neither a transpiled version to ES5 nor compatibility with (yuck!) IE11.
If you are a big corporation with customers that insists on using IE11 even though it’s 2020 and even Microsoft moved on then this is not for you, I am really sorry.
If you decide to try H3 out, consider using it as it is meant to be used – with an ES6 module script tag loading the entry point of your SPA:
<!doctype html>
<html lang="en">
<head>
<title>My H3-powered App</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<script type="module" src="js/app.js"></script>
</body>
</html>
Yes, you can do that. And no, you don’t need WebPack either. Let your JavaScript live free, untranspiled and unminified even! Yes OK you can minify it if you really really want to, but I won’t do it for you (plus you’d probably want to do it yourself within your toolchain anyway).
Sure, H3 will never become the next React and will never have legions of caffeine-driven developers pouring code into it… but just creating it taught me a lot about SPAs and frameworks, and perhaps you can learn something from it too!
At any rate, feel free to open issues and suggest (or even make) changes to the source if you want to.