Skip to content

Commit

Permalink
Merge pull request #428 from meteor/async-attributes-and-content
Browse files Browse the repository at this point in the history
Implemented async attributes and content.
  • Loading branch information
Grubba27 authored Nov 16, 2023
2 parents 6ef78a3 + 78ffe3f commit c6bdf78
Show file tree
Hide file tree
Showing 6 changed files with 127 additions and 43 deletions.
13 changes: 13 additions & 0 deletions packages/blaze/builtins.js
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,19 @@ Blaze.Each = function (argFunc, contentFunc, elseFunc) {
return eachView;
};

/**
* Create a new `Blaze.Let` view that unwraps the given value.
* @param {unknown} value
* @returns {Blaze.View}
*/
Blaze._Await = function (value) {
return Blaze.Let({ value }, Blaze._AwaitContent);
};

Blaze._AwaitContent = function () {
return Blaze.currentView._scopeBindings.value.get()?.value;
};

Blaze._TemplateWith = function (arg, contentFunc) {
var w;

Expand Down
65 changes: 49 additions & 16 deletions packages/blaze/materializer.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,14 @@ var materializeDOMInner = function (htmljs, intoArray, parentView, workStack) {
}
return;
} else {
if (htmljs instanceof Blaze.Template) {
// Try to construct a `Blaze.View` out of the object. If it works...
if (isPromiseLike(htmljs)) {
htmljs = Blaze._Await(htmljs);
} else if (htmljs instanceof Blaze.Template) {
htmljs = htmljs.constructView();
// fall through to Blaze.View case below
}

// ...materialize it.
if (htmljs instanceof Blaze.View) {
Blaze._materializeView(htmljs, parentView, workStack, intoArray);
return;
Expand All @@ -89,6 +93,33 @@ var materializeDOMInner = function (htmljs, intoArray, parentView, workStack) {
throw new Error("Unexpected object in htmljs: " + htmljs);
};

const isPromiseLike = x => !!x && typeof x.then === 'function';

function waitForAllAttributesAndContinue(attrs, fn) {
const promises = [];
for (const [key, value] of Object.entries(attrs)) {
if (isPromiseLike(value)) {
promises.push(value.then(value => {
attrs[key] = value;
}));
} else if (Array.isArray(value)) {
value.forEach((element, index) => {
if (isPromiseLike(element)) {
promises.push(element.then(element => {
value[index] = element;
}));
}
});
}
}

if (promises.length) {
Promise.all(promises).then(fn);
} else {
fn();
}
}

var materializeTag = function (tag, parentView, workStack) {
var tagName = tag.tagName;
var elem;
Expand Down Expand Up @@ -125,20 +156,22 @@ var materializeTag = function (tag, parentView, workStack) {
var attrUpdater = new ElementAttributesUpdater(elem);
var updateAttributes = function () {
var expandedAttrs = Blaze._expandAttributes(rawAttrs, parentView);
var flattenedAttrs = HTML.flattenAttributes(expandedAttrs);
var stringAttrs = {};
for (var attrName in flattenedAttrs) {
// map `null`, `undefined`, and `false` to null, which is important
// so that attributes with nully values are considered absent.
// stringify anything else (e.g. strings, booleans, numbers including 0).
if (flattenedAttrs[attrName] == null || flattenedAttrs[attrName] === false)
stringAttrs[attrName] = null;
else
stringAttrs[attrName] = Blaze._toText(flattenedAttrs[attrName],
parentView,
HTML.TEXTMODE.STRING);
}
attrUpdater.update(stringAttrs);
waitForAllAttributesAndContinue(expandedAttrs, () => {
var flattenedAttrs = HTML.flattenAttributes(expandedAttrs);
var stringAttrs = {};
for (var attrName in flattenedAttrs) {
// map `null`, `undefined`, and `false` to null, which is important
// so that attributes with nully values are considered absent.
// stringify anything else (e.g. strings, booleans, numbers including 0).
if (flattenedAttrs[attrName] == null || flattenedAttrs[attrName] === false)
stringAttrs[attrName] = null;
else
stringAttrs[attrName] = Blaze._toText(flattenedAttrs[attrName],
parentView,
HTML.TEXTMODE.STRING);
}
attrUpdater.update(stringAttrs);
});
};
var updaterComputation;
if (parentView) {
Expand Down
46 changes: 25 additions & 21 deletions packages/spacebars-tests/async_tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,20 @@ function asyncSuite(templateName, cases) {
}
}

const getter = async () => 'foo';
const thenable = { then: resolve => Promise.resolve().then(() => resolve('foo')) };
const value = Promise.resolve('foo');

asyncSuite('access', [
['getter', { x: { y: async () => 'foo' } }, '', 'foo'],
['thenable', { x: { y: { then: resolve => { Promise.resolve().then(() => resolve('foo')) } } } }, '', 'foo'],
['value', { x: { y: Promise.resolve('foo') } }, '', 'foo'],
['getter', { x: { y: getter } }, '', 'foo'],
['thenable', { x: { y: thenable } }, '', 'foo'],
['value', { x: { y: value } }, '', 'foo'],
]);

asyncSuite('direct', [
['getter', { x: async () => 'foo' }, '', 'foo'],
['thenable', { x: { then: resolve => { Promise.resolve().then(() => resolve('foo')) } } }, '', 'foo'],
['value', { x: Promise.resolve('foo') }, '', 'foo'],
['getter', { x: getter }, '', 'foo'],
['thenable', { x: thenable }, '', 'foo'],
['value', { x: value }, '', 'foo'],
]);

asyncTest('missing1', 'outer', async (test, template, render) => {
Expand All @@ -44,29 +48,29 @@ asyncTest('missing2', 'inner', async (test, template, render) => {
test.throws(render, 'Binding for "b" was not found.');
});

asyncTest('attribute', '', async (test, template, render) => {
Blaze._throwNextException = true;
template.helpers({ x: Promise.resolve() });
test.throws(render, 'Asynchronous values are not serializable. Use #let to unwrap them first.');
});
asyncSuite('attribute', [
['getter', { x: getter }, '<img>', '<img class="foo">'],
['thenable', { x: thenable }, '<img>', '<img class="foo">'],
['value', { x: value }, '<img>', '<img class="foo">'],
]);

asyncTest('attributes', '', async (test, template, render) => {
Blaze._throwNextException = true;
template.helpers({ x: Promise.resolve() });
test.throws(render, 'Asynchronous attributes are not supported. Use #let to unwrap them first.');
});

asyncTest('value_direct', '', async (test, template, render) => {
Blaze._throwNextException = true;
template.helpers({ x: Promise.resolve() });
test.throws(render, 'Asynchronous values are not serializable. Use #let to unwrap them first.');
});
asyncSuite('value_direct', [
['getter', { x: getter }, '', 'foo'],
['thenable', { x: thenable }, '', 'foo'],
['value', { x: value }, '', 'foo'],
]);

asyncTest('value_raw', '', async (test, template, render) => {
Blaze._throwNextException = true;
template.helpers({ x: Promise.resolve() });
test.throws(render, 'Asynchronous values are not serializable. Use #let to unwrap them first.');
});
asyncSuite('value_raw', [
['getter', { x: getter }, '', 'foo'],
['thenable', { x: thenable }, '', 'foo'],
['value', { x: value }, '', 'foo'],
]);

asyncSuite('if', [
['false', { x: Promise.resolve(false) }, '', '2'],
Expand Down
10 changes: 5 additions & 5 deletions packages/spacebars/spacebars-runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ Spacebars.mustache = function (value/*, args*/) {

if (result instanceof Spacebars.SafeString)
return HTML.Raw(result.toString());
else if (isPromiseLike(value))
throw new Error('Asynchronous values are not serializable. Use #let to unwrap them first.');
else if (isPromiseLike(result))
return result;
else
// map `null`, `undefined`, and `false` to null, which is important
// so that attributes with nully values are considered absent.
Expand Down Expand Up @@ -113,7 +113,7 @@ Spacebars.dataMustache = function (value/*, args*/) {
Spacebars.makeRaw = function (value) {
if (value == null) // null or undefined
return null;
else if (value instanceof HTML.Raw)
else if (value instanceof HTML.Raw || isPromiseLike(value))
return value;
else
return HTML.Raw(value);
Expand Down Expand Up @@ -175,7 +175,7 @@ Spacebars.call = function (value/*, args*/) {
}
};

const isPromiseLike = x => typeof x?.then === 'function';
const isPromiseLike = x => !!x && typeof x.then === 'function';

// Call this as `Spacebars.kw({ ... })`. The return value
// is `instanceof Spacebars.kw`.
Expand Down Expand Up @@ -233,7 +233,7 @@ Spacebars.dot = function (value, id1/*, id2, ...*/) {
return Spacebars.dot.apply(null, argsForRecurse);
}

if (typeof value === 'function')
while (typeof value === 'function')
value = value();

if (! value)
Expand Down
4 changes: 4 additions & 0 deletions packages/spacebars/spacebars_tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ Tinytest.addAsync("spacebars - async - Spacebars.dot", async test => {
test.equal(await Spacebars.dot(Promise.resolve({ x: async () => o }), 'x', 'y'), 1);
test.equal(await Spacebars.dot({ x: { then: resolve => resolve(o) } }, 'x', 'y'), 1);
test.equal(await Spacebars.dot({ x: Promise.resolve(o) }, 'x', 'y'), 1);
test.equal(await Spacebars.dot({ x: () => () => o }, 'x', 'y'), 1);
test.equal(await Spacebars.dot({ x: () => async () => o }, 'x', 'y'), 1);
test.equal(await Spacebars.dot({ x: async () => () => o }, 'x', 'y'), 1);
test.equal(await Spacebars.dot({ x: async () => async () => o }, 'x', 'y'), 1);
test.equal(await Spacebars.dot({ x: async () => o }, 'x', 'y'), 1);
test.equal(await Spacebars.dot(() => ({ x: async () => o }), 'x', 'y'), 1);
test.equal(await Spacebars.dot(async () => ({ x: async () => o }), 'x', 'y'), 1);
Expand Down
32 changes: 31 additions & 1 deletion site/source/api/spacebars.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,12 +168,22 @@ and not all tags are allowed at all locations.
A double-braced tag at element level or in an attribute value typically evalutes
to a string. If it evalutes to something else, the value will be cast to a
string, unless the value is `null`, `undefined`, or `false`, which results in
nothing being displayed. `Promise`s are not supported and will throw an error.
nothing being displayed. `Promise`s are also supported -- see below.

Values returned from helpers must be pure text, not HTML. (That is, strings
should have `<`, not `&lt;`.) Spacebars will perform any necessary escaping if
a template is rendered to HTML.

### Async content

> This functionality is considered experimental and a subject to change. For
> details please refer to [#424](https://github.com/meteor/blaze/pull/428).
The values can be wrapped in a `Promise`. When that happens, it will be treated
as `undefined` while it's pending or rejected. Once resolved, the resulting
value is used. To have more fine-grained handling of non-resolved states, use
`#let` and the async state helpers (e.g., `@pending`).

### SafeString

If a double-braced tag at element level evalutes to an object created with
Expand All @@ -193,6 +203,16 @@ An attribute value that consists entirely of template tags that return `null`,
`undefined`, or `false` is considered absent; otherwise, the attribute is
considered present, even if its value is empty.

### Async attributes

> This functionality is considered experimental and a subject to change. For
> details please refer to [#424](https://github.com/meteor/blaze/pull/428).
The values can be wrapped in a `Promise`. When that happens, it will be treated
as `undefined` while it's pending or rejected. Once resolved, the resulting
value is used. To have more fine-grained handling of non-resolved states, use
`#let` and the async state helpers (e.g., `@pending`).

### Dynamic Attributes

A double-braced tag can be used in an HTML start tag to specify an arbitrary set
Expand Down Expand Up @@ -256,6 +276,16 @@ insert `"</div><div>"` to close an existing div and open a new one.

This template tag cannot be used in attributes or in an HTML start tag.

### Async content

> This functionality is considered experimental and a subject to change. For
> details please refer to [#424](https://github.com/meteor/blaze/pull/428).
The raw HTML can be wrapped in a `Promise`. When that happens, it will not
render anything if it's pending or rejected. Once resolved, the resulting value
is used. To have more fine-grained handling of non-resolved states, use `#let`
and the async state helpers (e.g., `@pending`).

## Inclusion Tags

An inclusion tag takes the form `{% raw %}{{> templateName}}{% endraw %}` or `{% raw %}{{> templateName
Expand Down

0 comments on commit c6bdf78

Please sign in to comment.