mozilla

Revision 527599 of Creating Reusable Modules

  • Revision slug: Mozilla/Add-ons/SDK/Tutorials/Creating_reusable_modules
  • Revision title: Creating Reusable Modules
  • Revision id: 527599
  • Created:
  • Creator: tomica
  • Is current revision? No
  • Comment
Tags: 

Revision Content

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:

const {Cc, Ci} = require("chrome");
 
// Implement getCurrentPosition by loading the nsIDOMGeoGeolocation
// XPCOM object.
function getCurrentPosition(callback) {
  var xpcomGeolocation = Cc["@mozilla.org/geolocation;1"]
                      .getService(Ci.nsISupports);
  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:

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 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.

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.

Revision Source

<div class="note">
 To follow this tutorial you'll need to have <a href="/en-US/Add-ons/SDK/Tutorials/Installation">installed the SDK</a> and learned the <a href="/en-US/Add-ons/SDK/Tutorials/Getting_Started_With_cfx">basics of <code>cfx</code></a>.</div>
<p>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 <code>require()</code> statement, in exactly that same way that you import core SDK modules like <a href="/en-US/Add-ons/SDK/High-Level_APIs/widget"><code>widget</code></a> or <a href="/en-US/Add-ons/SDK/High-Level_APIs/panel"><code>panel</code></a>.</p>
<p>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.</p>
<p>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.</p>
<p>In this tutorial we'll do exactly that with a module that exposes the geolocation API in Firefox.</p>
<h2 id="Using_Geolocation_in_an_Add-on">Using Geolocation in an Add-on</h2>
<p>Suppose we want to use the <a href="https://developer.mozilla.org/en-US/docs/WebAPI/Using_geolocation">geolocation API built into Firefox</a>. The SDK doesn't provide an API to access geolocation, but we can <a href="/en-US/Add-ons/SDK/Tutorials/Chrome_Authority">access the underlying XPCOM API using <code>require("chrome")</code></a>.</p>
<p>The following add-on adds a <a href="/en-US/Add-ons/SDK/Tutorials/Adding_a_Button_to_the_Toolbar">button to the toolbar</a>: when the user clicks the button, it loads the <a href="https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/NsIDOMGeoGeolocation">XPCOM nsIDOMGeoGeolocation</a> object, and retrieves the user's current position:</p>
<pre class="brush: js">
const {Cc, Ci} = require("chrome");
 
// Implement getCurrentPosition by loading the nsIDOMGeoGeolocation
// XPCOM object.
function getCurrentPosition(callback) {
  var xpcomGeolocation = Cc["@mozilla.org/geolocation;1"]
                      .getService(Ci.<code>nsISupports</code>);
  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);
    });
  }
});
</pre>
<p>Try it out:</p>
<ul>
 <li>create a new directory called "whereami" and navigate to it</li>
 <li>execute <code>cfx init</code></li>
 <li>open "lib/main.js" and add the code above</li>
 <li>execute <code>cfx run</code>, then <code>cfx run</code> again</li>
</ul>
<p>You should see a button added to the "Add-on Bar" at the bottom of the browser window:</p>
<p><img alt="" src="https://mdn.mozillademos.org/files/6541/widget-mozilla.png" style="display: block; margin-left: auto; margin-right: auto;" /></p>
<p>Click the button, and after a short delay you should see output like this in the console:</p>
<pre>
info: latitude:  29.45799999
info: longitude:  93.0785269
</pre>
<p>So far, so good. But the geolocation guide on MDN tells us that we must <a href="https://developer.mozilla.org/en-US/docs/WebAPI/Using_geolocation#Prompting_for_permission">ask the user for permission</a> before using the API.</p>
<p>So we'll extend the add-on to include an adapted version of the code in that MDN page:</p>
<pre class="brush: js">
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);
      }
    });
  }
});
</pre>
<p>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.</p>
<p>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.</p>
<h2 id="Creating_a_Separate_Module">Creating a Separate Module</h2>
<h3 id="Create_geolocation.js">Create <code>geolocation.js</code></h3>
<p>First create a new file in "lib" called "geolocation.js", and copy everything except the widget code into this new file.</p>
<p>Next, add the following line somewhere in the new file:</p>
<pre class="brush: js">
exports.getCurrentPosition = getCurrentPositionWithCheck;
</pre>
<p>This defines the public interface of the new module. We export a single function to prompt the user for permission and get the current position if they agree.</p>
<p>So "geolocation.js" should look like this:</p>
<pre class="brush: js">
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;
</pre>
<h3 id="Update_main.js">Update <code>main.js</code></h3>
<p>Finally, update "main.js". First add a line to import the new module:</p>
<div>
 <div>
  <div>
   <div class="container">
    <pre class="line number1 index0 alt2">
<code class="js keyword">var</code> <code class="js plain">geolocation = require(</code><code class="js string">"./geolocation"</code><code class="brush: js">);</code></pre>
   </div>
  </div>
 </div>
</div>
<p>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.</p>
<p>Edit the widget's call to <code>getCurrentPositionWithCheck()</code> so it calls the geolocation module's <code>getCurrentPosition()</code> function instead:</p>
<pre class="brush: js">
geolocation.getCurrentPosition(function(position) {
  if (!position) {
</pre>
<p>Now "main.js" should look like this:</p>
<pre class="brush: js">
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);
      }
    });
  }
});
</pre>
<h2 id="Packaging_the_Geolocation_Module">Packaging the Geolocation Module</h2>
<p>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.</p>
<h3 id="Code_Changes">Code Changes</h3>
<p>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:</p>
<pre class="brush: js">
let pref = "extensions.whereami.allowGeolocation";
let message = "whereami Add-on wants to know your location."
</pre>
<p>Instead we'll use the <a href="/en-US/Add-ons/SDK/High-Level_APIs/self"><code>self</code></a> module to ensure that they are specific to the add-on:</p>
<pre class="brush: js">
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."
</pre>
<h3 id="Repackaging">Repackaging</h3>
<p>Next we'll repackage the geolocation module.</p>
<ul>
 <li>create a new directory called "geolocation", and run <code>cfx init</code> in it.</li>
 <li>delete the "main.js" that <code>cfx</code> generated, and copy "geolocation.js" there instead.</li>
</ul>
<h3 id="Editing_.22package.json.22">Editing "package.json"</h3>
<p>The "package.json" file in your package's root directory contains metadata for your package. See the <a href="/en-US/Add-ons/SDK/Tools/package_json">package specification</a> 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.</p>
<h2 id="Learning_More">Learning More</h2>
<p>To see some of the modules people have already developed, see the page of <a href="https://github.com/mozilla/addon-sdk/wiki/Community-developed-modules">community-developed modules</a>. To learn how to use third-party modules in your own code, see the <a href="/en-US/Add-ons/SDK/Tutorials/Add_a_Menu_Item_to_Firefox">tutorial on adding menu items</a>.</p>
Revert to this revision