aurelia: [RFC]: async/pending/fulfilled/rejected template controllers

šŸ’¬ RFC

This RFC proposes a way to work with promises directly in the markup in a declarative way.

šŸ”¦ Context

It is a common use case that before showing a view, data for that view is asynchronously fetched. While that promise can be returned from the lifecycle hooks, and Aurelia will wait for that promise, it necessarily delays (partial) rendering of the view. Sometimes this also means the delay of visual cues to the end-user. In some cases, such delay might be justified where the view is made solely to present the data being fetched.

In other cases, it might not; and as developer I want to display the subset of data that has already been fetched while some parts of the view wait for some specific promises, still not settled. I am of this opinion that this pattern offers better UX.

To this end, the idea is to have a set of strongly coupled template controllers named, async, pending, fulfilled, and rejected. ā€˜Strongly coupled’ behause:

  • pending, fulfilled, or rejected should not work in isolation, and
  • async without at least one of those might not make much sense. That is possibly an edge case especially in context of working with promises in view.

Following are some examples to describe the idea.

šŸ’» Examples

Example#1: The basic use-case

<template async.bind="answerPromise">
  <span pending> Still computing... šŸ’» </span>
  <span fulfilled> 42 šŸŽ‰ </span>
  <span rejected> What was question? 😔 </span>
</template>

Although it is self explanatory, for completeness here goes the short explanation. The view of the pending template controller will be shown while the promise is running. Once it is settled, then either the fulfilled or the rejected view will be shown depending on whether the promise was resolved or rejected, respectively.

Example#2: Accessing the resolved data or rejection reason

<template async.bind="answerPromise">
  <span pending> Still computing... šŸ’» </span>
  <span fulfilled.from-view="resolvedData"> ${resolvedData} šŸŽ‰ </span>
  <span rejected.from-view="reason"> ${reason.message} 😔 </span>
</template>

Note that the usage of from-view is intentional as binding in the other direction is not very useful IMO. Consequently, the binding mode for async should always be to-view.

Example#3: Optional pending/fulfilled/rejected

These three template controllers should be optional. This means that following examples should work.

<template async.bind="promise1">
  <span pending> The promise is still running, and I will vanish once it settles. Who knows how it is settled! šŸ¤” </span>
</template>

<template async.bind="promise2">
  <span fulfilled> Nice! The promise is now resolved. What do you mean that you didn't know about that promise? 😮 </span>
</template>

<template async.bind="promise3">
  <span rejected> If you are seeing this, then a very bad thing happened!! 😱 </span>
</template>

<template async.bind="promise4">
  <span> It will always be shown. </span>
</template>

Example#4: Forbid usage of pending/fulfilled/rejected in isolation

The following are some examples of incorrect usage, and Aurelia should throw errors in those cases.

<span pending> Does not work! </span>

<span fulfilled> Does not work! </span>

<span rejected> Does not work! </span>

Example#5: Handling multiple promises with different modes

To work with multiple promises with different settlement modes, an optional @bindable resolve can be supported, where resolve takes one of the following values: all, allSettled, any, and race.

<template async="value.bind: [promise1, promise2]; resolve: all">
  <template fulfilled.from-view="dataArray"></template>
</template>

Example#6: Nested async.binds.

Purely from academical standpoint this example gives a way to chain promises. Might not be a real-life use case though.

<template async.bind="fetchData">
  <template fulfilled.from-view="data" async.bind="data.json()">
    <template fulfilled.from-view="json"> ${json.foo} </template>
  </template> 
</template>

Open questions

  1. Most important question first. Do the names look right? Should the async be renamed to promise? Or any of the rest for that matter?
  2. Should binding the resolved data or rejected reason to view is needed at all? Usually the devs have access to the resolved data or the reason already in the view-model.
  3. Should error be thrown in following cases:
    • <template async.bind="promise">w/o any of the other TCs </template>.
    • async.from-view
    • fulfilled.to-view
    • rejected.to-view

