Skip to content

Commit

Permalink
Merge pull request #463 from meteor/release-2.9
Browse files Browse the repository at this point in the history
Release 2.9
  • Loading branch information
StorytellerCZ authored Mar 14, 2024
2 parents adc11c9 + d513757 commit 804e3e5
Show file tree
Hide file tree
Showing 19 changed files with 210 additions and 83 deletions.
8 changes: 8 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@

## v2.9.0 2024-Mar-XX

* [#460](https://github.com/meteor/blaze/pull/460) Implemented async dynamic attributes.
* [#458](https://github.com/meteor/blaze/pull/458) Blaze._expandAttributes returns empty object, if null.



## v2.8.0 2023-Dec-28

* [#431](https://github.com/meteor/blaze/pull/431) Depracate Ui package.
Expand Down
12 changes: 6 additions & 6 deletions packages/blaze/.versions
7 changes: 7 additions & 0 deletions packages/blaze/exceptions.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ Blaze._reportException = function (e, msg) {
debugFunc()(msg || 'Exception caught in template:', e.stack || e.message || e);
};

// It's meant to be used in `Promise` chains to report the error while not
// "swallowing" it (i.e., the chain will still reject).
Blaze._reportExceptionAndThrow = function (error) {
Blaze._reportException(error);
throw error;
};

Blaze._wrapCatchingExceptions = function (f, where) {
if (typeof f !== 'function')
return f;
Expand Down
44 changes: 34 additions & 10 deletions packages/blaze/materializer.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,29 +95,53 @@ const materializeDOMInner = function (htmljs, intoArray, parentView, workStack)

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

function waitForAllAttributesAndContinue(attrs, fn) {
function then(maybePromise, fn) {
if (isPromiseLike(maybePromise)) {
maybePromise.then(fn, Blaze._reportException);
} else {
fn(maybePromise);
}
}

function waitForAllAttributes(attrs) {
// Non-object attrs (e.g., `null`) are ignored.
if (!attrs || attrs !== Object(attrs)) {
return {};
}

// Combined attributes, e.g., `<img {{x}} {{y}}>`.
if (Array.isArray(attrs)) {
const mapped = attrs.map(waitForAllAttributes);
return mapped.some(isPromiseLike) ? Promise.all(mapped) : mapped;
}

// Singular async attributes, e.g., `<img {{x}}>`.
if (isPromiseLike(attrs)) {
return attrs.then(waitForAllAttributes, Blaze._reportExceptionAndThrow);
}

// Singular sync attributes, with potentially async properties.
const promises = [];
for (const [key, value] of Object.entries(attrs)) {
if (isPromiseLike(value)) {
promises.push(value.then(value => {
attrs[key] = value;
}));
}, Blaze._reportExceptionAndThrow));
} else if (Array.isArray(value)) {
value.forEach((element, index) => {
if (isPromiseLike(element)) {
promises.push(element.then(element => {
value[index] = element;
}));
}, Blaze._reportExceptionAndThrow));
}
});
}
}

if (promises.length) {
Promise.all(promises).then(fn);
} else {
fn();
}
// If any of the properties were async, lift the `Promise`.
return promises.length
? Promise.all(promises).then(() => attrs, Blaze._reportExceptionAndThrow)
: attrs;
}

