Stimulus Discourse

Multiple actions - prevent other actions from firing?

Hi, I was hoping that if I add multiple actions:

data-action="schedulelist#tryedit schedulesummary#edit"

That returning false or similar would prevent the second action from firing. That doesn’t seem to be the case, so I was wondering how best to achieve this. Basically I have an outer controller with multiple child containers, of which only one can be edited at any one time. So when you click the edit button, I first wanted to call the outer controller to check if this was allowed (that’s the tryedit) and then if so, allow the actual edit method on the child controller to execute.

Thanks for any ideas on this.

Matt.

Hey @Matt_Roberts!

Totally doable! One of the great things about Stimulus is it just gets out of the way and lets you use everyday JS to handle tricky problems like these. In this case, you’ll want to dispatch a CustomEvent and be sure to set it to be cancelable. Then, the outer controller can call CustomEvent.preventDefault() if editing shouldn’t be allowed. The dispatching element then just needs to check the response of the dispatch call - if it’s false, the event was cancelled and editing isn’t allowed.

Here’s a simple example:

OuterController (outer_controller.js)

import { Controller } from 'stimulus'

export default class extends Controller {
  connect() {
    // We bind the function to 'this' to ensure scoping is correct within handleBeginEdit
    this.beginEditHandler = this.handleBeginEdit.bind(this)
    // This needs to set the "useCapture" option to true
    this.element.addEventListener('beginEdit', this.beginEditHandler, true)
  }

  disconnect() {
    // Be sure to clean up after ourselves on disconnect!
    this.element.removeEventListener('beginEdit', this.beginEditHandler, true)
  }

  handleBeginEdit(evt) {
    console.log(evt)
    // We'll control whether edits are allowed via a data property
    if(this.data.get("allow") == "false")) {
      evt.preventDefault()
    }
  }
}

InnerController (inner_controller.js)

import { Controller } from 'stimulus'

export default class extends Controller {
  tryEdit() {
    console.log("Trying to edit...")
    // Dispatch the event. Be sure to make it cancelable!
    const evt = new CustomEvent('beginEdit', {cancelable: true})
    if(this.element.dispatchEvent(evt)) {
      console.log("I can edit, let's go!")
    }
  }
}

And the corresponding HTML:

<!-- This will allow edits -->
<div data-controller="outer">
  <div data-controller="inner" data-action="click->inner#tryEdit">Click to edit</div>
<div>

<!-- This will cancel the edit -->
<div data-controller="outer" data-outer-allow="false">
  <div data-controller="inner" data-action="click->inner#tryEdit">Click to edit</div>
<div>

Hope this helps!

3 Likes

@welearnednothing Awesome stuff, thanks so much for going abover and beyond there to explain. I was thinking that something involving events could be a possible solution, but you’ve filled in all the blanks with this, so thank you :slight_smile:

1 Like

I think what you are looking for is event.stopImmediatePropagation()

this will prevent all subsequent actions

1 Like

Good call, @adrienpoly! That’s much simpler :slight_smile: For some reason, I wasn’t thinking of them as two event listeners on the same element, but they totally are.

There are some advantages to using CustomEvents, but in Matt’s case (and in many cases), stopImmediatePropagation() is the easiest path. @Matt_Roberts, just be aware that the inner click handler won’t be called at all here when editing isn’t allowed. So if the inner click handler wanted to know that it was cancelled so it could do something differently (change the style, provide messaging), that’s not possible with stopImmediatePropagation().

Thanks @adrienpoly!