Stop Fighting the Browser

The ability to do almost anything with JavaScript is a blessing and a curse, often encouraging developers to re-implement existing functionality rather than use what already works.

One of the things that continually fascinate me is how much web developers want to overrule, override, second guess, reimplement, or just plain break standard browser behaviour.

This is particularly common among junior developers, many of whom are just becoming familiar with the rich interactions JavaScript can provide a page. JavaScript becomes the hammer and everything is a nail. What this often means, though, is that huge chunks of already working functionality are re-written. JavaScript is most powerful when used to augment rather than replace the markup and standard behaviours of web development.

Here’s a perfect example I saw recently.

If only there was another way to do this.

Another good example came up for me recently when I code reviewed a more junior developer’s work. The actual requirement was a JavaScript-based interface to update a given property on a series of customers. Think something like an approval queue. The app was a large backend Laravel application, which was already able to get all of the customers in the approval queue.

This is a simplification of the actual queue, which had a table with more data. I’m taking us back to essentials for the sake of a clearer illustration.

The JavaScript used a relatively simple pattern, an ES6 module bound by jQuery on data-approval-queue which creates a “component” and injects the actual jQuery element into the constructor. We end up with something like this:

I haven’t actually run this, I wrote it from an approximation of memory, and I don’t much care if it’s right. There are a few things to highlight here.

First of all, we have a combination of explicit state management, and direct DOM manipulation. No good can come of this. The two can easily end up out of sync. It’s exactly this sort of issue we’d often use a library like React or Vue, but that’s not the fix I’m proposing here.

The second is this line, which is a strange conditional:

What happens is that we have an event triggering on clicking the li, which will check the checkbox. But if the actual element we click on is the checkbox, it will double-check, or uncheck instantly. We’re actually fighting the browser at this point.

Moving our State to the DOM

The first thing to say is that managing state in the DOM isn’t ideal. But it’s better than maintaining it twice. Secondly, there are a bunch of types of state, and some sort of formal React-style immutable state object is only one kind and this is only like that because we made it that way in the first place.

There’s actually a much better way to store this stuff as state directly on the DOM. We already have form elements, why not use them? For some reason people routinely don’t realise that form elements can be named as arrays. Or to put it more accurately they can be named in such a way that a back end application such as Laravel (or even PHP itself, we’ve been doing this for literally decades) can interpret it as an array.

This means we can completely eliminate the this.approveList property entirely, and we can also remove the click event handler.

Regaining Lost Ground

We’ve lost functionality by doing that, though. Because previously you could click the name, and now you have to click the checkbox. But that’s easy to fix.

Again, it’s important to note that the label tag when wrapped around a checkbox will also trigger the browser’s normal behaviour. A line or two of CSS (which we don’t need to show here) will make the label have whatever padding and display properties are needed, specifically display: block, which isn’t typical for a label.

Getting Our Companies Array

Obviously we need to get hold of the values again, but that’s that difficult, a single line of JavaScript in our handleSubmit function will do it.

The fact is the form sending is the only time we actually care about those values. There’s no reason to keep track of it, it only matters on submitting. Great, now we can pass in that variable to the form post. But here’s the thing. What if there’s a better way? I mean, we’re already treating this like a form. Maybe we should just… make it a form?

Making this a form

There are a few reasons to make this a form. One of them is… well… this is a form. It’s a bunch of inputs and a submit button. But the main one, the more concrete one, is that we can put all of the expected functionality back into the form and take it out of JavaScript land. This is especially true of something like Laravel. Laravel is the framework maintaining the application. It already knows what the route we want to submit this form to is. We can use its existing route handling to provide the post action for this form.

We can replace our existing <div data-approval-queue> tag with the following, and get a whole bunch of benefits.

