New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Template class object binding #3950
base: master
Are you sure you want to change the base?
Conversation
@@ -1,4 +1,4 @@ | |||
<template> | |||
<!-- style attr is validated at compile time --> | |||
<p class={num} data-attr={num}>{num}</p> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove the class
from the hydration test mismatch as before this change the class={123}
was serialized to class="123"
but after this change, the same expression gets serialized to class=""
.
As far as I can tell, we are only running the hydration suite for the latest API version. Because of this, there is no way to check the before and after behavior. That said, I don't think this change reduces the coverage of this test as it already checks attribute coercion with the data-attr
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we want class={123}
to become nothing, rather than a string? 123
is not an "empty" value, so my baseline expectation is that it would be shown, rather than dropped. Or, if we definitely don't want to allow non-string values, we should throw a TypeError.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@wjhsf If possible, I would prefer to avoid making assumptions about what the component author intended.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The real question here is whether or not the engine should surface an error when the class
receives a value that is neither a string, an array nor a JavaScript plain object. I would use "least astonishment principle" here and follow the semantics as defined by other popular UI frameworks and libraries:
- Vue ignores all non-string, array, and objects in both devs and production.
classnames
coerce number to string (commit), but ignore other objects in dev and prod.
If we take a step back, I don't expect developers to define class names as numbers. Developers are asking for trouble if they are doing this. For this reason, my recommendation would be to stick to Vue's semantics regarding number handling.
All of the frameworks and libraries silently ignore all the "invalid" class name values in both dev and prod today and I haven't seen any pushback from the community. It should be safe for LWC to do the same for now. That said, if we see developers struggling with this, we can always add warnings in dev mode and turn this warning into an error in the next major version.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This behavior is surprising to me. In the current implementation, numbers are coerced to strings for both class
and data-foo
:
<h1 data-foo={num} class={num}>Hello world!</h1>
Renders:
<h1 class="123" data-foo="123">Hello world!</h1>
Rendering to an empty string definitely seems wrong to me, and it seems wronger to have a hydration mismatch. We have plenty of confusion already with hydration mismatches; IMO we shouldn't be adding on to them (as well as breaking existing functionality unnecessarily). /cc @divmain
(I agree that numbers as class
es is rare BTW. But I still don't see a reason to break it.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After some thought, I think we should just fix the hydration mismatch issue. A developer may want to have an "invalid" value here, e.g. a value that is either "foo"
or false
(e.g. someCondition && "foo"). The framework should be smart enough to recognize that the server rendered the correct thing if the value is
false`.
@@ -1,4 +1,4 @@ | |||
<template> | |||
<!-- style attr is validated at compile time --> | |||
<p class={num} data-attr={num}>{num}</p> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@wjhsf If possible, I would prefer to avoid making assumptions about what the component author intended.
packages/@lwc/integration-karma/test/template/attribute-class/object-values.spec.js
Show resolved
Hide resolved
packages/@lwc/integration-karma/test/template/attribute-class/object-values.spec.js
Show resolved
Hide resolved
packages/@lwc/integration-karma/test/template/attribute-class/object-values.spec.js
Outdated
Show resolved
Hide resolved
for (let i = 0; i < value.length; i++) { | ||
const normalized = ncls(value[i]); | ||
if (normalized) { | ||
res += normalized + ' '; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for (let i = 0; i < value.length; i++) { | |
const normalized = ncls(value[i]); | |
if (normalized) { | |
res += normalized + ' '; | |
} | |
} | |
res += ArrayMap.call(value, ncls).filter(Boolean).join(' '); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't have a strong opinion on this @ravijayaramappa. At this point, it is unclear to me whether or not this approach is more performant (new array allocation + extra invocations in the filter). The only thing I can say is that it is less readable than the current implementation.
@nolanlawson Any additional thoughts on this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don’t have a strong opinion either. My suggestion was to make the code concise with native Array APIs. I am fine either way 🙂
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add me to the list of people without a strong opinion on this, but I will say that both versions are equally readable for me 😆
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for (let i = 0; i < value.length; i++) { | |
const normalized = ncls(value[i]); | |
if (normalized) { | |
res += normalized + ' '; | |
} | |
} | |
res += ArrayReduce.call(value, (acc, item) => { | |
const normalized = ncls(item); | |
return normalized ? `${acc} ${normalized}` : acc; | |
}, '') |
Since there are so many strong opinions, here's a third option! Should be more performant than map/filter/join, but I don't know how it compares to the for loop. It's probably the least readable, though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A for
loop is always fastest. In fact we recently refactored an Array.prototype.reduce
to a for
loop to get some perf gains: #3729
Normally I would not care, but this code is in the hot path, so we should prefer the for
loop.
I am not an "official LWC maintainer" anymore, I don't want to merge it myself. |
@@ -2,7 +2,7 @@ | |||
"files": [ | |||
{ | |||
"path": "packages/@lwc/engine-dom/dist/index.js", | |||
"maxSize": "22KB" | |||
"maxSize": "22.10KB" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not super important, but I wonder if any of the suggested implementations for ncls
would avoid the need for this size bump.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@wjhsf This test really exists to catch unintended size increases, so this increase isn't an issue.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's why I prefaced with "not super important" 😜
packages/@lwc/integration-karma/test/template/attribute-class/object-values.spec.js
Outdated
Show resolved
Hide resolved
testClassNameValue('false', false, isObjectBindingEnabled ? '' : 'false'); | ||
testClassNameValue('null', null, isObjectBindingEnabled ? '' : ''); | ||
testClassNameValue('undefined', undefined, isObjectBindingEnabled ? '' : ''); | ||
testClassNameValue('number', 1, isObjectBindingEnabled ? '' : '1'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd like to see 0
and NaN
here as well for completeness, since they are falsy.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have some remaining minor nitpicks about tests, but overall this LGTM.
I think we should do this as part of 7.0.0 with API versioning to 1) avoid breaking changes, and 2) to offer a carrot for API versioning upgrades.
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.
This new feature will only be enabled for components running on API version 61 and above.
Does this pull request 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?
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.