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

Added support for Promises in #if and #each. #424

Merged
merged 9 commits into from
Nov 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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