diff --git a/packages/blaze/view.js b/packages/blaze/view.js index 7a9f33fe4..aa3e95dd4 100644 --- a/packages/blaze/view.js +++ b/packages/blaze/view.js @@ -540,6 +540,12 @@ Blaze._isContentEqual = function (a, b) { */ Blaze.currentView = null; +/** + * @template T + * @param {Blaze.View} view + * @param {() => T} func + * @returns {T} + */ Blaze._withCurrentView = function (view, func) { var oldView = Blaze.currentView; try { diff --git a/packages/spacebars/spacebars-runtime.js b/packages/spacebars/spacebars-runtime.js index f02849c8d..55fa952b3 100644 --- a/packages/spacebars/spacebars-runtime.js +++ b/packages/spacebars/spacebars-runtime.js @@ -117,9 +117,49 @@ Spacebars.makeRaw = function (value) { return HTML.Raw(value); }; +// FIXME: Remove once `spacebars` can depend on `tracker@1.3.0` (Meteor 2.10.0). +const _withComputation = Tracker.withComputation || function (computation, f) { + var previousComputation = Tracker.currentComputation; + + Tracker.currentComputation = computation; + Tracker.active = !!computation; + + try { + return f(); + } finally { + Tracker.currentComputation = previousComputation; + Tracker.active = !!previousComputation; + } +}; + +/*** + * @sumamry Executes `fn` with the resolved value of `promise` while preserving + * the context, i.e., `Blaze.currentView` and `Tracker.currentComputation`. + * @template T + * @template U + * @param {Promise} promise + * @param {(x: T) => U} fn + * @returns {Promise} + */ +function _thenWithContext(promise, fn) { + const computation = Tracker.currentComputation; + const view = Blaze.currentView; + return promise.then(value => + Blaze._withCurrentView(view, () => + _withComputation(computation, () => + fn(value) + ) + ) + ); +} + // If `value` is a function, evaluate its `args` (by calling them, if they // are functions), and then call it on them. Otherwise, return `value`. // +// If any of the arguments is a `Promise` or a function returning one, then the +// `value` will be called once all of the arguments resolve. If any of them +// rejects, so will the call. +// // If `value` is not a function and is not null, then this method will assert // that there are no args. We check for null before asserting because a user // may write a template like {{user.fullNameWithPrefix 'Mr.'}}, where the @@ -128,9 +168,15 @@ Spacebars.call = function (value/*, args*/) { if (typeof value === 'function') { // Evaluate arguments by calling them if they are functions. var newArgs = []; + let anyIsPromise = false; for (var i = 1; i < arguments.length; i++) { var arg = arguments[i]; newArgs[i-1] = (typeof arg === 'function' ? arg() : arg); + anyIsPromise = anyIsPromise || newArgs[i-1] instanceof Promise; + } + + if (anyIsPromise) { + return _thenWithContext(Promise.all(newArgs), newArgs => value.apply(null, newArgs)); } return value.apply(null, newArgs); @@ -170,6 +216,10 @@ Spacebars.SafeString.prototype = Handlebars.SafeString.prototype; // a wrapped version of `baz` that always uses `foo.bar` as // `this`). // +// If any of the intermediate values is a `Promise`, the result will be one as +// well, i.e., accessing a field of a `Promise` results in a `Promise` of the +// accessed field. Rejections are passed-through. +// // In `Spacebars.dot(foo, "bar")`, `foo` is assumed to be either // a non-function value or a "fully-bound" function wrapping a value, // where fully-bound means it takes no arguments and ignores `this`. @@ -200,6 +250,9 @@ Spacebars.dot = function (value, id1/*, id2, ...*/) { if (! value) return value; // falsy, don't index, pass through + if (value instanceof Promise) + return _thenWithContext(value, value => Spacebars.dot(value, id1)); + var result = value[id1]; if (typeof result !== 'function') return result; diff --git a/packages/spacebars/spacebars_tests.js b/packages/spacebars/spacebars_tests.js index 16207a260..926e468e4 100644 --- a/packages/spacebars/spacebars_tests.js +++ b/packages/spacebars/spacebars_tests.js @@ -57,3 +57,27 @@ Tinytest.add("spacebars - Spacebars.dot", function (test) { }, 'inc')(8), 9); }); + +Tinytest.add("spacebars - async - Spacebars.call", async test => { + const add = (x, y) => x + y; + test.equal(await Spacebars.call(add, 1, Promise.resolve(2)), 3); + test.equal(await Spacebars.call(add, Promise.resolve(1), 2), 3); + test.equal(await Spacebars.call(add, Promise.resolve(1), Promise.resolve(2)), 3); + test.equal(await Spacebars.call(add, 1, async () => 2), 3); + test.equal(await Spacebars.call(add, async () => 1, 2), 3); + test.equal(await Spacebars.call(add, async () => 1, async () => 2), 3); + test.equal(await Spacebars.call(add, Promise.reject(1), 2).catch(x => x), 1); + test.equal(await Spacebars.call(add, 1, Promise.reject(2)).catch(x => x), 2); + test.equal(await Spacebars.call(add, Promise.reject(1), Promise.reject(2)).catch(x => x), 1); +}); + +Tinytest.add("spacebars - async - Spacebars.dot", async test => { + test.equal(await Spacebars.dot(Promise.resolve(null), 'foo'), null); + test.equal(await Spacebars.dot(Promise.resolve({ foo: 1 }), 'foo'), 1); + test.equal(await Spacebars.dot(Promise.resolve({ foo: () => 1 }), 'foo'), 1); + test.equal(await Spacebars.dot(Promise.resolve({ foo: async () => 1 }), 'foo'), 1); + test.equal(await Spacebars.dot({ foo: Promise.resolve(1) }, 'foo'), 1); + test.equal(await Spacebars.dot({ foo: async () => 1 }, 'foo'), 1); + test.equal(await Spacebars.dot(() => ({ foo: async () => 1 }), 'foo'), 1); + test.equal(await Spacebars.dot(async () => ({ foo: async () => 1 }), 'foo'), 1); +});