diff --git a/README.md b/README.md
index 53abecfba..1ed63d69c 100644
--- a/README.md
+++ b/README.md
@@ -152,6 +152,37 @@ Opera (including Android, but not Opera Mini), Safari (including iOS), IE 6+ (bu
Boomerang also fires the `onBeforeBoomerangBeacon` and `onBoomerangBeacon` events just before and during beaconing.
+#### 3.4. Method queue pattern
+
+If you want to call a public method that lives on `BOOMR`, but either don't know if Boomerang has loaded or don't want to wait, you can use the method queue pattern!
+
+Instead of:
+```javascript
+BOOMR.addVar('myVarName', 'myVarValue')
+```
+
+... you can write:
+```javascript
+BOOMR_mq = window.BOOMR_mq || [];
+BOOMR_mq.push(['addVar', 'myVarName', 'myVarValue']);
+```
+
+Or, if you care about the return value, instead of:
+```javascript
+var hasMyVar = BOOMR.hasVar('myVarName');
+```
+... you can write:
+```javascript
+var hasMyVar;
+BOOMR_mq = window.BOOMR_mq || [];
+BOOMR_mq.push({
+ arguments: ['hasVar', 'myVarName'],
+ callback: function(returnValue) {
+ hasMyVar = returnValue;
+ }
+});
+```
+
docs
---
Documentation is in the docs/ sub directory, and is written in HTML. Your best bet is to check it out and view it locally, though it works best through a web server (you'll need cookies).
diff --git a/plugins.json b/plugins.json
index eaee21b9f..3f0c2485c 100644
--- a/plugins.json
+++ b/plugins.json
@@ -18,6 +18,7 @@
"plugins/errors.js",
"plugins/third-party-analytics.js",
"node_modules/usertiming-compression/src/usertiming-compression.js",
- "plugins/usertiming.js"
+ "plugins/usertiming.js",
+ "plugins/mq.js"
]
}
diff --git a/plugins/mq.js b/plugins/mq.js
new file mode 100644
index 000000000..a4fb5e1de
--- /dev/null
+++ b/plugins/mq.js
@@ -0,0 +1,66 @@
+/**
+\file mq.js
+Plugin to implement the method queue pattern
+http://www.lognormal.com/blog/2012/12/12/the-script-loader-pattern/#the_method_queue_pattern
+*/
+
+(function() {
+ function processEntry(args, callback, thisArg) {
+ var methodName = args.shift();
+ if (typeof methodName !== "string") {
+ return;
+ }
+
+ var split = methodName.split("."), method = BOOMR, _this = BOOMR;
+ if (split[0] === "BOOMR") {
+ // the BOOMR namespace is inferred, remove it if it was specified
+ split.shift();
+ }
+
+ // loop through all of `split`, stepping into only objects and functions
+ while (split.length &&
+ method && // `null` is an object, skip it
+ (typeof method === "object" || typeof method === "function")) {
+ var word = split.shift();
+ method = method[word];
+ if (split.length) {
+ _this = _this[word]; // the `this` is everything up until the method name
+ }
+ }
+
+ // if we've used all of `split`, and have resolved to a function, call it
+ if (!split.length && typeof method === "function") {
+ var returnValue = method.apply(_this, args);
+
+ // pass the return value of the resolved function as the only argument to the
+ // optional `callback`
+ if (typeof callback === "function") {
+ callback.call(thisArg, returnValue);
+ }
+ }
+ }
+ function processEntries(entries) {
+ for (var i = 0; i < entries.length; i++) {
+ var params = entries[i];
+ if (!params) {
+ continue;
+ }
+ if (BOOMR.utils.isArray(params)) {
+ processEntry(params);
+ }
+ else if (typeof params === "object" && BOOMR.utils.isArray(params.arguments)) {
+ processEntry(params.arguments, params.callback, params.thisArg);
+ }
+ }
+ }
+
+ var mq = BOOMR.window.BOOMR_mq;
+ if (BOOMR.utils.isArray(mq)) {
+ processEntries(mq);
+ }
+ BOOMR.window.BOOMR_mq = {
+ push: function() {
+ processEntries(arguments);
+ }
+ };
+})();
diff --git a/tests/page-templates/00-basic/10-method-queue.html b/tests/page-templates/00-basic/10-method-queue.html
new file mode 100644
index 000000000..c5feb3ae5
--- /dev/null
+++ b/tests/page-templates/00-basic/10-method-queue.html
@@ -0,0 +1,21 @@
+<%= header %>
+<%= boomerangSnippet %>
+
+
+<%= footer %>
diff --git a/tests/page-templates/00-basic/10-method-queue.js b/tests/page-templates/00-basic/10-method-queue.js
new file mode 100644
index 000000000..285656c5c
--- /dev/null
+++ b/tests/page-templates/00-basic/10-method-queue.js
@@ -0,0 +1,22 @@
+/*eslint-env mocha*/
+/*global BOOMR_test,assert*/
+
+describe("e2e/00-basic/10-method-queue", function() {
+ var tf = BOOMR.plugins.TestFramework;
+
+ it("Should have sent a beacon", function() {
+ assert.isTrue(tf.fired_onbeacon);
+ });
+
+ it("Should support entries queued up before boomerang loaded", function() {
+ var b = tf.lastBeacon();
+ assert.equal(b.var1, "value1");
+ assert.equal(b.var2, "value2");
+ });
+
+ it("Should support calls to `push()` after boomerang loaded", function() {
+ var b = tf.lastBeacon();
+ assert.equal(b.var3, "value3");
+ assert.equal(b.var4, "value4");
+ });
+});
diff --git a/tests/unit/04-plugins-mq.js b/tests/unit/04-plugins-mq.js
new file mode 100644
index 000000000..21317dd6a
--- /dev/null
+++ b/tests/unit/04-plugins-mq.js
@@ -0,0 +1,160 @@
+/*eslint-env mocha*/
+/*global chai*/
+
+describe("BOOMR.plugins.mq", function() {
+ var assert = chai.assert;
+
+ describe("exports", function() {
+ it("Should have a BOOMR_mq object", function() {
+ assert.isObject(BOOMR.window.BOOMR_mq);
+ });
+
+ it("Should have a push() function", function() {
+ assert.isFunction(BOOMR.window.BOOMR_mq.push);
+ });
+ });
+
+ describe("push()", function() {
+ it("Should handle degenerate cases", function() {
+ assert.doesNotThrow(function() {
+ BOOMR_mq.push(
+ null,
+ undefined,
+ false,
+ true,
+ "null",
+ "undefined",
+ "false",
+ "true",
+ "",
+ 0,
+ 1,
+ 27,
+ [],
+ {},
+ ["foo"],
+ ["foo.bar"],
+ ["foo.bar.baz"]
+ );
+ });
+ });
+
+ describe("array pattern", function() {
+ it("Should call methods on BOOMR", function(done) {
+ BOOMR.method = function() {
+ done();
+ };
+ BOOMR_mq.push(["method"]);
+ });
+
+ it("Should call namespaced methods on BOOMR", function(done) {
+ BOOMR.method = function() {
+ done();
+ };
+ BOOMR_mq.push(["BOOMR.method"]);
+ });
+
+ it("Should pass all arguments", function(done) {
+ BOOMR.method = function() {
+ assert.lengthOf(arguments, 3);
+ assert.equal(arguments[0], 0);
+ assert.equal(arguments[1], 1);
+ assert.equal(arguments[2], 2);
+ done();
+ };
+ BOOMR_mq.push(["method", 0, 1, 2]);
+ });
+
+ it("Should support `push` with multiple arguments", function(done) {
+ var results = [];
+ BOOMR.method1 = function() {
+ results.push("method1");
+ };
+ BOOMR.method2 = function() {
+ results.push("method2");
+ };
+ BOOMR.method3 = function() {
+ assert.lengthOf(results, 2);
+ assert.equal(results[0], "method1");
+ assert.equal(results[1], "method2");
+ done();
+ };
+ BOOMR_mq.push(
+ ["method1"],
+ ["method2"],
+ ["method3"]
+ );
+ });
+
+ it("Should step into objects on BOOMR", function(done) {
+ BOOMR.obj = {
+ method: function() {
+ done();
+ }
+ };
+ BOOMR_mq.push(["obj.method"]);
+ });
+
+ it("Should step into functions on BOOMR", function(done) {
+ BOOMR.func = function() {};
+ BOOMR.func.method = function() {
+ done();
+ };
+ BOOMR_mq.push(["func.method"]);
+ });
+
+ it("Should use appropriate context", function(done) {
+ BOOMR.obj = {
+ method1: function() {
+ this.method2();
+ },
+ method2: function() {
+ done();
+ }
+ };
+ BOOMR_mq.push(["obj.method1"]);
+ });
+ });
+
+ describe("object pattern", function() {
+ it("Should support `arguments`", function(done) {
+ BOOMR.method = function() {
+ done();
+ };
+ BOOMR_mq.push({
+ arguments: ["method"]
+ });
+ });
+ it("Should support `callback`", function(done) {
+ BOOMR.method = function() {
+ return 123;
+ };
+ BOOMR_mq.push({
+ arguments: ["method"],
+ callback: function() {
+ assert.lengthOf(arguments, 1);
+ assert.equal(arguments[0], 123);
+ done();
+ }
+ });
+ });
+ it("Should support `thisArg`", function(done) {
+ function Item(value) {
+ this.value = value;
+ }
+ Item.prototype.callback = function() {
+ assert.equal(this.value, 123);
+ done();
+ };
+
+ BOOMR.method = function() {};
+ BOOMR_mq.push({
+ arguments: ["method"],
+ callback: Item.prototype.callback,
+ thisArg: new Item(123)
+ });
+ });
+ });
+ });
+
+});
diff --git a/tests/unit/index.html b/tests/unit/index.html
index 1f1d979dd..db4da5a6a 100644
--- a/tests/unit/index.html
+++ b/tests/unit/index.html
@@ -41,6 +41,7 @@
+