"Double-click to submit" pattern with Stimulus
While tinkering around with a home lab and CasaOS, I encountered this pattern where certain actions require a "double click", like this:
I wanted to see how hard it was to copy this with Stimulus, and it turns out it is not that hard.
Let's start with a controller:
import { Controller } from '@hotwired/stimulus'
// Connects to data-controller="double-confirm"
export default class extends Controller {
static targets = ['text'] // The text we want to switch
static values = { clicked: Boolean }
connect () {
this.clickedValue = false
this.originalText = this.textTarget.innerText
}
submit (event) {
event.preventDefault()
if (this.clickedValue) {
this.element.requestSubmit()
} else {
this.clickedValue = true
this.textTarget.innerText = 'Are you sure?'
}
}
}
Upon submission, we first prevent the default behavior so we can either swap the text or submit the form.
Now, connecting it to our form:
<%= form_with url: user_path(user), method: :delete, data: { controller: "double-confirm" } do |form| %>
<% form.button type: :submit, data: { action: "click->double-confirm#submit" }, class: "button success" do %>
<span data-double-confirm-target="text">Delete</span>
<% end %>
<% end %>
And that's almost it. There is no way to cancel the action. Once clicked, there's no coming back; you either have to submit the form by clicking it again or refresh the page so that the button resets to its initial state.
Let's go with a simple solution: cancel the action whenever the user clicks outside the button. We start by adding stimulus-use
:
yarn add stimulus-use @hotwired/stimulus
and then we make the controller aware of any clicks outside its element:
import { Controller } from '@hotwired/stimulus'
++ import { useClickOutside } from 'stimulus-use'
// Connects to data-controller="double-confirm"
export default class extends Controller {
static targets = ['text']
static values = { clicked: Boolean }
connect () {
this.clickedValue = false
this.originalText = this.textTarget.innerText
++ useClickOutside(this, { element: this.element })
}
submit (event) {
event.preventDefault()
if (this.clickedValue) {
this.element.requestSubmit()
} else {
this.clickedValue = true
this.textTarget.innerText = 'Are you sure?'
}
}
++ clickOutside (event) {
++ if (this.clickedValue) {
++ this.clickedValue = false
++ this.textTarget.innerText = this.originalText
++ }
++ }
}
And this is the final result