diff --git a/History.md b/History.md index 3c86fddf58..3add885bb3 100644 --- a/History.md +++ b/History.md @@ -22,6 +22,9 @@ For more information on Meteor Accounts, see http://docs.meteor.com/#accounts +* The new function `Meteor.autorun` allows you run any code in a reactive + context. See http://docs.meteor.com/#meteor_autorun + * Arrays and objects can now be stored in the `Session`; mutating the value you retrieve with `Session.get` does not affect the value in the session. diff --git a/docs/client/api.html b/docs/client/api.html index 18fbd7fc59..c7d8245b90 100644 --- a/docs/client/api.html +++ b/docs/client/api.html @@ -2110,6 +2110,34 @@ might think of it as a dynamically scoped ("special") variable. (That just means that [`run`](#run) sets it, runs some user-supplied code, and then restores its previous value.) +{{> api_box autorun }} + +`Meteor.autorun` allows you to set up your own reactive context, where you can +perform arbitrary actions when dependencies change. For example, you can monitor +a cursor (which is a reactive data source) and aggregate it into a session +variable: + + Meteor.autorun(function() { + var oldest = _.max(Monkeys.find().fetch(), function (monkey) { + return monkey.age; + }); + if (oldest) + Session.set("oldest", oldest.name); + }); + +Or you can wait for a session variable to get a certain value, and do something +the first time it does so, using the `stop` handle to prevent further runs: + + Meteor.autorun(function(handle) { + if (!Session.equals("shouldAlert", true)) return; + handle.stop(); + alert("Oh no!"); + }); + +The function is invoked immediately and — like all reactive +sources — the rerun occurs at the time of the next +[`Meteor.flush`](#meteor_flush). + {{> api_box flush }} @@ -2143,8 +2171,6 @@ elements are cleaned up by logic that is triggered by context invalidations. - -

Meteor.http

