Lit-html: Consider removing built-in Promise support in favor of `until()` directive.

Created on 9 Oct 2018  路  57Comments  路  Source: Polymer/lit-html

Currently we don't support Promises in attributes (#272, #544). I've been doing some work to support this, but it starts getting pretty complex as we try to answer things like:

  • What is the value of a fully-controlled attribute before Promise resolution? Empty string or not present?
  • What is the value of an interpolated expression before Promise resolution?
  • What about boolean attributes? If the default value is not present, that's the same as defaulting to false. Is that correct?
  • Do Promises also auto-resolve in property bindings? If so, how would we pass Promises as properties to components that actually _accept_ a promise?

While Promises in Node parts generally have none of these problems, support there sets the expectation that Promises just work in attributes as well. Promises support in NodePart also currently has some bugs due to race conditions. We have outstanding code to fix this, but it adds complexity to core.

So, one solution that limits complexity and keeps support consistent is to remove built-in Promise support from NodePart and require the until() directive to use Promises. This means that templates have to be explicitly aware of async values, which is also probably a good thing.

until() itself would get more complicated as it can't rely on built-in promise support, and has to track Promise identity and resolved state, but that complexity is pay-for-play.

This is obviously a breaking change, so deciding on this blocks a 1.0 release. I'm curious if anyone in the community has feedback here. Upgrading to a new version shouldn't be very difficult if you know where Promises are used in templates.

API Critical Bug

Most helpful comment

I think it is better to require use of until so people get into the habit of always providing a default value (even if it is empty string).

I don't think it is worth the complexity and flaky-ness of trying to guess what a default value should be. We would end up deciding that an unresolved promise renders as X but its almost guaranteed _many_ people would rather it be Y.

It is better to be explicit than not.

Are you aware (or anyone else) of a use case until does not cover which promises in templates currently do?

until() itself would get more complicated as it can't rely on built-in promise support, and has to track Promise identity and resolved state, but that complexity is pay-for-play.

Can you elaborate? What complexity is needed?

All 57 comments

I think it is better to require use of until so people get into the habit of always providing a default value (even if it is empty string).

I don't think it is worth the complexity and flaky-ness of trying to guess what a default value should be. We would end up deciding that an unresolved promise renders as X but its almost guaranteed _many_ people would rather it be Y.

It is better to be explicit than not.

Are you aware (or anyone else) of a use case until does not cover which promises in templates currently do?

until() itself would get more complicated as it can't rely on built-in promise support, and has to track Promise identity and resolved state, but that complexity is pay-for-play.

Can you elaborate? What complexity is needed?

I think use of until() is a more natural fit for this issue than trying force a value for Promises when we may not always know what that is. It seems the far less complex solution.

One use case where we use promises a LOT is for translations.

const msg = () => { return Promise(..) }

render(html`
  <my-el label=${msg('my-el:label')}> // that does not work currently
     <h2>${msg('my-el:heading')}></h2> // works fine
  <my-el>
`, target);

See "working" example:
https://codepen.io/daKmoR/pen/xyqEEz?editors=1010

So whenever this is rendered and the translations files are not yet loaded it's not a problem as it will auto-resolve when it does.

I would understand that to reduce complexity to drop Promise support... however I'm not sure if it would be currently possible to provide ONE function/directive that could handle promise results for attribute and node cases?

e.g. would I need something like this?

const msgForAttributes = Directive();
const msgForProperty = Directive();
const msgForNode= Directive();

render(html`
  <my-el label=${msgForAttributes('my-el:label')} .description=${msgForProperty('my-el:label')}>
     <h2>${msgForNode('my-el:heading')}></h2>
  <my-el>
`, target);

if so it seems complexity remains => we just push it to our users

=> so overall I am a little torn apart... I understand the need to reduce complexity but having multiple "same" function for attribute, property, node seems rather unfortunate

PS: if one directive can handle all cases then it would be a whole different story

Regarding the until directive:
It currently does NOT support attributes (as it assumes lit-html core does it)
https://codepen.io/daKmoR/pen/XxMjGP?editors=1010

