Creating Reusable Modules

To follow this tutorial you'll need to have installed the SDK and learned the basics of cfx.

With the SDK you don't have to keep all your add-on in a single "main.js" file. You can split your code into separate modules with clearly defined interfaces between them. You then import and use these modules from other parts of your add-on using the require() statement, in exactly that same way that you import core SDK modules like widget or panel.

It can often make sense to structure a larger or more complex add-on as a collection of modules. This makes the design of the add-on easier to understand and provides some encapsulation as each module will export only what it chooses to, so you can change the internals of the module without breaking its users.

Once you've done this, you can package the modules and distribute them independently of your add-on, making them available to other add-on developers and effectively extending the SDK itself.

In this tutorial we'll do exactly that with a module that exposes the geolocation API in Firefox.

Using Geolocation in an Add-on

Suppose we want to use the geolocation API built into Firefox. The SDK doesn't provide an API to access geolocation, but we can access the underlying XPCOM API using require("chrome").

The following add-on adds a button to the toolbar: when the user clicks the button, it loads the XPCOM nsIDOMGeoGeolocation object, and retrieves the user's current position:

var {Cc, Ci} = require("chrome");

// Implement getCurrentPosition by loading the nsIDOMGeoGeolocation
// XPCOM object.
function getCurrentPosition(callback) {
  var xpcomGeolocation = Cc["@mozilla.org/geolocation;1"]
                      .getService(Ci.nsIDOMGeoGeolocation);
  xpcomGeolocation.getCurrentPosition(callback);
}

var widget = require("sdk/widget").Widget({
  id: "whereami",
  label: "Where am I?",
  contentURL: "http://www.mozilla.org/favicon.ico",
  onClick: function() {
    getCurrentPosition(function(position) {
      console.log("latitude: ", position.coords.latitude);
      console.log("longitude: ", position.coords.longitude);
    });
  }
});

Try it out:

  • create a new directory called "whereami" and navigate to it
  • execute cfx init
  • open "lib/main.js" and add the code above
  • execute cfx run, then cfx run again

You should see a button added to the "Add-on Bar" at the bottom of the browser window:

Mozilla icon widget

Click the button, and after a short delay you should see output like this in the console:

info: latitude:  29.45799999
info: longitude:  93.0785269

So far, so good. But the geolocation guide on MDN tells us that we must ask the user for permission before using the API.

So we'll extend the add-on to include an adapted version of the code in that MDN page:


var activeBrowserWindow = require("sdk/window/utils").getMostRecentBrowserWindow();
var {Cc, Ci} = require("chrome");

// Ask the user to confirm that they want to share their location.
// If they agree, call the geolocation function, passing the in the
// callback. Otherwise, call the callback with an error message.
function getCurrentPositionWithCheck(callback) {
  let pref = "extensions.whereami.allowGeolocation";
  let message = "whereami Add-on wants to know your location."
  let branch = Cc["@mozilla.org/preferences-service;1"]
               .getService(Ci.nsIPrefBranch2);
  if (branch.getPrefType(pref) === branch.PREF_STRING) {
    switch (branch.getCharPref(pref)) {
    case "always":
      return getCurrentPosition(callback);
    case "never":
      return callback(null);
    }
  }
  let done = false;

function remember(value, result) {
    return function () {
      done = true;
      branch.setCharPref(pref, value);
      if (result) {
        getCurrentPosition(callback);
      }
      else {
        callback(null);
      }
    }
  }

let self = activeBrowserWindow.PopupNotifications.show(
               activeBrowserWindow.gBrowser.selectedBrowser,
               "geolocation",
               message,
               "geo-notification-icon",
    {
      label: "Share Location",
      accessKey: "S",
      callback: function (notification) {
        done = true;
        getCurrentPosition(callback);
      }
    }, [
      {
        label: "Always Share",
        accessKey: "A",
        callback: remember("always", true)
      },
      {
        label: "Never Share",
        accessKey: "N",
        callback: remember("never", false)
      }
    ], {
      eventCallback: function (event) {
        if (event === "dismissed") {
          if (!done)
            callback(null);
          done = true;
          PopupNotifications.remove(self);
        }
      },
      persistWhileVisible: true
    });
}

// Implement getCurrentPosition by loading the nsIDOMGeoGeolocation
// XPCOM object.
function getCurrentPosition(callback) {
  var xpcomGeolocation = Cc["@mozilla.org/geolocation;1"]
                      .getService(Ci.nsIDOMGeoGeolocation);
  xpcomGeolocation.getCurrentPosition(callback);
}

var widget = require("sdk/widget").Widget({
  id: "whereami",
  label: "Where am I?",
  contentURL: "http://www.mozilla.org/favicon.ico",
  onClick: function() {
    getCurrentPositionWithCheck(function(position) {
      if (!position) {
        console.log("The user denied access to geolocation.");
      }
      else {
       console.log("latitude: ", position.coords.latitude);
       console.log("longitude: ", position.coords.longitude);
      }
    });
  }
});

This works fine: when we click the button, we get a notification box asking for permission, and depending on our choice the add-on logs either the position or an error message.

But the code is now somewhat long and complex, and if we want to do much more in the add-on, it will be hard to maintain. So let's split the geolocation code into a separate module.

Creating a Separate Module

Create geolocation.js

