Labs/JS Modules

From MozillaWiki
Jump to: navigation, search

The modules described here have been superseded by the Addon-SDK. What is described on this page is now out of date.

Here you'll find a collection of modules which you can import into your extension. The goal is to make extension development easier by implementing common functionality as reusable libraries. If you would like to contribute a new module, get in touch with us at #labs!

Usage

The modules here are designed to be imported with Components.utils.import(). To avoid namespace collisions with core code and other extensions, use the two-parameter form of import() to import the module's symbols into a namespace defined by your extension, i.e.:

let MyExtension = {};
Components.utils.import("resource://myextension/modules/SomeModule.js", MyExtension);

Modules

JSON

The JSON module wraps the incompatible Gecko 1.9.0 (Firefox 3.0) and Gecko 1.9.1 (Firefox 3.5) JSON APIs, presenting the Gecko 1.9.1 API on both versions, for extensions that support multiple versions of Gecko-based applications.

Import this module into your extension to parse and stringify JSON in both Firefox 3.0 and 3.5 (and other Gecko-based applications, like Thunderbird) without checking the application's version each time.

Note: don't import this into the global namespace! If you do, you'll hork native application code that expects the Gecko 1.9.0 API. Instead, import it into your own object like this:

let MyExtension = {
  JSON: null,
  ...
};
Components.utils.import("chrome://myextension/modules/JSON.js", MyExtension);
// Now MyExtension.JSON is an object implementing the Gecko 1.9.1 JSON API.

The Gecko 1.9.1 (Firefox 3.5) JSON API is documented in the article Using JSON in Firefox.

Logging

This is a partial implementation of the Log4* interfaces (for example, see log4j or log4net). The original implementation came from Michael Johnston, but it was heavily modified by Dan Mills to get it into Weave.

To use it, create one or more appenders while initializing your add-on, and create loggers in the places where you have logging messages to emit. Both loggers and appenders have minimum log levels to allow you to customize your logging output without changing any code. For example:

function setupLogging() {
  // The basic formatter will output lines like:
  // DATE/TIME	LoggerName	LEVEL	(log message) 
  let formatter = new Log4Moz.BasicFormatter();

  // Loggers are hierarchical, lowering this log level will affect all output
  let root = Log4Moz.repository.rootLogger;
  root.level = Log4Moz.Level["All"];

  // A console appender outputs to the JS Error Console
  let capp = new Log4Moz.ConsoleAppender(formatter);
  capp.level = Log4Moz.Level["Warn"];
  root.addAppender(capp);

  // A dump appender outputs to standard out
  let dapp = new Log4Moz.DumpAppender(formatter);
  dapp.level = Log4Moz.Level["Debug"];
  root.addAppender(dapp);
}

// ... in some other code, but after setupLogging() has been done

// Get a logger, give it a name unique to this chunk of code.
// Use dots to create a hierarchy, this way you can later change
// the log level of sets of loggers under some common root
let logger = Log4Moz.repository.getLogger("MyExtension.MyClass");
logger.level = Log4Moz.Level["Debug"];

// Log some messages
// Given our settings, the error would show up everywhere, but the
// debug one would only show up in stdout
logger.error("Oh noes!! Something bad happened!");
logger.debug("Details about bad thing only useful during debugging");

The implementation is much less comprehensive than what you might see in the log4* projects at Apache, however. For example, support for serializing the logging configuration or reading it in is non-existent. Also, note that this module used to be an XPCOM component and that influenced some of the interface design (e.g., Service.newAppender() instead of making a new Appender() directly). That will change in the future.

Get it here:

Observers

The Observers module provides an API for sending and observing notifications using the observer service. It is designed to make it easy for JavaScript code to send notifications containing JavaScript values (including data structures such as Arrays and Objects) to JavaScript observers.

Notifying Observers

Notify observers by calling Observers.notify with the topic, subject, and data of the notification:

Observers.notify("topic", someSubject, "some data");

The subject and data parameters are both optional. The subject parameter can be any JS value. The data parameter must be a string, and it is deprecated, since strings can just as easily be passed through the subject parameter:

Observers.notify("topic", "some data");

If you need to pass multiple values, pass them through the subject parameter via a JavaScript data structure that stores multiple values, like an Array or an Object:

Observers.notify("topic", [17, 33, 10]);
Observers.notify("topic", { foo: 1, bar: true, baz: "stuff" });

Registering a Function Observer

Register a function as an observer by calling Observers.add with the topic to observe and the function to observe it:

function someFunction(subject, data) {...};
Observers.add("something", someFunction);

When receiving a notification, the function will be called with the notification's subject and data parameters. The function will not be given the notification's topic parameter.

Registering a Method Observer

Register a method as an observer by calling Observers.add with the topic to observe, the method to observe it, and the object to which the method belongs:

let someObject = {
  onSomething: function(subject, data) {...},
};
Observers.add("something", someObject.onSomething, someObject);

