diff --git a/HISTORY.md b/HISTORY.md index 8aaa02ee3..e80f9edfe 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,3 +1,8 @@ +## v2.7.0, 2023-May-XX + +* [#413](https://github.com/meteor/blaze/pull/413) Added support for Promises in Spacebars.call and Spacebars.dot. +* [#412](https://github.com/meteor/blaze/pull/412) Implemented async bindings in #let. + ## v2.6.2, 2023-April-21 * [#403](https://github.com/meteor/blaze/pull/403) Add TS types to core diff --git a/packages/blaze/.versions b/packages/blaze/.versions index 9a67b546b..6587020c0 100644 --- a/packages/blaze/.versions +++ b/packages/blaze/.versions @@ -1,23 +1,23 @@ allow-deny@1.1.1 -babel-compiler@7.10.3 +babel-compiler@7.10.4 babel-runtime@1.5.1 base64@1.0.12 binary-heap@1.0.11 -blaze@2.6.2 +blaze@2.7.0 blaze-tools@1.1.3 boilerplate-generator@1.7.1 caching-compiler@1.2.2 caching-html-compiler@1.1.2 -callback-hook@1.5.0 +callback-hook@1.5.1 check@1.3.2 ddp@1.4.1 ddp-client@2.6.1 ddp-common@1.4.0 -ddp-server@2.6.0 +ddp-server@2.6.1 diff-sequence@1.1.2 -dynamic-import@0.7.2 -ecmascript@0.16.6 -ecmascript-runtime@0.8.0 +dynamic-import@0.7.3 +ecmascript@0.16.7 +ecmascript-runtime@0.8.1 ecmascript-runtime-client@0.12.1 ecmascript-runtime-server@0.11.0 ejson@1.1.3 @@ -28,28 +28,28 @@ htmljs@1.1.1 id-map@1.1.1 inter-process-messaging@0.1.1 jquery@1.11.10 -local-test:blaze@2.6.2 +local-test:blaze@2.7.0 logging@1.3.2 -meteor@1.11.1 -minimongo@1.9.2 +meteor@1.11.2 +minimongo@1.9.3 modern-browsers@0.1.9 modules@0.19.0 modules-runtime@0.13.1 -mongo@1.16.5 +mongo@1.16.6 mongo-decimal@0.1.3 mongo-dev-server@1.1.0 mongo-id@1.0.8 -npm-mongo@4.14.0 +npm-mongo@4.16.0 observe-sequence@1.0.16 ordered-dict@1.1.0 promise@0.12.2 random@1.2.1 -react-fast-refresh@0.2.6 +react-fast-refresh@0.2.7 reactive-var@1.0.12 reload@1.3.1 retry@1.1.0 routepolicy@1.1.1 -socket-stream-client@0.5.0 +socket-stream-client@0.5.1 spacebars@1.0.15 spacebars-compiler@1.1.2 templating@1.3.2 @@ -58,7 +58,7 @@ templating-runtime@1.3.2 templating-tools@1.1.2 test-helpers@1.3.1 tinytest@1.2.2 -tracker@1.3.1 -underscore@1.0.12 -webapp@1.13.4 +tracker@1.3.2 +underscore@1.0.13 +webapp@1.13.5 webapp-hashing@1.1.1 diff --git a/packages/blaze/builtins.js b/packages/blaze/builtins.js index 044e68a45..c48009297 100644 --- a/packages/blaze/builtins.js +++ b/packages/blaze/builtins.js @@ -32,22 +32,41 @@ Blaze.With = function (data, contentFunc) { return view; }; + +/** + * @summary Shallow compare of two bindings. + * @param {Binding} x + * @param {Binding} y + */ +function _isEqualBinding(x, y) { + return x && y ? x.error === y.error && x.value === y.value : x === y; +} + /** * Attaches bindings to the instantiated view. * @param {Object} bindings A dictionary of bindings, each binding name * corresponds to a value or a function that will be reactively re-run. - * @param {View} view The target. + * @param {Blaze.View} view The target. */ Blaze._attachBindingsToView = function (bindings, view) { + function setBindingValue(name, value) { + if (value && typeof value.then === 'function') { + value.then( + value => view._scopeBindings[name].set({ value }), + error => view._scopeBindings[name].set({ error }), + ); + } else { + view._scopeBindings[name].set({ value }); + } + } + view.onViewCreated(function () { Object.entries(bindings).forEach(function ([name, binding]) { - view._scopeBindings[name] = new ReactiveVar(); + view._scopeBindings[name] = new ReactiveVar(undefined, _isEqualBinding); if (typeof binding === 'function') { - view.autorun(function () { - view._scopeBindings[name].set(binding()); - }, view.parentView); + view.autorun(() => setBindingValue(name, binding()), view.parentView); } else { - view._scopeBindings[name].set(binding); + setBindingValue(name, binding); } }); }); @@ -149,7 +168,7 @@ Blaze.Each = function (argFunc, contentFunc, elseFunc) { for (var i = from; i <= to; i++) { var view = eachView._domrange.members[i].view; - view._scopeBindings['@index'].set(i); + view._scopeBindings['@index'].set({ value: i }); } }; @@ -240,7 +259,7 @@ Blaze.Each = function (argFunc, contentFunc, elseFunc) { itemView = eachView.initialSubviews[index]; } if (eachView.variableName) { - itemView._scopeBindings[eachView.variableName].set(newItem); + itemView._scopeBindings[eachView.variableName].set({ value: newItem }); } else { itemView.dataVar.set(newItem); } diff --git a/packages/blaze/lookup.js b/packages/blaze/lookup.js index 87d427238..9111cd120 100644 --- a/packages/blaze/lookup.js +++ b/packages/blaze/lookup.js @@ -1,6 +1,37 @@ import has from 'lodash.has'; -Blaze._globalHelpers = {}; +/** @param {(binding: Binding) => boolean} fn */ +function _createBindingsHelper(fn) { + /** @param {string[]} names */ + return (...names) => { + const view = Blaze.currentView; + + // There's either zero arguments (i.e., check all bindings) or an additional + // "hash" argument that we have to ignore. + names = names.length === 0 + // TODO: Should we walk up the bindings here? + ? Object.keys(view._scopeBindings) + : names.slice(0, -1); + + return names.some(name => { + const binding = _lexicalBindingLookup(view, name); + if (!binding) { + throw new Error(`Binding for "${name}" was not found.`); + } + + return fn(binding.get()); + }); + }; +} + +Blaze._globalHelpers = { + /** @summary Check whether any of the given bindings (or all if none given) is still pending. */ + '@pending': _createBindingsHelper(binding => binding === undefined), + /** @summary Check whether any of the given bindings (or all if none given) has rejected. */ + '@rejected': _createBindingsHelper(binding => !!binding && 'error' in binding), + /** @summary Check whether any of the given bindings (or all if none given) has resolved. */ + '@resolved': _createBindingsHelper(binding => !!binding && 'value' in binding), +}; // Documented as Template.registerHelper. // This definition also provides back-compat for `UI.registerHelper`. @@ -103,9 +134,8 @@ function _lexicalKeepGoing(currentView) { return undefined; } -Blaze._lexicalBindingLookup = function (view, name) { +function _lexicalBindingLookup(view, name) { var currentView = view; - var blockHelpersStack = []; // walk up the views stopping at a Spacebars.include or Template view that // doesn't have an InOuterTemplateScope view as a parent @@ -113,14 +143,16 @@ Blaze._lexicalBindingLookup = function (view, name) { // skip block helpers views // if we found the binding on the scope, return it if (has(currentView._scopeBindings, name)) { - var bindingReactiveVar = currentView._scopeBindings[name]; - return function () { - return bindingReactiveVar.get(); - }; + return currentView._scopeBindings[name]; } } while (currentView = _lexicalKeepGoing(currentView)); return null; +} + +Blaze._lexicalBindingLookup = function (view, name) { + const binding = _lexicalBindingLookup(view, name); + return binding && (() => binding.get()?.value); }; // templateInstance argument is provided to be available for possible diff --git a/packages/blaze/package.js b/packages/blaze/package.js index fec862739..44693e29b 100644 --- a/packages/blaze/package.js +++ b/packages/blaze/package.js @@ -1,7 +1,7 @@ Package.describe({ name: 'blaze', summary: "Meteor Reactive Templating library", - version: '2.6.2', + version: '2.7.0', git: 'https://github.com/meteor/blaze.git' }); @@ -14,7 +14,7 @@ Npm.depends({ Package.onUse(function (api) { api.use('jquery@1.11.9 || 3.0.0', { weak: true }); // should be a weak dep, by having multiple "DOM backends" - api.use('tracker@1.2.0'); + api.use('tracker@1.3.0'); api.use('check@1.3.1'); api.use('observe-sequence@1.0.16'); api.use('reactive-var@1.0.11'); @@ -62,7 +62,7 @@ Package.onTest(function (api) { api.use('test-helpers@1.2.0'); api.use('jquery@1.11.9 || 3.0.0'); // strong dependency, for testing jQuery backend api.use('reactive-var@1.0.11'); - api.use('tracker@1.1.0'); + api.use('tracker@1.3.0'); api.use('blaze'); api.use('blaze-tools@1.1.3'); // for BlazeTools.toJS diff --git a/packages/blaze/view.js b/packages/blaze/view.js index 185197e62..aa3e95dd4 100644 --- a/packages/blaze/view.js +++ b/packages/blaze/view.js @@ -33,6 +33,14 @@ /// general it's good for functions that create Views to set the name. /// Views associated with templates have names of the form "Template.foo". +/** + * A binding is either `undefined` (pending), `{ error }` (rejected), or + * `{ value }` (resolved). Synchronous values are immediately resolved (i.e., + * `{ value }` is used). The other states are reserved for asynchronous bindings + * (i.e., values wrapped with `Promise`s). + * @typedef {{ error: unknown } | { value: unknown } | undefined} Binding + */ + /** * @class * @summary Constructor for a View, which represents a reactive region of DOM. @@ -81,6 +89,7 @@ Blaze.View = function (name, render) { this._hasGeneratedParent = false; // Bindings accessible to children views (via view.lookup('name')) within the // closest template view. + /** @type {Record>} */ this._scopeBindings = {}; this.renderCount = 0; @@ -531,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 { diff --git a/packages/observe-sequence/.versions b/packages/observe-sequence/.versions index 708081f5c..da95679ae 100644 --- a/packages/observe-sequence/.versions +++ b/packages/observe-sequence/.versions @@ -1,49 +1,49 @@ -allow-deny@1.1.0 -babel-compiler@7.6.1 -babel-runtime@1.5.0 +allow-deny@1.1.1 +babel-compiler@7.10.4 +babel-runtime@1.5.1 base64@1.0.12 binary-heap@1.0.11 boilerplate-generator@1.7.1 -callback-hook@1.3.0 -check@1.3.1 -ddp@1.4.0 -ddp-client@2.4.1 +callback-hook@1.5.1 +check@1.3.2 +ddp@1.4.1 +ddp-client@2.6.1 ddp-common@1.4.0 -ddp-server@2.3.3 -diff-sequence@1.1.1 -dynamic-import@0.6.0 -ecmascript@0.15.1 -ecmascript-runtime@0.7.0 -ecmascript-runtime-client@0.11.1 -ecmascript-runtime-server@0.10.1 -ejson@1.1.1 -fetch@0.1.1 -geojson-utils@1.0.10 +ddp-server@2.6.1 +diff-sequence@1.1.2 +dynamic-import@0.7.3 +ecmascript@0.16.7 +ecmascript-runtime@0.8.1 +ecmascript-runtime-client@0.12.1 +ecmascript-runtime-server@0.11.0 +ejson@1.1.3 +fetch@0.1.3 +geojson-utils@1.0.11 id-map@1.1.1 inter-process-messaging@0.1.1 -local-test:observe-sequence@1.0.19 -logging@1.2.0 -meteor@1.9.3 -minimongo@1.6.2 -modern-browsers@0.1.5 -modules@0.16.0 -modules-runtime@0.12.0 -mongo@1.11.1 -mongo-decimal@0.1.2 +local-test:observe-sequence@1.0.21 +logging@1.3.2 +meteor@1.11.2 +minimongo@1.9.3 +modern-browsers@0.1.9 +modules@0.19.0 +modules-runtime@0.13.1 +mongo@1.16.6 +mongo-decimal@0.1.3 mongo-dev-server@1.1.0 mongo-id@1.0.8 -npm-mongo@3.9.0 -observe-sequence@1.0.19 +npm-mongo@4.16.0 +observe-sequence@1.0.21 ordered-dict@1.1.0 -promise@0.11.2 -random@1.2.0 -react-fast-refresh@0.1.1 +promise@0.12.2 +random@1.2.1 +react-fast-refresh@0.2.7 reload@1.3.1 retry@1.1.0 -routepolicy@1.1.0 -socket-stream-client@0.3.3 -tinytest@1.1.0 -tracker@1.2.0 -underscore@1.0.10 -webapp@1.10.1 -webapp-hashing@1.1.0 +routepolicy@1.1.1 +socket-stream-client@0.5.1 +tinytest@1.2.2 +tracker@1.3.2 +underscore@1.0.13 +webapp@1.13.5 +webapp-hashing@1.1.1 diff --git a/packages/observe-sequence/package.js b/packages/observe-sequence/package.js index c9756dec6..bff628d0f 100644 --- a/packages/observe-sequence/package.js +++ b/packages/observe-sequence/package.js @@ -1,10 +1,10 @@ Package.describe({ summary: "Observe changes to various sequence types such as arrays, cursors and objects", - version: "1.0.20" + version: "1.0.21" }); Package.onUse(function (api) { - api.use('tracker@1.2.0'); + api.use('tracker@1.3.0'); api.use('mongo-id@1.0.8'); // for idStringify api.use('diff-sequence@1.1.1'); api.use('random@1.2.0'); diff --git a/packages/spacebars-tests/.versions b/packages/spacebars-tests/.versions index b755ea9f3..b983167d0 100644 --- a/packages/spacebars-tests/.versions +++ b/packages/spacebars-tests/.versions @@ -1,23 +1,23 @@ allow-deny@1.1.1 -babel-compiler@7.10.3 +babel-compiler@7.10.4 babel-runtime@1.5.1 base64@1.0.12 binary-heap@1.0.11 -blaze@2.5.0 +blaze@2.7.0 blaze-tools@1.1.0 boilerplate-generator@1.7.1 caching-compiler@1.2.2 caching-html-compiler@1.2.0 -callback-hook@1.5.0 +callback-hook@1.5.1 check@1.3.2 ddp@1.4.1 ddp-client@2.6.1 ddp-common@1.4.0 -ddp-server@2.6.0 +ddp-server@2.6.1 diff-sequence@1.1.2 -dynamic-import@0.7.2 -ecmascript@0.16.6 -ecmascript-runtime@0.8.0 +dynamic-import@0.7.3 +ecmascript@0.16.7 +ecmascript-runtime@0.8.1 ecmascript-runtime-client@0.12.1 ecmascript-runtime-server@0.11.0 ejson@1.1.3 @@ -25,45 +25,45 @@ es5-shim@4.8.0 fetch@0.1.3 geojson-utils@1.0.11 html-tools@1.1.0 -htmljs@1.1.0 +htmljs@1.1.1 id-map@1.1.1 inter-process-messaging@0.1.1 jquery@1.11.10 -local-test:spacebars-tests@1.3.2 +local-test:spacebars-tests@1.3.3 logging@1.3.2 markdown@1.0.14 -meteor@1.11.1 -minimongo@1.9.2 +meteor@1.11.2 +minimongo@1.9.3 modern-browsers@0.1.9 modules@0.19.0 modules-runtime@0.13.1 -mongo@1.16.5 +mongo@1.16.6 mongo-decimal@0.1.3 mongo-dev-server@1.1.0 mongo-id@1.0.8 -npm-mongo@4.14.0 +npm-mongo@4.16.0 observe-sequence@1.0.16 ordered-dict@1.1.0 promise@0.12.2 random@1.2.1 -react-fast-refresh@0.2.6 +react-fast-refresh@0.2.7 reactive-dict@1.3.1 reactive-var@1.0.12 reload@1.3.1 retry@1.1.0 routepolicy@1.1.1 session@1.2.1 -socket-stream-client@0.5.0 -spacebars@1.2.0 +socket-stream-client@0.5.1 +spacebars@1.4.0 spacebars-compiler@1.2.0 -spacebars-tests@1.3.2 +spacebars-tests@1.3.3 templating@1.4.1 templating-compiler@1.4.1 templating-runtime@1.5.0 templating-tools@1.2.0 test-helpers@1.3.1 tinytest@1.2.2 -tracker@1.3.1 -underscore@1.0.12 -webapp@1.13.4 +tracker@1.3.2 +underscore@1.0.13 +webapp@1.13.5 webapp-hashing@1.1.1 diff --git a/packages/spacebars-tests/async_tests.html b/packages/spacebars-tests/async_tests.html new file mode 100644 index 000000000..f590218df --- /dev/null +++ b/packages/spacebars-tests/async_tests.html @@ -0,0 +1,69 @@ + + + + + + + + + + + + + diff --git a/packages/spacebars-tests/async_tests.js b/packages/spacebars-tests/async_tests.js new file mode 100644 index 000000000..84f2b5519 --- /dev/null +++ b/packages/spacebars-tests/async_tests.js @@ -0,0 +1,82 @@ +function asyncTest(templateName, testName, fn) { + Tinytest.addAsync(`spacebars-tests - async - ${templateName} ${testName}`, test => { + const template = Blaze.Template[`spacebars_async_tests_${templateName}`]; + const templateCopy = new Blaze.Template(template.viewName, template.renderFunction); + return fn(test, templateCopy, () => { + const div = renderToDiv(templateCopy); + return () => canonicalizeHtml(div.innerHTML); + }); + }); +} + +function asyncSuite(templateName, cases) { + for (const [testName, helpers, before, after] of cases) { + asyncTest(templateName, testName, async (test, template, render) => { + template.helpers(helpers); + const readHTML = render(); + test.equal(readHTML(), before); + await new Promise(Tracker.afterFlush); + test.equal(readHTML(), after); + }); + } +} + +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'], +]); + +asyncSuite('direct', [ + ['getter', { x: async () => 'foo' }, '', 'foo'], + ['thenable', { x: { then: resolve => { Promise.resolve().then(() => resolve('foo')) } } }, '', 'foo'], + ['value', { x: Promise.resolve('foo') }, '', 'foo'], +]); + +asyncTest('missing1', 'outer', async (test, template, render) => { + Blaze._throwNextException = true; + test.throws(render, 'Binding for "b" was not found.'); +}); + +asyncTest('missing2', 'inner', async (test, template, render) => { + Blaze._throwNextException = true; + test.throws(render, 'Binding for "b" was not found.'); +}); + +// In the following tests pending=1, rejected=2, resolved=3. +const pending = new Promise(() => {}); +const rejected = Promise.reject(); +const resolved = Promise.resolve(); + +// Ignore unhandled rejection error. +rejected.catch(() => {}); + +asyncSuite('state1', [ + ['pending', { x: pending }, '1 a1', '1 a1'], + ['rejected', { x: rejected }, '1 a1', '2 a2'], + ['resolved', { x: resolved }, '1 a1', '3 a3'], +]); + +asyncSuite('state2flat', [ + ['pending pending', { x: pending, y: pending }, '1 a1 b1 ab1', '1 a1 b1 ab1'], + ['pending rejected', { x: pending, y: rejected }, '1 a1 b1 ab1', '1 2 a1 b2 ab1 ab2'], + ['pending resolved', { x: pending, y: resolved }, '1 a1 b1 ab1', '1 3 a1 b3 ab1 ab3'], + ['rejected pending', { x: rejected, y: pending }, '1 a1 b1 ab1', '1 2 a2 b1 ab1 ab2'], + ['rejected rejected', { x: rejected, y: rejected }, '1 a1 b1 ab1', '2 a2 b2 ab2'], + ['rejected resolved', { x: rejected, y: resolved }, '1 a1 b1 ab1', '2 3 a2 b3 ab2 ab3'], + ['resolved pending', { x: resolved, y: pending }, '1 a1 b1 ab1', '1 3 a3 b1 ab1 ab3'], + ['resolved rejected', { x: resolved, y: rejected }, '1 a1 b1 ab1', '2 3 a3 b2 ab2 ab3'], + ['resolved resolved', { x: resolved, y: resolved }, '1 a1 b1 ab1', '3 a3 b3 ab3'], +]); + +asyncSuite('state2nested', [ + ['pending pending', { x: pending, y: pending }, '1 a1 b1 ab1', '1 a1 b1 ab1'], + ['pending rejected', { x: pending, y: rejected }, '1 a1 b1 ab1', '2 a1 b2 ab1 ab2'], + ['pending resolved', { x: pending, y: resolved }, '1 a1 b1 ab1', '3 a1 b3 ab1 ab3'], + ['rejected pending', { x: rejected, y: pending }, '1 a1 b1 ab1', '1 a2 b1 ab1 ab2'], + ['rejected rejected', { x: rejected, y: rejected }, '1 a1 b1 ab1', '2 a2 b2 ab2'], + ['rejected resolved', { x: rejected, y: resolved }, '1 a1 b1 ab1', '3 a2 b3 ab2 ab3'], + ['resolved pending', { x: resolved, y: pending }, '1 a1 b1 ab1', '1 a3 b1 ab1 ab3'], + ['resolved rejected', { x: resolved, y: rejected }, '1 a1 b1 ab1', '2 a3 b2 ab2 ab3'], + ['resolved resolved', { x: resolved, y: resolved }, '1 a1 b1 ab1', '3 a3 b3 ab3'], +]); diff --git a/packages/spacebars-tests/package.js b/packages/spacebars-tests/package.js index 856bd2b26..dd6e17906 100644 --- a/packages/spacebars-tests/package.js +++ b/packages/spacebars-tests/package.js @@ -1,7 +1,7 @@ Package.describe({ name: 'spacebars-tests', summary: "Additional tests for Spacebars", - version: '1.3.2', + version: '1.3.3', git: 'https://github.com/meteor/blaze.git' }); @@ -16,19 +16,21 @@ Package.onTest(function (api) { 'reactive-var@1.0.11', 'markdown@1.0.14 || 2.0.0', 'minimongo@1.6.2', - 'tracker@1.2.0', + 'tracker@1.3.0', 'mongo@1.11.1', 'random@1.2.0', 'session@1.2.0' ]); api.use([ - 'spacebars@1.2.0', - 'blaze@2.5.0' + 'spacebars@1.4.0', + 'blaze@2.7.0' ]); api.use('templating@1.4.1', 'client'); api.addFiles([ + 'async_tests.html', + 'async_tests.js', 'template_tests.html', 'template_tests.js', 'templating_tests.html', diff --git a/packages/spacebars/.versions b/packages/spacebars/.versions index 879ae5321..3c5e66287 100644 --- a/packages/spacebars/.versions +++ b/packages/spacebars/.versions @@ -1,53 +1,53 @@ -allow-deny@1.1.0 -babel-compiler@7.6.1 -babel-runtime@1.5.0 +allow-deny@1.1.1 +babel-compiler@7.10.4 +babel-runtime@1.5.1 base64@1.0.12 binary-heap@1.0.11 -blaze@2.5.0 +blaze@2.7.0 boilerplate-generator@1.7.1 -callback-hook@1.3.0 -check@1.3.1 -ddp@1.4.0 -ddp-client@2.4.0 +callback-hook@1.5.1 +check@1.3.2 +ddp@1.4.1 +ddp-client@2.6.1 ddp-common@1.4.0 -ddp-server@2.3.2 -diff-sequence@1.1.1 -dynamic-import@0.6.0 -ecmascript@0.15.1 -ecmascript-runtime@0.7.0 -ecmascript-runtime-client@0.11.0 -ecmascript-runtime-server@0.10.0 -ejson@1.1.1 -fetch@0.1.1 -geojson-utils@1.0.10 -htmljs@1.1.0 -id-map@1.1.0 +ddp-server@2.6.1 +diff-sequence@1.1.2 +dynamic-import@0.7.3 +ecmascript@0.16.7 +ecmascript-runtime@0.8.1 +ecmascript-runtime-client@0.12.1 +ecmascript-runtime-server@0.11.0 +ejson@1.1.3 +fetch@0.1.3 +geojson-utils@1.0.11 +htmljs@1.1.1 +id-map@1.1.1 inter-process-messaging@0.1.1 -local-test:spacebars@1.2.0 -logging@1.2.0 -meteor@1.9.3 -minimongo@1.6.2 -modern-browsers@0.1.5 -modules@0.16.0 -modules-runtime@0.12.0 -mongo@1.11.0 -mongo-decimal@0.1.2 +local-test:spacebars@1.4.0 +logging@1.3.2 +meteor@1.11.2 +minimongo@1.9.3 +modern-browsers@0.1.9 +modules@0.19.0 +modules-runtime@0.13.1 +mongo@1.16.6 +mongo-decimal@0.1.3 mongo-dev-server@1.1.0 -mongo-id@1.0.7 -npm-mongo@3.9.0 +mongo-id@1.0.8 +npm-mongo@4.16.0 observe-sequence@1.0.16 ordered-dict@1.1.0 -promise@0.11.2 -random@1.2.0 -react-fast-refresh@0.1.0 -reactive-var@1.0.11 +promise@0.12.2 +random@1.2.1 +react-fast-refresh@0.2.7 +reactive-var@1.0.12 reload@1.3.1 retry@1.1.0 -routepolicy@1.1.0 -socket-stream-client@0.3.1 -spacebars@1.2.0 -tinytest@1.1.0 -tracker@1.2.0 -underscore@1.0.10 -webapp@1.10.1 -webapp-hashing@1.1.0 +routepolicy@1.1.1 +socket-stream-client@0.5.1 +spacebars@1.4.0 +tinytest@1.2.2 +tracker@1.3.2 +underscore@1.0.13 +webapp@1.13.5 +webapp-hashing@1.1.1 diff --git a/packages/spacebars/package.js b/packages/spacebars/package.js index a9ba4b0e1..02b7c9066 100644 --- a/packages/spacebars/package.js +++ b/packages/spacebars/package.js @@ -1,7 +1,7 @@ Package.describe({ name: 'spacebars', summary: "Handlebars-like template language for Meteor", - version: '1.3.0', + version: '1.4.0', git: 'https://github.com/meteor/blaze.git' }); @@ -15,12 +15,12 @@ Package.describe({ Package.onUse(function (api) { api.use('observe-sequence@1.0.16'); - api.use('tracker@1.2.0'); + api.use('tracker@1.3.0'); api.export('Spacebars'); api.use('htmljs@1.1.1'); - api.use('blaze@2.5.0'); + api.use('blaze@2.7.0'); api.addFiles([ 'spacebars-runtime.js' diff --git a/packages/spacebars/spacebars-runtime.js b/packages/spacebars/spacebars-runtime.js index f02849c8d..5cbd49a61 100644 --- a/packages/spacebars/spacebars-runtime.js +++ b/packages/spacebars/spacebars-runtime.js @@ -117,9 +117,34 @@ Spacebars.makeRaw = function (value) { return HTML.Raw(value); }; +/*** + * @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} promise + * @param {(x: T) => U} fn + * @returns {Promise} + */ +function _thenWithContext(promise, fn) { + const computation = Tracker.currentComputation; + const view = Blaze.currentView; + return promise.then(value => + Blaze._withCurrentView(view, () => + Tracker.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 @@ -128,9 +153,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); @@ -170,6 +201,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`. @@ -200,6 +235,9 @@ Spacebars.dot = function (value, id1/*, id2, ...*/) { if (! value) return value; // falsy, don't index, pass through + if (value && typeof value.then === 'function') + return _thenWithContext(value, value => Spacebars.dot(value, id1)); + var result = value[id1]; if (typeof result !== 'function') return result; diff --git a/packages/spacebars/spacebars_tests.js b/packages/spacebars/spacebars_tests.js index 16207a260..9444ca2e5 100644 --- a/packages/spacebars/spacebars_tests.js +++ b/packages/spacebars/spacebars_tests.js @@ -57,3 +57,30 @@ 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, { then: resolve => 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, { then: (_, reject) => reject(2) }).catch(x => x), 2); + 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: { then: resolve => resolve(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); +}); diff --git a/packages/templating-runtime/.versions b/packages/templating-runtime/.versions index d8cf6dfe2..8a5ab678b 100644 --- a/packages/templating-runtime/.versions +++ b/packages/templating-runtime/.versions @@ -1,5 +1,5 @@ allow-deny@1.1.1 -babel-compiler@7.9.0 +babel-compiler@7.10.4 babel-runtime@1.5.1 base64@1.0.12 binary-heap@1.0.11 @@ -8,55 +8,55 @@ blaze-tools@1.1.0 boilerplate-generator@1.7.1 caching-compiler@1.2.2 caching-html-compiler@1.2.0 -callback-hook@1.4.0 -check@1.3.1 -ddp@1.4.0 -ddp-client@2.5.0 +callback-hook@1.5.1 +check@1.3.2 +ddp@1.4.1 +ddp-client@2.6.1 ddp-common@1.4.0 -ddp-server@2.5.0 -diff-sequence@1.1.1 -dynamic-import@0.7.2 -ecmascript@0.16.2 -ecmascript-runtime@0.8.0 +ddp-server@2.6.1 +diff-sequence@1.1.2 +dynamic-import@0.7.3 +ecmascript@0.16.7 +ecmascript-runtime@0.8.1 ecmascript-runtime-client@0.12.1 ecmascript-runtime-server@0.11.0 -ejson@1.1.2 -fetch@0.1.1 -geojson-utils@1.0.10 +ejson@1.1.3 +fetch@0.1.3 +geojson-utils@1.0.11 html-tools@1.1.0 htmljs@1.1.1 id-map@1.1.1 inter-process-messaging@0.1.1 -local-test:templating-runtime@1.6.1 -logging@1.3.1 -meteor@1.10.0 -minimongo@1.8.0 -modern-browsers@0.1.8 -modules@0.18.0 -modules-runtime@0.13.0 -mongo@1.15.0 +local-test:templating-runtime@1.6.3 +logging@1.3.2 +meteor@1.11.2 +minimongo@1.9.3 +modern-browsers@0.1.9 +modules@0.19.0 +modules-runtime@0.13.1 +mongo@1.16.6 mongo-decimal@0.1.3 mongo-dev-server@1.1.0 mongo-id@1.0.8 -npm-mongo@4.3.1 +npm-mongo@4.16.0 observe-sequence@1.0.16 ordered-dict@1.1.0 -promise@0.12.0 -random@1.2.0 -react-fast-refresh@0.2.3 -reactive-var@1.0.11 +promise@0.12.2 +random@1.2.1 +react-fast-refresh@0.2.7 +reactive-var@1.0.12 reload@1.3.1 retry@1.1.0 routepolicy@1.1.1 -socket-stream-client@0.5.0 +socket-stream-client@0.5.1 spacebars@1.3.0 spacebars-compiler@1.2.0 templating-compiler@1.4.1 -templating-runtime@1.6.1 +templating-runtime@1.6.3 templating-tools@1.2.0 -test-helpers@1.3.0 -tinytest@1.2.1 -tracker@1.2.0 -underscore@1.0.10 -webapp@1.13.1 -webapp-hashing@1.1.0 +test-helpers@1.3.1 +tinytest@1.2.2 +tracker@1.3.2 +underscore@1.0.13 +webapp@1.13.5 +webapp-hashing@1.1.1 diff --git a/packages/templating-runtime/package.js b/packages/templating-runtime/package.js index 70197594b..5dde3edb1 100644 --- a/packages/templating-runtime/package.js +++ b/packages/templating-runtime/package.js @@ -1,7 +1,7 @@ Package.describe({ name: 'templating-runtime', summary: "Runtime for compiled .html files", - version: '1.6.1', + version: '1.6.3', git: 'https://github.com/meteor/blaze.git', documentation: null }); @@ -49,7 +49,7 @@ Package.onTest(function (api) { 'tinytest@1.1.0', 'test-helpers@1.2.0', 'reactive-var@1.0.11', - 'tracker@1.2.0' + 'tracker@1.3.0' ]); api.use([ diff --git a/site/source/api/spacebars.md b/site/source/api/spacebars.md index c17020a24..e73455884 100644 --- a/site/source/api/spacebars.md +++ b/site/source/api/spacebars.md @@ -101,6 +101,11 @@ trying to index into a non-object or an undefined value. In addition, Spacebars will call functions for you, so `{% raw %}{{foo.bar}}{% endraw %}` may be taken to mean `foo().bar`, `foo.bar()`, or `foo().bar()` as appropriate. +Similarly, if the accessed object is wrapped in a `Promise`, Spacebars will +defer the path evaluation in a `Promise` as well. That is, +`{% raw %}{{foo.bar}}{% endraw %}` will evaluate to `foo().then(x => x.bar)`. +Both pending and rejected states will result in `undefined`. + ## Helper Arguments An argument to a helper can be any path or identifier, or a string, boolean, or @@ -122,6 +127,11 @@ frob(a, b, c, Spacebars.kw({verily: true})) The helper's implementation can access the current data context as `this`. +If any of the arguments is a `Promise`, Spacebars will defer the call as long as +all of the arguments will resolve. That is, `{% raw %}{{foo x y z}}{% endraw %}` +will evaluate to `Promise.all([x, y, z]).then(args => foo(...args))`. Both +pending and rejected states will result in `undefined`. + ## Inclusion and Block Arguments Inclusion tags (`{% raw %}{{> foo}}{% endraw %}`) and block tags (`{% raw %}{{#foo}}{% endraw %}`) take a single @@ -457,6 +467,86 @@ Variables introduced this way take precedence over names of templates, global helpers, fields of the current data context and previously introduced variables with the same name. +Additionally, `#let` is capable of unwrapping `Promise` objects. That is, if +any of the bindings is to one, the bound value won't be a `Promise`, but the +resolved value instead. Both pending and rejected states will result in +`undefined`. + +### Async states + +> This functionality is considered experimental and a subject to change. For +> details please refer to [#412](https://github.com/meteor/blaze/pull/412). + +There are three global helpers used to query the state of the bound `Promise`s: +* `@pending`, which checks whether any of the given bindings is still pending. +* `@rejected`, which checks whether any of the given bindings has rejected. +* `@resolved`, which checks whether any of the given bindings has resolved. + +```html +{{#let name=getNameAsynchronously}} + {{#if @pending 'name'}} + We are fetching your name... + {{/if}} + {{#if @rejected 'name'}} + Sorry, an error occured! + {{/if}} + {{#if @resolved 'name'}} + Hi, {{name}}! + {{/if}} +{{/let}} +``` + +All of them accept a list of names to check. Passing no arguments is the same as +passing all bindings from the inner-most `#let`. + +```html +{{#let name=getNameAsynchronously}} + {{#let color=getColorAsynchronously}} + {{#if @pending}} + We are fetching your color... + {{/if}} + {{#if @rejected 'name'}} + Sorry, an error occurred while fetching your name! + {{/if}} + {{#if @resolved 'color' 'name'}} + {{name}} gets a {{color}} card! + {{/if}} + {{/let}} +{{/let}} +``` + +### Async synchronization + +> This functionality is considered experimental and a subject to change. For +> details please refer to [#412](https://github.com/meteor/blaze/pull/412). + +The bindings are **not** synchronized. That means, bindings store that _latest +resolved value_, not _value of the latest `Promise`_. If the resolution time +varies (e.g., involves network), it may result in desynchronized UI. In the +below example, the rendered text is **not** guaranteed to be the result of the +latest `getName` execution. + +```html + +``` + +```js +Template.example.helpers({ + async getName() { + const userId = Meteor.userId(); // Reactive data source. + const profile = await fetch(/* ... */); // Async operation. + return profile.name; + }, +}); +``` + +If a well-defined order of resolutions is required, consider using an external +synchronization mechanism, e.g., a queue of pending async operations. + ## Custom Block Helpers To define your own block helper, simply declare a template, and then invoke it diff --git a/test-app/.meteor/packages b/test-app/.meteor/packages index 668d61eaa..ac86d42c6 100644 --- a/test-app/.meteor/packages +++ b/test-app/.meteor/packages @@ -6,14 +6,14 @@ meteor-base@1.5.1 # Packages every Meteor app needs to have mobile-experience@1.1.0 # Packages for a great mobile UX -mongo@1.14.6 # The database Meteor supports right now +mongo@1.16.6 # The database Meteor supports right now static-html@1.3.2 # Define static page content in .html files -reactive-var@1.0.11 # Reactive variable for tracker -tracker@1.2.0 # Meteor's client-side reactive programming library +reactive-var@1.0.12 # Reactive variable for tracker +tracker@1.3.2 # Meteor's client-side reactive programming library -standard-minifier-css@1.8.1 # CSS minifier run for production mode -standard-minifier-js@2.8.0 # JS minifier run for production mode +standard-minifier-css@1.9.2 # CSS minifier run for production mode +standard-minifier-js@2.8.1 # JS minifier run for production mode es5-shim@4.8.0 # ECMAScript 5 compatibility for older browsers -ecmascript@0.16.2 # Enable ECMAScript2015+ syntax in app code -typescript@4.5.4 # Enable TypeScript syntax in .ts and .tsx modules +ecmascript@0.16.7 # Enable ECMAScript2015+ syntax in app code +typescript@4.9.4 # Enable TypeScript syntax in .ts and .tsx modules shell-server@0.5.0 # Server-side component of the `meteor shell` command diff --git a/test-app/.meteor/release b/test-app/.meteor/release index 8e3f1708a..e8cfc7ec4 100644 --- a/test-app/.meteor/release +++ b/test-app/.meteor/release @@ -1 +1 @@ -METEOR@2.7.1 +METEOR@2.12 diff --git a/test-app/.meteor/versions b/test-app/.meteor/versions index 0876ac00b..8b5eb38ce 100644 --- a/test-app/.meteor/versions +++ b/test-app/.meteor/versions @@ -1,68 +1,68 @@ allow-deny@1.1.1 autoupdate@1.8.0 -babel-compiler@7.9.0 -babel-runtime@1.5.0 +babel-compiler@7.10.4 +babel-runtime@1.5.1 base64@1.0.12 binary-heap@1.0.11 -blaze-tools@1.1.2 +blaze-tools@1.1.3 boilerplate-generator@1.7.1 caching-compiler@1.2.2 caching-html-compiler@1.2.1 -callback-hook@1.4.0 -check@1.3.1 -ddp@1.4.0 -ddp-client@2.5.0 +callback-hook@1.5.1 +check@1.3.2 +ddp@1.4.1 +ddp-client@2.6.1 ddp-common@1.4.0 -ddp-server@2.5.0 -diff-sequence@1.1.1 -dynamic-import@0.7.2 -ecmascript@0.16.2 -ecmascript-runtime@0.8.0 +ddp-server@2.6.1 +diff-sequence@1.1.2 +dynamic-import@0.7.3 +ecmascript@0.16.7 +ecmascript-runtime@0.8.1 ecmascript-runtime-client@0.12.1 ecmascript-runtime-server@0.11.0 -ejson@1.1.2 +ejson@1.1.3 es5-shim@4.8.0 -fetch@0.1.1 -geojson-utils@1.0.10 +fetch@0.1.3 +geojson-utils@1.0.11 hot-code-push@1.0.4 -html-tools@1.1.2 +html-tools@1.1.3 htmljs@1.1.1 id-map@1.1.1 inter-process-messaging@0.1.1 launch-screen@1.3.0 -logging@1.3.1 -meteor@1.10.0 +logging@1.3.2 +meteor@1.11.2 meteor-base@1.5.1 -minifier-css@1.6.0 -minifier-js@2.7.4 -minimongo@1.8.0 +minifier-css@1.6.4 +minifier-js@2.7.5 +minimongo@1.9.3 mobile-experience@1.1.0 mobile-status-bar@1.1.0 -modern-browsers@0.1.7 -modules@0.18.0 -modules-runtime@0.13.0 -mongo@1.14.6 -mongo-decimal@0.1.2 +modern-browsers@0.1.9 +modules@0.19.0 +modules-runtime@0.13.1 +mongo@1.16.6 +mongo-decimal@0.1.3 mongo-dev-server@1.1.0 mongo-id@1.0.8 -npm-mongo@4.3.1 +npm-mongo@4.16.0 ordered-dict@1.1.0 -promise@0.12.0 -random@1.2.0 -react-fast-refresh@0.2.3 -reactive-var@1.0.11 +promise@0.12.2 +random@1.2.1 +react-fast-refresh@0.2.7 +reactive-var@1.0.12 reload@1.3.1 retry@1.1.0 routepolicy@1.1.1 shell-server@0.5.0 -socket-stream-client@0.4.0 -spacebars-compiler@1.3.0 -standard-minifier-css@1.8.1 -standard-minifier-js@2.8.0 +socket-stream-client@0.5.1 +spacebars-compiler@1.3.1 +standard-minifier-css@1.9.2 +standard-minifier-js@2.8.1 static-html@1.3.2 -templating-tools@1.2.1 -tracker@1.2.0 -typescript@4.5.4 -underscore@1.0.10 -webapp@1.13.1 -webapp-hashing@1.1.0 +templating-tools@1.2.2 +tracker@1.3.2 +typescript@4.9.4 +underscore@1.0.13 +webapp@1.13.5 +webapp-hashing@1.1.1