Hacking web apps with bookmarklets (part 1/2)
In the last decade, cloud-based SaaS solutions have taken over a large part of the software market, including the market for our office tools: think of Slack, Salesforce and Google Docs. But sometimes these tools are missing a small feature that could speed up our work. At Columbia Road we’re all about automation and trying out little experiments to improve a situation, so when we had a small issue with our time tracker Harvest, our developer Paul took a shot: what if we could hack in the solution ourselves?
SaaS software is often accessible through web apps, and with web apps it’s possible to change things on the client-side. This series of articles will explain how to hack the web to your needs with bookmarklets and web extensions, and when or when not to consider these hacks.
In this post we’ll take a look at bookmarklets, a way to add small behaviours to a web app activated on a click. We’ll demonstrate step-by-step how to use it to solve a small hypothetical problem similar to one we encountered.
Caution: some web development experience is useful to follow along. Let’s get started!
Nothing is perfect :(
Take this scenario: you want to start tracking time in your business. You compare your options and eventually tool X looks the most promising. So you try it out and after evaluating it for a month, you start using it company wide.
All looks good, until you and some colleagues discover a very specific need that tool X is not addressing. Your tool has a view that summarises all the work your company tracked. It includes the specific work item that was tracked, which project team did the work, and the related number of tracked hours:
Here comes the problem. Every year you need to report to your manager how many hours were spent by each team. But unfortunately, your tool can’t group the information that way.
What to do? Sure, you can calculate the sums manually, but this is frustrating and error-prone. What if the list is very long? Moreover, if many colleagues have the same problem frequently, this sums up to a lot of lost time. You could consider these options:
- Contact customer support of tool X and make a feature request for this view
- Find a new tool that fits your needs better
Option 1) is worth trying. Maybe tool X’s maker has a dedicated page for feature requests. Unfortunately, it will likely take some time for them to consider, develop, implement and deploy the change you requested — if they do it at all.
Option 2) is also not ideal. You just invested big time in a tool that works well 95% of the time. It’s only this one feature that is missing. The cost of switching to another tool is high enough to make it unjustified, plus you may be missing out on other features with other tools.
So, is this a hopeless situation? There might be a third option, if you can code it yourself. This particular problem could be solved with a nifty bit of browser magic called bookmarklets!
Bookmarklets: code snippets in a click
A bookmarklet simply is a bookmarked link. However, unlike the HTML usual <a> link that has a href that starts with https://, it instead starts with javascript:. With this kind of link we can store a small custom JavaScript snippet that executes on the page where it is clicked. It’s a different protocol, just like the more common mailto: links.
Here’s an example:
If you click the link, the alert(‘Hey!’) code will run on your browser page.
If you drag that link to your bookmark toolbar, it becomes a bookmarklet. You can now execute this code on every page.
So what can we do with this? Bookmarklets can for example be used to send selected text to a web service. But in our case, we can use one to solve our grouping problem.
The javascript: link above reports the grouped hour count for each team. Let’s break down how it works, and how we make a bookmarklet from it.
On a high level, we want our code to do this:
- Extract the data on teams and related counted hours from the table
- Sum all the counted hours for each team (grouping)
- Present the result
The following code is what we’ll end up with.
alert(
JSON.stringify(
Array.from(document.querySelectorAll("tr:nth-of-type(n+2)")).reduce(
(acc, row) => {
const team = row.children[1].textContent;
const hours = +row.children[2].textContent;
return { ...acc, [team]: (acc[team] || 0) + hours };
},
{}
)
)
);
Step 1: Extract the items from the DOM
We start by extracting all the rows from the HTML <table> in the DOM of this blog page. In this case there is only one <table> in this article, so we can keep this example code simple & generic.
Array.from(document.querySelectorAll(“tr:nth-of-type(n+2)”))
- document.querySelectorAll() should be familiar: it allows us to get all elements matching a CSS query, and it returns them in a NodeList.
- Our query tr:nth-of-type(n+2) uses the nth-of-type pseudo class. It will get all table rows (tr), starting from the 2nd one. We want to skip the 1st row because that’s the one that contains table headers (<th>) irrelevant for our goal.
- Array.from() is an ES6 method supported by modern browsers that can convert a NodeList of HTML Elements into a regular Array, so we can use methods like forEach and reduce it.
We now have an array with all table rows!
Step 2: Group the items by team
Next, we need to group the tracked items to their respective teams. The aim is to group them in an object of the form {“Tigers”: a,”Panthers”: b,”Lions”: c} with the final tracked hour totals.
We’re using reduce here to process each row, adding every hour to the object that is passed as an accumulator, keeping track of the sum for every row up to the current one. This expression finally resolves with the desired totals.
.reduce(
(acc, row) => {
const team = row.children[1].textContent;
const hours = +row.children[2].textContent;
return { ...acc, [team]: (acc[team] || 0) + hours };
},
{}
)
Step 3: Alert the grouped data as a string
This will only pr esent the stringified object, but you could adjust it to a nicer table layout as well.
alert(JSON.stringify(reducedObject))
Step 4: Embedding in a link
Now to the interesting part: let’s put this code into a javascript: link. We can’t just copy-paste the code because we need to make sure the link is valid as a href attribute. Therefore, we first need to URL encode it.
URL encoding is hard to do manually, so we can use a web tool for that. This one is tailor-made for bookmarklets: https://mrcoles.com/bookmarklet/. One nice feature it adds is that it wraps your code in an Immediately Invoked Function Expression, which avoids interference with the existing variable scope of the JavaScript on the web page.
Copy-pasting the code into that tool gives us the final link we can put into an <a> link on a website and share that way, or add manually as a new bookmark in you bookmark toolbar.
<a title="Bookmarklet to group items by team" href="javascript:(function()%7Balert(JSON.stringify(Array.from(document.querySelectorAll(%22tr%3Anth-of-type(n%2B2)%22)).reduce((acc%2C%20row)%20%3D%3E%20%7Bconst%20team%20%3D%20row.children%5B1%5D.textContent%3Bconst%20hours%20%3D%20%2Brow.children%5B2%5D.textContent%3Breturn%20%7B%20...acc%2C%20%5Bteam%5D%3A%20(acc%5Bteam%5D%20%7C%7C%200)%20%2B%20hours%20%7D%3B%7D%2C%7B%7D)))%7D)()">Bookmarklet link</a>
That was it! Check out this CodePen for the full code & more details, including two more ways to present content instead of alert(): one with a bookmarklet document, and one where you inject the answer back into a page.
To conclude
This post demonstrated a simple bookmarklet with information extraction, processing and presentation using a single JS expression. Here some ideas of what else you could do:
- Inject CSS that increases the font of some painfully small print
- When viewing a contact in your CRM: extract the name and other details from a page, combine it in an email template (“Hello , …”) and open a new Gmail tab pre-filled with this template.
Bookmarklets can provide small solutions to small client-side problems in web apps. They can save a lot of time. But of course there are also limitations:
- Then can only run on the click of a bookmark in a toolbar. Not everyone uses bookmark toolbars.
- It is restricted to the browser JS context of the active page
- A site’s Content Security Policies may completely block its functionality, it’s not applicable everywhere.
- There are no good tools or development processes available to maintain them.
For more complex issues and to circumvent some of the limitations above, you could jump to the big brother of bookmarklets: Web Extensions. You’re probably using some already, so why not make one yourself? Web extensions have access to a rich set of browser APIs that give them much more power. Moreover, they can be developed and distributed in a better way. We’ll dive into the world of extensions in the next post. Until then, check out our other tech blog posts!
Originally published at https://www.columbiaroad.com.