New to stimulus and trying to find the correct pattern here. I have a collection of form items with a hidden submit button by default. When you click into the input the submit button reveals.
As you click into other inputs the previously active submit button disappears and the new one reveals. If you click outside all inputs, there is a global data-action="click@window->editor#hideButton"
that hides the submit button.
If you run the snippet it has the exact UX I'm looking to implement. However, attaching the global window event to every instance of the controller seems like overkill. Additionally, this global click is fired every time there is a click on the page for other actions.
This pattern works in a modal context, but feels off in my collection example. In my example the hideButton
function will be called 4 times on every click. Even when interacting with other elements on the page.
https://discuss.hotwired.dev/t/best-practices-for-handling-clicks-outside-element/1266
I've read creating a controller for each item in the collection is the correct approach.
Is leaning on this global event the right approach for Stimulus? Or, should I rethink the approach entirely to achieve the UX (realize I may be way off in my architecture).
const application = Stimulus.Application.start()
application.register("editor", class extends Stimulus.Controller {
static targets = ["button"]
showButton() {
this.buttonTarget.classList.remove("hide")
}
hideButton() {
if (this.element === event.target || this.element.contains(event.target)) return;
this.buttonTarget.classList.add("hide")
}
})
.hide {
display: none;
}
li {
margin: 10px;
background: gray;
width: 230px;
}
input:hover {
cursor: pointer;
}
ul {
list-style: none;
}
<script src="https://unpkg.com/stimulus@2.0.0/dist/stimulus.umd.js"></script>
<ul>
<li data-controller="editor" data-action="click@window->editor#hideButton">
<form>
<label>Item1</label><br>
<input type="text" data-action="click->editor#showButton">
<input class="hide" type="submit" value="Submit" data-editor-target="button">
</form>
</li>
<li data-controller="editor" data-action="click@window->editor#hideButton">
<form>
<label >Item2</label><br>
<input type="text" data-action="click->editor#showButton">
<input class="hide" type="submit" value="Submit" data-editor-target="button">
</form>
</li>
<li data-controller="editor" data-action="click@window->editor#hideButton">
<form>
<label>Item4</label><br>
<input type="text" data-action="click->editor#showButton">
<input class="hide" type="submit" value="Submit" data-editor-target="button">
</form>
</li>
<li data-controller="editor" data-action="click@window->editor#hideButton">
<form>
<label>Item4</label><br>
<input type="text" data-action="click->editor#showButton">
<input class="hide" type="submit" value="Submit" data-editor-target="button">
</form>
</li>
</ul>
If you can, it would be good to leverage a CSS only solution, this is going to be more performant, simpler to manage and provides a more accessible user interface (example below).
You can use the :focus-within
CSS pseudo selector to achieve the same goal.
This allows you to leverage CSS features like opacity
for a transition effect also.
.form li [type='submit'] {
opacity: 0;
transition: opacity 300ms ease-in-out;
}
.form li:hover [type='submit'],
.form li:focus-within [type='submit'] {
opacity: 1;
}
focusin
& focusout
eventsHowever, there may be other behaviour you are trying to work in here so if you must use JavaScript I would recommend avoiding the global click and the input clicks here and use the focusin
and focusout
events instead.
These events bubble from any inputs inside the up to container that the listener (action) is which means you can have any kind of focusable elements inside the li
without having to register individual click actions.
You should also consider the implications of this behaviour for keyboard only users and mobile only users, hence the setTimeout
in the code example below.
I have also leveraged the CSS Classes feature of Stimulus in the code example below but you do not need to do this.
<ul>
<li
data-controller="editor"
data-action="focusin->editor#showButton focusout->editor#hideButton"
data-editor-hidden-class="is-hidden"
>
<label>Item1</label><br />
<input type="text" />
<input
class="is-hidden"
type="submit"
value="Submit"
data-editor-target="button"
/>
</li>
<li
data-controller="editor"
data-action="focusin->editor#showButton focusout->editor#hideButton"
data-editor-hidden-class="is-hidden"
>
<label>Item2</label><br />
<input type="text" />
<input
class="is-hidden"
type="submit"
value="Submit"
data-editor-target="button"
/>
</li>
</ul>
import { Controller } from '@hotwired/stimulus';
class Editor extends Controller {
static classes = ['hidden'];
static targets = ['button'];
showButton() {
// ensure focus can 'move' to next target in container (e.g. press 'tab')
setTimeout(() => this.buttonTarget.classList.remove(this.hiddenClass));
}
hideButton() {
// ensure focus can 'move' to next target in container (e.g. press 'tab')
setTimeout(() => this.buttonTarget.classList.add(this.hiddenClass));
}
}
export default Editor;