By doing this we take the responsibility for the specific url string out of the JavaScript and place it in the application, which lets us get it back out using simple calls. We also now have a form, which gives us new events. For example, we don’t actually care whether someone clicks Approve. We care when someone submits the form. That’s the real event. And now we have the ability to trap that event. And because the form itself is an injected property of the class, it’s really easy to do. Additionally, because the form is now a form it has methods on it that jQuery (or fetch if we want to be all grown up) can action directly.

Because we have so much less JavaScript now some of our patterns for setting up actions become simply unnecessary. We can simplify these calls out. We end up with something like the following.

We’ve also ended up with marginally more markup in our Blade templates, but not significantly so.

This looks notably longer because I space things out a lot and indent fussily, but the principle is pretty clear. We’ve made our HTML marginally longer — arguably made it more correct — but in doing so we’ve made our JavaScript vastly more simple and comprehensible. We’ve also gained a number of key benefits. As if simplicity and readability weren’t enough.

Maintainability

The new code has an advantage in that it has few if any ties to our specific DOM implementation. If the form requirement changes at either end, the JavaScript simply doesn’t care.

More pivotally, if something like an API endpoint’s url were to change this form would not break. Laravel’s settings for the route would simply update the action here.

Accessibility

One thing we haven’t talked about yet is accessibility. The JavaScript example we started with was dire for accessibility.

It’s easy to dismiss that and say no blind people are using your admin panel. But the behaviour we had previously would mean that standard browser behaviour simply wouldn’t work. For example, tabbing and pressing space on your checkbox inputs. The browser still supports them but they wouldn’t update the state in the module. So a user would potentially be checking the box and then submitting the form not knowing that the values the field clearly shows were not actually the ones sent. And if they tried to hit enter to send the form that wouldn’t do anything at all, possibly leading to a bug being logged.

We’ve gained all that behaviour back now. Tabbing, spacebar, enter to submit, all of these things will work. The browser’s basic features will work automatically. Maybe you don’t have power users who tab through and hit space on inputs. But maybe they do. And maybe it’s reasonable to expect form inputs to work.

As another example, that broken-ass JavaScript based hyperlink at the start of this article will look identical to the browser user if they click it. But what if they right click it? What if they control- or command-click it? I command-click shit all the time and am routinely annoyed by that basic functionality being broken.

Reusability

There’s actually nothing about the new JavaScript that ties it to the specific DOM or specific form. In fact, the text in the alert is the only thing that does. And it would be trivial to put that in as a data- attribute in the form. This would mean that the JS we’ve written here could potentially be re-used without changes for any other form exhibiting similar behaviour.

This isn’t a feature we’ve gone for. It’s a natural outcome of moving towards idiomatic, semantic document markup.

Resiliency

Do you know what will happen if there’s a JavaScript error on this page, which kills our submit event handling? Nothing. Everything will still work fine. It’s still just a form. Admittedly the API endpoint might spit about being a full page post, rather than the data, and you’d have to redirect the user back to the form. But that’s all achievable and the form at least fundamentally works. No “I pressed the button but it does nothing” bug reports.

To Conclude

JavaScript is a powerful tool, but there’s a principle in software called the Rule of Least Power. This principle suggests that if you need to do a task you use the least capable and powerful tool that will accomplish your requirement. This “rule” is why we use CSS hovers to trigger mouseover states now rather than JS, and why we use CSS transitions rather than jQuery animation.

The least power here is the markup. There’s nothing wrong with using JavaScript to enhance or improve an application, to provide a smoother and more intuitive interface. But you do that most effectively by getting the markup as standard and complete as possible and then enhancing it. Work with the browser. Know HTML’s actual functionality. Use standard elements and form controls wherever possible, and don’t override or reimplement standard behaviour without a really compelling reason.

Starting with valid, compliant and semantic HTML and enhancing it with minimal JavaScript you can stop fighting and forcing the browser and instead work with it for better software all round.

Senior Web Developer based in Bangkok, Thailand. Javascript, Web and Blockchain Developer.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store