Skip to content

Commit

Permalink
Merge pull request #424 from meteor/async-if-each
Browse files Browse the repository at this point in the history
Added support for `Promise`s in `#if` and `#each`.
  • Loading branch information
Grubba27 authored Nov 21, 2023
2 parents 0d6bb29 + f5b1152 commit 9e1def0
Show file tree
Hide file tree
Showing 8 changed files with 323 additions and 85 deletions.
142 changes: 99 additions & 43 deletions packages/blaze/builtins.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,32 +48,66 @@ function _isEqualBinding(x, y) {
}
}

/**
* @template T
* @param {T} x
* @returns {T}
*/
function _identity(x) {
return x;
}

/**
* Attaches a single binding to the instantiated view.
* @template T, U
* @param {ReactiveVar<U>} reactiveVar Target view.
* @param {Promise<T> | T} value Bound value.
* @param {(value: T) => U} [mapper] Maps the computed value before store.
*/
function _setBindingValue(reactiveVar, value, mapper = _identity) {
if (value && typeof value.then === 'function') {
value.then(
value => reactiveVar.set({ value: mapper(value) }),
error => reactiveVar.set({ error }),
);
} else {
reactiveVar.set({ value: mapper(value) });
}
}

/**
* @template T, U
* @param {Blaze.View} view Target view.
* @param {Promise<T> | T | (() => Promise<T> | T)} binding Binding value or its getter.
* @param {string} [displayName] Autorun's display name.
* @param {(value: T) => U} [mapper] Maps the computed value before store.
* @returns {ReactiveVar<U>}
*/
function _createBinding(view, binding, displayName, mapper) {
const reactiveVar = new ReactiveVar(undefined, _isEqualBinding);
if (typeof binding === 'function') {
view.autorun(
() => _setBindingValue(reactiveVar, binding(), mapper),
view.parentView,
displayName,
);
} else {
_setBindingValue(reactiveVar, binding, mapper);
}

return reactiveVar;
}

/**
* 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 {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(undefined, _isEqualBinding);
if (typeof binding === 'function') {
view.autorun(() => setBindingValue(name, binding()), view.parentView);
} else {
setBindingValue(name, binding);
}
view._scopeBindings[name] = _createBinding(view, binding);
});
});
};
Expand Down Expand Up @@ -101,18 +135,26 @@ Blaze.Let = function (bindings, contentFunc) {
* `elseFunc` is supplied, no content is shown in the "else" case.
*/
Blaze.If = function (conditionFunc, contentFunc, elseFunc, _not) {
var conditionVar = new ReactiveVar;
const view = Blaze.View(_not ? 'unless' : 'if', function () {
// Render only if the binding has a value, i.e., it's either synchronous or
// has resolved. Rejected `Promise`s are NOT rendered.
const condition = view.__conditionVar.get();
if (condition && 'value' in condition) {
return condition.value ? contentFunc() : (elseFunc ? elseFunc() : null);
}

var view = Blaze.View(_not ? 'unless' : 'if', function () {
return conditionVar.get() ? contentFunc() :
(elseFunc ? elseFunc() : null);
return null;
});
view.__conditionVar = conditionVar;
view.onViewCreated(function () {
this.autorun(function () {
var cond = Blaze._calculateCondition(conditionFunc());
conditionVar.set(_not ? (! cond) : cond);
}, this.parentView, 'condition');

view.__conditionVar = null;
view.onViewCreated(() => {
view.__conditionVar = _createBinding(
view,
conditionFunc,
'condition',
// Store only the actual condition.
value => !Blaze._calculateCondition(value) !== !_not,
);
});

return view;
Expand Down Expand Up @@ -167,7 +209,7 @@ Blaze.Each = function (argFunc, contentFunc, elseFunc) {
eachView.stopHandle = null;
eachView.contentFunc = contentFunc;
eachView.elseFunc = elseFunc;
eachView.argVar = new ReactiveVar;
eachView.argVar = undefined;
eachView.variableName = null;

// update the @index value in the scope of all subviews in the range
Expand All @@ -183,23 +225,24 @@ Blaze.Each = function (argFunc, contentFunc, elseFunc) {
};

eachView.onViewCreated(function () {
// We evaluate argFunc in an autorun to make sure
// Blaze.currentView is always set when it runs (rather than
// passing argFunc straight to ObserveSequence).
eachView.autorun(function () {
// argFunc can return either a sequence as is or a wrapper object with a
// _sequence and _variable fields set.
var arg = argFunc();
if (isObject(arg) && has(arg, '_sequence')) {
eachView.variableName = arg._variable || null;
arg = arg._sequence;
}

eachView.argVar.set(arg);
}, eachView.parentView, 'collection');
// We evaluate `argFunc` in `Tracker.autorun` to ensure `Blaze.currentView`
// is always set when it runs.
eachView.argVar = _createBinding(
eachView,
// Unwrap a sequence reactively (`{{#each x in xs}}`).
() => {
let maybeSequence = argFunc();
if (isObject(maybeSequence) && has(maybeSequence, '_sequence')) {
eachView.variableName = maybeSequence._variable || null;
maybeSequence = maybeSequence._sequence;
}
return maybeSequence;
},
'collection',
);

eachView.stopHandle = ObserveSequence.observe(function () {
return eachView.argVar.get();
return eachView.argVar.get()?.value;
}, {
addedAt: function (id, item, index) {
Tracker.nonreactive(function () {
Expand Down Expand Up @@ -309,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
4 changes: 4 additions & 0 deletions packages/htmljs/visitors.js
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,10 @@ TransformingVisitor.def({
}

if (attrs && isConstructedObject(attrs)) {
if (typeof attrs.then === 'function') {
throw new Error('Asynchronous 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
34 changes: 34 additions & 0 deletions packages/spacebars-tests/async_tests.html
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,37 @@
{{/let}}
{{/let}}
</template>

<template name="spacebars_async_tests_attribute">
<img class={{x}}>
</template>

<template name="spacebars_async_tests_attributes">
<img {{x}}>
</template>

<template name="spacebars_async_tests_value_direct">
{{x}}
</template>

<template name="spacebars_async_tests_value_raw">
{{{x}}}
</template>

<template name="spacebars_async_tests_if">
{{#if x}}1{{/if}}
{{#if x}}1{{else}}2{{/if}}
</template>

<template name="spacebars_async_tests_unless">
{{#unless x}}1{{/unless}}
{{#unless x}}1{{else}}2{{/unless}}
</template>

<template name="spacebars_async_tests_each_old">
{{#each x}}{{.}}{{else}}0{{/each}}
</template>

<template name="spacebars_async_tests_each_new">
{{#each y in x}}{{y}}{{else}}0{{/each}}
</template>
Loading

0 comments on commit 9e1def0

Please sign in to comment.