A spreadsheet like controller for a table with two controllers with shared target

Since I didn’t get any opinions on last post /one-controller-and-multiple-repeating-target-or-multiple-controller-for-each-area(Is anyone using this site? Is Stimulus.js still alive?) I decided to try another approach.

Since most of my work is using accounting like applications, I have a lot of tables (ledgers etc). One of the applications I’m rewriting deals with keeping scores for a golf groups. You can look at a golf score card in visualize a spreadsheet. You put your scores down for each hole, sum the front and back and come up with a total score. We play teams so there are three or four players on the card, and there our four or so teams. Each team turns in their card with scores (front, back and total) and someone wins.

The scores are then entered into a Rails application and new quotas or a handicap is computed. That’s the simplified version of ptgolf.us. The rewrite is de-jquery-ing what little coffeescript I had. One involved entering the scores. I basically display a golf scorecard as a table where the teams are scored by entering what each teammate scored on the front and the back and computes a total. That info is converted to a Game model that has a Round model to post the scores, recompute quotas, who won, how much, etc.

What I ended up with is a page where the form has a scoreTeam controller and a scoreMate controller on each teammate table row.

The scoreMate controller is were the scores are entered and the scoreTeam(s) controller summarizes the results.

The controllers

// scoreMate.controller
import { Controller } from "stimulus"

export default class extends Controller {
  static targets = ["team","mateFront","mateBack","mateTotal","mateQuota","matePM"]

  connect() {
    // console.log("score mate")

  }

  front(){
    const f = this.frontVal
    const b = this.backVal
    if (b != 0 && f != 0) {
      const t =f + b
      this.mateTotalTarget.value = t
      this.matePMTarget.innerHTML =  t - this.quotaVal
      this.fireCompute()
    }

  }
  back(){
    const f = this.frontVal
    const b = this.backVal
    if (b != 0 && f != 0) {
      const t =f + b
      this.mateTotalTarget.value = t
      this.matePMTarget.innerHTML =  t - this.quotaVal
      this.fireCompute()
    }
  }

  fireCompute(){
    var id = "teamUpdate"+this.teamTarget.value
    document.getElementById(id).click()
  }

  get frontVal(){
    const toNum =  Number(this.mateFrontTarget.value)
    if (isNaN(toNum)) {
      return(0)
    }else{
      return(toNum)
    }
  }

  get backVal(){
    const toNum =  Number(this.mateBackTarget.value)
    if (isNaN(toNum)) {
      return(0)
    }else{
      return(toNum)
    }
  }
  
  get quotaVal(){
    const toNum =  Number(this.mateQuotaTarget.value)
    if (isNaN(toNum)) {
      return(0)
    }else{
      return(toNum)
    }
  }

}

The scoreMate controller uses information from the Team model and only inputs two values (front, back) and computes the total

The scoreTeam controller

\\ scoreTeam.controller
import { Controller } from "stimulus"

export default class extends Controller {
  static targets = [ "team","teamQuota","teamFront","teamBack","teamTotal","teamPM","mateFront","mateBack","mateTotal" ]

  connect() {
    // console.log("score team")
  }

  compute(){
    // console.log("something fired and I have to got to work")
    var tback = this.teamBackTarget
    var tfront = this.teamFrontTarget
    var ttotal = this.teamTotalTarget
    var tPM = this.teamPMTarget
    const quota = Number(this.teamQuotaTarget.value)
    const side = quota/2.0
    const mfront = this.front
    const mback = this.back
    const mtotal = mfront + mback

    tfront.innerHTML = `${mfront}/${mfront - side}`
    tback.innerHTML = `${mback}/${mback - side}`
    ttotal.innerHTML = `${mtotal}/${mtotal - quota}`
    tPM.innerHTML = mtotal - quota
  }

  get front(){
    const mates = this.mateFrontTargets
    var total = 0
    for (var i = 0;  i < mates.length; i++) {
      const toNum =  Number(mates[i].value)
      if (isNaN(toNum)) {
      }else{
        total += toNum
      }
    }
    return total
  }

  get back(){
    const mates = this.mateBackTargets
    var total = 0
    for (var i = 0;  i < mates.length; i++) {
      const toNum =  Number(mates[i].value)
      if (isNaN(toNum)) {
      }else{
        total += toNum
      }
    }
    return total
  }
}

The scoreTeam controller basically just sums the scores for each team. The two are line using somewhat of a kludge where after each teammate score is entered it trigger a click event on a div that is in the scoreTeam scope,

The Rails code that generates the table (simplified test version and I use slim)


- teams = [[{n:'joe',q:28},{n:'john',q:22},{n:'jim',q:18}],[{n:'tom',q:31},{n:'dick',q:24},{n:'harry',q:15}]]
- tquota = []

- teams.each do|t|
  - q = 0
  - t.each do |m|
    - q += m[:q]
  - tquota << q

