Going Generic

In our case, what do generic Stimulus controllers look like?

What we have now is one controller that responds to a click by showing or hiding a target element and by changing the text of a target.

Let’s think a bit more abstractly, and what we have here are two separate actions that respond to a click: one that adds and removes a CSS class and one that changes the text of a button. we’re going to split those into two separate controllers to show how Stimulus allows for composition of small, generic pieces of action.

Now, there’s an obvious downside here, which is that the code and the HTML are going to get more verbose. Let’s look at the code and then talk about the upsides.

Both the generic Stimulus controllers are mostly subsets of our previous controller. Here’s the text one:

 import​ { Controller } ​from​ ​"stimulus"
 
 export​ ​default​ ​class​ TextController ​extends​ Controller {
 static​ targets = [​"elementWithText"​]
  elementWithTextTarget: HTMLElement
 
 static​ values = { status: Boolean, on: String, off: String }
  offValue: string
  onValue: string
  statusValue: ​boolean
 
  toggle(): ​void​ {
 this​.flipState()
  }
 
  flipState(): ​void​ {
 this​.statusValue = !​this​.statusValue
  }
 
  statusValueChanged(): ​void​ {
 this​.updateText()
  }
 
  newText(): string {
 return​ ​this​.statusValue ? ​this​.onValue : ​this​.offValue
  }
 
  updateText(): ​void​ {
 this​.elementWithTextTarget.innerText = ​this​.newText()
  }
 }

We’ve got three values: the status of the controller, text for when the status is “on,” and text for when the status is “off.” When the status changes, the “on” text is used if the status is true; otherwise, the “off” text is used. Both the text values are expected to be defined in the HTML markup as data attributes.

The CSS controller is similar:

 import​ { Controller } ​from​ ​"stimulus"
 
 export​ ​default​ ​class​ CssController ​extends​ Controller {
 static​ classes = [​"css"​]
  cssClass: string
 
 static​ targets = [​"elementToChange"​]
  elementToChangeTarget: HTMLElement
 
 static​ values = { status: Boolean }
  statusValue: ​boolean
 
  toggle(): ​void​ {
 this​.flipState()
  }
 
  flipState(): ​void​ {
 this​.statusValue = !​this​.statusValue
  }
 
  statusValueChanged(): ​void​ {
 this​.updateCssClass()
  }
 
  updateCssClass(): ​void​ {
 for​ (​const​ oneCssClass ​of​ ​this​.cssClass.split(​" "​)) {
 this​.elementToChangeTarget.classList.toggle(
  oneCssClass,
 this​.statusValue
  )
  }
  }
 }

The CSS controller only expects one value to add or remove that value, but it allows the value to be multiple CSS classes separated by a space. The updateCssClass method splits the value and adjusts the CSS list for each entry. (Again, a future version of Stimulus will likely do this for us.)

Now we can do a lot just in our HTML without writing more JavaScript. Here’s what the Show/Hide button looks like in the favorites list:

 <%=​ turbo_frame_tag(​"favorite-concerts"​) ​do​ ​%>
  <section class=​"my-4"
  data-controller=​"css"
  data-css-css-class=​"hidden"
  data-css-status-value=​"false"​>
  <div class=​"text-3xl font-bold"​>
  Favorite Concerts
  <button class=​"​​<%=​ SimpleForm.​button_class​ ​%>​​ py-1 text-xl font-semibold"
  data-favorite-toggle-target=​"elementWithText"
  data-controller=​"text"
  data-text-target=​"elementWithText"
  data-text-status-value=​"false"
  data-text-off-value=​"Hide"
  data-text-on-value=​"Show"
  data-action=​"click->css#toggle click->text#toggle"​>
  Hide
  </button>
  </div>
  <div data-css-target=​"elementToChange"​>
  <div class=​"text-xl font-bold"​ id=​"no-favorites"​>
 <%​ ​if​ current_user.​favorites​.​empty?​ ​%>
  No favorite concerts yet
 <%​ ​end​ ​%>
  </div>
  <div id=​"favorite-concerts-list"​>
 <%​ current_user.​favorites​.​each​ ​do​ |favorite| ​%>
 <%=​ render(favorite) ​%>
 <%​ ​end​ ​%>
  </div>
  </div>
  </section>
 <%​ ​end​ ​%>

The outer section declares the css controller and specifies that it will use hidden as the class to toggle. The inner span for the actual button declares the text controller, the off value of “Hide,” and the on value of “Show.” The button also declares itself as the target of the text controller, meaning it is the element whose text changes, and it declares that clicking on it triggers the toggle action in both the CSS and text controllers. Below, the div surrounding the actual value declares itself the target of the CSS controller, meaning it is the element that gets the hidden class added and removed from it. One other change here is that the button does not actually have HTML text in it—the Stimulus controller will call its statusValueChanged method when it connects and will set the text for us.

This works, and we can now adapt it around the page.

We can add a similar structure to the buttons for each day of the schedule, allowing those to be shown or hidden:

 <section data-controller=​"css"
  data-css-css-class=​"hidden"
  data-css-status-value=​"false"​>
  <h2 class=​"text-3xl font-bold"​>
 <%=​ schedule_day.​day​.​by_example​(​"Monday, January 2, 2006"​) ​%>
  <span class=​"​​<%=​ SimpleForm.​button_class​ ​%>​​ py-1 text-xl font-semibold"
  data-controller=​"text"
  data-text-target=​"elementWithText"
  data-text-status-value=​"false"
  data-text-off-value=​"Hide Day"
  data-text-on-value=​"Show Day"
  data-action=​"click->css#toggle click->text#toggle"​>
  </span>
  </h2>
 <%​ ​if​ show ​%>
  <section data-css-target=​"elementToChange"​>
 <%​ schedule_day.​concerts​.​sort_by​(&​:start_time​).​each​ ​do​ |concert| ​%>
 <%=​ render(concert) ​%>
 <%​ ​end​ ​%>
  </section>
 <%​ ​end​ ​%>
 </section>

This has nearly the same structure, with an outer element declaring the CSS controller and an inner button declaring the text controller and the actions. This now also works, with no additional JavaScript.

The calendar dates at the top of the page are meant to be part of a filter system that we’ll define more in Chapter 11, Managing State in Stimulus Code. But we do want to be able to show whether the filter is active with, say, a red border:

 <div class=​"text-center border-b-2 border-transparent"
  data-controller=​"css"
  data-css-css-class=​"border-red-700"
  data-css-status-value=​"false"
  data-css-target=​"elementToChange"
  data-action=​"click->css#toggle"​>
 <%=​ schedule_day.​day​.​by_example​(​"Jan 2"​) ​%>
 </div>

This element declares the CSS controller, with a CSS class of border-red-700, and it declares itself the target and the action. And now, clicking on one of those dates gives you a thin red border. Again, no new JavaScript.

The benefit of these small, generic controllers is that a very large amount of common, boilerplate interactions can be added to your site without any new JavaScript. This approach isn’t perfect. You might not like the use of data attributes, though I find I like the explicitness of them. Stimulus doesn’t currently support two instances of the same controller on the same element, so if you have multiple CSS actions on the same element, that’s going to be more complicated (nested elements might also be a problem). Like a lot of the Rails aesthetic, the goal here is to be able to do simple, common things with little to no code so that you can focus your code and time more precisely on the complex and uncommon things.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.22.100.180