Closed alexlafroscia closed 8 years ago
Can you please elaborate on the use case a bit more?
Basically, I've noticed there are times that you need to render something asynchronously, but that thing turns out to not be an array or object, but a string or number. For example, if you want to get the count of some thing that's gathered asynchronously, you can't just return a promise. This is very similar to some code that I'm playing with in our app right now:
// desired
import Ember from 'ember';
const { Component, RSVP, computed } = Ember;
export default Component.extend({
numForms: computed(function() {
return this.store.find('forms').then(function(forms) {
return RSVP.all(forms.invoke('getCount'));
}).then(function(counts) {
return counts.reduce((prev, current) => (prev + current), 0);
});
})
});
I would love to use the template like this:
{{numForms}}
But unfortunately, returning a promise from the property doesn't render the eventual resolution. Instead, we have to do something like this:
// required
import Ember from 'ember';
const { Component, RSVP, on } = Ember;
export default Component.extend({
getNumForms: on('init', function() {
return this.store.find('forms').then(function(forms) {
return RSVP.all(forms.invoke('getCount'));
}).then((counts) => {
const total = counts.reduce((prev, current) => (prev + current), 0);
this.set('numForms', total);
});
}),
numForms: 0
});
which does achieve the same effect, but I generally think that there should be a way to use a single computed property to make this work. Do you have any solutions? We've had great success using PromiseObject
and PromiseArray
for similar solutions for Objects and Arrays, but there is not analogue for "primitives" yet.
I haven't seen a better solution to that pre Glimmer but with thew new helpers API you can create a helper that will automatically update the template when the promise resolves.
Here is a demo of how it works
Here is the template
<h2 id="title">Promises in Glimmer</h2>
<p><button {{action 'new'}}>New Date</button></p>
<p><strong>Date</strong>: {{resolve date}}</p>
<p><strong>Formatted</strong>: {{format-date (resolve date) format="L"}}</p>
And here is the helper
import Ember from "ember";
export default Ember.Helper.extend({
compute(params) {
const [value] = params;
const isPromise = value && value.then;
if (!isPromise) {
return value;
}
const isPending = value._state === 0 || value._state === undefined;
if (isPending) {
value.then(()=>{
this.recompute();
}).catch(()=>{
this.recompute();
});
return 'Loading...';
}
const isFulfilled = value._state === 1;
if (isFulfilled) {
return value._result;
}
return `Error: ${value._result}`;
}
});
Here is the repo: https://github.com/embersherpa/glimmer-promises
Thanks! That's what I thought of too, but unfortunately we're stuck on Ember 1.12 for now. I'm currently playing with a "old style" helper that tries to do the same thing, but without the ability to do
value.then(()=>{
this.recompute();
});
it doesn't look like it's possible.
I'm curious: why is the conditional necessary here?
const isFulfilled = value._state === 1;
if (isFulfilled) {
return value._result;
}
return value._result;
Oh, and I was able to create a component that has essentially the same behavior:
import Ember from 'ember';
const { computed } = Ember;
export default Ember.Component.extend({
tagName: 'span',
/**
* The promise to resolve
*
* @property {Promise} promise
*/
promise: null,
/**
* The value to display to the template
*
* @property {Any} value
*/
value: computed('promise', 'initialValue', function() {
const promise = this.get('promise');
const initial = this.get('initialValue');
if (!promise || !promise.then) {
return promise;
} else {
promise.then(() => this.rerender());
}
const isPending = promise._state === 0 || promise._state === undefined;
if (isPending) {
return initial;
} else if (promise._state === 1) {
return promise._result;
}
}),
/**
* The default value to display before the promise reolves
*
* @property {Any} initialValue
*/
initialValue: ''
});
where the template is just {{value}}
.
You use it like
{{resolve-promise promise=somePromise initialValue='Loading...'}}
It's not quite as elegant but for now it's pretty close to the helper behavior.
I'm curious: why is the conditional necessary here?
@alexlafroscia The conditional was intended for error reporting. I updated the code example to show error handling.
Nice job on the component.
The biggest difference between the helper and the component is that you can use the helper as a subexpression to evaluate value before passing it to another helper. {{format-date (resolve-promise date) format="L"}}
.
I've solved this problem in the Heroku dashboard with a decorator:
/*
* A decorator which returns a lazy loader computed property.
* Executes the given loaderFn when it's first needed and replaces the property
* with the result. For convenience, isLoading${propertyName} is set to true
* until the promise completes. The loader function will only be called once,
* subsequent calls return null until the property is resolved.
*
* e.g.
* lazyLoad(function() {
* return App.all();
* })
*
* @function lazyProperty - A computed property which lazily loads a value when it is first needed
* @callback loaderFn - The callback is evaluated and the property is replaced with the result once it's resolved
* @returns {Ember.computed} - A computed property to add to an Ember.Object
*/
function lazyProperty(loaderFn) {
return Ember.computed(function(propertyName) {
const loadingProperty = `isLoading${propertyName.capitalize()}`;
if (this.get(loadingProperty)) { return null; }
this.set(loadingProperty, true);
Ember.RSVP.resolve(loaderFn.call(this)).then((value) => {
this.set(propertyName, value);
}).finally(() => this.set(loadingProperty, false));
});
}
export default lazyProperty;
and then in your component:
import Ember from 'ember';
import lazyProperty from './wherever';
const { Component, RSVP, computed } = Ember;
export default Component.extend({
numForms: lazyProperty(function() {
return this.store.find('forms').then(function(forms) {
return RSVP.all(forms.invoke('getCount'));
}).then(function(counts) {
return counts.reduce((prev, current) => (prev + current), 0);
});
})
});
Seems like pretty much what you're after and as a bonus lets you account for loading states in the UI.
@appleton Thank you for sharing your solution.
I like that it provides a built in loading state, but I'm conflicted by the fact that it's a computed property that doesn't behave like a computed property because it can't have dependent properties and it doesn't evaluate to anything. It also doesn't give you the benefit of working with a promise either because you can't get the promise and bind to success and error states.
This macro is great when you need to receive the value once, kind of like an improved on('init')
callback.
Thank you for sharing, I hope you don't find my comments discouraging in commenting in the future :smile:
@appleton that's really similar to the current version of the component that I came up with; instead of using a decorator, value
just calls this.set('value', resultOfPromise)
since calling this.rerender()
didn't end up working for me. The decorator idea is really neat though, I like that you can then apply it to components wherever you need it.
@taras good point about it not including the re-computation behaviour. In my specific use cases I've not wanted this - usually I'm lazy loading a model based on user interaction (e.g. click to show some list of secondary information).
@alexlafroscia yeah, I wrote it after realising that I'd ad-hoc implemented it about three times in different places :)
I know that rendering an Array or Object to a template asynchronously can be achieved with a
PromiseArray
orPromiseObject
, but what's the right way to approach a situation where the thing you want to resolve is a String or Number?