Coarse Programmer's guide to reactive UIs: Intercooler.js
June 9, 2020
This is part of a series I call the Guide to Coarse Programming: How to Program Without Being a Programmer. Part 1 is here.
One of the perks of being a coarse programmer is that, since you have so little at stake, you don't feel a constant need to rush after the latest technology. While the hipsters are surfing the wave of React, Svelte and its ilk, I'm sloshing about in the shallows, sand oozing between my feet as I search for interesting-looking shells. One of those shells is intercooler.js, a javascript package that allows you to achieve a bare modicum, almost a facsimile, of reactive UI, without touching a drop of javascript.
In a nutshell, intercooler.js allows you to bind ajax calls to click or other DOM events and then swap out the contents of the element with whatever you get back in the response. It's not a groundbreaking task, but it would normally take a few lines of jquery, probably a few minutes to look up how to do it (in my case every... single... time...), time out to remember to import your .js file into your html, and a few more to make sure your ids, classes and data-attributes are square across the two files. With intercooler.js you can do that with a single custom html attribute.
<button ic-get-from="/ic_demo/1"> Click me! </button>
The server doesn't return JSON, it returns HTML.
def ic_demo(request): return HttpResponse("<h5 style='border:1px solid grey;border-radius:4px; padding: 20px;'>It was all very well to say “Click me,” but the wise little Alice was not going to do that in a hurry.</h5>")
This seems to fly in the face of modern web development. The creator of intercooler.js knows just how idiosyncratic his product is, and has an entire philosophy around why we should be sending HTML and not JSON through our APIs.
and...
I find this inspiring. I don't really understand it, but I'm inspired. I like the idea of championing a humanistic vision of the web with my code, maybe less facebook and more geocities. In more practical terms, I think it means I can make a somewhat reactive page with all my logic running on the server and all my HTML created by the server, just as I have always done. I enjoy hand-coding HTML and I love the Django templating system. With intercooler.js I can create template fragments that I use equally with Django's {% include %} tag, and deliver by ajax.
Here's a clunky demo that also demonstrates some of the other features of intercooler.js. This particular example would actually be more efficient with javascript but with some imagination you can see how it could be used for more complex, and potentially more interesting, interactions with the server.
The balloon is an HTML fragment that is included into the main Django template. It takes three variables: the text to display, the diameter of the balloon, and the half diameter for the border radius.
# templates/fragments/balloon.html <span class="balloon" style="width:{{ size }}px; height:{{ size }}px; border-radius:{{ halfsize }}px;" ic-get-from="/ic_demo" ic-include='{"size":"{{size}}"}' ic-replace-target="true" > {{ text }} </span>
The span also has the following intercooler.js attributes:
- ic-get-from tells intercooler.js to make a GET request by ajax to the given address when the balloon is clicked. (Clicks are the default event, you can specify others).
- ic-include adds the given parameter to the GET query string.
- ic-replace-target tells intercooler.js to replace the span itself rather than its child nodes with the ajax response.
On page load, I include the template fragment into the blog page.
# blog_page.html <p>...interactions could be carried out with the server.</p> {% include "fragments/balloon.html" with size=50 halfsize=25 text="click me" %} <p>The balloon is an HTML fragment...</p>
Then I've got an url pattern set up to channel the request to the correct view.
# urls.py ulrpatterns = [ path("ic_demo", views.ic_demo), ]
The view extracts the current balloon size from the GET querystring, increments it and decides what hilarious text to display.
# views.py from django.http import HttpResponse from django.template.loader import render_to_string def ic_demo(request, did): size = int(request.GET["size"]) + 5 if size > 250: text = "I can't take responsibility for what might happen if you keep clicking." elif size > 200: text = "You can probably stop clicking now." elif size > 150: text = "You know, the last visitor clicked much faster than that." elif size > 135: text = "Click. Click. Click. Don't stop" elif size > 100: text = "You REALLY like clicking things." elif size > 80: text = "Boy you sure like clicking things." else: text = "click me" return HttpResponse(render_to_string("blog/includes/balloon.html", {"size":size, "halfsize":size/2, "text":text}))
Here's a more practical example that shows just how quirky intercooler.js is. Link Browser, is a simple web app that allows you to browse all the available URLs on a webpage to learn their structure and the layout of the site without having to wade through actual content. It is a part of a larger app that I am building for scraping and analyzing news websites.
I wrote about Link Browser in an earlier post. It is free for anyone to use as a standalone app.
But as part of the scraping suite, it also allows you to create a black list of links you don't want the scraping spider to touch. The "ignore links" are defined as a list in a JSON field on the model for the news outlet.
class Outlet(models.Model): name = models.CharField(max_length=100) domain = models.URLField() ignore_links = JSONField(blank=True, null=True) def __str__(self): return self.name
You can add or remove an ignore link by clicking on the little "x" button.
The button is contained within a span defined in a Django template fragment. The template takes one variable, "url", a tuple which consists of a boolean that is true if the URL is in the ignore_links list and the URL itself.
<!-- link_browser/fragments/ignore_link_toggle.html --> <span class="ignore-link-toggle {% if url.0 %}ignore{% endif %}" ic-get-from="ic_toggle_ignore_link" ic-replace-target="true" ic-include='{"url":"{{url.1}}"}' > <i class="far fa-times-circle"></i><i class="fas fa-times-circle"></i> </span>
The span also has intercooler.js attributes so that when it is clicked it will send a GET request, with the URL in the query string, to the server. The request is channelled through my url patterns...
# link_browser/urls.py from django.urls import path from link_browser import views urlpatterns = [ path("", views.link_browser), path("ic_toggle_ignore_link", views.ic_toggle_ignore_link), ]
...to the view which checks if the URL is in the outlet's ignore_links field. If it is it removes it and sets ignore to false. If not, it adds it and sets ignore to true. It then returns the exact same span template, differing only in whether it possesses the "ignore" css class or not.
def ic_toggle_ignore_link(request): url = request.GET["url"] outlet = Outlet.objects.get(id = request.session["oid"]) if url in outlet.ignore_links: outlet.ignore_links.remove(url) ignore = False else: outlet.ignore_links.append(url) ignore = True outlet.save() context = {"url": (ignore, url)} return HttpResponse(render_to_string("link_browser/fragments/ignore_link_toggle.html", context))
That's a lot of redundant data coming back in the response. If I had done this with javascript I would have just expected a simple json value like {"ignore": true} and then toggled the class at the client end. But intercooler.js can only receive HTML, so you need to swap the entire element, class and all. I could have made this example simpler by swapping only the X button inside the span. But I also want to the link next to the button to turn red when the button is clicked. In fact, I could have also swapped out everything associated with this entry in the list -- the button, the link and everything else on the line -- but that just seems gratuitous. By swapping out the span, which is adjacent to the link, I can use a simple css rule to turn the link red.
.ignore-link-toggle.ignore ~ a { color: red; }
So, this is less code than using jquery and less faffing around with npm etc. to use react. But it still requires a few contortions and hacks. Is it worth it? Well, contorting and hacking is usually what I do when I go in the sea, and I always have a good time. As the finer programmers than me glide elegantly past on their frameworks du jour, I'm happy enough splashing about with intercooler.js.