Search code examples
javascriptstimulusjs

Stimulus JS: Changing stimulus class values?


I have a Stimulus JS controller with a target called colorKey. ColorKey can be one of 5 different colors, set by adding 1 of 5 different CSS classes ('bg-green', 'bg-blue', etc)

From my reading of the css classes section of the stimulusJS docs, it seems I'm expected to update the color by using classList.add/classList.remove calls. That works, but it means I'm running:

this.colorKeyTarget.classList.remove(this.orangeClass)
this.colorKeyTarget.classList.remove(this.redClass)
this.colorKeyTarget.classList.remove(this.blueClass)
this.colorKeyTarget.classList.remove(this.blackClass)
this.colorKeyTarget.classlist.add(this.greenClass)

which isn't exactly elegant to put it mildly.

I was hoping/assuming I could do something like:

<div data-bg-color-class="bg-orange"></div>

in the dom, and then simply call:

this.bgColorClass = "bg-green"

instead. I assume there's some way to do something similar to the latter, but I can't figure out what it is. Anyone have any suggestions on best practices for assigning one of 5 classes to a DOM via stimulus in a way that doesn't involve removing every possible state before adding the one state you want?


Solution

  • For what you are trying to achieve, the Stimulus CSS Classes approach may not be the most suitable. It seems like you are setting up something more like a background preview picker.

    You could use classes here but as noted, it becomes a bit complex to add more and more options. Classes are better used for things like 'loading/active' classes, a small set of discrete states that can be reflected also by class name changes.

    Instead, you may want a Stimulus value approach here and something that is easier to map through to switch all things off except something that is selected.

    I recommend a Stimulus Object value, this is a JSON string that can be stored on the controller and can easily map key/value pairs.

    Example

    • In our HTML we set up the JSON value for all the color keys and their applicable classes. A reminder that this MUST be correctly formatted JSON and if you are using " for attributes, you will need to escape this correctly. Your rendering library should do this for you but if you are hand-writing this value, double check the output.
    • You can use the Stimulus action params also, making it easier to prepare a button or similar to toggle the colours.
    • Finally, I recommend revising the name of your target from colorKey to something simpler as you likely have the word color already in the HTML in many places.
    <div data-controller="bg-color" data-bg-color-classes-value='{"red": "something-red","blue":"something-borrowed","orange":"something-new","black":"bg-000000","green":"i-choose-green"}'>
      <div data-bg-color-target="container">
        CONTENT!
      </div>
      <button data-action="bg-color#show" data-bg-color-key-param="red">Go Red!</button>
      <button data-action="bg-color#show" data-bg-color-key-param="green">Go Green!</button>
    </div>
    
    • In the JS, we wull out the key from the show method either at the CustomEvent detail or the Action params. This could be done a few different ways, depending on how you plan to trigger things.
    • In the show method, we do a few checks for things before we attempt to mutate the DOM, returning early if something is missing.
    • We read out the entire Object value (Stimulus will convert this from JSON to an object), then map through the key/value pairs using Object.entries to either add the class or remove the class. This means that the only code needing to be changed if you wanted to add/remove colours is the HTML data attribute for the value.
    • The rest of the Controller is agnostic to how many colours you have, making this more reusable and flexible in the process.
    class BgColor extends Controller {
      static targets = ['container'];
      static values = { classes: Object };
      
      show(event) {
        const { key } = event?.params || event?.detail || {}; // destructure key from either Stimulus action params for dispatched event detail.
        const element = this.containerTarget;
        const classes = this.hasClassesValue ? this.classesValue : {};
        const currentColorClass = classes[colorKey];
        if (!currentColorClass || !element) return; // ignore unmatched class silently (could throw an error also)
        Object.entries(classes).forEach(([colorKey, value]) => {
          if (key === colorKey) {
            element.classList.add(value);
          } else {
            element.classList.remove(value);
          }
        });
      }
    }
    
    • Reminder: classList.add/remove will not work if you give a space separated string, it only will look at the first part of that string. If you expect that your classes will be something like "color color--blue" you will need to change your usage of classList.add/remove. Something like element.classList.add(...value.split(" ")); should work no matter what format the string comes in.