DEV Community

John Colagioia (he/him)
John Colagioia (he/him)

Posted on • Originally published at john.colagioia.net on

Writing Browser Extensions with Configuration

As a quick note, I released this post on my blog yesterday, so it can get to be (as I tend to be) a bit rambling. Oh, and the original text is on GitHub (licensed CC-BY-SA), so if anything seems muddy, by all means:

  • Leave a comment here,
  • Leave a comment on the blog,
  • File an issue on GitHub, or
  • Add a pull request!

This follows on to my post from last week.

To recap it briefly, I have recently been investigating some possible projects that would benefit from having a simple browser extension to pass along real-time data on the user’s actions. It’s simple enough, but has enough detail to make a viable post…or two.

Plugs

In this case, our extension is going to report every visited URL to a configurable remote address. The URL Rat extension, by last week’s post, was able to record every visited URL and send that information to a hard-coded URL. So far, so good. I’ll assume that you read that, since it’s short.

But now, we need to make that user-configurable, so we need to work with the browser storage.

Setting Configuration

First, we need to set up the extension to allow for some configuration work, which means adding some elements to manifest.json.

"options_ui": {
  "page": "popup/configure.html",
  "browser_style": true
},
"browser_action": {
  "default_icon": "icons/urlrat32.png",
  "default_title": "URL Rat"
},
"background": {
  "scripts": [
    "background.js"
  ]
}
Enter fullscreen mode Exit fullscreen mode

Each of the three does its small part.

Browser Action

The browser_action element creates a toolbar button, in this case with an additional image that fits the button profile. All things considered, it’s pretty boring. Name and image.

The handlers go in…

Background

In the background.scripts element, we list the files containing the toolbar button’s event handlers. In this case, we just have one file, since we’re only handling one click. But if we had multiple buttons and/or had multiple features, we might consider separating that code into multiple files, listing them there.

I’ll talk about our click-handler in a bit.

Configuration Interface

Finally, the options_ui element tells the browser where to find the page with the configuration controls. In my case, I created a popup folder—even though it doesn’t actually pop up, but I initially considered that approach and never changed the name—where I’ve dumped all the code related to that options page.

These three items guide most of the work from here.

Event Handlers

As mentioned, we keep the toolbar handlers in a script that the browser runs in the background. In our case, we don’t have much to do, so it’s just this.

function handleClick() {
  browser.runtime.openOptionsPage();
}
browser.browserAction.onClicked.addListener(handleClick);
Enter fullscreen mode Exit fullscreen mode

Just…open the options page when we hear that the button has been clicked.

Honestly, we don’t even need this, since the options page is going to be accessible from the list of browser extensions, but we might want to add features later and I wanted to make the extension visible, since it’s such a terrible idea to run it.

Configuration

I’ll spare you the boring HTML and CSS for URL Rat ’s options page. It’s a form with controls. If you’re not familiar with how those work, I’m not being dismissive when I say that you can find a better explanation than I could write here in just about any HTML tutorial.

Instead, we’ll just focus on the JavaScript code, since that’s the part that interacts with the browser storage. There are a few pieces.

function saveOptions(e) {
  browser.storage.sync.set({
    dest: document.querySelector('#dest').value,
    isActive: document.querySelector('#on').checked.toString()
  });
  e.preventDefault();
}
document.querySelector('form').addEventListener('submit', saveOptions);
Enter fullscreen mode Exit fullscreen mode

Save options will take our two options (a URL and an on/off setting) and push them to browser.storage.sync, where we can pick them up later. I slightly reorganized the file for clarity, but the call happens when our options form is submitted.

In other words, the user clicks “Save” and the settings are stored in the browser.

function restoreOptions() {
  var storageItem = browser.storage.managed.get();
  storageItem.then((res) => {
    setOptions(res);
  });

  var gettingItem = browser.storage.sync.get();
  gettingItem.then((res) => {
    setOptions(res);
  });
}
document.addEventListener('DOMContentLoaded', restoreOptions);
Enter fullscreen mode Exit fullscreen mode

When we open the options page, we want the controls to reflect the saved data. So, restoreOptions() takes two steps, instead of just the one involved in saving.

The first step is to pull the default options out of the browser.storage.managed area. After pushing that information to the options page, we check browser.storage.sync to see if the user has saved anything and, if so, set those options and overwrite the managed version.

The setOptions() function isn’t worth showing here, just a utility function to avoid duplicating some updates to the controls. You can find it in the repository along with the HTML and CSS, if you need it.

Wait, What Default Values?

You noticed that there’s no way to populate the manage storage, too, right? Getting this to work was probably the least-appealing part of the process for me.

This may only be for Firefox, but we need a JSON file to get information there. You might remember that the manifest.json file included a browser_specific_settings.gecko.id element. Implausible as it sounds, we use that ID to identify a new JSON file that holds our default values. In this case, it’s literally named urlrat@example.com.json, because our identifier is that imaginary e-mail address. And it looks like the following.

{
  "name": "urlrat@example.com",
  "description": "ignored",
  "type": "storage",
  "data":
  {
    "dest": "http://localhost:8080/",
    "isActive": "true"
  }
}
Enter fullscreen mode Exit fullscreen mode

Copy, link, or move that file—depending on your preferences and how often you expect to update and keep the file around—to your ~/.mozilla/managed-storage/ folder, and you have now initialized your browser’s Managed Storage.

I did warn you that it wasn’t appealing.

Use the Configuration

From here, we should already know what to do. The main extension code, url-rat.js for this example, now needs to replace its hard-coded values with values from the browser’s storage. I took the following approach.

browser.storage.managed
  .get()
  .then((managed) => {
    browser.storage.sync
      .get()
      .then((local) => {
        var store = Object.assign(managed, local);

        // Assign the values in the "store" variable to individual
        // variables already used by the extension, and then put the
        // original extension code here.
      });
  });
Enter fullscreen mode Exit fullscreen mode

I nested the then() handlers to make sure we have the data, even though that’s probably unnecessary, and then used Object.assign() to merge the two configuration objects into one.

Like the comment says, I should now have store.dest with the saved URL and store.isActive to decide whether to send the current URL to the server.

Overall

All things considered, if we ignore the managed storage shenanigans, the development process seems smooth. The only real snag I hit was in not realizing that the sync and managed storage areas were different, breaking the configuration page.

Also, the configuration page is ugly.

Other than minor issues like that, though, the fact that it only took a couple of hours in total to hack out a fully functional browser extension that performs a defined task starts to make this look appealing for certain kinds of projects.


Credits : The header image is Untitled by an anonymous PxHere user, made available under the terms of the CC0 1.0 Universal Public Domain Dedication.

Top comments (0)