Using Data Values for Logic

Let’s go back to our schedule page. At the top of the page is a run of calendar dates and the Show All button. Earlier, in Chapter 3, Stimulus, we added the CssController to make it so that these dates show and hide a red border to indicate which one is active. What we didn’t do at the time was wire that state up to allow those dates to act as a filter on the schedule display.

The functionality we want is as follows:

  • If none of the date buttons are active, all dates are shown.

  • If any of the date buttons are active, only the dates with active buttons are shown.

  • The Show All button returns all buttons to the inactive state, making all dates visible.

Two factors make this feature slightly more difficult than the previous Stimulus example. Whether an item displays is dependent on not just the state of that item, but also on the state of the group as a whole. Also, the buttons affect a part of the page that is outside their own subtree of the DOM.

You can manage this in Stimulus in a few different ways. Here’s the base HTML for the solution I picked (it’s in the schedule show page, surrounding the calendar header code):

 <div class=​"grid grid-cols-7 gap-0 mb-6"
  data-controller=​"calendar"​>
 <%​ @schedule.​schedule_days​.​each​ ​do​ |schedule_day| ​%>
 <%​ date_string = schedule_day.​day​.​by_example​(​"2006-01-02"​) ​%>
  <div class=​"text-center border-b-2 border-transparent"
  id=​"calendar-day-​​<%=​ schedule_day.​day_string​ ​%>​​"
  data-controller=​"css"
  data-css-css-class=​"border-red-700"
  data-css-status-value=​"false"
  data-css-target=​"elementToChange"
  data-calendar-target=​"calendarDay"
  data-schedule-id=​"day-body-​​<%=​ schedule_day.​day_string​ ​%>​​"
  data-action=​"click->css#toggle click->calendar#filter"​>
 <%=​ schedule_day.​day​.​by_example​(​"Jan 2"​) ​%>
  </div>
 <%​ ​end​ ​%>
  <div data-action=​"click->calendar#showAll"​>
  Show All
  </div>
 </div>

Here’s my rationale behind these choices.

There’s a new Stimulus controller, calendar, which is defined such that it contains all the calendar dates and the Show All button but does not contain the schedule displays for each date, which are lower on the page. I see two competing possibilities for the scope of the calendar controller: I could have chosen to make it bigger such that it incorporated basically the entire page, including both the date displays at the top and the schedule page at the bottom. Or I could have made the scope of the controller smaller such that each individual calendar date got its own controller instance.

I think that even if both the date and the schedule were inside the controller, I still would need logic tying each date to its schedule part, so it’s not clear to me what advantage I get from making the controller bigger. At the same time, having all the dates inside the same controller makes the logic for determining status based on the state of the group as a whole somewhat easier, so making the controller smaller seems like it would result in more complex code.

For each date inside the loop, this code adds two Stimulus attributes: data-calendar-target="calendarDay" to identify the date as a target of the calendar controller and data-schedule-id to link the element to the DOM ID of the schedule part displaying the same day. A second action, click->calendar#filter, was also added to the element. Remember, Stimulus guarantees that the second action won’t be fired until the first one is complete, so we know that the data-css-status-value targeted by the first action will have already flipped when we evaluate the second action.

Lower in the view, the “Show All” display is annotated to a different action, cleverly named click->calendar#showAll.

We need to add the expected DOM ID to the schedule day display in app/views/schedule_days/_schedule_day.html.erb:

 <section id=​"day-body-​​<%=​ schedule_day.​day_string​ ​%>
  data-controller="​css​"
  data-css-css-class=​"hidden"
  data-css-status-value=​"false"​>

And here’s the Stimulus controller that connects all those pieces:

 import​ { Controller } ​from​ ​"stimulus"
 
 export​ ​default​ ​class​ CalendarController ​extends​ Controller {
 static​ targets = [​"calendarDay"​]
  calendarDayTargets: HTMLElement[]
 
  everyDayUnselected(): ​boolean​ {
 return​ ​this​.calendarDayTargets.every((target: HTMLElement) => {
 return​ target.dataset.cssStatusValue === ​"false"
  })
  }
 
  filter(): ​void​ {
 const​ everyDayUnselected = ​this​.everyDayUnselected()
 this​.calendarDayTargets.forEach((target: HTMLElement) => {
 const​ show =
  everyDayUnselected || target.dataset.cssStatusValue === ​"true"
 const​ schedule = document.getElementById(target.dataset.scheduleId)
  schedule.classList.toggle(​"hidden"​, !show)
  })
  }
 
  showAll(): ​void​ {
 this​.calendarDayTargets.forEach((target: HTMLElement) => {
  target.dataset.cssStatusValue = ​"false"
 const​ schedule = document.getElementById(target.dataset.scheduleId)
  schedule.classList.toggle(​"hidden"​, ​false​)
  })
  }
 }

The filter action is the more complicated of the two actions. We start by using the DOM data attributes to determine the state of the system in the everyDayUnselected method. The everyDayUnselected method returns true if and only if the cssStatusValue for every one of the targets is "false". Unfortunately, we have to check against the string version of "false" because the value is part of a different controller; otherwise, we’d be able to use the Stimulus Values API to define the value as a Boolean and have Stimulus typecast it for us.

For each of our calendarDayTargets, meaning for each of the date displays, we determine if the corresponding schedule item should be shown. The answer is yes if the cssStatusValue is "true" or if every day is unselected. Then we use the regular DOM getElementById method to determine our matching schedule element and set a hidden class on that element. This works because we know that the click->css#toggle action has already happened, so we know that the cssStatusValue has already changed to its new value.

The showAll method similarly loops over all our calendar day targets. But instead of using the filter logic, it just sets the cssStatusValue of all of them to false. Doing so triggers the cssStatusValueChanged method of the CssController, which removes the border class we set. It doesn’t also automatically reset the corresponding schedule item, so we need to do that manually.

And this works, showing how we can use the DOM to store the data we need for more complex client-side effects.

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

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