Skip to content
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

Add [key]Classes method to better handle multiple CSS classes #344

Merged
merged 8 commits into from
Mar 24, 2021

Conversation

swanson
Copy link

@swanson swanson commented Dec 8, 2020

Closes #341

👋

Utility-first CSS is becoming increasingly popular as an alternative to BEM (the convention used in the Handbook). This PR adds the [key]Classes property that maps a space separated list of classes into an array.

This new property can be combined with the spread operator for more elegant classList manipulation when you need to change multiple classes at once.

Here is an adapted example from my production codebase:

// highlight_controller.js
export default class extends Controller {
  static targets = [ "subject" ]
  static classes = [ "pulse" ]

  pulse() {
    this.subjectTarget.classList.add(...this.pulseClasses)
  }
}
<div data-controller="highlight" 
     data-highlight-pulse-class="animate-pulse ring-8 ring-teal-600 ring-offset-8 rounded">

  <button data-action="click->highlight#pulse">
    Show me
  </button>

  <span data-highlight-target="subject">
     ...
  </span>
</div>

Pros:

  • Cleaner usage with utility CSS (e.g. Tailwind)
  • ${key}Classes plural API mirrors target conventions

Cons:

  • Inconsistency that fooTargets is mapped by multiple data-x-y-targets attributes, while fooClasses is mapped by space separated string (too much magic?)

I've added some basic test cases and happy to add docs if this is something you'd like added. Open to any and all feedback or alternatives :)

@swanson swanson changed the title Add [key]Classes method to better handle multiple classes Add [key]Classes method to better handle multiple CSS classes Dec 8, 2020
@swanson
Copy link
Author

swanson commented Dec 10, 2020

Would appreciate any input on this proposal @dhh -- thanks!

@javan
Copy link
Contributor

javan commented Dec 10, 2020

Makes sense to support utility-first CSS conventions, but overloading the same data attribute to mean one or many class names seems problematic. If you can't trust this.fooClass to return a single class name then you can no longer do element.classList.add(this.fooClass) safely. Updating the singular property to return the plural property's first item might work. That's more or less what target properties do under the hood.

@dhh
Copy link
Member

dhh commented Dec 10, 2020

@javan You don't like having fooClass just return the string and then fooClasses return an array where split has been called on the string? Then they're just two types of accessors. One always returns a string, the other always returns an array (even if it might only have one element, if there are no spaces).

@swanson
Copy link
Author

swanson commented Dec 10, 2020

Makes sense to support utility-first CSS conventions, but overloading the same data attribute to mean one or many class names seems problematic. If you can't trust this.fooClass to return a single class name then you can no longer do element.classList.add(this.fooClass) safely. Updating the singular property to return the plural property's first item might work. That's more or less what target properties do under the hood.

Yeah, I wasn't sure if it was preferable to have the singular property blow up when using classList APIs if you pass a string with spaces (to notify you that you very likely have a typo/bug) or have it silently not apply anything other than the first CSS class.

Is throwing a Stimulus error (not the raw DomException) an option? Personally, I think I'd rather have that happen but I can see wanting to minimize possible errors in the library.

@javan
Copy link
Contributor

javan commented Dec 10, 2020

You don't like having fooClass just return the string and then fooClasses return an array where split has been called on the string?

Using the example above:

<div data-controller="highlight" 
     data-highlight-pulse-class="animate-pulse ring-8 ring-teal-600 ring-offset-8 rounded">

I don't like this.pulseClass returning the full string "animate-pulse ring-8 ring-teal-600 ring-offset-8 rounded" because it's not a singular class name and will throw an exception if you classList.add(this.pulseClass). I think it should return the first class instead, "animate-pulse". Just like targets.

@dhh
Copy link
Member

dhh commented Dec 10, 2020

Yeah, that's a great point. I buy that.

@javan
Copy link
Contributor

javan commented Dec 10, 2020

Is throwing a Stimulus error (not the raw DomException) an option? Personally, I think I'd rather have that happen but I can see wanting to minimize possible errors in the library.

