title | status | created_at | updated_at | champion | pr |
---|---|---|---|---|---|
Template class object binding |
DRAFTED |
2024-01-04 |
2024-01-04 |
Pierre-Marie Dartus (pmdartus) |
None |
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>
Manipulating an element's class list is a very common use case for UI frameworks. Since class
is a standard Element attribute, component authors currently bind those attributes with a string value. However, generating complex class names through string concatenation can be hard to read and error-prone. This is the reason why this RFC proposes enhancements to the element class
attribute binding.
Popular UI frameworks offer such capability out of the box:
- Vue: Use the standard
class
HTML attribute as described in this proposal. - Svelte: Use a custom template directive called
class:name
, to conditionally apply classes. - Angular: Use a custom template directive called
[ngClass]
, with a similar API as described in this proposal.
It is also worth mentioning that React has no built-in support for conditional style application. However, most React applications use the classnames NPM package to fill this gap (6.3M weekly downloads).
The classSet
utility method exposed from the Salesforce internal lightning/utils
module offers similar capabilities as the public classnames NPM package. This utility function is used by +500 components in Salesforce's internal code base. As this LWC module isn't exposed on the platform, platform components have to re-implement such utility methods for managing complex styles.
As a side note, modern UI frameworks also have built-in capabilities to manipulate element styles. This capability is out of the scope of this RFC and will be the subject of a follow-up proposal.
This RFC proposes to change the semantics of the template class
attribute. On top of accepting a string value, the class
attribute would now accept objects (plain objects and arrays) representing the CSS classes applied to the elements. The LWC engine would then interpret the object to determine which class names should be applied to the element.
The list of class names applied to a given element is defined as follows:
- If the value is a
string
, apply all the classes in the value. - If the value is an
array
, iterate over its entries and apply this algorithm recursively for each of its entries. - If the value is an
object
, iterate over the enumerable string properties own by the object (excluding symbols and properties on the proto-chain) and apply the classes with the keys associated with a truthy value. - Ignore all other values:
null
,undefined
, other primitive values, and complex objects.
Input | Output |
---|---|
'foo bar' |
'foo bar' |
{foo: true, bar: false, 'fiz buz': true } |
foo fiz buz |
{truthy: 1, falsey: '', alsoTruthy': Symbol() } |
truthy alsoTruthy |
[false, 'bar', null] |
'bar' |
['foo', { bar: true }, ['baz']] |
'foo bar baz' |
Is this a breaking change?
In theory, this proposal doesn't introduce a breaking change. However, it introduces a user-land observable change. Even if the class
attribute only accepts string
values today, the LWC engine doesn't warn or throw when passing a value of a different type. When passing a non-string value, the LWC engine automatically coerces the value to a string.
Input | Before | After |
---|---|---|
{ foo: true, bar: false } |
'[object Object]' |
'foo' |
[false, 'foo', null] |
'false','foo', |
'foo' |
As a side note, it's possible to match against an element with its class attribute set to
[object Object]
using the.\005Bobject
CSS selector. 🤯
Because of this change in behavior, this change will be conditionally enabled using LWC versioning.
This new feature will be gated through LWC built-in API versioning. Rolling it through API versioning would at the same time prevent breaking changes from sneaking in and also offers an additional incentive for developers to bump their components API version.
As it is a new backward-compatible feature, updating the LWC documentation and sample apps should be sufficient. The documentation should also encourage developers to use this feature when dealing with complex class names as it is more maintainable and less error-prone compared to string concatenation.
None at this time.