Stimulus Discourse

Binding actions in controller code

Hi there!

I have a stimulus controller that is designed to be installed on form nodes. I want to capture the form submit event and have it do something custom.

Currently I have an action defined in the controller, and in my HTML I write

<form data-controller="my-form" data-action="my-form#doTheThing">

Since this controller is always going to be on a form, is there a straightforward way to bind this action from JS, perhaps inside the connect method? I could attach the event handler directly I suppose but the lifecycle handling that an action gives me is nice to have.

You could simply do this.element.addEventListener(“submit”, …) in your connect method. I’ve done something like that at times when it just felt cleaner in the long run than adding actions onto the controlled element.

Hey @davefp!

Great question, I’ve run into this plenty of times. Unfortunately, there’s no automagic lifecycle handling way to do this that I’m aware of. I believe you’d have to navigate the internal Stimulus APIs to make this happen and while I haven’t puzzled together the exact code for that, it seems like it would be more effort than just managing the event handlers yourself.

Example:

connect() {
  this.submitHandler = this.doTheThing.bind(this)
  this.element.addEventListener('submit', this.submitHandler)
}

disconnect() {
  this.element.removeEventListener('submit', this.submitHandler)
}

doTheThing(evt) {
  // ...
}

Note that I’m using bind here so that this is the controller within doTheThing. Without that, this will end up being the DOM element of the form. And because bind returns a new function, the reference to that function must be maintained for removeEventListener to properly remove the listener.

Another option would be to leverage the EventListener interface (which Stimulus uses internally) which lets you forego binding but dictates all events be listened to by the same handleEvent method.

Example:

connect() {
  this.element.addEventListener('submit',this)
}

disconnect() {
  this.element.removeEventListener('submit', this)
}

handleEvent(evt) {
  if(evt.type == 'submit') {
    this.doTheThing()
  }
}

While these will work, in general I would consider whether you really want to force the action on all instances - it reduces reusability as there may be times when an instance, for whatever reason, wants to opt out of a particular action. That said, things like form submissions are a good example of where this approach can reduce the boilerplate necessary to use the controller.

Hope this helps!