(Very) Loose solution idea

  • Create four new template controllers with appropriate binding modes for value.
  • ā€œLinkā€ those accordingly.
  • The async TC tracks the promise and accordingly activates the views of other TCs.

As in Aurelia template controller is used to dynamically/conditionally render/attach views, using template controllers here seems like a natural choice. For completeness let me mention the obvious that it can and should not be dealt with binding behavior or value converter as those primarily deals in the realm of data binding.

Fin!

Edits:

  • 17.10.2020
    • Renamed unsettled to pending, then to fulfilled, and catch to rejected. Justification: reference#1, reference#2.
    • Added mode example.
    • Added nested async.bind example.
    • Added brief reasoning to use template controller over binding behavior or value converter.
  • 20.10.2020
    • Renamed mode to resolve.
    • Corrected some typos.

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 12
  • Comments: 38 (35 by maintainers)

Most upvoted comments

@Sayan751

From this perspective, I like @zewa666’s proposal to use the name promise instead. That way we can possibly use promise.any, promise.all, promise.allSettled, and promise.race. The naming feels more natural to me.

Although to support that syntax might be a major work. At first we might need something generic like the {TC}.PART attribute pattern also for template controllers, so that specific parts can be interpreted with specific meaning. If I am not mistaken, that’s a major infra to build.

That infra is already there. Whether to do it via @attributePattern or via @bindingCommand though depends on how you want to implement it.

The most obvious way that comes to mind is having 4 different template controllers for these 4 scenarios (probably one of those few cases where inheritance makes sense). Then you could ā€œredirectā€ with attribute patterns:

@attributePattern('promise.PART')
class PromiseAttributePattern {
  'promise.PART'(name, value, parts) {
    switch (parts[1]) {
      case 'any': return new AttrSyntax(name, value, 'promise-any', 'bind');
      case 'all': return new AttrSyntax(name, value, 'promise-all', 'bind');
      case 'allsettled': return new AttrSyntax(name, value, 'promise-allsettled', 'bind');
      case 'race': return new AttrSyntax(name, value, 'promise-race', 'bind'); 
      default: // throw
    }
  }
}

class PromiseTemplateController { /* magic */ }
@templateController('promise-any')
class PromiseAnyTemplateController extends PromiseTemplateController { /* magic */ }
@templateController('promise-all')
class PromiseAllsettledTemplateController extends PromiseTemplateController { /* magic */ }
@templateController('promise-allsettled')
class PromiseAllTemplateController extends PromiseTemplateController { /* magic */ }
@templateController('promise-race')
class PromiseRaceTemplateController extends PromiseTemplateController { /* magic */ }

export const PromiseTemplateControllerRegistration: IRegistry = {
  PromiseAttributePattern,
  PromiseAnyTemplateController,
  PromiseAllsettledTemplateController,
  PromiseAllTemplateController,
  PromiseRaceTemplateController,
};

Inheritance could be avoided by using binding commands. Then you’d have to create an accompanying instruction and renderer that transforms the any, all etc into a hydration operation that creates a binding and a meta property on the template controller to indicate which kind it is. A little more involved, maybe an optimization worth looking into at a later time.

But attribute patterns should be an easy way to get the ball rolling?

Side note: this implementation means users could also do promise-all.bind instead of promise.all. That’s not a problem though imo. And if you wanted to support multi-attr bindings in the future, that could be accomplished as well if it was really needed:

@attributePattern('promise.PART')
class PromiseAttributePattern {
  'promise.PART'(name, value, parts) {
    switch (parts[1]) {
      case 'any': return new AttrSyntax(name, value, 'promise-any', hasInlineBindings(value) ? '' : 'bind');
      // etc
    }
  }
}

Another example that we can do is a containerless/templateless custom element for fetching data

<fetch url="/my/graphql" type="graphql" result.bind="fetchUserPromise">
    {
        user {
            name
            friends(id: ${query.friendId}) {
                name
            }
        }
    }