I guess we need to put the complexity somewhere.
So either it's in the core for each "part" (node, attribute, property) or we need to combine them all in the until directive?

if until directive supports all of that I guess I could have a msg function that "extends" it.
I guess that could work...

I do think we should be more tolerant of changes which require "lit specific knowledge".

If we forever try to avoid introducing concepts only we make use of, we'll always be dancing around problems and adding complexity where complexity wouldn't otherwise be needed.

It is ok to expect people to learn how to use directives, especially once we have the docs finished... that is what separates lit from just using vanilla JS.

@justinfagnani I'm a strong proponent for putting the logic in the until directive, as I've already laid out in https://github.com/Polymer/lit-html/issues/544#issuecomment-427660318. Breaking API by removing support for promises in NodePart is a nice benefit in my opinion, because giving people to ways to do the same thing tends to lead to confusion and mistakes.

@daKmoR Multiple until directives would make things overly complex, so I hope we can make it work with the single directive we have now. That would mean your translation API doesn't need to change if you can make msg wrap the promise in a call to until

@43081j I may have badly articulated myself. I do agree that having some opinionated solutions will overall improve the situation. (I removed the relevant text from my comments)

@bgotink if that's possible then I'm all for it 馃憤 I could just replace the return "promise" function with a directive and no additional (user) code changes would be needed - sweet :)

@daKmoR you would end up with all your calls looking like this:

return html`
  <h2>${until(msgForNode('my-el:heading'), html``)}></h2>

or have msgForNode return until(...).

just as in your example codepen. and the same for attributes assuming thats how we go ahead.

edit:

maybe we could default the default value to empty string at least so the common use case becomes very simple? until(promise). though it doesn't read very well (until what?), so maybe i'm talking nonsense here.

My vote is for doing promises through directives, I would not call it until though as that implies an in between state which does not always apply.

It might be better for performance too, because regular renders dont need to do a promise check for each value.

If we change the name, I was thinking wait or waitFor since they're close to await.

@daKmoR as @43081j points out, you can wrap until, so that it's also a directive:

const msg = (id) => until(_promiseReturningImpl(id));

The notion of

async render() { ... }

was brought up in slack and might be worth some short consideration.

in any event this seems like a positive change.

The possibility of writing Promise-returning asyncRender() is one of the reasons I like built-in Promise support. asyncRender can collect Promise-valued expressions and wait for them while also allowing the incremental rendering. I've been thinking about how to do this with directives, when render wouldn't ever see the Promises. It'll require coordination between the directives and render function, I think.

If we rename it, I'd suggest waitFor of the two mentioned above.

An async render method could be nice whichever route we take I think.

The whole idea of built in promises means having a concept of what should exist by default until they resolve. I like using a directive because consumers have to specify this explicitly.

I suppose interpolating a promise into a string is a little unusual too. I'd personally expect to see the toString of the promise as that's how template literals would do it normally. At least directives make more logical sense as you would expect them to turn into a string.

because consumers have to specify this explicitly.

One mark against waitFor is that await is potentially valid inside a template in an async function and means the _opposite_.

This returns immediately, can render the <div> synchronously, and fills in the content with the Promise later:

const t = (promise) => html`<div>${waitFor(promise)}</div>`;

This returns a Promise, can't render the <div> synchronously, and waits for the Promise to resolve to return a result:

const t = async (promise) => html`<div>${await promise}</div>`;

Huh... async is a valid identifier:

const t = (promise) => html`<div>${async(promise)}</div>`;

Sure, we could make the render method async still. It just means you can either incrementally update asynchronously (directives), or render asynchronously (await in render), right?

One is incremental and one is all at once. You could support both

Something like whenResolved might work I guess. To avoid confusion.

it's probably a bad name 馃槀

Fwiw until is fine. It's semantic and reads left to right

Until future results, do this.

until(promise, default)

I am on board with this solution. I've been thinking about how to handle Promises for attributes as well, but it's rather difficult, and as you mentioned there is a lot of undefined behaviour. I'm also not a huge fan of the added complexity to make Promises work in general, since it adds additional work to the hot path of every render action.

I think wait and waitFor are not good naming schemes, as they signal that the render is blocked somehow. I much prefer the current until nomenclature.

Thinking about this more I think this solution will run into the same fundamental problem as Promises. How can a directive know upon resolution of the promise that it has not been superseded by a different render?

Consider the following case

template = (thing) => html`
  <div>${thing}</div>