- teams.each_with_index do |team,i|
  hr.underline
  table.small-table.w60p[data-controller="scoreTeam"]
    tr
      th colspan="3" 
        = "Team - #{i+1} Quota:#{tquota[i]} Side: #{tquota[i]/2.0} Hole: #{(tquota[i]/18.0).round(2)}"
        = hidden_field_tag("team[#{i+1}][quota]",tquota[i],data:{target:"scoreTeam.teamQuota"})
        div[id="teamUpdate#{i}" style="display:none;" data-action="click->scoreTeam#compute"]
      th[data-target="scoreTeam.teamFront"]
      th[data-target="scoreTeam.teamBack"]
      th[data-target="scoreTeam.teamTotal"]
      th[data-target="scoreTeam.teamPM"]
    tr
      th Name
      th Quota
      th Side
      th Front
      th Back
      th Total
      th &#177;
    - team.each_with_index do |tm,m|
      tr[data-controller="scoreMate"]
        - side = tm[:q]/2.0
        - opt = pulled_options(tm[:q])

        td 
          = tm[:n]
          = hidden_field_tag('team',i,data:{target:"scoreMate.team"})

        td
          = tm[:q]
          = hidden_field_tag("mate[#{m}][quota]",tm[:q],data:{target:"scoreMate.mateQuota"})
        td 
          = tm[:q]/2.0
          = hidden_field_tag("mate[#{m}][team]", side)
        td = select_tag("mate[#{m}][front]", options_for_select(opt[:side],'Select'),
          data:{target:'scoreMate.mateFront scoreTeam.mateFront',action:"change->scoreMate#front"},
          class:" w3-select")
        td = select_tag("mate[#{m}][back]", options_for_select(opt[:side],'Select'),
          data:{target:'scoreMate.mateBack scoreTeam.mateBack',action:"change->scoreMate#back"},
          class:" w3-select")
        td = select_tag("mate[#{m}][total]", options_for_select(opt[:total],'Select'),
          data:{target:'scoreMate.mateTotal scoreTeam.mateTotal'},
          class:" w3-select",disabled:true)
        td[id="mate#{m}_pmt" data-target="scoreMate.matePM"] 
  

Which generates a form that look like this [screenshot](https://user-images.githubusercontent.com/125716/78584674-184dd380-7828-11ea-8f3a-1c573fd65206.png)

That's all

[Screen Shot 2020-04-06 at 11 41 39 AM](https://user-images.githubusercontent.com/125716/78584674-184dd380-7828-11ea-8f3a-1c573fd65206.png)

Hey salex! I actually just responded to your other post. This site is definitely low traffic, but perhaps we can help change that.

Thanks for all of the code and especially the screenshot, really helps me visualize what’s going on.

Is the main problem you’re trying to solve for how to trigger the update in the scoreTeam controller without looking it up and triggering a click from scoreMate? I agree it’s a bit of a kludge now, but certainly not the worst thing in the world. Here are a few ideas that should be a bit cleaner.

1. Dispatch update events
Within scoreMateController#fireCompute, you can dispatch a custom event that is listened to by scoreTeamController and triggers the update. I won’t go into this as I don’t think it’s the correct solution here, but it’s a good pattern for decoupling that deserves some attention. I give some examples here.

2. Listen for changes directly from scoreTeamController
Stimulus controllers can overlap, which can prove extremely useful. You already have yours setup as such, where your targets are assigned to both scoreMateController and scoreTeamController. As such, you can update your cells to trigger two actions, as such:

td = select_tag("mate[#{m}][front]", options_for_select(opt[:side],'Select'),
    data:{target:'scoreMate.mateFront scoreTeam.mateFront',
    action:"change->scoreMate#front change->scoreTeam#compute"},
    class:" w3-select")

And with that, remove scoreMateController#fireCompute altogether.

I didn’t take the time to fully understand every step of the code, so it’s possible I’m missing something (e.g. a race condition in computing values). The events do seem to trigger in order, so if scoreMate does need to compute first, just make sure that action is listed first and you should be fine.

Hope this helps!

One additional tool that might prove useful for Rails developers working with Stimulus is to consider the situations and scenarios in which making use of StimulusReflex would allow drastic simplification of your client Stimulus controllers.

It’s true that this forum can feel like a bit of a ghost town at times. I assure you that the Stimulus community is alive and well. We’re also patiently waiting for the pending release of Stimulus v2.0, which has a really exciting new data attributes API.

If you, @welearnednothing or anyone else reading this craves a more lively ongoing conversation about Stimulus, there’s a really active #stimulus channel on the StimulusReflex Discord server.

1 Like

@leastbad with all the respect I have for Stimulus Reflex, I am not sure that this forum is the best place to advertise for an alternative forum that has no official endorsement from the maintainers (my personal opinion here).

As you said this forum is not very active but it now centralizes lots of information and one thing is sure, if we start spreading the info and communities in various forums none will be very active.

How about asking maintainers to create a Stimulus Reflex category?

We actually have a StimulusReflex Discourse, and nothing I’m saying here is an attempt to lobby for a SR category on this forum. Our Discourse has a similar activity cycle - which is to say that it’s extremely quiet - whereas the organic growth of the Stimulus channel on our Discord happened in response to it flooding the main channel with people pairing up to build new controllers and launch new Stimulus resources.

I appreciate what you’re trying to say, and I assure you that I’m not here to poach anymore. However, I also don’t feel guilty that we have a few dozen Stimulus enthusiasts actually discussing Stimulus. It’s happening organically, which suggests there’s a demand for it. It might be that Discourse isn’t the right structure for the kind of conversations that are happening. I’m sure that if the Basecamp folks launched a Discord, a lot of that conversation would move and we wouldn’t stop it.

In the meantime, it’s a shame that you’re not there with us.