</fetch>
<template promise.bind="fetchUserPromise">
  <div then.bind="user">
    <b if.bind="user">${user.name}....</b>
    <span else>Not exist</span>
  </div>
  <div catch.bind="ex">Error fetching user data. ${ex.message}</div>
</template>

Apologies if I am missing the use case. But before bikeshedding the keywords, do we really need this much declarative power in html? To me, it seems simpler, more readable, and more powerful (since you can differentiate show vs if) to keep the promise resolving and catching in JS and simply put a show.bind or if.bind in the html.

resolving promises is a capability we absolutely need. although you can do everything from the VM, allowing binding directly to a promise can help to reduce boilerplate code (as already mentioned by other). but it’s absolutely needed in cases where you pass a value through a value-converter that return a promise. I find myself copying to every project I create a special binding-behaviour that handle promises just for this specific task.

but: on the other hand, I think we should not take it too far. for example: the next scenario is too far - in my opinion.

<template async.bind="promise1">
  <template fulfilled.from-view="data" async.bind="data.json()">
    <template fulfilled.from-view="json"> ${json.foo} </template>
  </template> 
</template>

It’s obvious that such channing should happened in the VM, and the view can be bounded to the resulting Promise. you don’t lose any capabilities - with a cleaner way of working.

I would also argue that allowing all, any, race. etc. are also an abuse and should not be implemented at all. all of those are functions that produce a single promise at the end. so it’s enough to allow a binding to that Promise and by that - allowing all of the scenarios mentioned and more.

so for instance:

instead of

<template async="value.bind: [promise1, promise2]; resolve: all">

we can do:

<template async.bind="Promise.all([promise1, promise2])">

or better yet as a developer, move the Promise.all([promise1, promise2])" part to the VM - save this expression as a variable, and bind the async to that.

to summarize: we need to find the fine path between nothing at all (what we have right now in V1), and absolute crazy stuff…

I really liked this: by @bigopon

<div await.bind="promise"></div>
<div then.bind="value">${value}</div>
<div catch.bind="ex">${ex.message}</div>

this is clean, understandable… and have all the power we need.

I think our aim shouldn’t be ā€œmimic the way promise worksā€ in html. With that in mind, we should simply the async/unsettled/fulfilled combo to just 2, with more natural name: await/then. The following example illustrates my comment:

<div await.bind="promise"></div>
<div then.bind="value">${value}</div>
<div catch.bind="ex">${ex.message}</div>

<div await.any="promises"></div>
<div then.bind="value">${value}</div>
<div catch.bind="ex">${ex.message}</div>

<div await.all="promises"></div>
<div then.bind="values" repeat.for="value of values"></div>
<div catch.bind="ex">${ex.message}</div>

<div await.race="promises"></div>
<div then.bind="values" repeat.for="value of values"></div>
<div catch.bind="ex">${ex.message}</div>

Doing it with await keyword, we avoid occupying the word async, and can save it for later use with observables. At least we don’t give the wrong impression that any kind of async will work. It’s only promise that is intended for this feature.

In order to have await.any/await.race/await.all, we need to enhance the template compiler, so that custom attribute can participate in the compilation process as well. In case some commands have special meanings when used with it. In this case .any/.all/.race is clearer.

I like the clear unsettled.from-view="...", in our implementation, we can make this default binding mode and simply using .bind will suffice.

Yeah it could be done but I don’t know what problem that would solve that would make it worth the perf impact of the extra parsing/binding overhead. These things can easily be accomplished programmatically as well. I’ve never seen a situation where I needed to dynamically change the event handled, only ignore it under certain circumstances (that’s what the event handler method is for…)

If it’s an avenue that must be explored, though, let’s do that in a separate RFC to stay on-topic 😃

Another example that we can do is a containerless/templateless custom element for fetching data

<fetch url="/my/graphql" type="graphql" value.bind="fetchUser">
    {
        user {
            name
            friends(id: ${query.friendId}) {
                name
            }
        }
    }