First create a new file in "lib" called "geolocation.js", and copy everything except the widget code into this new file.

Next, add the following line somewhere in the new file:

exports.getCurrentPosition = getCurrentPositionWithCheck;

This defines the public interface of the new module. We export a single a function to prompt the user for permission and get the current position if they agree.

So "geolocation.js" should look like this:


var activeBrowserWindow = require("sdk/window/utils").getMostRecentBrowserWindow();
var {Cc, Ci} = require("chrome");

// Ask the user to confirm that they want to share their location.
// If they agree, call the geolocation function, passing the in the
// callback. Otherwise, call the callback with an error message.
function getCurrentPositionWithCheck(callback) {
  let pref = "extensions.whereami.allowGeolocation";
  let message = "whereami Add-on wants to know your location."
  let branch = Cc["@mozilla.org/preferences-service;1"]
               .getService(Ci.nsIPrefBranch2);
  if (branch.getPrefType(pref) === branch.PREF_STRING) {
    switch (branch.getCharPref(pref)) {
    case "always":
      return getCurrentPosition(callback);
    case "never":
      return callback(null);
    }
  }
  let done = false;

function remember(value, result) {
    return function () {
      done = true;
      branch.setCharPref(pref, value);
      if (result) {
        getCurrentPosition(callback);
      }
      else {
        callback(null);
      }
    }
  }

let self = activeBrowserWindow.PopupNotifications.show(
               activeBrowserWindow.gBrowser.selectedBrowser,
               "geolocation",
               message,
               "geo-notification-icon",
    {
      label: "Share Location",
      accessKey: "S",
      callback: function (notification) {
        done = true;
        getCurrentPosition(callback);
      }
    }, [
      {
        label: "Always Share",
        accessKey: "A",
        callback: remember("always", true)
      },
      {
        label: "Never Share",
        accessKey: "N",
        callback: remember("never", false)
      }
    ], {
      eventCallback: function (event) {
        if (event === "dismissed") {
          if (!done)
            callback(null);
          done = true;
          PopupNotifications.remove(self);
        }
      },
      persistWhileVisible: true
    });
}

// Implement getCurrentPosition by loading the nsIDOMGeoGeolocation
// XPCOM object.
function getCurrentPosition(callback) {
  var xpcomGeolocation = Cc["@mozilla.org/geolocation;1"]
                      .getService(Ci.nsIDOMGeoGeolocation);
  xpcomGeolocation.getCurrentPosition(callback);
}

exports.getCurrentPosition = getCurrentPositionWithCheck;

Update main.js

Finally, update "main.js". First add a line to import the new module:

var geolocation = require("./geolocation");

When importing modules that are not SDK built in modules, it's a good idea to specify the path to the module explicitly like this, rather than relying on the module loader to find the module you intended.

Edit the widget's call to getCurrentPositionWithCheck() so it calls the geolocation module's getCurrentPosition() function instead:

geolocation.getCurrentPosition(function(position) {
  if (!position) {

Now "main.js" should look like this:


var geolocation = require("./geolocation");

var widget = require("sdk/widget").Widget({
  id: "whereami",
  label: "Where am I?",
  contentURL: "http://www.mozilla.org/favicon.ico",
  onClick: function() {
    geolocation.getCurrentPosition(function(position) {
      if (!position) {
        console.log("The user denied access to geolocation.");
      }
      else {
       console.log("latitude: ", position.coords.latitude);
       console.log("longitude: ", position.coords.longitude);
      }
    });
  }
});

Packaging the Geolocation Module

So far, this is a useful technique for structuring your add-on. But you can also package and distribute modules independently of your add-on: then any other add-on developer can download your module and use it in exactly the same way they use the SDK's built-in modules.

Code Changes

First we'll make a couple of changes to the code. At the moment the message displayed in the prompt and the name of the preference used to store the user's choice are hardcoded:

let pref = "extensions.whereami.allowGeolocation";
let message = "whereami Add-on wants to know your location."

Instead we'll use the self module to ensure that they are specific to the add-on:

var addonName = require("sdk/self").name;
var addonId = require("sdk/self").id;
let pref = "extensions." + addonId + ".allowGeolocation";
let message = addonName + " Add-on wants to know your location."

Repackaging

Next we'll repackage the geolocation module.

  • create a new directory called "geolocation", and run cfx init in it.
  • delete the "main.js" that cfx generated, and copy "geolocation.js" there instead.

Documentation

If you document your modules, people who install your package and execute cfx docs will see the documentation integrated with the SDK's own documentation.

You can document the geolocation module by creating a file called "geolocation.md" in your package's "doc" directory. This file is also written in Markdown, although you can optionally use some extended syntax to document APIs.

Try it:

  • add a "geolocation.md" under "doc"
  • copy your geolocation package under the "packages" directory in the SDK root
  • execute cfx docs

Once cfx docs has finished, you should see a new entry appear in the sidebar called "Third-Party APIs", which lists the geolocation module.

Editing "package.json"

The "package.json" file in your package's root directory contains metadata for your package. See the package specification for full details. If you intend to distribute the package, this is a good place to add your name as the author, choose a distribution license, and so on.

Learning More

To see some of the modules people have already developed, see the page of community-developed modules. To learn how to use third-party modules in your own code, see the tutorial on adding menu items.