Show HN: Swap.js – a JavaScript micro-framework (HTML fragments over the wire) (github.com)
Hi HN! I created this lib in the need of a simple and tiny framework to easily do AJAX-style navigation / replacement of fragments in the page, in a web application.
For people who don't want to use client-side-rendering and complex frameworks à la React, there are nowadays a few "HTML-over-the-wire" libraries, like HTMX, Unpoly or this super-tiny one Swap.js :)
One other key thing is that no external tool is needed: no bundler, no webpack, no TypeScript compiler, no minification needed. Just write HTML, JS (+ your preferred server-side language: PHP, Python, etc.) and it works.
The framework makes use of fetch (of course) but also MutationObserver API to be able to launch actions when parts of the DOM change.
Let me know what you think!
48 comments
[ 2.7 ms ] story [ 99.7 ms ] threadI guess the creator will think about the minimal features to add in their remaining 50 LoC budget. (IMO a minimized KB target would be a better ceiling).
These 2 other libraries are really great.
For something as important as the choice of a web framework, I realized I really wanted to 100% understand the internals, then I starting working on Swap.
The key things for me are:
- no tool required (just a text editor, no compiler, no bundler/packer, no minification even needed: I just wanted to write JS and that's all).
- be able to re-read the source code in 10 minutes and 100% understand how it works, which is impossible with most frameworks
- even if simple, I wanted that non-trivial features are included (such as browser history, when using previous / forward browser buttons), because I don't want to have to think about this when creating a new web appliation
My first prototypes took a few hundreds of lines of code (for the same result). I spent some good time redesigning it, and finally I was surprised I can cover 80% of my navigation needs (when doing web applications), with only 50 to 100 LoC.
For the rest (features not enhanced by Swap, like form submission), of course, I just write normal JS, but I think I would have done it anyway whatever the framework.
TL;DR: in my lib I just focused on swapping HTML content (parts of the page) when navigating, thus the name "Swap" :) + I wanted the lib to do the boring stuff for me (make sure the browser history still works, etc.)
You can also work with just one event handler rather than many, which is generally easier, and more reliable if other scripts ever touch the DOM.
Here’s something you could replace the first register_links() call with (and drop the other register_links call and definition):
Some other minuscule patches for size and/or performance (of things a minifier won’t do): You can also simplify the code a tad by inlining function dom_changes and dom_load—though any minifier will do this for you, but I find it nicer having those functions inline to begin with. On style, I also wish for consistent single or double quotes rather than mixed.The document.createElement('html') dance feels wrong. I feel like it should be using new DOMParser().parseFromString(). I’m not sure off the top of my head if it will matter, but it feels like you’re inviting mXSS attacks.
On update, you’re only applying body stuff, but you normally want to merge head changes in as well, especially document title.
You’re going to run into problems with replacing document.body.outerHTML. Browser extensions and other page JavaScript regularly rely on being able to inject their own stuff onto that element, and on a stable identity for the element, too much—so you’re going to break or be broken by various user extensions and other scripts that you load beside swap.js. Major UI libraries have sometimes started with working directly on document.body, but they’ve all ended up recommending that you mount them in a subelement instead, because otherwise too many things mysteriously break. I recommend getting away from this somehow, but I’m not sure quite how—it’d definitely add required complexity.
I’m not fond of how loading works: you rely upon this script being loaded blockingly, with customisation also blocking and after it. Some major uses of this script could otherwise be async and completely optional (progressive enhancement territory). Unfortunately, because of how you handle loaders, you can’t just switch to `document.readyState == "loading" ? addEventListener("DOMContentLoaded", dom_load) : dom_load()` to make it support <script async> usage, without losing functionality. You’d need to expose some other way of adding to Swap.loaders.
Seeing projects spawn that the default that ssr was and making those early, simple optimisation tools much more streamlined, understandable, accessible and purely functional is great!
But be careful with hooking to links, there situations you want to test for that might need to work like a normal link (target blank or external url for example)
Nice, I like that. I dread the modern way of having to jump through random hoops to get a garbled output that is supposed to contain my code as well. This is way more as I feel it is supposed to work: include a script tag.
PS: some music I made if you want a musical background:) https://afewthingz.com/since
<a href="newpage.html" swap-target="#container" swap-history="true">Click me</a>
Shouldn't that be data-swap-target="#container" data-swap-history="true"? Is custom properties okay these days without data?
Any time I'm considering a library like this my number one question is always the impact it will have on accessibility tech, most specifically screenreaders.
I would love it if libraries could get into the habit of describing this impact in their documentation - and ideally if they could compete on providing the best possible accessibility as one of their promoted features.
I would say the construction of the HTML fragments (done by the user of this lib) is the most impactful part.
Since the URL can change after a HTML-fragment-swapping with this lib, I can imagine that screenreaders will detect the URL change and take the new layout into consideration, is that right?
Polling is typically the kind of thing I usually prefer to handle fully manually in JS. That's what I do for my own web apps.
Even if I added a specific HTML tag attribute for polling, at the end, there will always be a missing something, and I'll end up writing JS anyway. So for this kind of thing, I don't think a lib is necessary: you just setTimeout / setInterval in the script that runs when the view is loaded:
For a real example, see my email client demo: when you open an email, there is a timer. Here it's done: https://github.com/josephernest/Swap/blob/main/demo3-email/a...My use case was progressive enhancement for pagination and filtering for lists, preserving all functionality with JS turned off, and using shareable "isomorphic" URLs.
It worked out pretty well, my headaches mostly stemmed from the backend and the "isomorphic" part: slight tendencies towarda overengineering and hard-to-fix bugs all stemmed from the backend ("ajax vs non-ajax", simply put).
Cool URIs don't change.
My two cents:
1. If there is a solution tailored to your backend, use it (unless it involves complex DSLs or many dependencies)
2. If not, write the code by yourself. It's not actually hard, most work is in the backend. Use "pushState" instead of "replaceState" only after everything is proved to work. Only push URLs, no data.
This is simple, low-hanging 2006ish JS. Don't use backend-agnostic cruft for this. It'll waste time.
It you need complex client-side interactivity as well as SSR and/or SSG, use a full-stack TS solution or an SPA with SSR.