Hacker News new | past | comments | ask | show | jobs | submit login
Show HN: Swap.js – a JavaScript micro-framework (HTML fragments over the wire) (github.com/josephernest)
127 points by josephernest on May 19, 2023 | hide | past | favorite | 45 comments
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!




I’m bought in to this approach after using Hotwire, but I’m concerned that all the competing implementations are going to undermine adoption. Maybe we should all just use htmx and call it a day.


It's even my hope that the things in HTMX will become native to HTML one day.


Definitely this. It would be great if HTMX has a similar path to jQuery, where the browsers adopt enough of the features as native that it is no longer required.


The nice thing is the simplicity of all these libraries means changing from one to another is no big deal. Most of the work is server side and already done. It is not like you need to swap all your React components to Vue ones.


Feature request: documentation describing the accessibility impact this will have.

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.


Good question... I haven't thought about it deeply, but I would say that using this lib itself won't have impact on accessibility: if the HTML fragments are carefully crafted to take accessibility into account, then after HTML fragments swapping the DOM will keep this aspect.

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?


I'm not 100% sure myself. My understanding is that if you swap out a piece of the page you need to sleet screenreader users to the change, but I don't know the best way to do that - I presume it involves ARIA attributes.


Someone who used jQuery.load a lot back in the day, it's nice to see this trend coming back. Only thing that stands out to me is using custom properties without data prefix, e.g.

<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?


I believe doing this is already quite common and there's no real drawback. As far as I know the main advantage of the data- prefix is that there's a guarantee that no official spec will use it.


How would you compare yourself to HTMX and Unpoly? I'm keenly interesting in this space for sure and love that you're working on another!


Thanks for the comment!

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.)


Lib is EXTREMELY minimal. Only GET requests with anchor tags. That you can’t POST forms or send metadata make it mostly useless for anything but the most basic website.

I 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).


I mean, you can just write some code to post data to an API.


It's not about the POSTing. Its how you handle the response. The paradigm of this library is that you replace segments of the page with the response, which it doesn't support for POST requests.


Isn't that what <form> is for?


Was. for, "modern" things (libs, framework, etc) intercept these bog standard bits with their "magic" rebuilding to maybe 70% of what already worked.


Refreshing the page to send an upvote isn’t peak UX.


Link handling needs to check keyboard modifier state, so that you don’t intercept things like Ctrl+click.

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):

  addEventListener("click", event => {
      var link;
      if (
          !event.button &&
          !event.altKey &&
          !event.ctrlKey &&
          !event.metaKey &&
          !event.shiftKey &&
          (link = event.target.closest("[swap-target]"))
      ) {
          update(link.href, link.getAttribute('swap-target'), link.getAttribute('swap-history'));
          event.preventDefault();
      }
  });
Some other minuscule patches for size and/or performance (of things a minifier won’t do):

  -document.querySelector("html")
  +document.documentElement

  -window.addEventListener
  +addEventListener

  -'*[swap-target]'
  +'[swap-target]'

  -fallback = null
  +fallback
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.


I've been working on my own library and as soon as I saw the code I realized I didn't have handling for keyboard modifiers either - so even if the original doesn't see your comment - it wasn't a waste! Thank you! :)


These are all great comments. Doesn't look like the author read them (or at least they never responded). Hopefully they read them at some point and use your good info.


Hi, author here, I just woke up and see all these comments :) I'm reading and I'll answer!


That's my bad. In my caffeine-fueled thirstiness I forget people need sleep :) Thank you for your work.


Haha! I don't drink coffee, and can say chocolate-fueled doesn't work so well to work without sleep :)


Thanks for your detailed comment, the in-depth reading, and for this insightful advice. I'm going to think about all these points!


I wish someone would do this for any of my libraries. Good stuff.


I wrote something similar by myself for a PHP CMS project with server side templating.

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.


> vanilla JS (no external tool needed: no bundler, no webpack, no TypeScript compiler, no minification needed...)

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.


Oh nice, I am working on something similar, but didn't think of using MutationObserver for anything.

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)


I read through the swap.js file and there's a lot of things happening. I also like the fact that it has no dependencies and it's cool that it achieves so much with so little code.


I'm getting a bit of deja vu from this recebt rediscovery of ajax but i really like it. Back when js was obscure and jQuery became ubiquitous, that practice was a go to and sites started to feel, albeit sluggish, like more and more like todays spa centric ecosystem.

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!


Ah the good old element.innerHTML = xss_vector_here


Demo showing the library in action: https://afewthingz.com/swap-library/email/

PS: some music I made if you want a musical background:) https://afewthingz.com/since


This is much better than htmx. I like the focus on just swapping content


Why is it much better?


Wow. That is beautiful. So concise and yet easy to understand.


Thanks a lot! This is very motivating!


I like it, but it feels funny to have this discussion without acknowledging this is really similar to pjax/turbolinks. Ok, now I'm feeling better; carry on!


Impressive, especially considering the implementation size. Just by looking at swap.js file, I could not actually believe it really works but it clearly gets the job done.


Yes, I think it’s quite a delightful example. Small enough that it’s quite realistic to be able to read it, understand it, and add to it.


Why fight evolution?


I built something very similar ages ago with a short php script and a line in the .htaccess It’s cool to see these ideas coming back around.


Have you thought about adding polling functionality, so that a given element can check with the server for updates at a set interval?


Thanks for looking at the lib!

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:

    Swap.loaders[".my_view_with_polling"] = () => {
        update_task = setTimeout(do_polling, 1000);
        ...
        return () => {
            // unloader function when we quit this view
            clearTimeout(update_task);
        };
    };
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...


Ah makes sense, thanks for the example!


A new world: Part Three.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: