Stimulus Discourse

What is best way to fire event on controller connect()?

I’m writing a controller to have a check box automatically disable/enable other controls.

But I’m trying to have this run on page load in addition to when the check box is checked/unchecked. So if a page loads with the check box checked, the corresponding controls will be enabled.

I came up with the following, but is there a cleaner way to do this?

// check_controller.js
export default class extends Controller {
  static targets = [ 'checkBox', 'controlled' ];

  connect() {
    // is there a better way to do this?
    this.checkBoxTargets.forEach((check => {
      let evt = new Event('change');
      check.dispatchEvent(evt)
      this.controlIt(evt);
    }))
  }

  controlIt(event) {
    this.controlledTargets.forEach(element => element.disabled = !event.target.checked);
  }
}

And the HTML:

<div data-controller="check">
  <input data-action="check#controlIt" data-target="check.checkBox" name="custom_paint" type="checkbox" />
  <input name="custom_paint_color" type="text" data-target="check.controlled" />
</div>

Hey @floydj!

Welcome! First question - Do you intend to have multiple checkboxes and corresponding controlled targets in one controller? That’s not what’s indicated in your sample HTML, but the JS does imply it. In which case, you have a bug - you’re dispatching an event for each target, but then each event triggers controlIt which then loops over all controlled targets, but only comparing them all, each time, against a single check box.

This would leave every controlled target in the enabled state of only the last checkbox. Doh! Moreover, if you have 4 checkboxes/controlled targets, controlIt would be called 16 times.

There are threads like this that discuss some of the merits and solutions for using one controller for all boxes/targets or one controller for each pair. One controller for each pair for a simple use case such seems much more straight forward, and gets rid of the situation described above, so I’ll describe what that solution would look like.

// check_controller.js
export default class extends Controller {
  static targets = [ 'checkBox', 'controlled' ];

  connect() {
    this.update()
  }

  update() {
    this.controlledTarget.disabled == !this.checkBoxTarget.checked
  }
}

And the HTML:

<div data-controller="check">
  <input data-action="check#update" data-target="check.checkBox" name="custom_paint" type="checkbox" />
  <input name="custom_paint_color" type="text" data-target="check.controlled" />
</div>

Pretty straightforward!

Now, one last thing I would strongly suggest - if you’re rendering the HTML dynamically (e.g. via Rails), set the state in the HTML itself rather than relying on JS. This would allow you remove the connect() call altogether, but more importantly it removes N number of repaints from your page. If you have a list of 20 of these, they each may be repainted as soon as the controller connects. It may seem minor, but it just adds overhead every time the page is loaded.

So then you’d have HTML that looks like:

<!-- Checked + enabled -->
<div data-controller="check">
  <input data-action="check#controlIt" data-target="check.checkBox" name="custom_paint" type="checkbox" checked />
  <input name="custom_paint_color" type="text" data-target="check.controlled" />
</div>

<!-- Not checked and disabled -->
<div data-controller="check">
  <input data-action="check#controlIt" data-target="check.checkBox" name="custom_paint" type="checkbox" />
  <input name="custom_paint_color" type="text" data-target="check.controlled" disabled />
</div>

Hope this helps!

1 Like

Thanks for the reply - all good stuff.

Intention is actually only one checkbox for one or more controls per controller. Your solution looks better than mine!

Right on. Unless the controller is going to end up with much more involved functionality or maybe unless you’re going to have hundreds on a page, I’d recommend just doing one pair per controller. Pairing up the check boxes with the corresponding target just introduces complexity into what is otherwise a very trivial controller. The trade offs likely aren’t there to warrant making it more complex.

I was going to update something someplace about this topic and how it relates to a couple of posts I wrote a few months ago (One controller or multiple controller and A spreadsheet like controller…), one which was referenced. Guess here is as good as place than any.

I was trying to do something similar in the spreadsheet post, firing another controller. For instance you have two numeric inputs (e.g., frontTarget and backTarget that you want to sum and update a totalTarget, then have another controller add the totalTargets and set a total cell.

I had some kludge that fired the other controller. The solution provided by welearnednothing was simple: the front and back targets are referenced in both controllers and the change action does the addition in the first controller and the sum or compute actions is handed by the second controller. Since tables are like spreadsheets, the data rows(TR) contain the inner controller and the table contains the outer controller.

td= text_field_tag("participant[#{mate.id}][front]", mate.front,
  data:{target:'scoreMate.mateFront scoreTeam.mateFront',
  action:"change->scoreMate#front change->scoreTeam#compute"},class:"w100")
td= text_field_tag("participant[#{mate.id}][back]",mate.back, 
  data:{target:'scoreMate.mateBack scoreTeam.mateBack',
  action:"change->scoreMate#back change->scoreTeam#compute"},class:"w100")

I’ve also implemented it with a single controller using all kinds of indexes, but the two controller is much more readable and maintainable.

Just my2cents.