Skip to content

Commit

Permalink
Merge pull request #413 from meteor/async-call-and-dot
Browse files Browse the repository at this point in the history
Added support for `Promise`s in `Spacebars.call` and `Spacebars.dot`.
  • Loading branch information
Grubba27 authored May 12, 2023
2 parents 2bf7eb3 + 63bddc9 commit eb8db10
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 0 deletions.
6 changes: 6 additions & 0 deletions packages/blaze/view.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
53 changes: 53 additions & 0 deletions packages/spacebars/spacebars-runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,49 @@ Spacebars.makeRaw = function (value) {
return HTML.Raw(value);
};

// FIXME: Remove once `spacebars` can depend on `[email protected]` (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<T>} promise
* @param {(x: T) => U} fn
* @returns {Promise<U>}
*/
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
Expand All @@ -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);
Expand Down Expand Up @@ -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`.
Expand Down Expand Up @@ -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;
Expand Down
24 changes: 24 additions & 0 deletions packages/spacebars/spacebars_tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

0 comments on commit eb8db10

Please sign in to comment.