We have two feature requests related to conditional rendering:
switch directive.We can resolve both of these requests by generalizing when to introspect it's arguments to operate in two modes:
if modeWhen the second argument is a function or TemplateResult, we treat the condition as a truthy value and render either the second or third argument.
html`${when(condition,
() => html`condition is true`,
() => html`condition is false`
)}`
switch modeWhen the second argument is an object, we treat the condition as simple value, and the second argument as a map of cases:
html`${when(value, {
'one': () => html`value is one`,
'two': () => html`value is two`,
default: () => html`value is neither`,
})}`
In switch mode, cases can only be keyed by strings, number and symbols. If in the future we want to lift this restriction, we can accept arrays:
html`${when(value,
['one', () => html`value is one`],
['two': () => html`value is two`]
)}`
But this seems better to leave off for now.
One of the benefits of handling both the if and switch use cases in one directive is that we don't have to choose two new names, since we can't name a variable if, switch or case. It should also simplify choices.
Currently when caches the DOM created by the true and false cases. This should increase performance on fast/frequently changing values, but this might not always be the case, and it increases complexity, code size and overhead. To make complexity pay-for-play we should remove caching from the when directive and add a separate cachingWhen directive. cachingWhen can locally be renamed to when when imported:
import {cachingWhen as when} from 'lit-html/directives/caching-when.js';
I had the same ideas when doing the initial implementation. I could take a stab at this if you werent already planning on doing it yourself?
It is not clear above what happens in relation to #511 it is it permissible to miss off the third argument? when in If Mode? I have lots of cases where I've had to add
() => html``
to my when directives
Also in switch mode, a missing default should also default to effectively an empty render.
@akc42 both of those would be the case.
I don't really understand the suggestion of removing caching from this directive. Given that condition ? truthy : falsy-like methods of conditional rendering are already lazy (see appended code), the only real feature of this directive is that rendered results are cached (when switching back and forth).
Removing the caching feature effectively renders this directive mostly useless, in my opinion.
tag = (strings, values) => console.log(strings[0]);
true ? tag`truthy` : tag`falsy`;
// logs 'truthy'
false ? tag`truthy` : tag`falsy`;
// logs 'falsy'
@ruphin we've had feedback that the directive-style control flow is easier to read for some people, and it translates to declarative flavors of lit (like, lit-in-html, which we have internal requests for right now, or heavily-lint-restricted lit-html that disallows arbitrary expressions, which we also have teams investigating internally).
when without caching satisfies those users, allows leaving off the failure condition, supports "switch" mode without caching, and it's one conditional construct to reach for, which is simpler for some people.
In the case of having both branches, if you don't care about the style, it absolutely isn't better than a ternary. Even in JSX though, some people use an <If> HOC 🤷♂️
That's fair. I think it's very reasonable to have a builtin helper to toggle between values. I like the suggested architecture of allowing both a binary toggle and a switch in the same directive.
We'll need a separate cached version as you suggested. Since the current implementation is already cached, perhaps @LarsDenBakker can look into modifying the current implementation to support the new switch mode and optional false value? I can write an implementation for a cacheless version, unless you already fixed this as well.
As a potential user of the switch form of this I am quite confused as to whether I should be using the cached or non cached version and what are the benefits of the cache v not having it. The decision above to default to a non caching version will mean that the majority of users will select that which might not be the right choice.
The scenario I am trying to envisage is a page switch at the top level of my application. For each page switch there will effectively at the top level only be one new element to instantiate, but as each one of those instantiates, it will trigger a rattle down a hierarchy of elements in their shadowRoot which will also have to be instantiated. This process will continue several levels deep (I think my biggest hierarchy is at least 8 levels deep and even at the bottom level I am referencing paper=* elements at the moment - soon to be replaced by mwc-* elements - which may go a level or so below that)
Does caching mean the difference between lit-html having to reparse the templates into strings and values before applying them v just applying them or am I misconstruing what the cache does.
If I am right - I would say that caching should be the default
Uncached when is basically sugar upon a ternary. These are identical:
render(html`${someCondition
? html`<div>its true</div>`
: html`<div>its false</div>`
}`, document.body)`
render(html`${when(someCondition,
() => html`<div>its true</div>`,
() => html`<div>its false</div>`
}, document.body}`
When someCondition switches, the dom nodes are re-created. If there are custom elements inside the templates, the connectedCallback is re-run. This is actually a good thing, caching can lead to unexpected behavior. Lit-html does not need to re-evaluate the templates etc. it's just starting in a clean dom container.
By default people should use the uncached version, and then only use the cached version when they have a need for it. For example when you have a location in your app where you are switching between states very frequently or when you are hiding/showing large dom trees. An app level router might want to consider caching, though I personally quite like to re-run all the lifecycle callbacks on page switching and clear all the states.
Even though the uncached version is just syntax sugar, I think it is much more readable. Especially for an if condition without else:
html`${when(someCondition, () => html`<div>Its true</div>`}`
is much better than this weirdness:
html`${someCondition ? html`<div>Its true</div>` : undefined}`
html`${someCondition ? html`<div>Its true</div>` : null}`
html`${someCondition ? html`<div>Its true</div>` : ''}`
I definitely want the lifecycle callbacks to run on the switch. I was planning on using these to initiate elements. The microsoft access application that my web app will replace/sit alongside uses a psuedo lock (a table in the database) when a user starts editing one of the key entities, and releases it when complete. Some database stored procedures interact with this to queue updates when changes have to be made in parallel, the unlocking then applies the queues updates.
My application has to do the same when a particular page is entered and when the user leaves it. At the moment I detect activation by a number of means (active attribute on the selected element, a distributed route passed in to the element which has an active property). I can replace this with a consistent use of connectedCallback and disconnectedCallback
Why would caching prevent these from running, they are taken out of the dom when held in the cache aren't they, and then replaced back again when reselected by the when? I need to understand and I don't yet! I suppose have to try it and single step through with the debugger - that normally helps me to understand these things.
Actually what I said isn't entirely correct. When caching, connectedCallback and disconnectedCallback are run but the constructor is not.
Consider this simplified example:
Non-cached:
// create and render element-a
const elementA = document.createElement('element-a');
document.body.appendChild(elementA);
// remove element-a, create and render element-b
document.body.removeChild(elementA);
const elementB = document.createElement('element-b');
document.body.appendChild(elementB);
// remove element-b, create a new instance of element -a
document.body.removeChild(elementB);
const elementA2 = document.createElement('element-a');
document.body.appendChild(elementA);
Cached:
// create and render element-a
const elementA = document.createElement('element-a');
document.body.appendChild(elementA);
// remove element-a, create and render element-b
document.body.removeChild(elementA);
const elementB = document.createElement('element-b');
document.body.appendChild(elementB);
// difference with the non-cached version is below
// remove element-b, don't create a new instance of element-a
// but re-append the elementA which was created above
document.body.removeChild(elementB);
document.body.appendChild(elementA);
As you can see in the second example, we keep a reference to the elementA variable. When re-rendering elementA we just append that one to the DOM again instead of recreating the nodes. Similar logic happens inside the when and cachingWhen directives.
Re-appending will run the connectedCallback again, but it won't create a new instance so not the constructor. Also any properties, inline styles, expanded/collapsed elements and menus etc. will all remain the way they were when using the cached version.
Yes - thats makes more sense now. It was what I was expecting. In my case the disconnectedCallback will reset all the properties that effect that sort of thing and then the next connectedCallback will change the properties to the new conditions so a new render will follow shortly afterwards to update the values.
Edited to add
My existing way of doing things of course doesn't even disconnect and reconnect the element so my logic has always had to handle the fact that the constructor will only be called once.
firstUpdated is normally where I create some variable like this.dialog = this.shadowRoot.queryselector('#dialog'); Presumable if the constructor is not called again in the caching case, the state the lit-elements UpdatingElement holds will not have been updated and so firstUpdated will not get called again. Does this mean the variables will no longer be pointing at the correct place?
Edited to comment on the firstUpdated question.
I think this.dialog points to the Object that is the element it points to. As is pointed out those objects still exist in the cache and are not recreated. So this.dialog still points to the correct element even thought its been removed and then replaced into the dom.
So we'e been talking on the team about the right factoring to have for a number of different features that the über when() directive have:
It may be convenient to get all of these in one place, but when trying to figure out exactly how it should work, it all depends on why a user used then directive in the first place. It could be a user reached for a syntax they liked, and didn't know they were getting caching and it's memory overhead. It seems dangerous to tie all these together, and see the discussion here about caching by default or not.
It's useful to ask why when() is a directive at all. It's really only for the caching which needs a part reference for keying state and getting direct access to the DOM. If not for that when() could be a simple function.
So... what we're considering now is just offering a cache() directive. It would keep DOM in a cache keyed off the Part and TemplateResult.template. We can then use cache() with any control flow expressions, including ternaries, Array.find, app-logic in methods/functions, or very simple if/switch/pick utilities.
We'd probably _not_ vend a when() function, because it's sole use would be for syntax/laziness on the JS side - ie, it'd be solving a JavaScript problem in general, not a templating problem, and thus be out of scope for lit-html. It'd also be very trivial to create.
Some examples:
// caching ternary
html`${cache(v ? html`true case` : html`false case`}`
// switch as object notation:
html`${cache({
one: html`case one`,
two: html`case two`
}[v]}`
// switch as object notation:
html`${cache([
['one', () => html`value is one`],
['two': () => html`value is two`],
].find(([c]) => c === v)}`
// use a method
html`${cache(this.getPage(routeName))}`
// using lodash cond
html`${cache(_.cond([
[_.matches({ 'a': 1 }), () => html`matches A`],
[_.conforms({ 'b': _.isNumber }), html`matches B`],
[_.stubTrue, () => html`no match`]
]))({ 'a': 1, 'b': 2 })}`
This generalizes better, and we can add back when(), but a much simpler version, if there's a need for it.
Thoughts?
Yes. This exactly echoes my original sentiment on caching being the only reason a when directive is needed.
It is a great idea to rename the directive to cache, since that name is more in line with its intended function. The provided examples all look very nice and clean, and I like how the cache directive itself makes no assumptions on the structure of its arguments, it simply gets passed a TemplateResult, and applies caching when passed a different one.
Thumbs up from me
I like the cache approach better as well.
I do think the uncached when is something that would be useful to offer from the lit-html library as a utility/helper function. It's a pattern that is encountered by a lot of users writing lit templates, so it will be helpful for promoting writing elegant code.
I like this idea.
Can I presume this would be a valid approach to handling the "no false" case?
${condition ? cache(html`<section>some html only used if condition is met</section>`): ''}
I don't think I agree with @LarsDenBakker about using directives as a pattern. In fact perhaps the repeat directive can also be replaced with
html`
<ul>
${myArray.map(item => cache(html`<li>Loop using ${item.name}</li>`))
</ul>
`
If I understand this correctly (ie my examples hold up) this should be the pattern that users should be encouraged to use
As I understand more about caching v not (thanks @rupin I watched a video of your yesterdays seminar and it help a lot) I think there are subtle differences been caching and not caching. I think my example of doing away with the repeat directive may be flawed - in that the cache is about storing a complete hierarchy of nodes not a template result
What if that example were written like
html`
<ul>
${cache(myArray.map(item => html`<li>Loop using ${item.name}</li>`))}
</ul>
`
I think it important that what a cache is doing and not doing should be quite carefully documented around this particular directive as I don't think it is easily understood
cache directiveWhat the cache directive does is enable a Part to switch between different templates without destroying and re-creating them. Take the following example:
html`
<div class="user">
${ userLoggedIn ?
html`<span>${ userName }</span>` :
html`<a href="/login">Log in</a>`
}
</div>
`;
Here, the part inside div.user will render one of two different TemplateResults, depending on the userLoggedIn variable. When userLoggedIn changes, the part will _destroy_ the old DOM, and render the new template.
If we use the proposed cache directive, instead of destroying the old DOM when the userLoggedIn variable changes, it will move that DOM into a "cache". Then, if the variable changes again, it can take that cached DOM and place it back. So the purpose of the cache directive is to be able to switch between different templates without having to destroy and rebuild the DOM each time.
In order to do this, the cache directive needs to wrap _all_ the options that the part could render, so the first example you posted would require a small syntax change:
${ cache( condition ? html`<section>conditional content</section>` : '' ) }
Note that the only added benefit of the cache directive here is if condition would change multiple times, and you want to avoid re-creating the DOM for the conditional content.
One example where this is likely to happen is with pages. You could implement pages like this:
const loginPage = () => html`<div> ...`;
const mainPage = () => html`<span>...`;
const pages = {main: mainPage, login: loginPage}
// In your rendering loop:
render(html`${ cache( pages(currentPage)() ) }`, document.body)
This way you don't destroy pages when you navigate away from them, which makes switching back and forth between pages more efficient.
The purpose of cache is to be able to switch between different templates without destroying and rebuilding DOM. It probably should only accept a single argument like a TemplateResult or a literal, and not arrays or other composed values, because it is very difficult to reason about what it should do in those cases. Note that it is still possible to cache switching between different lists, by wrapping them in a template:
html`
${ cache( condition ?
html`${ items.map( item => html`<div>${ item.name }</div>` )}` :
html`${ items.map( item => html`<span>${ item.name }</span>` )}`
)}
`;
repeat directiveThe repeat directive can not be replaced by the cache directive, as they perform two different functions. The purpose of the repeat directive is to manipulate the _order_ of rendered items with maintaining the identity of the rendered items. In other words, when I use repeat to render items in a list (e.g. [1, 2, 3]), and in a subsequent render I change the order of the items ([1,3,2]) the repeat directive will move the existing rendered DOM nodes around to match the order of the items in the list. This is nothing to do with switching between different templates.
The cache directive on the other hand has no concept of ordering, and all it can do is switch between different templates whilst maintaining the old DOM in a cache. This is a totally unrelated task, and the two directives each have a different unrelated function.
I am still a fan of the nothing sentry; a value that can be passed to signal that the Part should render no content. Similar to the noChange sentry, which signals that the Part should not change the currently rendered content.
It was discussed previously, but I'm not sure if it was ever implemented. It allows things like this:
html`
${ condition ? html`<div></div>` : nothing }
<div attr=${ condition ? 'value' : nothing }></div>
`
It handles all the different cases where you need to pass either null, undefined, or '' (emptystring) to get the desired result. It clearly signals what it does, and it looks fairly elegant to me.
Then, if the variable changes again, it can take that cached DOM and place it back. So the purpose of the cache directive is to be able to switch between different templates without having to destroy and rebuild the DOM each time.
But the template in that cache will still be evaluated / updated ... right? Otherwise that might be confusing and a source of bugs or unexpected content.
Given this example:
${ cache(userLoggedIn
? html`<span>${ userName }</span>`
: html`<a href="/login">Log in</a>`)}
If the userLoggedIn flag was true, so userName was rendered and then both were changed and then userLoggedIn changed back to true, the new value would be rendered ... wouldn't it?. Or does it effectively make the whole node branch static?
Yes, the template will be evaluated and updated as usual. The only difference is that instead of cloning a new section of DOM from the template, and rendering the values into that new DOM, it re-uses the old DOM that was previously rendered in that place.
Again, normally when you switch between two templates (imagine the above example without the cache directives), all existing DOM nodes are _destroyed_. So when you switch around and render a template that you had previously rendered, those DOM nodes no longer exist, and the Part will have to clone the template again to create new DOM, and render the values into that. The cache directive makes it so that cloning the template again is no longer necessary, because it keeps the previously rendered DOM around instead of deleting it, and uses that to render the values to.
Remember that with lit-html, _updating_ values is really cheap, because it re-uses the static parts of the DOM, but _creating_ the DOM in the first place is just as expensive as with other systems. So if you keep destroying and creating DOM, it's about as efficient as using innerHTML.
@ruphin As I was writing my entry above I kept getting stuck with this difference between cached and non cached nodes. When a TemplateResult is created a dom-tree with all the Parts replaced with a comment node is created and attached to a <template> tag which is then stored in the TemplateResult So when you switch around during re-render you say all the DOM notes are _destroyed_ this isn't entirely true (is it)? Some (most? - certainly in our examples above most are) of the parts are themselves TemplateResults and they too will have a dom-tree attached to them.
So recreating a huge hierarchical dom-tree will involve fetching lots of partially cached existing dom-nodes and joining them together. I don't know if that is expensive?. Only at the very leafs of the tree is it likely there will be some parts that create dom nodes from scratch - and those are ones that are just as likely to need changing again.
In @CaptainCodeman 's example above the only node being recreated is the text node of the username and that likely will be changing anyway.
So in theory caching sounds nice but is it in practice.
That is actually not correct.
TemplateResults do not have DOM trees attached to them, they only have a reference to the Template they represent. Only a TemplateInstance actually holds a part of DOM. In the example above, without the cache directive, the entire DOM associated with the template has to be re-instantiated, including the <span>.
When rendering templates, DOM nodes are _only_ re-used when you render the same template. If you render a different template, it destroys the old DOM, and instantiates the new DOM from the new template.
Take the following example:
html`
<div>
${ condition ?
html`<p>Hello ${ name }</p>` :
html`<h1>Goodbye ${ name }</h1>`
}
</div>
`
When you render twice, and condition stays the same, the only thing that changes is the content of the TextNode that represents the name variable. When a Part renders a TemplateResult with the same Template as the previously rendered TemplateResult, it will keep the DOM and only update the values inside.
When you render twice, and condition changes, all the DOM nodes inside the <div> will be destroyed (Including the <p> or <h1> nodes), and a set of new nodes will be cloned from the Template that is being rendered. Then, the name variable will be rendered into the right TextNode. It doesn't matter how many times you switch between condition being true or false, it will always destroy the old nodes and re-clone from the template. When a Part renders a TemplateResult with a different Template as the previously rendered TemplateResult, it always destroys all currently rendered content, clones the Template from the new TemplateResult, inserts that into the tree, and updates the values into it.
The cache directive makes it so that the Part will not destroy the currently rendered content, and instead place it in a cache to be re-used if it is rendered again in the future.
Thanks - now I understand that - I must go and read the code again - I misunderstood.
For what it's worth, trying to write about this, and it is far, far harder to write about cache comprehensibly than the the caching when. Caching when is a logical extension of the ternary, and it's easy to explain that it caches the DOM for both branches.
cache is really logical... If you understand Part and TemplateResult.
Not sure if I have a conclusion here, except that I'm going to move this out of the section on conditionals and document it somewhere else.
Any updates to the cache coming soon - both #565 and #606 are causing me issues and at the moment I have to patch my version of lit to fix one and use a workaround for the other. The when directive goes away when cache comes so almost no point in fixing the above two.
Most helpful comment
So we'e been talking on the team about the right factoring to have for a number of different features that the über
when()directive have:It may be convenient to get all of these in one place, but when trying to figure out exactly how it should work, it all depends on why a user used then directive in the first place. It could be a user reached for a syntax they liked, and didn't know they were getting caching and it's memory overhead. It seems dangerous to tie all these together, and see the discussion here about caching by default or not.
It's useful to ask why
when()is a directive at all. It's really only for the caching which needs a part reference for keying state and getting direct access to the DOM. If not for thatwhen()could be a simple function.So... what we're considering now is just offering a
cache()directive. It would keep DOM in a cache keyed off the Part and TemplateResult.template. We can then usecache()with any control flow expressions, including ternaries, Array.find, app-logic in methods/functions, or very simple if/switch/pick utilities.We'd probably _not_ vend a
when()function, because it's sole use would be for syntax/laziness on the JS side - ie, it'd be solving a JavaScript problem in general, not a templating problem, and thus be out of scope for lit-html. It'd also be very trivial to create.Some examples:
This generalizes better, and we can add back
when(), but a much simpler version, if there's a need for it.Thoughts?