Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamically mounting an action to Stimulus #478

Open
elvinaspredkelis opened this issue Oct 28, 2021 · 9 comments
Open

Dynamically mounting an action to Stimulus #478

elvinaspredkelis opened this issue Oct 28, 2021 · 9 comments

Comments

@elvinaspredkelis
Copy link

Hey!

First of all, I am a big fan of Stimulus 🎉

Recently, I found myself missing a way to dynamically adding actions. Is there a way to do so? Or, perhaps, there only be a way to do it eventually?

Use case

Handling a dropdown menu via JS, for example. Currently, I am collapsing such menus via something like click@window->dropdown#hide which is not ideal: it adds a lot of unnecessary events, fills up the debug mode and it only gets worse if there are more such dropdowns.

Obviously, it is possible add window.addEventListener(...), however, it becomes "detached" from the controller. So it would be nice to keep things consistent.

Expected functionality

Perhaps having an exposed Action class and/or mounting method could be an option.

Referring to the above mentioned use case, it would be possible to mount a new action when the dropdown is opened: new Action(element, index, descriptor) and then destroy this action upon disconnect.


P.s.
Not sure if this is a question or a feature request. I have tried searching for an answer, however, with no luck. Also, I am not that sure if this is a bad practice and/or is inconsistent with the Stimulus codebase or the way it is mounted.

@seanpdoyle
Copy link
Contributor

Would declaring an event listener within your controller suit your needs?

// dropdown controller
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  connect() {
    addEventListener("click", this.hide)
  }

  disconnect() {
    removeEventListener("click", this.hide)
  }

  hide = (event) => {
    // ...
  }
}

If you'd need to conditionally listen based on some other dropdown action, you could mount it within that action:

// dropdown controller
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  disconnect() {
    removeEventListener("click", this.hide)
  }

  expand() {
   // ... show the dropdown
    
    addEventListener("click", this.hide)
  }

  hide = (event) => {
    // ... hide the dropdown
    removeEventListener("click", this.hide)
  }
}

@elvinaspredkelis
Copy link
Author

elvinaspredkelis commented Nov 2, 2021

@seanpdoyle thanks for coming back to me!

Indeed, it is possible to do so, this is my current implementation:

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["button", "dropdown"]

  toggle (event, shouldHide) {
    let hidden = !!shouldHide ? shouldHide : this.dropdownTarget.classList.contains("hidden")

    this.dropdownTarget.classList.toggle("hidden", !hidden)
    this.dropdownTarget.setAttribute("aria-expanded", !hidden.toString())

    hidden ? addEventListener("click", this.hide, true) : removeEventListener("click", this.hide, true)
  }

  hide = event => {
    if (!this.buttonTarget.contains(event.target)) this.toggle(true)
  }
}

However, I think there are several drawbacks:

  • The event bubbling is not handled by default. I.e. In this case, it is necessary to set capture = true when adding the event listener. Otherwise, the event is triggered right away.
  • It is necessary to define the hiding method as hide = event => {} as opposed to hide (event) {}. Otherwise, the targets get to be undefined. Maybe negligible, but it is inconsistent and takes a while to figure out (I would think, at least)
  • The the debug mode does not capture the added listener.

@pinzonjulian
Copy link

How about this implementation @elvinaspredkelis ?

https://stimulus-use.github.io/stimulus-use/#/use-click-outside

Adds one new clickOutside behavior to your Stimulus controller as well as a new click:outside event when ever a click is received outside of the controller element.

@wdiechmann
Copy link

Sorry to waist everyones bandwidth here - but I can't help thinking if the issue is related to my "observation" ?

(quick resumé: if an element is 'hidden' it's action will not "work" if the element at some later point in time gets 'unhiddened' - or, that's my observation, at least, and as I duly posit: I'm sure it's on me)

am I way off here?

@lb-
Copy link
Contributor

lb- commented Nov 17, 2022

Would the 3.1.1 feature registerActionOption help with this?

See docs - https://stimulus.hotwired.dev/reference/actions#options
See PR - #567

@marcoroth
Copy link
Member

marcoroth commented Feb 2, 2023

Another way to dynamically add an action is to actually (re)-write the data-action attribute on the controller element. So you could do something like this inside the controller:

this.element.dataset.action = `click->${this.identifier}#myDynamicAction`

or if you want the respect the previous value:

this.element.dataset.action = `${this.element.dataset.action} click->${this.identifier}#myDynamicAction`

We could also abstract it away in a function call itself, here's a rough sketch from a few months ago: https://gist.github.com/marcoroth/64c308d9e7be047a2061469ebe58daea

@MrHubble
Copy link

@marcoroth do you know if a specific version of stimulus is required to dynamically add an action by re-writing the data-action attribute? I'm currently using 3.0.1 and it doesn't seem to be working for me.

@marcoroth
Copy link
Member

Hey @MrHubble, no this shouldn't be version dependent as it's just setting an attribute via the native DOM API. Can you share what you've got?

@MrHubble
Copy link

@marcoroth of course after coming back to this I can no longer replicate this issue 🤦‍♂️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

No branches or pull requests

7 participants