`

render(template(some_promise, 'hello'));
render(template('goodbye'));

When some_promise resolves, how does until know not to override The 'goodbye' value? It could check the value of the NodePart and compare with the fallback value, but then you still have the same problem with render(template('hello')).

The only way for until to know if it is the current rendered value is if NodePart sets or clears some property whenever it renders, similar to how rendering Promises is implemented.

The new directive in #555 @ruphin.

It keeps a weak map of parts to promise state so each time a new promise is passed, the old one does nothing once it resolves.

There's no problem with default content either as it has to be explicitly specified.

If your example was using the directive you would be expected to pass a resolved promise for the goodbye value (Promise.resolve('goodbye'))

Yes, I understand that if you only ever render to that NodePart through the directive, it can be trivially solved. However, that does mean the NodePart contract becomes more restrictive. (You can render any value, but if you ever render an until directive value, you cannot render a non-until directive value anymore without breaking)

Do you have a real world use case for doing such a thing?

Sounds like something in need of a refactor to me. Why not use a promise which resolves to your "non-until" value? Why are you doing your switching outside the promise?

I get your point but it sounds like there are better ways to engineer such a thing.

I imagine the promise would resolve and overwrite the non-async render, which is fine.

@justinfagnani I personally don't think the asyncRender function requires built-in support for promises. What that function does (dumbed down) is await on all promises passed into the html template tag before performing a sync render operation. Moving promise handling in the sync render case into a separate directive is still useful in the case of an asyncRender scenario:

const somePromise = Promise.resolve('value');
const longPromise = new Promise(resolve => setTimeout(resolve, 2000, 'long');

render(html`<div>${somePromise} ${longPromise}</div>`, container);
// <div>[object Promise]</div>
// once microtask queue is flushed: <div>[object Promise]</div>

render(html`<div>${async(somePromise, 'temp')}</div>`, container);
// <div>temp</div>
// once microtask queue is flushed: <div>value</div>

asyncRender(html`<div>${somePromise} ${longPromise}</div>`, container);
// doesn't render
// once microtask queue is flushed: doesn't render
// once 2 seconds pass and microtask queue is flushed: <div>value</div>

// -> This is awesome, I can block the render until promises resolve but I can also tell the renderer to use a temporary value instead
asyncRender(html`<div>${somePromise} ${async(longPromise, 'temp')}</div>`, container);
// doesn't render
// once microtask queue is flushed: <div>value temp</div>
// once 2 seconds pass and microtask queue is flushed: <div>value long</div>

@bgotink i dont think your example is quite right..

see below where i realise im talking nonsense.

You are thinking of LitElement render(), not lit-html render(). The examples you show can be supported even with a synchronous render from lit-html.

What I mentioned before is not an issue of supporting certain use cases, it's about the shape of the API. 'The API works fine as long as you use it right' is a bad excuse for having edge cases in your API. A good API produces expected results regardless of what the user does. My point is that the suggested directive adds edge cases that have unexpected results, which adds cost. Either users need to be educated on edge cases which adds documentation complexity and increases the learning curve, or they randomly run into issues and using the library feels like a minefield.

true i am.

i don't see any use in an async lit-html render then, only a lit element one.

I never said the api works fine as long as you use it right.

If you try render an async value then render a non-async value meanwhile (in a second render call), it is perfectly logical to overwrite it once the promise one resolves. Nothing is unexpected about that at all, with basic understanding of promises it is exactly what one would expect.

let x;

new Promise((resolve) => {
  setTimeout(() => {
    x = 1;
    resolve();
  }, 1000);
});

x = 2;

If you understand promises at all, of course this means x will ultimately be 1. Why would we want to implement any other behaviour? This is entirely expected behaviour of promises and async logic.

const fn = (anything) =>
 html`<div>${anything}</div>`;
const promise = new Promise((resolve) => {
  setTimeout(() => resolve('baz'), 1000);
});

render(fn(async(promise, 'foo')); // <div>foo</div>
render(fn('bar')); // <div>bar</div>
await promise; // <div>baz</div>

I don't think there's any unexpected result here. If you want to mix async and not, thats your choice and (unrelated to lit) you'll deal with the fact things may resolve out of order. Like with any async work in any JS.

So you're saying that if you use an API to render a promise, it's expected behaviour that you can't undo or cancel that promise, and you're effectively locked to rendering it once it resolves?

Do you think it's a nice API that the only way to cancel a promise is to render another promise?

Yes I am saying that. It should behave the same way as any async functionality in JS. If you want to prevent it, reject the promise or some such thing.

How do you normally "cancel" a promise? I think you're trying to treat lit differently to a normal async function and shouldn't be. None of this concept is specific to lit, treat it like you do any promise.

It's not about changing the resolution of the promise, but about using the result. I think it's perfectly valid to say that at some point you don't care about the result of a promise anymore. Consider the following scenario:

You want to show a message, with a loading indicator. You implement like this:

html`
  <div id="message">
    ${ until(this.message, 'Loading...') }
  </div>