</fetch>
<template promise.bind="fetchUser">
  <div then.bind="user">
    <b if.bind="user">${user.name}....</b>
    <span else>Not exist</span>
  </div>
  <div catch.bind="ex">Error fetching user data. ${ex.message}</div>
</template>

noooooo lol, this is even more crazy than previous suggestions.

Besides what @Sayan751 's noted, we should also be aware all these resources are optional, which mean if you don’t use it, it won’t be in your final bundle, if that is also one of the concerns. All our our resources in v2 can be simply dropped if not needed, and it’s simpler to do so.

@akircher You are right that we can keep the resolving and catching of the promise in view-model and use a if/show in view to display the results accordingly. This is a perfectly valid solution.

However, if you have such instances in abundance, then it considerably increases the amount of boilerplate code. A natural progression thereafter can be a custom element that monitors the bindable promise, and has 2 slots for fulfilled and rejected content. And you can use that custom element to get rid of the boilerplate code. Again a valid solution.

One inherent limitation of the custom element approach is how it is styled, and whether or not it can be used seamlessly (style-wise) everywhere. On the other hand, having template controllers gives you freedom of using this construct anywhere possible, and those elements can be styled anyway wanted.

Even with these new template controllers, you might still want to encapsulate the code in a custom element (as said before) and use that. However, wherever you can’t use that custom element due to styles/layout issues, the new template controllers can be directly. IMO this brings more flexibility to play.

Apologies if I am missing the use case. But before bikeshedding the keywords, do we really need this much declarative power in html? To me, it seems simpler, more readable, and more powerful (since you can differentiate show vs if) to keep the promise resolving and catching in JS and simply put a show.bind or if.bind in the html.

I like the idea of renaming mode to resolve. Seems more natural to me.

