feat: Template class object binding
Details
This PR implements the following RFC: Template class object binding. This proposal aims to improve the Developer Experience (DX) around managing components with complex styles by enabling developers to describe the classes applied to an element using JavaScript objects.
export default class CustomButton extends LightningElement {
variant = null;
position = null;
fullWidth = false;
stretch = false;
get computedClassNames() {
return [
"button__icon",
this.variant && `button_${this.variant}`,
this.position && `button_${this.position}`,
{
"button_full-width": this.fullWidth,
"button_stretch": this.stretch,
},
];
}
}
<template>
<button class={computedClassNames}>
<slot></slot>
</button>
</template>
This new feature will only be enabled for components running on API version 61 and above.
Does this pull request introduce a breaking change?
- ✅ No, it does not introduce a breaking change.
This change is guarded behind API versioning. Refer to the RFC for more details.
Does this pull request introduce an observable change?
- ⚠️ Yes, it does include an observable change.
LWC components that are always running on the latest LWC version (OSS and Salesforce internal components) will see a difference in behavior if they are already binding the class property with a non-string value. This change of behavior is discussed in the RFC.
GUS work item
No work item.
I am not an "official LWC maintainer" anymore, I don't want to merge it myself. Please merge this PR and release it at your convenience.
Merged with master. There was also a conflict with the recent static content optimization work (#4071), but I resolved those in 8c32821.
Summary of major breaking changes:
<template>
<div class={myClass}></div>
</template>
export default class extends LightningElement {
myClass = false
myClass = true // or
myClass = 1 // or
}
In the above cases, previously LWC would render:
<div class="false"></div>
<div class="true"></div>
<div class="1"></div>
Now, it will render:
<div class=""></div>
<div class=""></div>
<div class=""></div>
In short: due to the new class object binding, booleans and numbers are not directly converted into classes, but are stripped instead.
This may break a component if it is using a CSS selector like these:
.false {}
.true {}
[class="1"] {}
Or if it is using querySelector or other traversal techniques that rely on the class name:
this.template.querySelector('.false')
this.template.querySelector('.true')
this.template.querySelector('[class="1"]')
This can also cause breakages across component boundaries. For example, if a global style is relying on light DOM/synthetic shadow and thus the ability to pierce inside of components you don't own, then the above CSS selectors would break in that case as well.
This change also applies to Objects/Arrays/functions, which previously would be stringified into classes, but now will be subject to the new class object binding feature. (This is judged to be an unlikely source of breakages, because class="[object Object]" or class="function () {}" are not very useful classes.)
Overall, this is judged to be a very unlikely source of breakages, but component authors should be aware in case it does affect them, or their snapshots (e.g. in Jest tests).