`;

Now you want to add localization to this template. You have a localize() function that returns a promise, because you don't want your render to be blocked on loading the translations. Your template becomes this:

html`
  <div id="message">
    ${ until(this.message, localize('Loading...')) }
  </div>
`;

But now things break, because your localize() can finish after this.message. It seems obvious to me that once you render this.message you no longer care about the localize() promise, and you don't want to render that anymore.

An other example is a promise on a long-running request. If the user requests some kind of thing and it takes a while to load, and while it's still loading, the user changes the view or requests some other kind of data, you no longer want to render the result of the original request when it resolves.

It's not about promises behaving like in pure JS, it's about API design and user expectation. I expect that if I use an API to render X, and then afterwards I render Y, then the rendered result should always be Y. It doesn't matter if X is a promise or not. You are suggesting that I concede all control over what actually happens the moment I decide to render a promise, and what shows up on my screen will be determined by whichever item happens to resolve last. That seems like an unusable API to me.

You're kind of misusing until, there, I would say.

The whole point about async (until) is to render the result of a promise, but until then, render a default value. It makes no sense for the default value to be _another promise_. This shouldn't even be allowed, the async directive exists for the reason of rendering something until a promise resolves. where's the sense in using yet another promise? you defeat the whole point of the directive, in that case you'd need a 3rd non-async value to render until both promises resolve.

async(Promise, primitive) is the correct signature as far as im concerned.

As for your request example, reject the promise... you know you've sent another, newer request off, so when the old one resolves, you should know it isn't the current one and reject it or _do nothing_. This isn't a question of lit's logic, its how _you_ decide how to deal with promises in general.

User expectation should be that async does what it exists for: renders non-async content until async content resolves. Make a new directive if you want something else, I'd say.

I'm with @43081j here on using the primitive.

Using the extra promise for localization is a great example, but an implementation of that will likely end up just using the same async(Promise, primitive). So does it make sense to add the complexity to this directive, or implement this logic elsewhere?

I would much rather lit-html was only concerned with rendering templates and kept as simple as possible with as few directives as possible so it doesn't try to turn into a language itself. Concrete values are converted into DOM nodes / attributes - that should be its sole purpose IMO.

Any handling of Promises, Observables or anything else should be done elsewhere, in higher-order components or some other part of the app (e.g. Redux saga's handling async and producing concrete values to feed the templates).

I get that it will be 'neat' for demos and examples, "look how easy it is to show a loading message, fetch data and then render it!", but IMO it doesn't really add that much that wouldn't be clearer by separating that logic out of the template.

We're getting bogged down into details. The use of until in the example was just to illustrate a point. It doesn't matter which deferring mechanism you use, or what the API or intended usage of that mechanism is, there is a simple fundamental problem here.

There are plausible scenarios in which you render a promise of which the result is no longer relevant before it resolves. Whatever system you use, there has to be a way to override that promise, or to stop it from rendering the irrelevant result once it resolves.

As I mentioned before, it is _technically impossible_ to do this, unless either lit-html has some form of builtin support for it, OR you accept an API that breaks if you render any other value after rendering a promise (through whatever system you use, be it an async directive, or an until directive, or whatever API you want).


I think there is a lot of value in lit-html having a deferred rendering system. If you only render static values, that means the entire template needs to re-render whenever any one deferred value resolves. Imagine a page with 10 different values that are all resolved through some higher order logic, redux or whatever. Do you think it's a good idea to just re-render the entire page every time one of these values resolve? I think there clearly is added value in allowing partial deferred rendering, so you only defer the part of the template that has not resolved yet.

async is for rendering a non async value until an async value resolves.

Any variation of that such as with the default being a promise belongs in its own directive I think.

Simple enough

If you only render static values, that means the entire template needs to re-render whenever any one deferred value resolves ... Do you think it's a good idea to just re-render the entire page every time one of these values resolve?

Isn't the point of lit-html that it does this efficiently? (with immutable state update patterns helping that to happen)

A bunch of things to tease out here.

First, I think Promises being canceled is fine, but that's a general Promise issue that applies to all of JavaScript. The initial idea there was cancelable Promises, but that stalled (it introduced a new third state to function completion: return, throw and now cancel). The current approach is AbortController/AbortSignal: https://developer.mozilla.org/en-US/docs/Web/API/AbortController as used by fetch(). fetch() will reject its Promise with an AbortError when aborted.

If a Promise passed until()/async() rejects for any reason, it won't render.

Second, there are some really tricky cases with directives in general that are caused by them being values, and not actually being part of the template. That falls out from our desire to put most features on the expression side of template, and not encode them into strings (binding type being the exception). Directives don't really have any identity or state, so directives that need to keep state, so far, have done so by storing it with the Part. That causes problems with composition, because in a pattern like async(promise1, async(promise2)), we can't currently associate state with each directive call separately.

I suspect the vast majority of the time directives are used "statically" (they appear inline with the template) and one-at-a-time, not composed. We have some tests with say, repeat rendering async list items, but directives don't compose that well generally, not without careful handling in the directive implementations.

Third, on async in general, especially in relation to state management - I view lit-html as a low-level, standalone rendering piece that doesn't assume Web Components, or any component library, and doesn't assume state management. At the same time asynchronous values are becoming more and more central to JS development. We can break down the core types of values in JS according to sync/async, and single/multi axes:

| | Single | Multi |
| --: | :-: | :-: |
| Sync | T | Iterable<T> |
| Async | Promise<T> | AsyncIterable<T> |

I think lit-html should be able to handle all these types of values to some degree - they're that fundamental.

lit-html also follows a pattern where a TemplateResult is just a description of the DOM, that can be rendered by code in another place, at another time, even multiple times. Adding some amount of async handling means that the template definition and the template rendering don't necessarily have to coordinate. Yes, you can remove asynchronicity in templates by re-rendering, but lit-html does not provide a way for a template to tell a renderer to re-render. LitElement does, Redux-like patterns do, but those are not lit-html.

So, I think it's really valuable, even if there are some caveats currently. The caveats are actually with directives in general, and I hope they can be removed in general.

Right now I'm trying to think if there's a way that lit-html can help with managing state for directives so that composition works better. It's possible that directives should work more like html and TemplateResults, so that directive arguments are part of the tree of values contained in a TemplateResult - currently only the closure is a value and it's arguments are hidden in the closure. An approach like that might let us pass previous state back to the directive as long as the structure of directives doesn't change between renders - very similar to how we decide to re-render based on the type of TemplateResult rendered to a Part.

Allowing directives to have access to some kind of state helps, but again, it won't solve the particular issue of rendering some value (any value) after rendering some promise-like deferred value (using whatever mechanism).

It's still totally an option to accept that this is simply invalid behaviour and just isn't supported, but I think we should be explicit about making that choice, and it should be explicitly documented.

Allowing directives to have access to some kind of state helps, but again, it won't solve the particular issue of rendering some value (any value) after rendering some promise-like deferred value (using whatever mechanism).

I think it could, by letting a directive query the state object to see if it's still valid. If another value renders there, it could invalidate the state. This is basically a lightweight disconnect signal.

It's still totally an option to accept that this is simply invalid behaviour and just isn't supported, but I think we should be explicit about making that choice, and it should be explicitly documented.

No matter what, we'll do this. We might even still do that even if we have a design for better stateful directives.

I'm in favor of offering more hooks to directives about their context / state. The WeakMap bookkeeping takes a lot of knowledge of how directives work, and it's really easy to mess up performance.

I'm a little confused by this change. Will someone please do a pros/cons before and after this change (and code would be helpful here)?

I agree there's been a lot of opinionated drill down as to methods. It
would be helpful if we can see a few before and after code examples on
proposed changes.

Following was suggested (or maybe partially supported already):

const promise = Promise.resolve('foo');
render(html`<div>${promise}</div>`);
// eventually renders <div>foo</div>

Following will be required in future to be more explicit about what non-async value to render meanwhile (until promise resolves):

const promise = Promise.resolve('foo');
render(html`<div>${async(promise, 'default')}</div>`);
// renders <div>default</div>
// eventually renders <div>foo</div>

Its pretty straight forward and logical without the extra discussion and ideas bouncing around here. We need a way to support rendering a non-async value until an async value has resolved.. this is it.

Okay got it. Thanks for the comparison. I'm not seeing anything in the latest proposed change that worries me. I end up doing the most complex logic in redux or similar tools anyway, but using async method would be fine I'd reckon.

@43081j please don't confuse the issue.

First, rendering Promises is currently already supported by lit-html without use of directives. As you can see here, NodePart currently has support for rendering Promises, which allow a deferred partial render, or an asynchronous render, if you will. When you render a Promise, lit-html renders nothing, and once the promise resolves, lit-html will render the result of the promise in that location.

The current implementation in lit-html to render Promises is very inefficient. There is an outstanding PR that fixes this performance issue, but it adds some unavoidable complexity to the lit-html core, since fixing Promise rendering essentially boils down to fixing cache invalidation, which is a hard problem in computer science.

Second, there currently already exists a directive that renders a Promise (or async value) with a placeholder (a synchronous value) until it resolves, the until directive. Because lit-html currently supports rendering Promises internally, the implementation of until is really trivial.


Now, the proposed change is to move the complexity of rendering Promises out of lit-html core altogether, and into the until directive (or a similar directive with another name).

The advantages of this approach are:

  • It simplifies the core implementation of lit-html.
  • The new directive would be able to render Promises to both NodeParts _and_ AttributeParts (lit-html currently only supports Promises in NodeParts).

The problems of this approach are:

  • It adds several edge cases that cause unexpected values to render.
  • The solution to this requires directives to have access to some kind of state of the Parts they are rendering to, but they are currently stateless.

@justinfagnani suggests that we may be able to alleviate these problems by tracking some more internal state in Parts, so that directives have more information into the current state of the Part, which solves the above problems.


My personal views

The solution suggested by @justinfagnani effectively requires the same type of internal state tracking as the outstanding PR to fix Promises, which means that some complexity in lit-html core is unavoidable. The unfortunate part is that this added complexity adds some performance overhead (I estimate around 1-2%) for every render, but this is _technically unavoidable_ if we want any kind of reliable asynchronous rendering.

However, since tracking this additional state offers added benefits for other directives, _and_ is capable of supporting promises for AttributeParts, I think the suggested solution is clearly a superior implementation over the current PR, because it allows more flexibility for future directives to use.

I am not confusing the issue.

Whatever happens, until (async) is used to render a non-async value until an async value resolves. It is a pretty trivial concept and we should try keep it that way.

We don't need to support promises in templates, as we now have the async directive (and did before, but can move some complexity to it now).

You wanted the ability to have a promise as the default value, but that is not what this directive is for.

  • What is the value of a fully-controlled attribute before Promise resolution? Empty string or not present?

async(promise, defaultValue), we no longer have to answer this as you are required to be explicit about it.

  • What is the value of an interpolated expression before Promise resolution?

Again, no longer applies, we explicitly must provide a default non-async value.

  • What about boolean attributes? If the default value is not present, that's the same as defaulting to false. Is that correct?

Doesn't really apply, we have a default value and it should be true or false.

The issue was these questions and requiring use of the async directive answers all of them as long as we make it supported in all parts.

The issue you speak of, "state tracking", is in fact separate to the original post in this issue.

I never wanted the ability to have promises as default values. I used the example to illustrate a potential problem with this solution.

Let me try again this time with using all the proper syntax as you suggest. If I get the syntax wrong again, please let me know.

Do you think it is a non-issue that the following template can lead to all sorts of weird race conditions when the loggedIn state changes?

html`
  <div>
    ${ when(loggedIn, 
         async(asyncContent, 'Loading content'), 
         'Please log in to see your content'
    )}
  </div>