One down side of using ( is that following with space will break, and user might be confused why it didn’t work.

<div catch( ex )>${ex.message}</span>

There are different individual preferences and we won’t be able to satisfy all. Fortunately, it is trivial to re-configure the binding syntax. We can provide an OOTB example or two on the most popular alternatives to the one we went with, so folks with strong alternative preferences can still have it their way. No problem with that.

It’s tricky to find a truly consistent syntax since the other control flow TC’s are all statements:

if (condition) {
  oneThing;
} else {
  anotherThing;
}
<div if.bind="condition">oneThing</div>
<div else>anotherThing</div>

And

switch (state) {
  case 1: thing1;
  case 2: thing2;
  default: somethingElse;
}
<div switch.bind="state">
  <div case="1">thing1</div>
  <div case="2">thing2</div>
  <div default-case>somethingElse</div>
</div>

So what syntactic concept are we trying to mimic for promises?

// option 1: old fashioned chaining
somethingWhileWaiting;
Promise.resolve(promise)
.then(value => {
  somethingOnResolve;
})
.catch(reason=> {
  somethingOnReject;
});
<div promise.resolve="promise">somethingWhileWaiting</div>
<div then.bind="value">somethingOnResolve</div>
<div catch.bind="reason">somethingOnReject</div>
// option 2: async/await
(async () => {
  somethingWhileWaiting;
  try {
    await promise;
    somethingOnResolve;
  } catch (err) {
    somethingOnReject;
  }
})();
<div async.bind="promise">somethingWhileWaiting</div>
<div await.bind="value">somethingOnResolve</div>
<div catch.bind="reason">somethingOnReject</div>

In neither case I see a 100% convincing parallel in anything that’s not super verbose (certainly not looking forward to nesting a try/catch in an async in a template), but option 1 does feel a lot more like it resembles the actual code. Maybe someone else has a way to present async/await in a more natural manner but so far I don’t think that will be the right terminology to use here.

The examples provided by @Sayan751 with async and pending/fulfilled/rejected don’t directly mimic a syntactic structure that is seen in real code unless you imagine the promise’s internal slots being accessible. Though with promise.resolve it is a bit easier to display it in psuedo code I think:

// hypothetical code if internal slots were accessible
const promise = Promise.resolve(input);
while (promise['[[PromiseState]]'] === 'pending') {
  somethingWhileWaiting;
}
while (promise['[[PromiseState]]'] === 'fulfilled') {
  somethingOnResolve;
}
while (promise['[[PromiseState]]'] === 'rejected') {
  somethingOnReject;
}
<div promise.resolve="input">
  <div pending>somethingWhileWaiting</span>
  <div fulfilled.bind="value">somethingOnResolve</span>
  <div rejected.bind="reason">somethingOnReject</span>
</div>

When taking that liberty of imagining the internal slots being accessible, I do think @Sayan751’s variant is the best in terms of consistency.

@Sayan751 You could also have a single template controller registered with 4 aliases, and then inject the instruction (just like compose does) to look at the original name that was declared, and use the res name from the instruction to determine the behavior. So the aliases would be promise.all, etc. You’d still need the attribute pattern to effectively turn promise.all into promise.all.bind then.

Cool. Just to further clarify then: attribute patterns are a template compiler extensibility point and they’re used in the broadest sense for determining resources and their bindings. This is also the api one would use to emulate the Vue or Angular syntax.

It might help to think of the parallel with configured routes. You register a route pattern to map from a url segment to a component / redirect / param, in much the same way you register an attribute pattern to map from an attribute to a either a binding, a custom attribute + binding, or simply a custom attribute. If you would not register the DotSeperatedAttributeSyntax then if.bind would cause the compiler to look for something declared as either @templateController('if.bind') or @customAttribute('if.bind'), neither of which would exist, so it then assumes if.bind is just an ordinary native attribute and do nothing with it 😃.

The only thing to be careful with is clashing. Come to think of it, it’s probably not good to throw in the default case, because that would make it impossible for anyone to declare a binding with the name promise. This is probably a better and more compatible idea:

@attributePattern('promise.PART')
class PromiseAttributePattern {
  'promise.PART'(name, value, parts) {
    switch (parts[1]) {
      case 'any': return new AttrSyntax(name, value, 'promise-any', 'bind');
      case 'all': return new AttrSyntax(name, value, 'promise-all', 'bind');
      case 'allsettled': return new AttrSyntax(name, value, 'promise-allsettled', 'bind');
      case 'race': return new AttrSyntax(name, value, 'promise-race', 'bind'); 
      // this is identical to the behavior of DotSeperatedAttributeSyntax
      default: return new AttrSyntax(name, value, parts[0], parts[1]);
    }
  }
}

Just had some more thoughts on this. For await.bind, it’s a bit off to use await to express an async piece of UI for the reason in your comments, and pending state.

Which means the following:

<div await.bind="fetchInfo(userName)">Loading...</div>
<div then.bind="user">Loaded. User name is: ${user.name}</div>

is probably not the best.

But if we are to map our naming 100% to Promises, and the real meaning of its internal states, we will have to do:

<template async.bind="fetchInfo(userName)">
  <div pending>Loading...</div>
  <div fullfilled.bind="user">Loaded. User name is: ${user.name}</div>
</template>

It feels a bit unnecessarily detailed.

Though with my proposal, we cannot express the following as there’s no nesting:

dont show any loading state
<template async.bind="fetchInfo(userName)">
  <div fullfilled.bind="user">Loaded. User name is: ${user.name}</div>
</template>

So maybe we cannot use await.bind. I don’t have a better naming pairs now, so I think while there’s some issue with async in terms of naming, it’s probably not bad to go ahead with it. If we really want to have another name, I think resolve is a better name than promise.

For

<template async="value.bind: [promise1, promise2]; mode: all">

Maybe rename mode to resolve?

<template async="value.bind: [promise1, promise2]; resolve: all">
<template async="value.bind: [promise1, promise2]; resolve: any">
<template async="value.bind: [promise1, promise2]; resolve: race">
<template async="value.bind: [promise1, promise2]; resolve: allSettled">