When receiving a notification, the method will be called with the notification's subject and data parameters. The method will not be given the notification's topic parameter.

Registering an Object Observer

Register an object that implements the nsIObserver interface as an observer by calling Observers.add with the topic to observe and the object to observe it:

let someObject = {
  observe: function(subject, topic, data) {...}
};
Observers.add("something", someObject);

When receiving a notification, the object's observe method will be called with the notification's subject, topic, and data parameters, in that order, which is compatible with the type signature of the nsIObserver interface. Note, however, that unlike the nsIObserver interface the value of the subject parameter will be a JavaScript value, not an XPCOM object, when the notifying code uses Observers.notify to send the notification.

Unregistering Observers

Unregister observers by calling Observers.remove with the same parameters you passed to Observers.add:

Observers.remove("something", someFunction);
Observers.remove("something", someObject.onSomething, someObject);
Observers.remove("something", someObject);

Note: the Observers module wraps observers in a wrapper that implements nsISupportsWeakReference, so you don't necessarily have to remove your observers to avoid leaking memory. Nevertheless, it is good practice to do so anyway.

Version 0.1 API

Version 0.1 of the module had the following incompatible API:

You can register ordinary functions as observers:

function someFunction(subject, topic, data) { /* do something */ };
Observers.add(someFunction, "test");
Observers.remove(someFunction, "test");

You can also register objects that implement the nsIObserver interface as observers:

let someObject = {
  observe: function(subject, topic, data) { /* do something */ }
};
Observers.add(someObject, "test");
Observers.remove(someObject, "test");

And you can send notifications whose subject is an arbitrary JavaScript object:

let someObject = { /* some properties & methods */ };
Observers.notify(someObject, "test", null);

Note: you can use this module to observe notifications generated by any XPCOM component, and you can send notifications whose subject is an XPCOM component, but only JavaScript consumers of this API can send and receive notifications whose subject is an arbitrary JavaScript object.

Upgrading from 0.1 to 0.2

To upgrade an application that uses version 0.1 of the module to version 0.2, do the following:

  1. switch the order of the first two arguments in all your calls to Observers.notify;
  2. remove the topic parameter from the type signature of any function observer;
  3. if a function observer handles multiple topics, refactor it into multiple functions that each handle one topic, or convert it into an nsIObserver object with an observe method.

Changelog

  • 0.2
    • changed interface of Observers.add and Observers.remove to take an optional thisObject parameter;
    • stopped passing topic parameter to observers that are functions;
    • switched order of topic and subject parameters to Observers.notify;
    • fixed bug that sometimes caused an observer to unregister another observer of the same topic;
    • fixed bug that subjects that are JS XPCOM components and have wrappedJSObjects are unwrapped.
  • 0.1
    • Initial release.

Preferences

Getting & Setting

The Preferences module provides an API for accessing application preferences. Getting and setting prefs is easy:

let foo = Preferences.get("extensions.test.foo");
Preferences.set("extensions.test.foo", "some value");

As with FUEL's preferences API, datatypes are auto-detected, and you can pass a default value that the API will return if the pref doesn't have a value:

let foo = Preferences.get("extensions.test.nonexistent", "default value");
// foo == "default value"

Unlike FUEL, which returns null in the same situation, the module returns undefined when you get a nonexistent pref without specifying a default value:

let foo = Preferences.get("extensions.test.nonexistent");
// typeof foo == "undefined"

