Rails "Disable-able" button component
Here’s a simple ViewComponent/Stimulus controller for a disable-able button; that is, a button that you can programmatically disable/enable. You could use this to prevent form submission until all fields are valid.
If anyone has a better name for this component, please let me know!
First, the Template
Pretty simple: we render a button in a span, passing along some tag options from the component class.
1
2
3
4
5
6
7
<%=
tag.span(**container_options) do
tag.button(**button_options) do
content
end
end
%>
The Component
The component itself has a few things going on.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
module UI
class DisableableButton < ApplicationComponent
attr_reader :button_options, :container_options
def initialize(
disabled: false,
disable_events: [],
enable_events: [],
disabled_tooltip: "",
variant: "light",
tag_options: {}
)
container_options = {
tabindex: 0,
title: disabled_tooltip,
class: "d-inline-block",
data: {
controller: "disableable-button tooltip",
action: [
enable_events.map { |ev| "#{ev}->disableable-button#enable" },
enable_events.map { |ev| "#{ev}->tooltip#disable" },
disable_events.map { |ev| "#{ev}->disableable-button#disable" }
].flatten.join(" ")
}
}
button_options = {
disabled: disabled,
class: %W[btn btn-#{variant}],
data: {
"disableable-button-target": "button"
}
}
@container_options = container_options
@button_options = tag_options.deep_merge(button_options)
end
end
end
Let’s go over the initializer params:
1
2
3
4
5
6
7
8
def initialize(
disabled: false,
disable_events: [],
enable_events: [],
disabled_tooltip: "",
variant: "light",
tag_options: {}
)
-
disabled: set the default state of the button.
-
disable_events/enable_events: a list of events that the component will respond to, which control the state of the button.
-
disabled_tooltip: popover text that will appear when hovering over the disabled button. Note that this requires another Stimulus controller (“tooltip”) to work.
-
variant: passed along as a CSS class to the button. This allows us to set a default variant that can be overridden if required.
-
tag_options: additional options that can be passed along to the button tag. Allows arbitrary customization of the button tag.
We then create the container options hash, which is used to create the container span tag. This is where we set up our tooltip, because the tooltip library I’m using (Bootstrap) doesn’t work on disabled buttons themselves.
We also indicate the stimulus controllers to use (“disableable-button” and “tooltip”) and build a list of event listeners, which are flattened into a single string.
1
2
3
4
5
6
7
8
9
10
11
12
13
container_options = {
tabindex: 0,
title: disabled_tooltip,
class: "d-inline-block",
data: {
controller: "disableable-button tooltip",
action: [
enable_events.map { |ev| "#{ev}->disableable-button#enable" },
enable_events.map { |ev| "#{ev}->tooltip#disable" },
disable_events.map { |ev| "#{ev}->disableable-button#disable" }
].flatten.join(" ")
}
}
Finally, we set some parameters for the button itself, include indicating that it is the “target” that will be used by the Stimulus controller.
1
2
3
4
5
6
7
button_options = {
disabled: disabled,
class: %W[btn btn-#{variant}],
data: {
"disableable-button-target": "button"
}
}
Speaking of the Stimulus Controller…
…it’s dead simple. It has two methods that set or remove the “disabled” attribute on the button.
1
2
3
4
5
6
7
8
9
10
11
12
13
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["button"]
enable(event) {
this.buttonTarget.removeAttribute("disabled")
}
disable(event) {
this.buttonTarget.setAttribute("disabled", "disabled")
}
}
That’s it!
Now we render the button.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<%= render UI::DisableableButton.new(
variant: "primary",
disabled: true,
enable_events: ["grades:grades_valid@window"],
disable_events: ["grades:grades_invalid@window"],
disabled_tooltip: "Please assign grades for all assignments",
tag_options: {
data: {
turbo_frame: "_top",
}
}) do %>
<%= render UI::Icon.new("submit") %>
Submit Grades
<% end %>