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

Release 2.9 #463

Merged
merged 10 commits into from
Mar 14, 2024
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.
12 changes: 6 additions & 6 deletions packages/blaze/.versions
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@ babel-compiler@7.10.5
babel-runtime@1.5.1
base64@1.0.12
binary-heap@1.0.11
blaze@2.8.0
blaze@2.9.0
blaze-tools@1.1.4
boilerplate-generator@1.7.2
caching-compiler@1.2.2
@@ -24,13 +24,13 @@ ejson@1.1.3
fetch@0.1.4
geojson-utils@1.0.11
html-tools@1.1.4
htmljs@1.2.0
htmljs@1.2.1
id-map@1.1.1
inter-process-messaging@0.1.1
jquery@1.11.10
local-test:blaze@2.8.0
local-test:blaze@2.9.0
logging@1.3.3
meteor@1.11.4
meteor@1.11.5
minimongo@1.9.3
modern-browsers@0.1.10
modules@0.20.0
@@ -60,6 +60,6 @@ test-helpers@1.3.1
tinytest@1.2.3
tracker@1.3.3
typescript@4.9.5
underscore@1.0.13
webapp@1.13.6
underscore@1.6.0
webapp@1.13.8
webapp-hashing@1.1.1
7 changes: 7 additions & 0 deletions packages/blaze/exceptions.js
Original file line number Diff line number Diff line change
@@ -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;
44 changes: 34 additions & 10 deletions packages/blaze/materializer.js
Original file line number Diff line number Diff line change
@@ -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) {
@@ -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
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'
});

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

api.use('htmljs@1.2.0');
api.imply('htmljs@1.2.0');
api.use('htmljs@1.2.1');
api.imply('htmljs@1.2.1');

api.addFiles([
'preamble.js'
3 changes: 2 additions & 1 deletion packages/blaze/view.js
Original file line number Diff line number Diff line change
@@ -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) {
10 changes: 5 additions & 5 deletions packages/htmljs/.versions
Original file line number Diff line number Diff line change
@@ -19,12 +19,12 @@ ecmascript-runtime-server@0.11.0
ejson@1.1.3
fetch@0.1.4
geojson-utils@1.0.11
htmljs@1.2.0
htmljs@1.2.1
id-map@1.1.1
inter-process-messaging@0.1.1
local-test:htmljs@1.2.0
local-test:htmljs@1.2.1
logging@1.3.3
meteor@1.11.4
meteor@1.11.5
minimongo@1.9.3
modern-browsers@0.1.10
modules@0.20.0
@@ -45,6 +45,6 @@ socket-stream-client@0.5.2
tinytest@1.2.3
tracker@1.3.3
typescript@4.9.5
underscore@1.0.13
webapp@1.13.6
underscore@1.6.0
webapp@1.13.8
webapp-hashing@1.1.1
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'
});

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

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

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

@@ -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++) {
@@ -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.");
14 changes: 7 additions & 7 deletions packages/spacebars-tests/.versions
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@ babel-compiler@7.10.5
babel-runtime@1.5.1
base64@1.0.12
binary-heap@1.0.11
blaze@2.8.0
blaze@2.9.0
blaze-tools@1.1.4
boilerplate-generator@1.7.2
caching-compiler@1.2.2
@@ -25,14 +25,14 @@ es5-shim@4.8.0
fetch@0.1.4
geojson-utils@1.0.11
html-tools@1.1.4
htmljs@1.2.0
htmljs@1.2.1
id-map@1.1.1
inter-process-messaging@0.1.1
jquery@1.11.10
local-test:spacebars-tests@1.3.4
local-test:spacebars-tests@1.4.0
logging@1.3.3
markdown@1.0.14
meteor@1.11.4
meteor@1.11.5
minimongo@1.9.3
modern-browsers@0.1.10
modules@0.20.0
@@ -56,7 +56,7 @@ session@1.2.1
socket-stream-client@0.5.2
spacebars@1.4.0
spacebars-compiler@1.3.2
spacebars-tests@1.3.4
spacebars-tests@1.4.0
templating@1.4.3
templating-compiler@1.4.2
templating-runtime@1.6.4
@@ -65,6 +65,6 @@ test-helpers@1.3.1
tinytest@1.2.3
tracker@1.3.3
typescript@4.9.5
underscore@1.0.13
webapp@1.13.6
underscore@1.6.0
webapp@1.13.8
webapp-hashing@1.1.1
4 changes: 4 additions & 0 deletions packages/spacebars-tests/async_tests.html
Original file line number Diff line number Diff line change
@@ -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>
67 changes: 44 additions & 23 deletions packages/spacebars-tests/async_tests.js
Original file line number Diff line number Diff line change
@@ -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) => {
@@ -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', [
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'
});

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

api.use([
'spacebars@1.4.0',
'blaze@2.8.0'
'blaze@2.9.0'
]);
api.use('templating@1.4.3', 'client');

Loading