const materializeTag = function (tag, parentView, workStack) {
Expand Down Expand Up @@ -156,8 +180,8 @@ const materializeTag = function (tag, parentView, workStack) {
const attrUpdater = new ElementAttributesUpdater(elem);
const updateAttributes = function () {
const expandedAttrs = Blaze._expandAttributes(rawAttrs, parentView);
waitForAllAttributesAndContinue(expandedAttrs, () => {
const flattenedAttrs = HTML.flattenAttributes(expandedAttrs);
then(waitForAllAttributes(expandedAttrs), awaitedAttrs => {
const flattenedAttrs = HTML.flattenAttributes(awaitedAttrs);
const stringAttrs = {};
Object.keys(flattenedAttrs).forEach((attrName) => {
// map `null`, `undefined`, and `false` to null, which is important
Expand Down
6 changes: 3 additions & 3 deletions packages/blaze/package.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Package.describe({
name: 'blaze',
summary: "Meteor Reactive Templating library",
version: '2.8.0',
version: '2.9.0',
git: 'https://github.com/meteor/blaze.git'
});

Expand All @@ -27,8 +27,8 @@ Package.onUse(function (api) {
'Handlebars'
]);

api.use('[email protected].0');
api.imply('[email protected].0');
api.use('[email protected].1');
api.imply('[email protected].1');

api.addFiles([
'preamble.js'
Expand Down
3 changes: 2 additions & 1 deletion packages/blaze/view.js
Original file line number Diff line number Diff line change
Expand Up @@ -490,8 +490,9 @@ Blaze._expand = function (htmljs, parentView) {

Blaze._expandAttributes = function (attrs, parentView) {
parentView = parentView || currentViewIfRendering();
return (new Blaze._HTMLJSExpander(
const expanded = (new Blaze._HTMLJSExpander(
{parentView: parentView})).visitAttributes(attrs);
return expanded || {};
};

Blaze._destroyView = function (view, _skipNodes) {
Expand Down
2 changes: 1 addition & 1 deletion packages/htmljs/package.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Package.describe({
name: 'htmljs',
summary: "Small library for expressing HTML trees",
version: '1.2.0',
version: '1.2.1',
git: 'https://github.com/meteor/blaze.git'
});

Expand Down
10 changes: 6 additions & 4 deletions packages/htmljs/visitors.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
isVoidElement,
} from './html';

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

var IDENTITY = function (x) { return x; };

Expand Down Expand Up @@ -156,6 +157,11 @@ TransformingVisitor.def({
// an array, or in some uses, a foreign object (such as
// a template tag).
visitAttributes: function (attrs, ...args) {
// Allow Promise-like values here; these will be handled in materializer.
if (isPromiseLike(attrs)) {
return attrs;
}

if (isArray(attrs)) {
var result = attrs;
for (var i = 0; i < attrs.length; i++) {
Expand All @@ -172,10 +178,6 @@ TransformingVisitor.def({
}

if (attrs && isConstructedObject(attrs)) {
if (typeof attrs.then === 'function') {
throw new Error('Asynchronous dynamic attributes are not supported. Use #let to unwrap them first.');
}

throw new Error("The basic TransformingVisitor does not support " +
"foreign objects in attributes. Define a custom " +
"visitAttributes for this case.");
Expand Down
4 changes: 4 additions & 0 deletions packages/spacebars-tests/async_tests.html
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@
<img {{x}}>
</template>

<template name="spacebars_async_tests_attributes_double">
<img {{x}} {{y}}>
</template>

<template name="spacebars_async_tests_value_direct">
{{x}}
</template>
Expand Down
67 changes: 44 additions & 23 deletions packages/spacebars-tests/async_tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,20 @@ function asyncSuite(templateName, cases) {
}
}

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

asyncSuite('access', [
['getter', { x: { y: getter } }, '', 'foo'],
['thenable', { x: { y: thenable } }, '', 'foo'],
['value', { x: { y: value } }, '', 'foo'],
['getter', { x: { y: getter('foo') } }, '', 'foo'],
['thenable', { x: { y: thenable('foo') } }, '', 'foo'],
['value', { x: { y: value('foo') } }, '', 'foo'],
]);

asyncSuite('direct', [
['getter', { x: getter }, '', 'foo'],
['thenable', { x: thenable }, '', 'foo'],
['value', { x: value }, '', 'foo'],
['getter', { x: getter('foo') }, '', 'foo'],
['thenable', { x: thenable('foo') }, '', 'foo'],
['value', { x: value('foo') }, '', 'foo'],
]);

asyncTest('missing1', 'outer', async (test, template, render) => {
Expand All @@ -49,27 +49,48 @@ asyncTest('missing2', 'inner', async (test, template, render) => {
});

asyncSuite('attribute', [
['getter', { x: getter }, '<img>', '<img class="foo">'],
['thenable', { x: thenable }, '<img>', '<img class="foo">'],
['value', { x: value }, '<img>', '<img class="foo">'],
['getter', { x: getter('foo') }, '<img>', '<img class="foo">'],
['thenable', { x: thenable('foo') }, '<img>', '<img class="foo">'],
['value', { x: value('foo') }, '<img>', '<img class="foo">'],
]);

asyncTest('attributes', '', async (test, template, render) => {
Blaze._throwNextException = true;
template.helpers({ x: Promise.resolve() });
test.throws(render, 'Asynchronous dynamic attributes are not supported. Use #let to unwrap them first.');
});
asyncSuite('attributes', [
['getter in getter', { x: getter({ class: getter('foo') }) }, '<img>', '<img>'], // Nested getters are NOT evaluated.
['getter in thenable', { x: thenable({ class: getter('foo') }) }, '<img>', '<img>'], // Nested getters are NOT evaluated.
['getter in value', { x: value({ class: getter('foo') }) }, '<img>', '<img>'], // Nested getters are NOT evaluated.
['static in getter', { x: getter({ class: 'foo' }) }, '<img>', '<img class="foo">'],
['static in thenable', { x: thenable({ class: 'foo' }) }, '<img>', '<img class="foo">'],
['static in value', { x: value({ class: 'foo' }) }, '<img>', '<img class="foo">'],
['thenable in getter', { x: getter({ class: thenable('foo') }) }, '<img>', '<img class="foo">'],
['thenable in thenable', { x: thenable({ class: thenable('foo') }) }, '<img>', '<img class="foo">'],
['thenable in value', { x: value({ class: thenable('foo') }) }, '<img>', '<img class="foo">'],
['value in getter', { x: getter({ class: value('foo') }) }, '<img>', '<img class="foo">'],
['value in thenable', { x: thenable({ class: value('foo') }) }, '<img>', '<img class="foo">'],
['value in value', { x: value({ class: value('foo') }) }, '<img>', '<img class="foo">'],
]);

asyncSuite('attributes_double', [
['null lhs getter', { x: getter({ class: null }), y: getter({ class: 'foo' }) }, '<img>', '<img class="foo">'],
['null lhs thenable', { x: thenable({ class: null }), y: thenable({ class: 'foo' }) }, '<img>', '<img class="foo">'],
['null lhs value', { x: value({ class: null }), y: value({ class: 'foo' }) }, '<img>', '<img class="foo">'],
['null rhs getter', { x: getter({ class: 'foo' }), y: getter({ class: null }) }, '<img>', '<img class="foo">'],
['null rhs thenable', { x: thenable({ class: 'foo' }), y: thenable({ class: null }) }, '<img>', '<img class="foo">'],
['null rhs value', { x: value({ class: 'foo' }), y: value({ class: null }) }, '<img>', '<img class="foo">'],
['override getter', { x: getter({ class: 'foo' }), y: getter({ class: 'bar' }) }, '<img>', '<img class="bar">'],
['override thenable', { x: thenable({ class: 'foo' }), y: thenable({ class: 'bar' }) }, '<img>', '<img class="bar">'],
['override value', { x: value({ class: 'foo' }), y: value({ class: 'bar' }) }, '<img>', '<img class="bar">'],
]);

asyncSuite('value_direct', [
['getter', { x: getter }, '', 'foo'],
['thenable', { x: thenable }, '', 'foo'],
['value', { x: value }, '', 'foo'],
['getter', { x: getter('foo') }, '', 'foo'],
['thenable', { x: thenable('foo') }, '', 'foo'],
['value', { x: value('foo') }, '', 'foo'],
]);

asyncSuite('value_raw', [
['getter', { x: getter }, '', 'foo'],
['thenable', { x: thenable }, '', 'foo'],
['value', { x: value }, '', 'foo'],
['getter', { x: getter('foo') }, '', 'foo'],
['thenable', { x: thenable('foo') }, '', 'foo'],
['value', { x: value('foo') }, '', 'foo'],
]);

asyncSuite('if', [
Expand Down
4 changes: 2 additions & 2 deletions packages/spacebars-tests/package.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Package.describe({
name: 'spacebars-tests',
summary: "Additional tests for Spacebars",
version: '1.3.4',
version: '1.4.0',
git: 'https://github.com/meteor/blaze.git'
});

Expand All @@ -24,7 +24,7 @@ Package.onTest(function (api) {

api.use([
'[email protected]',
'blaze@2.8.0'
'blaze@2.9.0'
]);
api.use('[email protected]', 'client');

Expand Down
Loading

0 comments on commit 804e3e5

Please sign in to comment.