(Although the preferences service doesn't currently store null values, other interfaces like nsIVariant and nsIContentPrefService and embedded storage engines like SQLite distinguish between the null value and "doesn't have a value," as does JavaScript, so it seems more consistent and robust to do so here as well.)

Because we aren't using XPCOM, we can include some interesting API features. First, as you may have noticed already, the interface doesn't require you to create a branch just to get a pref, but you can create one via an intuitive syntax:

let testBranch = new Preferences("extensions.test.");
// Preferences.get("extensions.test.foo") == testBranch.get("foo")

get uses polymorphism to enable you to retrieve multiple values in a single call, and, with JS 1.7's destructuring assignment, you can assign those values to individual variables:

let [foo, bar, baz] = testBranch.get(["foo", "bar", "baz"]);

And set lets you update multiple prefs in one call (although they still get updated individually on the backend, so each change results in a separate notification):

testBranch.set({ foo: 1, bar: "awesome", baz: true });

Resetting & Checking

You can also reset preferences (i.e. remove the user-set value) and check if they're set:

Preferences.reset("extensions.test.foo");
let isSetFoo = Preferences.isSet("extensions.test.foo");
// isSetFoo == false, since we've just reset it

isSet tells you whether or not the preference has a set value. Some preferences also have default values that apply if the preference doesn't have a set value. To find out if a preference has any value (whether set or default), use the has method:

Preferences.reset("extensions.test.foo");
let hasFoo = Preferences.has("extensions.test.foo");
// hasFoo == true, if the pref has a default value

To reset an entire branch of preferences, call resetBranch:

Preferences.resetBranch("extensions.test.");
// Any pref starting with "extensions.test." is now reset.

Locking and Unlocking

Preferences with default values can be locked to prevent users from changing them. While they are locked, get will always return the default value, even if the user set a value back when the preference was unlocked.

Use the lock and unlock methods to lock and unlock such preferences and the locked method to determine whether or not a preference is locked:

Preferences.lock("extensions.test.foo");
let lockedFoo = Preferences.locked("extensions.test.foo");
// lockedFoo == true, since we've just locked it

let foo = Preferences.get("extensions.test.foo");
// foo == the preference's default value, even if the user changed it

Preferences.unlock("extensions.test.foo");
lockedFoo = Preferences.locked("extensions.test.foo");
// lockedFoo == false, since we've just unlocked it

Registering a Function Observer

Register a function as an observer by calling observe with the name of the preference to observe and the function to observe it:

function someFunction(newValue) {...};
Preferences.observe("something", someFunction);

When receiving a notification that the preference has changed, the function will be provided the new value of the preference.

Registering a Method Observer

Register a method as an observer by calling observe with the name of the preference to observe, the method to observe it, and the object to which the method belongs:

let someObject = {
  onSomething: function(newValue) {...},
};
Preferences.observe("something", someObject.onSomething, someObject);

When receiving a notification that the preference has changed, the method will be provided the new value of the preference.

Registering an Object Observer

Register an object that implements the nsIObserver interface as an observer by calling observe with the name of the preference to observe and the object to observe it:

let someObject = {
  observe: function(subject, topic, data) {...}
};
Preferences.observe("something", someObject);

When receiving a notification that a preference has changed, the object's observe method will be provided the notification's subject, topic, and data parameters, in that order, which is compatible with the type signature of the nsIObserver interface.

Unregistering Observers

Unregister observers by calling ignore with the same parameters you passed to observe:

Preferences.ignore("something", someFunction);
Preferences.ignore("something", someObject.onSomething, someObject);
Preferences.ignore("something", someObject);

Note: the module wraps observers in a wrapper that implements nsISupportsWeakReference, so you don't necessarily have to remove your observers to avoid leaking memory. Nevertheless, it is good practice to do so anyway.


StringBundle

The StringBundle module makes it easier to access string bundles from JS modules and XPCOM components. To use the module, import it, create a new instance of StringBundle, and then use the instance's get and getAll methods to retrieve strings (you can get both plain and formatted strings with get):

let strings = new StringBundle("chrome://example/locale/strings.properties");
let foo = strings.get("foo");
let barFormatted = strings.get("bar", [arg1, arg2]);
for each (let string in strings.getAll())
  dump(string.key + " = " + string.value + "\n");

Note: in addition to the API described above, the StringBundle object supports the API for the stringbundle XBL binding to make it easier to switch from the binding to the module:

let strings = document.getElementById("myStringBundleElement");
              new StringBundle("chrome://example/locale/strings.properties");
let foo = strings.getString("foo");
let barFormatted = strings.getFormattedString("bar", [arg1, arg2]);
let enumerator = strings.strings;
while (enumerator.hasMoreElements()) {
  let string = enumerator.getNext().QueryInterface(Ci.nsIPropertyElement);
  dump(string.key + " = " + string.value + "\n");
}

Sync

The Sync module provides a simple way to write asynchronous code in a sequential, callback-free fashion that looks like synchronous code. It also provides a sleep function written using its functionality.

Using the sleep function is really easy. Just import the module, which adds the Sync symbol to your namespace, then call its Sync.sleep method:

Components.utils.import("resource://path/to/Sync.js");
Sync.sleep(1000); // sleep 1000 milliseconds

You can create your own such "synchronous asynchronous" function by adding the Sync.sync method to the Function prototype and calling it wherever you would normally call your function. Your function must then accept a callback as its first argument that it calls once it is done with its asynchronous work.

Here's how to implement and then call your own sleep function:

Components.utils.import("resource://path/to/Sync.js");
Function.prototype.sync = Sync.sync;
function sleep(callback, milliseconds) {
  setTimeout(callback, milliseconds);
}
sleep.sync(1000); // sleep 1000 milliseconds

URI

The URI module creates nsIURI objects from location strings. It wraps nsIIOService::newURI. There are two ways to use it. First, you can call it as a constructor:

let foo = new URI(spec, charset, baseURI);

Second, you can call its get method:

let bar = URI.get(spec, charset, baseURI);

In both cases, the parameters are the same, and only the spec parameter is required; the charset and baseURI parameters are optional. The difference between these two techniques is that the constructor throws an error if you pass it an invalid spec, while the get method returns null in that case.

In the future, the URI module may return JavaScript URI objects implementing a different interface than nsIURI.