Singular properties should throw an error. They do currently: https://github.com/stimulusjs/stimulus/blob/11370bc37ac07753dbc4d676eebb8534bb03d3f8/packages/%40stimulus/core/src/class_properties.ts#L18-L26

Unsure if plural properties should do the same.

@swanson
Copy link
Author

swanson commented Dec 10, 2020

Singular properties should throw an error. They do currently:

Sorry, I was unclear. I meant should we throw a different error ("Expected a string without spaces, did you mean fooClasses?"). Sounds like returning the first class was the preferred option.

Unsure if plural properties should do the same.

Yeah, I'm not sure either. Plural targets do not check for missing the key (maybe that is intentional to support 0..N targets, not 1..N targets?).

But in the utility CSS use-case, it would be nice if Stimulus errored if you typo the key or omit it. You're unlikely to use the singular property if you are expecting multiple classes so you are skipping the missing attribute checks.

Trying to think if there is any use-case where you would intentionally want to pass in an empty string for the class value...

Copy link
Contributor

@javan javan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for taking this on! Requesting a few minor changes.

docs/reference/css_classes.md Outdated Show resolved Hide resolved
packages/@stimulus/core/src/tests/modules/class_tests.ts Outdated Show resolved Hide resolved
packages/@stimulus/core/src/class_map.ts Outdated Show resolved Hide resolved
packages/@stimulus/core/src/class_map.ts Outdated Show resolved Hide resolved
packages/@stimulus/core/src/class_map.ts Outdated Show resolved Hide resolved
@swanson
Copy link
Author

swanson commented Dec 14, 2020

@javan thank you for reviewing. I've made the requested changes.

@swanson swanson requested a review from javan December 14, 2020 20:02
@swanson
Copy link
Author

swanson commented Dec 28, 2020

Holler if there is anything else we want to do with this, I have some spare time during the holidays. Otherwise, I think it's ready for final review/merge. I can imagine the past few weeks have been pretty hectic so I appreciate the time to review this!

@javan
Copy link
Contributor

javan commented Dec 28, 2020

Thanks, @swanson! We got a little sidelined getting hotwire out the door, and most of us are out for holidays now. Will resume soon!

Base automatically changed from master to main January 12, 2021 20:57
@swanson
Copy link
Author

swanson commented Feb 1, 2021

👋 Friendly bump :)

@sstephenson
Copy link
Contributor

This is at the top of our list when we return to Stimulus later this month 👍

sstephenson added a commit that referenced this pull request Mar 24, 2021
Add [key]Classes method to better handle multiple CSS classes
@sstephenson sstephenson merged commit aa01760 into hotwired:main Mar 24, 2021
@sstephenson
Copy link
Contributor

Wonderful work on this all around, @swanson! Merged with a few revisions to the documentation: ce942f9

@janko
Copy link

janko commented May 20, 2021

For anyone wanting to use this functionality while waiting on the next Stimulus release, this is what I came up with:

// app/javascripts/controllers/application_controller.js
import { Controller } from 'stimulus'

export default class extends Controller {
  // TODO: Remove when https://github.com/hotwired/stimulus/pull/344 gets released.
  initialize() {
    this.constructor.classes.forEach(logicalClass => {
      Object.defineProperty(this, `${logicalClass}Classes`, {
        get() {
          return this.[`${logicalClass}Class`].split(' ')
        }
      })
    })
  }
}

Other controllers can subclass from this controller.

@marcoroth
Copy link
Member

You can also swap the stimulus dependency in your package.json to the latest dev-build, like so:

"stimulus": "https://github.com/hotwired/dev-builds/archive/stimulus/848e0c7.tar.gz"

@@ -9,3 +9,7 @@ export function capitalize(value: string) {
export function dasherize(value: string) {
return value.replace(/([A-Z])/g, (_, char) => `-${char.toLowerCase()}`)
}

export function tokenize(value: string) {
return value.trim().split(/\s+/).filter(content => content.length)
Copy link
Contributor

@tleish tleish Aug 31, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A simpler (and slightly faster) method of doing this is

return value.match(/[^\s]+/g)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please do add a PR 👍

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR #430

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

Successfully merging this pull request may close these issues.

Add [logicalName]Classes
7 participants