Mastering DOM manipulation with vanilla JavaScript
phuoc.ng
phuoc.ng
As I've shifted away from platforms like React to smaller, minimalist implementations, I often have trouble finding ways for how to do complex patterns in standard JS. It's funny when you look at the code and think... huh, that's actually a lot easier than importing a huge library with way too many props :)
Thanks to the author for putting this together.
I had this realization after the 3rd or 4th RiotJS major version update. It started getting harder to do it the opinionated way. I realized that every minimalistic JS framework would eventually suffer this fate as totally innocent feature requests gradually accumulate into a monstrous pile of hooks, events and other side-effect bandaids.
I don't even use jQuery anymore. I will use things like ES modules to help organize code, but you don't need to run a goddamn npm command to use any of that technology. All of this is a global browser standard, not something you have to vendor in.
I look at MDN as the Bible these days. If you take a leap of faith, it will carry you almost 100% of the way. The frameworks will cripple you after a certain point. I can't say this would have been true ~5 years ago. Things have shifted rapidly with browser APIs converging on a powerful set of primitives (modules, flexbox, grid, etc) that are now very consistent across platforms.
React et al. exist because there weren’t global standards around these things and composability suffered. Now that we are all playing the same chorus, these frameworks provide little more than component libraries to reuse. This can be done with ES modules. JSX is why most React folks stick with React, not knowing they can use Preact or just NakedJSX if they wish.
I just wanted to verbally concur that vanilla JS is more than capable of doing everything you need. Custom tags. Shadow dom. Composed UI. etc. if you are ok with returning HTMLElement vs a JSX closure.
They are a better* way to manipulate DOM in that you no longer need to manipulate the DOM, you just build a function that returns what the DOM should look like and it figures out what transforms need to happen.
*They’re pitched as a better way, and I think it’s better, but people can reasonably disagree
- Manually keep track of UI state (which is complex, and often leads to hard to maintain spaghetti code)
- Recreate an entire DOM tree every time you re-render (which is slow to the extent that it will often lead to performance problems in practice). Apparently this is not as slow as it used to (so you could probably get away with this sometimes - and you could before), but it's still generally a better idea to use a framework as you get better performance for very low cost.
There are plenty of non-React frameworks that will you these benefits too. And many of them are much smaller. I think the reason to use React specifically is more "business reasons" such assurances that the framework will continue to be maintained, library ecosystem, developer pool, etc. Other frameworks are often technically superior, the difference just isn't that great.
Did something arrive to plain JS APIs to allow this in a simple enough way?
It was the ability to do efficient updates based on large VDOM diffs. The speed of updates is the same between a React VDOM-diff update plan and a direct update of the same size based on e.g. observers.
While React proper may be too large, I find the approach indispensable for more complex interfaces. One can pick Preact instead.
But it's indeed overkill for adding small bits of interactivity to large, mostly static documents.
They only do that if you build a dependency graph instead of a dependency tree and iff the observing framework allows that, and without any loop resolution method. The common technique for graphs under dumb observing is to create a single update routine and pass the name of an emitter to it. I’ve built literally hundreds of interfaces using this (most of them what they’d call “more complex”) and never met any issues React kids are taught to be scared of. UI dataflow was never even a serious concern to be worth a name.
The whole premise of React compares it to a “generic detergent” which doesn’t really exist.
I'm not an expert in frontend development, but all solutions I've seen either used a variant of virtual DOM, or two-way bindings (the latter a non-starter for me). What did I miss?
The difference is, React dataflow is "returns new state with old state and (returns new state with old state and (returns new state, which triggers the above internally), which triggers the above internally), which triggers the reconciliation".
And the classic way is "triggers a state, which triggers a set of states, which triggers a set of states, and every triggered control flags a redraw internally with a proper granularity at gui side".
or two-way bindings (the latter a non-starter for me). What did I miss?
That they may be a starter, I guess. Or you can use "control X observes data A", "data B, C observe control X" dataflow. It doesn't really matter, because two-way bindings simply push input down to the data level instead of treating controls as explicit data storage. With two-way bindings you can erase the UI layer completely and still get a fully functioning object, which is useful for automation and for reusing/macroing of a proper functionality.
The large ecosystem of component libraries and the backing of a large corporate company is why people continue to use React. That, and that the extra 100kb that comes with React is generally dwarfed by the actual application in any large app.
The observer pattern got pretty much standardized by the GoP book, but people overwhelmingly prefer to run some non-standard specialized thing, as it's usually much simpler. It's a pretty much solved problem since way before a lot of current developers were even born.
All of (ok, a lot of) the React's complexity comes from it being a generic library that must support every kind of usage. Any standard would have to be as complex.
React does provide components, but there's not so much different in React components than any other component system. The main reason to use React is the state management it provides on top of those components, making sure that state is in sync across the whole application.
In my experience, the hard part of larger applications is that handling of application state. Rendering data can be done in umpteen different ways, and they're all pretty much good enough, but handling application state in such a way that every component remains in sync with all other components is hard.
There is still no global standard for templates.
Just like mustache, Twig, Java Server Pages, Jinja, ERB… every language has this problem. You have a bunch of components written in HTML and you need to mix and match them together in a bigger unit. React+JSX let you do that easier than appending DOM components. Before React, you’d use different libraries that did what React did.
The ONLY major language I can think of that has templating baked in is PHP.
React is still easier, but it comes at a maintainability tax because you have to keep upgrading to new react versions and port to new react wrapper framework versions if you want to keep a codebase in active development. Vanilla web development has no such problem because web standards remain backwards compatible.
For a small project where I don’t care about SEO / server-side rendering I absolutely prefer vanilla web dev over React now.
There has been one breaking version of React (the recent React 18) in it's 8 years of history. Upgrading React versions is a non-issue.
Wrapper frameworks like next.js can come with a much higher maintenance cost, but you don't need to use those to use React (and I think most people don't).
Depends on what you mean exactly. For example, Go ships with templates. They don't mix HTML markup and Go code directly though.
I'm not sure what Preact or NakedJSX offers me that would make it a better choice than React.
NakedJSX is primarily a low friction static site generator, it is not intended as a React or Preact replacement. Except for cases where something like React is overkill.
For example, pretty much all of the TFA examples are code you'd have to write even if you were using React.
When I began the project, I told myself I wasn’t going to use a framework. After all, I’ve been doing this since 2009, I was working with JS for a long time before I ever touched a framework. And besides, the native DOM APIs have long been good enough. Right?
My god, it was an absolute slog. Marshalling data into and out of the DOM is so tedious and error prone that I began writing my own mini framework to save my own sanity. And then I remembered why I began using frameworks to begin with - because ultimately you’re going to be using a framework of some kind regardless, it’s just whether you want your own custom one or a community-built industry standard.
I do still think native DOM is great, if you’re working with small amounts of interactivity. Most sites that use React probably don’t need it. But if your product has even a little bit of nuance or complexity, I’d go with a framework and avoid the hassle.
In the end I migrated to Svelte, and I’m much happier as a result.
That being said, it was equally difficult picking a framework that had the least likelihood to handicap me down the road. And I am still using plenty native methods, especially for the CSSOM. That has yet to be sufficiently abstracted into any kind of framework.
And to be fair I’m sure the build systems have gotten better - they just all left a bad taste in my mouth. I spent more than enough time working with them, writing plug-ins, etc.
I eventually did choose Svelte, but I’m dockerizing it so I can still use the thing a year from now, unlike virtually all of my other projects.
#!/usr/bin/env nix-shell
#! nix-shell -i fish --pure
#! nix-shell -I https://github.com/NixOS/nixpkgs/archive/4ecab3273592f27479a583fb6d975d4aba3486fe.tar.gz
#! nix-shell --packages fish gcc gnumake gmp git cacert
...build script goes here...
-i fish: I am using fish shell.--pure: Environment is as pure as it can reasonably get.
--packages: Packages from nixpkgs to be made available in the environment.
-I <github_commit_hash>: pinned to a specific git commit of nixpkgs. This script will give you the exact same environment with the exact same packages with exact same dependencies forever.
You can drop into a shell with the exact same configuration by using the cli command nix-shell with the same options.
Admittedly this is not as declarative as using flakes, since it's a script, but hey, I'm lazy, still didn't sit down to learn them once and for all.
Reference: https://nixos.org/manual/nix/stable/command-ref/nix-shell
For websites that need interaction, DOM manipulation is IMO a better move due to it using a more fundamental/universal skill, light or no build process, faster execution, and explicitness.
This is a categorization, with lots of room for grey areas! I bring it up because your use case falls squarely in the category that benefits from a framework.
I'm still attracted to that idea, but it's not worth it in the short term.
Yeah, that article really shouldn't imply that sanitization is "that easy". It does at least mention https://github.com/cure53/DOMPurify at the end but it should LOUDLY argue against attempting to write this particular thing yourself and promote that exclusively in my opinion.
Filed an issue about this here: https://github.com/phuocng/html-dom/issues/281
https://developer.mozilla.org/en-US/docs/Web/API/HTML_Saniti...
Clicked on the first one and I noticed something.
They write this:
const setFavicon = () => {
const favicon = document.querySelector('link[rel="icon"]');
favicon.href = (window.matchMedia('(prefers-color-scheme: dark)').matches)
? 'dark-mode-favicon.png'
: 'light-mode-favicon.png';
};
setFavicon();
And then this:> We can use this function to update the user's color scheme preference whenever they make a change.
window
.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', setFavicon);
They are running "window.matchMedia('(prefers-color-scheme: dark)')" twice unnecessarily. If you're going to run `setFavicon` as the callback to the event listener, you can get the result from the `event` parameter passed to the callback like this: const setFavicon = (event) => {
const favicon = document.querySelector('link[rel="icon"]');
favicon.href = event.matches ? 'dark-mode-favicon.png' : 'light-mode-favicon.png';
This would be my preferred way of doing it because it supports users who are visiting with JS disabled. <link href="light-mode-favicon.png" rel="icon" media="(prefers-color-scheme: light)">
<link href="dark-mode-favicon.png" rel="icon" media="(prefers-color-scheme: dark)">
https://phuoc.ng/collection/html-dom/change-the-favicon-dyna...https://chriscoyier.net/2023/09/29/css-solves-auto-expanding...
Even without using this new CSS property, the technique that is used (adjusting the "height" of the element) is not ideal and can be glitchy.
A better approach is to use a mirror hidden element:
https://css-tricks.com/the-cleanest-trick-for-autogrowing-te...
For example, "replace an element" is
ele.parentNode.replaceChild(newEle, ele);
but unless you're targeting Chrome < 54, FF < 49, or Safari < 10, you can just do ele.replaceWith(newEle)
`replaceWith` also lets you replace a node with _multiple_ nodes, or with text.Similarly, "loop over a nodelist" wants you to use array spread and then forEach, instead of just
for (let ele of nodelist) ...I don't do web stuff often, but when I do I am completely demoralized by the state of framework-itis there. It's gotten completely out of control. A fresh React project is hundreds of dependencies. The weight of that complexity is astounding to me.
Meanwhile the core cross-platform browser tech shipped by default has never been more capable and the amount of code to do common things ... isn't very much.
Front-end devs: are you ok?
The problem with the JS/TS/Node ecosystem is the cultural tendency there to build and proselytize frameworks. Heavily coupling seems to be seen as a positive rather than negative.
So e.g. application state management and routing are two huge things you'll still need to implement yourself if you just use pure DOM APIs
Depending on the complexity of your app then that might not be such a big deal, but these extras are rapidly worth their weight in gold as soon as you go beyond a basic app. Unless you are using anything that needs NPM in which case you are fucked.
As a front-end dev who actually like the fundamental platform and the capacities it offers, I'm both happy to see so much nice things coming our way (nested CSS, container queries, and a generally nicer JS experience since ES6) and at the same time horrified by the amount of people using components for trivial things like buttons or bold text.
And I do like framework, but so much framework and library's marketing depend on making people believe that CSS and vanilla DOM manipulation is insanely hard (it's not once you know the fundamentals), and that pulling a random package on NPM is not just quick and easy, but also the professional thing to do.
Have you tried to use this "capable cross-platform tech" to do anything non-trivial? And React isn't the only thing out there.
Why does it seem that we don't know how to do these things anymore? ;]
At the time, XML was the hotness, so all of the server back end APIs accepted and responded with XML, so when we did actually want to manage state, we did it through modification of the XML representation of the data the user was working with.
The insertAdjacentHTML(position, text) method of the Element interface parses the specified text as HTML or XML and inserts the resulting nodes into the DOM tree at a specified position.
A contrived example of something that's a pain to do with raw DOM:
let anchors = ['#a', '#b', '#c'];
let el = document.querySelector('ul');
for (let a of anchors) {
el.insertAdjacentHTML(
'beforeend',
`<li><a href="${a}">${a.substr(1)}</a></li>`
);
}
Previously I would have to create the li element, then the a element, add it as a child, set the href attribute, then the text value of the a element, then insert it into the ul element. Or, I'd batch it all up as a big string and insert it using innerHTML. Modifying an existing list was even more painful and verbose.1. https://developer.mozilla.org/en-US/docs/Web/API/Element/ins...
This summer I built an irrigation system for my father's field, with the idea that the scheduling would be done in relative time (water this zone for 2h) instead of absolute (start watering this zone at 9AM and end at 11AM).
I kept it as simple as possible:
- 16 cheap relays for the 24V AC electric valves
- 1 beefy relay for the pump
- 1 Pi Pico W for controlling the relays
Here's a picture of the "low-tech" build: https://files.alinpanaitiu.com/lowtech-irigation.jpegThe Pico W would have a code.py with all the relay and schedule logic, and an index.html which would be a simple list of sliders to set how much time each valve should be open. The Pico W would function as a "hotspot" which the phone would connect to, and the "app" would be available at 192.168.4.1.
It can be added to homescreen and it looks just like a native app as far as my father is concerned. It doesn't feel like one though, he can tell.
The web page looks like this: https://shots.panaitiu.com/lHMGplFn
That's all there is to it, really. That simple single page with a few sliders and buttons took weeks to build and test thoroughly with only vanilla JS. I was obviously constrained by the flash memory, but there are KB-sized helper frameworks nowadays which I wasn't aware of.
It's also really hard to refactor. I initially stored the schedules at minute-resolution, but then had to store it in second-resolution to add a repeat schedule feature. Oh boy, so much code to parse and change, so many slider.value calculations to redo... yep, never doing this again
https://github.com/no-gravity/dqs.js
Which turns document.querySelector('.some .thing') into dqs('.some .thing').
The other one is Handlebars:
https://github.com/handlebars-lang/handlebars.js/
Which I use for all my frontend templating needs.
how much is truly saved by this? these seem like the things of a solo dev vs building something for other people to follow
You are talking to me on a site done by one.
This is the frontend code:
… upvote/downvotes are does by fetching an "image", i.e., it does a GET request with side-effects! … also seems like a trivial CSRF vuln, too…
const $ = document.querySelector.bind(document);
const $$ = document.querySelectorAll.bind(document); function $(selector, element = document) {
return element.querySelector(selector);
}
function $$(selector, element = document) {
return Array.from(element.querySelectorAll(selector));
}
Reference on more magic available in the console (there’s a lot of good stuff):• https://firefox-source-docs.mozilla.org/devtools-user/web_co... (I think “:help” is my new favourite: opens that URL, which is not particularly discoverable)
• https://developer.chrome.com/docs/devtools/console/utilities...
const $ = (query) => document.querySelector(query);
const $$ = (query) => document.querySelectorAll(query);
(How do I write monospace on HN?)
Line that start with four spaces
like this Looks like 2 spaces are sufficient. But I sympathize with 4-space indentation.Two-space indentation.
Thank you import {qsA} from './dqs.js';
rows = qsA(myTable, 'tr');myTable.qsA('tr')
TypeError: myTable.qsA is not a function
HTMLElement.prototype.qsA = HTMLElement.prototype.querySelectorAll;
Now myTable.qsA('tr') will work. You can try it right here on the HN page in the browser console: HTMLElement.prototype.qsA = HTMLElement.prototype.querySelectorAll;
document.querySelector('#hnmain').qsA('tr');
Why do you prefer chaining?I suppose habit plus I find it easier to grok in that order, targetelement->childelements->foreach.
The problem is the build requirements you run into quickly if you want to use plugins these days with vanilla js.
Elegance was being able to load actual modules from a CDN. Why did we need these anti-web build steps on top of JS?
I feel like there was a sweet spot of complexity around Vue 1 or say 2014.
The Vue 2-->3 jump illustrates the welcome but total breakdown of frameworks in my opinion. It constantly gets in your way and moving data on a page with a hierarchy you need some bizarrely complex data flow, so many folders, files, tools, brittle typescript linting, "auto" this and that, constant building and so many rules it's not fun to engineer anymore.
Anyway, i think we are very close to a sweet spot if we just use something like Petite-vue / Alpine on top of JS maybe with a tiny router and get creative with JS around that, or just early versions say Vue if we need anything more complex.
I feel like a lot of criticism toward Vue 3 is based on feelings and vibes. What exactly are your complaints toward v3?
https://phuoc.ng/collection/html-dom/sanitize-html-strings/#...
• “Using regular expressions”: it suggests that this approach is acceptable within its limits. It’s not at all. As a simple example, the expression shown is trivially bypassed by "<script>…</script >". This is why, unlike the post claims claims, using regular expressions for cleaning HTML is not a common approach.
• (“Eliminating the script tags”: I want to grumble about using `[...scriptElements].forEach((s) => s.remove())` instead of `for (const s of scriptElements) { s.remove(); }` or even `Array.prototype.forEach.call(scriptElements, (s) => s.remove())`. Creating an array from that HTMLCollection is just unnecessary and a bad habit.)
• “Removing event handlers”: `value.startsWith('javascript:') || value.startsWith('data:text/html')` is inadequate. Tricks like capitalising and adding whitespace (which the browser will subsequently normalise) in order to bypass such poor checks have been common for decades.
• “Retrieving the sanitized HTML”: you are now vulnerable to mXSS attacks, which undo all your effort.
• “Elements and attributes to remove from the DOM tree”: this proposes a blacklist approach and mentions a few examples of things that should be removed. Each example misses adjacent but equally-important things that should be removed. You will not get acceptable filtering if you start from this approach.
• “Simplifying HTML sanitization with external libraries”: this is pitched merely as easier, faster and cheaper, rather than as the only way to have any confidence in the result.
• “Conclusion”: as I hope I’ve shown, “The DOMParser API is one tool you can use to get the job done right.” is not an acceptable position.
Really, the article could be significantly improved by presenting it as what a common developer might think, and then scribbling all over the problematic things with these explanations of why they’re so bad, and ending with the conclusion “so: just use the DOMPurify library; consider nothing else acceptable”. (There have at times been a couple of other libraries of acceptable quality, but as far as I’m concerned, DOMPurify has long been the one that everyone should use. I note also that this article is talking about client-side filtration. I’m not familiar with the state of the art in server-side HTML sanitisation, where you probably don’t have an actual DOM; this is also a reasonable place to wish to do filtering, but the remaining active mXSS vectors might pose a challenge. I’d want to research carefully before doing anything.)
I look forward to the Sanitizer API <https://wicg.github.io/sanitizer-api/> being completed and deployed, so that DOMPurify can become just a fallback library for older browsers.
My bad, I left a fragment in the URL.
Case in point: I'm working on a shipping Blazor WASM application, and I've had to drop into the DOM twice:
1: We're still using Leaflet.js for a map component. This occasionally requires some DOM calls to adapt between Blazor's worldview and Leaflet's worldview.
2: Blazor WASM loads slowly, so we put up a splash animation in HTML. Removing the splash animation at the "right" time requires DOM manipulation. (Because we want to keep the splash animation up while the Blazor side makes additional calls, so we use DOM manipulation to hide the Blazor-based UI until we've loaded everything.)
Of course you can do anything with Vanilla that you can do with e.g. jquery. Otherwise jquery obviously couldn't do it.
The problem is that the vanilla DOM API sucks. document.getElementById sucks vs $, document.querySelector sucks vs $. setTimeout and setInterval suck. addEventHandler really sucks.
There should be a way to do this with CSS and maybe some magic headers. It's super annoying that it has be done via JS.