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 @@ +