`Meteor.http` provides an HTTP API on the client and server. To use diff --git a/docs/client/api.js b/docs/client/api.js index e23c444d07..f5e7055f0a 100644 --- a/docs/client/api.js +++ b/docs/client/api.js @@ -608,6 +608,18 @@ Template.api.current = { descr: ["The current [`invalidation context`](#context), or `null` if not being called from inside [`run`](#run)."] }; +Template.api.autorun = { + id: "meteor_autorun", + name: "Meteor.autorun(func)", + locus: "Client", + descr: ["Run a function and rerun it whenever its dependencies change. Returns a handle that provides a `stop` method, which will prevent further reruns."], + args: [ + {name: "func", + type: "Function", + descr: "The function to run. It receives one argument: the same handle that `Meteor.autorun` returns."} + ] +}; + Template.api.flush = { id: "meteor_flush", name: "Meteor.flush()", diff --git a/docs/client/concepts.html b/docs/client/concepts.html index ca1a4a4640..51c69c9d4d 100644 --- a/docs/client/concepts.html +++ b/docs/client/concepts.html @@ -199,9 +199,10 @@ application with error-prone logic. These Meteor functions run your code in a reactive context: +* [Templates](#templates) * [`Meteor.render`](#meteor_render) and [`Meteor.renderList`](#meteor_renderlist) * [`Meteor.autosubscribe`](#meteor_autosubscribe) -* [Templates](#templates) +* [`Meteor.autorun`](#meteor_autorun) And the reactive data sources that can trigger changes are: diff --git a/docs/client/docs.js b/docs/client/docs.js index f7b096b7e4..68bf32d2ad 100644 --- a/docs/client/docs.js +++ b/docs/client/docs.js @@ -223,6 +223,7 @@ var toc = [ {instance: "context", name: "invalidate"} ], {name: "Meteor.deps.Context.current", id: "current"}, + "Meteor.autorun", "Meteor.flush" // ], diff --git a/examples/other/template-demo/client/template-demo.js b/examples/other/template-demo/client/template-demo.js index 92a490cafa..7959a9ca2c 100644 --- a/examples/other/template-demo/client/template-demo.js +++ b/examples/other/template-demo/client/template-demo.js @@ -207,7 +207,7 @@ Template.circles.rendered = function () { if (! self.handle) { d3.select(self.node).append("rect"); - self.handle = Meteor._autorun(function () { + self.handle = Meteor.autorun(function () { var circle = d3.select(self.node).selectAll("circle") .data(Circles.find({group: data.group}).fetch(), function (d) { return d._id; }); diff --git a/packages/accounts-password/email_tests.js b/packages/accounts-password/email_tests.js index fc7da6b80e..f3ff35701e 100644 --- a/packages/accounts-password/email_tests.js +++ b/packages/accounts-password/email_tests.js @@ -83,7 +83,7 @@ var waitUntilLoggedIn = function (test, expect) { var unblockNextFunction = expect(); var quiesceCallback = function () { - Meteor._autorun(function (handle) { + Meteor.autorun(function (handle) { if (!Meteor.userLoaded()) return; handle.stop(); unblockNextFunction(); diff --git a/packages/accounts-password/passwords_tests.js b/packages/accounts-password/passwords_tests.js index 99177c2474..4f8e11e19c 100644 --- a/packages/accounts-password/passwords_tests.js +++ b/packages/accounts-password/passwords_tests.js @@ -17,7 +17,7 @@ if (Meteor.isClient) (function () { test.equal(Meteor.user().username, someUsername); }); return function () { - Meteor._autorun(function(handle) { + Meteor.autorun(function(handle) { if (!Meteor.userLoaded()) return; handle.stop(); callWhenLoaded(); @@ -70,7 +70,7 @@ if (Meteor.isClient) (function () { // Set up a reactive context that only refreshes when Meteor.user() is // invalidated. var user; - var handle1 = Meteor._autorun(function () { + var handle1 = Meteor.autorun(function () { user = Meteor.user(); }); // At the beginning, we're not logged in. @@ -86,7 +86,7 @@ if (Meteor.isClient) (function () { handle1.stop(); }); var waitForLoaded = expect(function () { - Meteor._autorun(function(handle2) { + Meteor.autorun(function(handle2) { if (!Meteor.userLoaded()) return; handle2.stop(); callWhenLoaded(); diff --git a/packages/deps/deps-utils.js b/packages/deps/deps-utils.js index 569d2deadd..cf39e39d6a 100644 --- a/packages/deps/deps-utils.js +++ b/packages/deps/deps-utils.js @@ -55,14 +55,14 @@ Meteor.deps._ContextSet = _ContextSet; - ////////// Meteor._autorun + ////////// Meteor.autorun // Run f(). Record its dependencies. Rerun it whenever the // dependencies change. // // Returns an object with a stop() method. Call stop() to stop the // rerunning. Also passes this object as an argument to f. - Meteor._autorun = function (f) { + Meteor.autorun = function (f) { var ctx; var slain = false; var handle = { diff --git a/packages/deps/deps_tests.js b/packages/deps/deps_tests.js new file mode 100644 index 0000000000..63e4e718ef --- /dev/null +++ b/packages/deps/deps_tests.js @@ -0,0 +1,46 @@ +Tinytest.add('deps - autorun', function (test) { + var listeners = new Meteor.deps._ContextSet; + var x = 0; + var handle = Meteor.autorun(function (handle) { + listeners.addCurrentContext(); + ++x; + }); + test.equal(x, 1); + Meteor.flush(); + test.equal(x, 1); + listeners.invalidateAll(); + test.equal(x, 1); + Meteor.flush(); + test.equal(x, 2); + listeners.invalidateAll(); + test.equal(x, 2); + Meteor.flush(); + test.equal(x, 3); + listeners.invalidateAll(); + // Prevent the function from running further. + handle.stop(); + Meteor.flush(); + test.equal(x, 3); + listeners.invalidateAll(); + Meteor.flush(); + test.equal(x, 3); + + Meteor.autorun(function (internalHandle) { + listeners.addCurrentContext(); + ++x; + if (x == 6) + internalHandle.stop(); + }); + test.equal(x, 4); + listeners.invalidateAll(); + Meteor.flush(); + test.equal(x, 5); + listeners.invalidateAll(); + // Increment to 6 and stop. + Meteor.flush(); + test.equal(x, 6); + listeners.invalidateAll(); + Meteor.flush(); + // Still 6! + test.equal(x, 6); +}); diff --git a/packages/deps/package.js b/packages/deps/package.js index f21ff22073..a38a57f69e 100644 --- a/packages/deps/package.js +++ b/packages/deps/package.js @@ -11,3 +11,9 @@ Package.on_use(function (api, where) { api.use('underscore', where); api.add_files(['deps.js', 'deps-utils.js'], where); }); + +Package.on_test(function (api) { + api.use('tinytest'); + api.use('deps'); + api.add_files('deps_tests.js', 'client'); +}); diff --git a/packages/session/session_tests.js b/packages/session/session_tests.js index 0a750b4086..d148914a1f 100644 --- a/packages/session/session_tests.js +++ b/packages/session/session_tests.js @@ -78,7 +78,7 @@ Tinytest.add('session - context invalidation for get', function (test) { var xGetExecutions = 0; - Meteor._autorun(function () { + Meteor.autorun(function () { ++xGetExecutions; Session.get('x'); }); @@ -99,7 +99,7 @@ Tinytest.add('session - context invalidation for equals', function (test) { var xEqualsExecutions = 0; - Meteor._autorun(function () { + Meteor.autorun(function () { ++xEqualsExecutions; Session.equals('x', 5); }); @@ -132,7 +132,7 @@ function (test) { // Make sure the special casing for equals undefined works. var yEqualsExecutions = 0; - Meteor._autorun(function () { + Meteor.autorun(function () { ++yEqualsExecutions; Session.equals('y', undefined); }); diff --git a/packages/spark/spark.js b/packages/spark/spark.js index ee4d0b69be..861bf417c1 100644 --- a/packages/spark/spark.js +++ b/packages/spark/spark.js @@ -795,7 +795,7 @@ Spark.isolate = function (htmlFunc) { var range; var firstRun = true; var retHtml; - Meteor._autorun(function (handle) { + Meteor.autorun(function (handle) { if (firstRun) { retHtml = renderer.annotate( htmlFunc(), Spark._ANNOTATION_ISOLATE,