Status: DRAFTED

Champion: Unknown

Revision: latest

RFC created: 1970/01/01

Last updated: unknown

This RFC has been merged

Else and Else-If Directives

Summary

This proposal supersedes the existing if:true and if:else directives to add directives that express "else" and "else if" logic.

Basic example

<!-- component.html -->
<template>
    <template lwc:if={abra}>
        Abra!
    </template>
    <template lwc:elseif={kadabra}>
        Kadabra!
    </template>
    <template lwc:else>
        Alakazam!
    </template>
</template>

Motivation

As of today, without "else" or "else if" directives, the developer has to chain if:true and if:false blocks together:

<template>
    <template if:true={abra}>
        Abra!
    </template>
    <template if:false={abra}>
        <template if:true={kadabra}>
            Kadabra!
        </template>
        <template if:false={kadabra}>
            Alakazam!
        </template>
    </template>
</template>

This is awkward and unintuitive, and has an additional performance cost in that the property getters (in this case, abra and kadabra) are called multiple times, when once should suffice.

Prior art

The concept of "else" and "else if" exists in most programming languages, and in many other frameworks:

Some other frameworks, such as React, Stencil, and Lit, have no built-in concept of else or else if, other than that provided by JavaScript itself.

In the case of LWC, since we already have if:else and if:false, and since there is no ergonomic way to do conditional rendering in JavaScript (other than render(), which requires creating separate template HTML files), it would be most natural to align with the first group of frameworks.

Detailed design

The basic design is to add three new directives:

The reason for not reusing the existing if:true or if:else is that it is more consistent to use lwc:* for all three. (If there is a pressing need for negation, then we may consider a {!foo} or similar expression syntax in the future.)

Syntax

Both lwc:elseif and lwc:else must be immediately preceded by a sibling lwc:if or lwc:elseif.

<!-- Valid -->
<template lwc:if={foo}></template>
<template lwc:else></template>
<!-- Invalid -->
<template lwc:if={foo}></template>
<div>Yolo!</div>
<template lwc:else></template>

Whitespace is ignored when considering siblinghood. (This is consistent with LWC's current behavior, which is to remove whitespace between elements.)

Furthermore, these directives (along with if:true and if:false) are mutually exclusive, and multiple cannot be applied to the same element:

<!-- Invalid -->
<template lwc:if={foo} if:true={foo}></template>
<!-- Invalid -->
<template lwc:if={foo}></template>
<template lwc:elseif={bar} lwc:else></template>

The existing if:true and if:else directives cannot be combined with lwc:elseif or lwc:else:

<!-- Invalid -->
<template if:true={foo}></template>
<template lwc:elseif={bar}></template>
<!-- Invalid -->
<template if:true={foo}></template>
<template lwc:else></template>

Code comments

In the case of code comments, the behavior depends on whether lwc:preserve-comments is enabled or not. When not preserving comments, comments may appear between conditional siblings:

<!-- Valid -->
<template>
    <template lwc:if={foo}></template>
    <!-- Comment! -->
    <template lwc:else></template>
</template>

Whereas when lwc:preserve-comments is enabled, comments become syntactically meaningful and therefore cannot be placed between sibling conditional directives:

<!-- Invalid -->
<template lwc:preserve-comments>
    <template lwc:if={foo}></template>
    <!-- Comment! -->
    <template lwc:else></template>
</template>

Accessors

Similar to if:true and if:false, the expression passed in to lwc:if and lwc:elseif must use simple dot notation.

More complex expressions (e.g. !foo, foo?.bar?.baz, or foo % 2 === 1) are currently not supported.

Slots

A developer may want to use if/else-if/else chains to render a <slot>:

<template>
    <template lwc:if={foo}>
        <slot></slot>
    </template>
    <template lwc:else>
        <slot></slot>
    </template>
</template>

In this case, the template compiler should not warn about duplicate slots with the same name. This is a perfectly valid use case, and the template compiler knows for certain that the <slot> will not be rendered twice.

You might contrast this with the equivalent code in if:true / if:false:

<template>
    <template if:true={foo}>
        <slot></slot>
    </template>
    <template if:false={foo}>
        <slot></slot>
    </template>
</template>

Today, the template compiler does warn about duplicate slots for this case. But this actually makes sense, because the compiler cannot be certain that the <slot> will only render once. (For instance, the foo getter may have an inconsistent return value, returning true the first time and false the second time.)

Attribute values

To avoid typos (e.g. using lwc:else when you mean lwc:elseif), lwc:else must not have an attribute value. Any attribute value is treated as a compile-time error:

<!-- Valid -->
<template lwc:if={foo}></template>
<template lwc:else></template>
<!-- Invalid -->
<template lwc:if={foo}></template>
<template lwc:else={yolo}></template>

Similarly, lwc:if and lwc:elseif must have an expression as their value:

<!-- Valid -->
<template lwc:if={foo}></template>
<template lwc:elseif={bar}></template>
<!-- Invalid -->
<template lwc:if="foo"></template>
<template lwc:elseif="bar"></template>
<!-- Invalid -->
<template lwc:if={foo}></template>
<template lwc:elseif></template>

Semantics

lwc:if behaves the same as if:true. lwc:elseif behaves the same as lwc:if, except that its behavior depends on any preceding lwc:if/lwc:elseif statements.

Similarly, the behavior of lwc:else depends on its previous siblings.

For example, in a series of sibling lwc:if / lwc:elseif / lwc:else directives, the inverse of the previous evaluated expression determines whether we reach the next directive:

<template lwc:if={abra}>
    Abra!
</template>
<template lwc:elseif={kadabra}>
    Kadabra!
</template>
<template lwc:elseif={hocus}>
    Hocus pocus!
</template>
<template lwc:else>
    Kadabra!
</template>

Rather than rehash the basics of conditional logic in programming, let's define this as being analogous to the if statement (and friends) in JavaScript:

if (component.abra) {
  // Abra!
} else if (component.kadabra) {
  // Kadabra!
} else if (component.hocus) {
  // Hocus pocus!
} else {
  // Kadabra!
}

Note that this means:

  1. The property getters are only accessed once per instance of an lwc:if or lwc:ifelse.
  2. Property getter access is determined by the ordering. In the above case, if component.abra is truthy, then none of the other property getters will be accessed.

Drawbacks

This implementation would require the template parser to have knowledge of sibling nodes, rather than treating each sibling node independently.

This implementation would also supersede the existing if:true and if:false directives, which may lead to some confusion for developers.

Alternatives

The impact of not doing this is that it's much trickier to do conditional logic in LWC templates.

Adoption strategy

This is a non-breaking change, so users can opt-in as they see fit.

How we teach this

This feature is very similar to the same concept in other frameworks (Vue, Svelte, Angular, etc.). It's also similar to if and else in JavaScript. So we can leverage that familiarity when teaching this.

As for the existing if:true and if:else statements, we will need to mark them as "legacy," since they are rendered redundant by the new lwc:if. To ease the transition, we can provide:

  1. Make the template compiler warn about using if:true and if:else.
  2. Codemods to transform code that uses if:true to the equivalent lwc:if usages instead. It should be possible for this codemod to be consistent and non-destructive. (if:false is not so easy to replace, but it may be possible with a future, more complex expression syntax.) It may also be possible to replace an if:true={foo}/if:false={foo} chain with the equivalent lwc:if/lwc:else chain, but this is not 100% semantically equivalent (due to the number of accessor calls), and would be trickier to implement.

Unresolved questions

The existing if:true and if:false directives work on arbitrary elements, not just <template>s. Should lwc:if/lwc:elseif/lwc:else work the same way?

How does this affect the template AST?

undefined