Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@JorisAerts
Copy link

@JorisAerts JorisAerts commented Oct 30, 2025

Fixes #4149
Fixes #3095
Fixes #2153 (while at it...)

This feature is fully backward compatible, the syntax is just extended to allow more cases, just like in other frameworks.

It also adds support for multi-class keys, like "foo bar".

It also supports passing nothing, which will would be the same a passing undefined, null or false. These negative parameters allow developers to pass object references, which may or not be falsy, depending on the context.

GIGO principle; none-string values which are not falsy or nothing, will get .toString()'ed, because just ignoring them would be like eating exceptions. They will also be split, so "[object Whatever]" will be two classes.

For example:

// original functionality still supported, of course
html`
  <input class="${classMap({
      'foo': this.isValid 
      'bar': this.isNotValid 
  })}">
`;

// support for spaces within class names
html`
  <input class="${classMap({
      'foo bar': this.isValid 
      'foo baz': this.isAlsoValid 
  })}">
`;

html`
  <input class="${classMap('foo bar')}">
`;

// support for regular (spread) classes
html`
  <input class="${classMap('class-1', 'class-2')}">
`;

// support for regular classes using (nested) arrays
html`
  <input class="${classMap(['class-1', 'class-2', ['class-3', 'foo bar']])}">
`;

// support for all types of combinations (object, array, single classes)
html`
  <input class="${classMap(
    'form-control', 
    'some-other-class', 
    { 
      'is-valid': this.isValid 
    }, 
    ['some-other-class-2'], 
    {
      'foo bar': this.isAlsoValid 
      'foo baz': this.isAlsoValid 
    }
  )}">
`;

Adding more parameters does not seem to break anything, because this directive can only be used as the single directive inside a class-attribute.
It would always just have one parameter in the past usage.

I haven't changed any documentation, I'm not sure I'm expected to, but I wouldn't mind either. There's a lot more to say about classMap, although I can't see any newly introduced unexpected behaviour.

@changeset-bot
Copy link

changeset-bot bot commented Oct 30, 2025

🦋 Changeset detected

Latest commit: 4c97750

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
lit-html Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@google-cla
Copy link

google-cla bot commented Oct 30, 2025

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@JorisAerts JorisAerts changed the title Feature/class map spreading classMap overloading and parameter spreading Oct 30, 2025
@justinfagnani
Copy link
Collaborator

Thanks for the PR @JorisAerts!

I worry a bit about the complexity and performance implications of making classMap() itself this expressive, vs allowing users to use whatever plain JS utilities they want to build up the ClassInfo object. We've already had report of classMap() being slow compared to string concatenation, and I'm wary of making it bigger and slower without some more benchmarking.

The recursive object walk can be done with object spread.

The nested array support is equivalent to

const classInfo = Object.fromEntries(classes.flat(Infinity).map((c) => [c, true]));

...${classMap(classInfo)}

and I'm concerned that people would use arrays unnecessarily, when they could just put static classes in the attribute next to the classMap() expression.

I'm more on board with the fixes for #3095 and #2153

@justinfagnani justinfagnani self-requested a review December 11, 2025 20:02
@JorisAerts
Copy link
Author

JorisAerts commented Dec 24, 2025

Thanks for the review @justinfagnani!

I based the functionality on how Vue.js handles the class attribute. In Vue.js, it's part of the internals, whereas in Lit you have to use classMap.
I also read into the issue about the String concatenation, but I don't see how you can create any function which evaluates user input, versus plain and simple string concatenation. This will always be slower, no matter what.

For performance, I extracted the regex to the module's global scope so it doesn't need recompiling every other time. For the recursion, I made one quite simple function that calls itself so it can get warmed up real quick.

Your solution with .flat maybe worth looking into to avoid recursion.

The reason this function is rather slow, I'd suspect, is possibly heavy DOM manipulation during update(....), because classes get added and removed one by one in for-loops, which causes a DOM manipulation for each class change upon each update. Complex CSS (using computed units like em, lh, ... for example) may require a lot unnecessary recalculation of the styles that way.
I'd also have to look into the JS engines to see how they optimize this, but normally speaking, when having the regex defined inside the function's scope would also have an impact on performance.

Next to that, each classMap-instance also holds reference to two separate Sets for calculating updates.

I would also look into how performance is measured. When long lists get rendered that use classMap, they may trigger — as stated above — a lot of separate DOM updates. Combine that with inefficient component updates at the user's side and you'll be running a lot of unnecessary logic.

If users want to use nested arrays, they now can at their own responsibility, it's not opinionated. That's the flexibility and responsibility I'd put into users' hands. The benefit is that — in a component framework for example — it's easier to compose class-objects and class-arrays using references that are managed elsewhere / in another scope. It enables composition, which was my goal here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants