Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for Promises in Spacebars.call and Spacebars.dot. #413

Merged
merged 1 commit into from
May 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/blaze/view.js
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,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);
});