`

If a Promise passed until()/async() rejects for any reason, it won't render

Presumably just that part not the whole template? No chance to render anything in it's place? Would it be left showing, for instance, a loading message / spinner for a failed fetch?

I think it's an issue for another directive, if any. Not async which is being discussed here as a replacement to the built in promise support.

It is a use case but I think tracking state across async calls is something we should have a dedicated issue for. As it probably affects more than the async directive too.

@CaptainCodeman It just won't render the result of the promise. The rest of the render will happen as expected.

@43081j It is not a general directive problem, it is specifically async that leads to undefined behaviour when combined with other primitives (any other primitive). Here's the same example, but without other directives this time:

html`
  <div>
    ${ loggedIn ?
         async(asyncContent, 'Loading content') :
         'Please log in to see your content'
    )}
  </div>
`

Do you still think it's not a problem that this produces unexpected results?

This problem is caused by the implementation of async, and it _cannot_ be fixed unless there is specific support for it from lit-html core, for example by adding state tracking, which is what @justinfagnani suggested.

Dynamically changing directives or composing directives doesn't work in general. It's not just a problem with async. Directives were intended to be relatively static - a part of the template - but because they're in expressions they have to be values, so they can be quite dynamic. This lead to us testing that certain dynamic conditions behave, but only a few situations have been covered by tests and it's still best that you don't change the directive at a part.

Where composition _does_ work is when a directive dynamically creates a part to host some content and what looks like composition is actually still a 1-1 part-directive situation. repeat() creates a new part per item for instance, so you can safely use directives as the return value from the mapper function. when() currently creates parts for both the true and false conditions, so that's safe too. That's pretty inside-baseball though, and I don't think it's be so easy to document that it's safe.

With the current behavior, if it's possible to require that a directive is only allowed at the top-level of an expression, we should do that. I think the de-thunking behavior that I added with the Part refactor was clever, but probably a mistake.

(to ruphins post:)

I don't think it's a problem for the async directive to tackle.

It exists to render a non-async value until an async one resolves.

It does that. Keep it simple.

Complex things like state tracking belong in another directive or somewhere else I would say. This directive answers all questions in the issue we are currently in, the original post.

This will simplify the core, not make it more complex.

It doesn't account for many edge cases really either, this discussion got blown up a little. It is an incredibly trivial directive to allow async values, if you want them (entirely optional).

Was this page helpful?
0 / 5 - 0 ratings

Related issues

MVSICA-FICTA picture MVSICA-FICTA  路  5Comments

Christian24 picture Christian24  路  4Comments

depeele picture depeele  路  3Comments

pjtatlow picture pjtatlow  路  3Comments

valdrinkoshi picture valdrinkoshi  路  5Comments