I keep thinking about this dream I had, and in order to get it out of my head so I can move on with my life I think I am going to have to write some words and code about it.
Before I get carried away with something else and forget, let's do this safely — since this is just going to run as a standalone script, first we need to open with an anonymous function to guard the rest of the code in this script within a closure. The code inside this closure won't be reachable from code outside the closure, minimizing the extent to which they can interfere with one another. This is a small isolated project, so there is no such competing code, but this is generally considered best practice for quick little scripts, so fine, I'll try to be a good role model here and do it anyway.
This is written in a style called "literate programming" which mixes prose and code. Code blocks like this one will be executed by a tool I wrote called lit-web.
(() => {
It is currently distressingly early in the morning, though there's really no point in me specifying an exact time since I guess I'm going to be sitting here editing this for a while. I can't sleep. I woke up and stumbled into the kitchen and ate some crackers, as one does, but I didn't find that fulfilling enough, so now I am sitting outside on a folding chair with my laptop. The sun should be coming up soon. I will keep you posted.
Web pages are built with something called the "document object model," which is a fancy way of referring to the structure of the content. Many of the functions from the browser's native APIs for working with the DOM require input parameters specifying a node – the specific piece on which they are operating. This is tedious and verbose — it makes for awkward code, and usually much more of it than there actually needs to be. Code sucks! We can't avoid it entirely, but we can move it out of the way.
You see, alternatively you can also write a function which closes over that node so you don't need to specify it at call time, which is much more pleasant syntactically. At times over the years I've reliably found myself frustrated by the verbosity of this API, which pales even next to an old tool like jQuery when it comes to the readability of the code you create with it, but it's really meant as a lower-level tool on top of which you'd build other more usable abstractions.
Like this one! Most of the time I'd probably reach for a library in order to handle the most tedious parts of the task, in this case manipulating the page markup. In this case I won't do that, though, and will try to quickly build up my own toolkit here as quickly and concisely as possible. That's because I'm writing about my dreams, and it feels purer somehow to write them directly into the internet without any intermediates. I think what's going on here is that I also want this code to feel technically elegant – or at least more so than it would be using the lower level DOM API – in the hopes that my writing will then attempt to keep up. That means defining a bunch of extra helper functions first.
I have a technique in mind for simplifying it here. The key to making it work is going to be that Function.name has finally graduated to official standardized metadata as of ES6, so it now behaves predictably after several years as an informal feature. It has been five years since this revision came out and I tend to write all my code using its constructs but still had never heard about this change until I went digging around for a way to simplify the DOM interactions. Among other things, this means you can reliably dynamically attach functions to host objects because you can now determine what those method names should be.
Ha! I am just trying to tell you about my dream and I am starting by building a DOM node manipulation library. I am aware that this is silly.
All of the above builds to the most abstract of the utility functions I'll be introducing: given a node and a function, attach the function to the node as a method using its Function.name
, after which you can run it by calling node.name()
. It's called chain()
because this is what lets you connect your method calls in a series.
const chain = (node, fn) => {
return Object.defineProperty(node, fn.name, {
value: (...args) => fn(node, ...args)
})
}
Okay, now that I have that part in place I can already see how this is all going to come together, so I can get back to telling you about the dream.
It took place inside a bar, which even from the outside was clearly a beloved neighborhood institution. It reminded me a lot of the places I ended up stumbling into when I visited New Orleans a few months ago. I can only vaguely remember the street outside, which seemed to be near the center of a suburban town. It was very quiet, but that was because everybody was inside.
A full-fledged library for manipulating a web page document would need to be able to do a lot of things. Fortunately I don't need to build a full-fledged library, however, for the following reasons:
-
I'm not actually manipulating the page content — I'm building it. This means, for instance, that I don't need a
remove()
function; the actual code I'm trying to run will just not add any unwanted content, and consequently there's no need to think about removal, which would otherwise be table stakes. -
This wasn't always the case, but for historical reasons a lot of the utility functions provided by libraries are wrappers on top of native functionality that is by now perfectly usable on its own. I shudder to imagine the edge cases that once drove jQuery to implement a
text()
function — if a custom tool needs a special function to print text on a web page, something has gone horribly wrong, because that has always been the whole point of web pages. It used to be a mess, but by now the browsers have made things a lot easier. We certainly won't need anything comparable here, because the DOM API lets you setnode.textContent
, which is perfectly sensible to begin with, unlike so many other things about the internet.
Which is all to say that I think I can get by here with only two functions:
add()
, to add new child nodes to an existing nodeattribute()
, to set the value of a specified attribute on a node
Ordinarily an attribute()
function would work bidirectionally and allow you to read existing attribute values, in addition to setting them. But since my purpose here is only to build, I won't need that — just like there's no need for a remove()
function if my code will only be creating page content, there will likewise be no attribute value unless I set one.
I spent a little while loitering outside in the grey light of the early evening, kind of like it is right now, waiting unsuccessfully for something interesting to happen. When nothing showed up, I finally gave up and walked into the bar. It was dank and dimly lit, the kind of place that is instantly identifiable as a dive from the smell alone, although I don't actually recall being able to smell anything because I guess I can't smell when I dream. This is the first time I've ever thought about that. Save for the bar stools at the far end, almost all the seating — booths, benches, tables — had been crafted out of a dark cherry colored wood, and most of them were covered in a thin but ultimately acceptable layer of grime.
In just a moment you'll be able to start to discern the shape of the DOM utility, which feels like it should in itself be a big climactic reveal at the end of this, but it is not, because it's actually just a means to an end — the tooling I'll use to build the thing that is the actual point. These are the "on" and "off" switches: given a node, add or remove the custom functionality.
Later I'll go on to discover that the safety checks inside the activate()
function are important, because they prevent nodes from being modified with the additional methods when it isn't necessary, which in practice seems to mean I no longer need to worry about occasional stack overflows.
The activate()
function returns the node so you can immediately call its new methods; this may allow me to make future code built on top of it a little more concise. It's probably not actually necessary for deactivate()
to return anything at all, but in the interest of symmetry I'll have it match the companion function.
const activate = (node) => {
if (! node.add) { chain(node, add) }
if (! node.attribute) { chain(node, attribute) }
return node
}
const deactivate = (node) => {
delete node.add
delete node.attribute
return node
}
As I walked in, two people were sitting at the booth to my left working together on a laptop; they were both totally engrossed, and whatever they were doing seemed to be very important. It was probably a homework assignment for school, I concluded; they seemed pretty young.
The interior of the bar was laid out in the shape of an L, starting with a long corridor from the doorway and then abruptly extending to the right and into a small main room lined with the same tables and booths. It grew more crowded as I walked toward the back. Everyone else was also young, in fact. This is around the time I realized I was still in high school, or perhaps that early phase of college where you still hang out with your high school friends whenever possible, and probably come back home to visit a whole lot more than you will in the years to come. I was young too, and I knew a lot of these people.
Now for that add()
function: append a node of the specified type to the parent, attach the usual functionality as provided by the chain()
method, and return it with all the additional utilities in place. One of the shortcuts I can take is always using the svg
namespace since over the course of this project I am not planning to use it for standard HTML.
const add = (parent, child) => {
const node = document.createElementNS('http://www.w3.org/2000/svg', child)
parent.appendChild(node)
deactivate(parent)
return activate(node)
}
In the little room to the right, there was a semicircle-shaped mob of teenagers surrounding someone who had just finished addressing the crowd, I think, though I'd missed the presentation. My friend David was at the front of the pack, entertaining them all with off-the-cuff jokes I couldn't quite make out, dressed in a long red shirt from some cool clothing brand I am sure I had not heard of. I guessed it probably cost about $50. He paused dramatically for a moment, giggled, then said something I still couldn't hear and everyone in the crowd exploded with laughter; he'd always known how to work a room. Chris and his younger brother were there, and I heard the tail end of a joke leveled at Chris: "He's not even allowed to check bags when he flies."
Okay, wait, that made a lot more sense as a punch line in the context of the dream than it did just now. I was able to extrapolate the gist of the body of the joke which I'd missed: Chris was quickly devolving into a fuck-up, generally — grades, drugs, inconsequential crimes executed with nowhere near enough planning for which he'd quickly be caught — and the joke was that his reputation preceded him with the airlines, who would let him board their aircraft only on the condition that he never attempt to bring anything large enough to be problematic.
The second function I'll need is one that sets an attribute value on a node. Most libraries would probably define this as both a getter and a setter, but that's not necessary if we're only trying to build a DOM without subsequently manipulating it. It's the attribute equivalent of the missing remove()
function — just don't set the damn attribute value in the first place, you idiot! Omitting that bit only saves about three lines of code, but every little bit makes me feel better. The activation step at the end here isn't actually necessary as far as I can tell, but it's included for consistency; this ensures that the returned nodes will consistently have the additional functionality attached, and also that the safety checks associated with activation will be robust and consistently applied every time the methods are added.
This is really just a more pleasant way of running setAttribute()
, I guess, but it's logically useful in that it strips set
from the method name I call, since I don't plan to get
any attributes.
const attribute = (node, key, value) => {
node.setAttribute(key, value)
return activate(node)
}
I'd never been entirely comfortable when David was doing crowd work, because I'd known him since before that more confident side emerged, and it just never really struck me as a natural fit. I turned around and slowly backed away toward the bar. That's when I noticed the people in uniform.
For reasons I've never fully grasped nor cared to study up on, even now despite explicitly writing about it here, items on a web page get collected not into a conventional array or list, but with something called a NodeList
, the primary difference being that it lacks all the usual niceties of a proper array. Converting that collected content into an array would save me a lot of boilerplate, because it means that I could then use all the usual ways of dealing with arrays to also work with my document. This is another case where the recent changes to modern versions of JavaScript make things much easier: instead of a loop or some bizarre incantation like Array.prototype.slice
or even just Array.from
, you can just spread it with ...
, copying it into a real array.
To kick things off, you'll need a selector which returns an array of nodes with the customizations added. Look at how succinct it is!
const select = (selector) => {
return [
...document.querySelectorAll(selector)
].map(activate)
}
Okay, fine, I gave in and looked at the NodeList
documentation, and while I am not yet convinced that they are anything other than a pain in the ass, I see that they do support ES6 iterables, so that's something. I'm still going to spread into an array so I can chain my functionality together with .map()
. This makes it only marginally more succinct, but I am deeply invested.
There were small card tables set up throughout the bar, each manned by a pair of people in various uniforms, all of whom seemed to be improbably tall. As I drew closer I could see that they were all military personnel. The tables were bare except for stacks of pamphlets in clear plastic stands. This was a recruitment event.
There's room for one more handy abstraction, but this time it's not attached to the nodes themselves.
The nodes are returned in an array, which means you can iterate over them using the array's .map()
method. The .map()
method runs a function for each item, like array.map(item => item.doSomething())
or whatever. But we can write a function factory that generates the functions for the map. This will clean up a lot of the boilerplate code and make it easier to read. But what should this factory wrapper be called? Here are some options, all of which I hate:
set()
is an upsetting possibility because I just wrote this whole thing above about how to generate a document you don't need bothget
andset
functions so it's cleaner to omit that verb entirely, and now I'm down here and I haven't learned the keyboard shortcuts of this code editor yet so I don't want to have to go back up and delete it.attribute()
would get confused with thenode.attribute()
method I was so excited about setting up before.apply()
would probably be confused withFunction.prototype.apply
, which is already annoying enough to explain.multiple()
does sort of imply that this whole thing exists only to operate on multiple nodes, but it sits on the wrong side of the interface: that is,.map()
already implies that there are multiple items, and the individual function returned by the factory doesn't actually care whether you use it once or several times.callback()
to generate a callback function is self-evidently gross, and although it hides some of the code characters, it creates an ununusal interface for a common pattern.
I have abstracted myself right out of a reasonable name!
Which leaves us with, I think, some kind of name that deliberately addresses the fact that it's an abstraction: adapt()
or interface()
or wrap()
or something.
But like, why not embrace the fact that there are no useful abstractions left to describe this one? With apologies to underscore.js, which is not in use here, we'll use that. The symbol for this will be the minimal name possible, a single character with no real meaning which roughly amounts to an empty space and is visually hard to discern at all.
const _ = (key, value) => (item) => item.attribute(key, value)
Did you know the Coast Guard is considered one of the core branches of the military? I had no idea. I guess I always considered them to be something more akin to a fire department, in part because popular culture references to them almost always have them dealing with some beach-related minor civilian clusterfuck, as in "Jaws" and "Baywatch," the latter of which was broadcast in syndication for a time right as we returned home after school.
Code always feels cleaner to me if arbitrary values are explicitly demarcated intead of sprinkled around inline, so let's start by setting up a configuration block. I'll try to use the minimal number of values here. Yes, I have used snake_case
for my variable names instead of the usual camelCase
. No, I don't care to hear your thoughts about that. I gravitate toward concise variable names when I'm coding in JavaScript. This is because I find camelCase
aesthetically infuriating and don't want to argue about it with anyone, so the best solution is to avoid ever introducing a second word, thereby avoiding the problem entirely.
const height = 500
const width = 960
const grid = 10
const padding = 0.2
const border = 0.25
const size = height * (1 - padding)
const bar_width = size * 0.25
const middle = width * 0.5
A couple of these values are arbitrary. Both the rotation values were my first guesses, believe it or not, and they make everything sit perfectly flush. This could probably be expressed with a fairly simple algorithm, but why bother? Let's keep things moving.
const green = '#00e600'
const rotation = {
degrees: 315,
extension: 1.2
}
Kids I'd grown up with, many of them my good friends, were milling around at a military recruitment considerably more coordinated and effective than anything we'd seen before. Occasionally there would be some guys from the army set up in the cafeteria during lunch, but nobody ever really paid attention. This time it felt like the recruiters were hunting us.
Let's make an image now.
Maybe I won't need to explain this much more, because by now I've spent a bunch of time building a little utility which makes the code I actually intended to write clearer. Let's see.
With an array of multiple nodes, you'd typically use array methods like .map()
and .forEach()
to act on each node, but in this case there's only one node, so it's easier to .pop()
the only item off the array and work with it directly.
// image
const svg = select('main')
.pop()
.add('svg')
.attribute('height', height)
.attribute('width', width)
Now that I can see it in action, this feels like kind of a neat approach to a DOM manipulation library — adding transient methods directly to the node objects and then removing them after you are done using them, leaving no trace that they were ever there.
I was so utterly unprepared for the world to start opening up to me at that age that I cannot fathom any teenager being adequately equipped for it. Even though on some level I understood why some people might have viewed joining the military as the right way forward, the recruiters setting up shop right in the school still always struck me as opportunistic. I never considered joining the military myself until later.
Kids can be swayed by the most ridiculous things. These guys were handing out free shirts.
The image node doesn't actually have anything in it yet. How about some shapes? First, a square.
// square
svg
.add('rect')
.attribute('height', size)
.attribute('width', size)
.attribute('x', (width - size) * 0.5)
.attribute('y', (height - size) * 0.5)
.attribute('stroke', 'black')
.attribute('stroke-width', size * border)
.attribute('fill', 'none')
As it turned out, the military had rebranded. All of it, all at once, in an effort to make it seem cool. There was a new unified logo, with each branch now using slight variants of the same base emblem.
Two rectangles:
const rectangle = () => select('svg').pop().add('rect')
const rectangles = Array.from({length: 2})
.map(rectangle)
Using the helper function I couldn't figure out how to name, set a bunch of attributes identically on both rectangles. I think I finally understand why I spent so much time setting up a toolkit at the beginning.
rectangles
.map(_('class', 'diagonal'))
.map(_('fill', green))
.map(_('height', height * rotation.extension))
.map(_('width', bar_width))
Trying to do this with all the little helpers and abstractions in place lets me get closer to the image logic without a bunch of lower level web nonsense getting in the way. This makes it feel more like I'm painting something I saw in a dream.
The horizontal positioning logic behaves differently for the two bars, shifting the first to the left and the second to the right.
const direction = index => index === 0 ? 1 : -1
const bar_spacing = index => {
const offset = bar_width * 2 * direction(index)
return middle - bar_width * 0.5 + offset
}
Set the attributes which differ based on the index.
rectangles
.map((item, index) => {
const x = bar_spacing(index)
const y = height * 0.5
return item
.attribute('x', x)
.attribute('transform', `rotate(${rotation.degrees} ${x} ${y})`)
})
The graphic rendered by this code was the cool new logo for the army as it appeared in my dream, expertly crafted by marketing consultants and designers to excite teenagers and to be screen printed on dozens of cheap shirts that would convince my childhood friends to enlist and possibly then march off to their deaths. I wanted to stop them but I couldn't figure out what to say.
The .append()
method runs deactivate()
on nodes before it moves on to return the child nodes it adds, but once you're done with the nested child nodes there's a final round of nodes which aren't automatically cleaned up because nothing is ever appended to them. These will thus need to be deactivated explicitly.
.forEach(deactivate)
I've never woken up and painted a picture I saw in my dreams before. Maybe next time I'll just do that. This was a lot of trouble.
We have now reached the end of the script.
})()