Skip to content

Commit 327f129

Browse files
Add ability to make fields conditional (#295)
Original PR here: #291 I had the base set to a different branch when I merged... ## Changes Adds conditional field components that are hidden based on the value of other components in a Strata form. Stimulus is added in the most lightweight way - by loading directly... Sprockets doesn't support ES6 and I'm hesitant to add importmaps in this PR. ## Context @bradleysmock [documented](https://docs.google.com/document/d/1sV61-d65OeCTJm5KAFt9PY0IbGglursr0f6Vbz-7UCo/edit?disco=AAAB0z5_79s) gaps when translating one app to Strata. One of the features that @kyeah implemented in strata-paidleave was making fields conditional. This ports that ability over with a new interface. ## Testing The lookbook components work as expected. Tested in a branch in [strata-paidleave](https://github.com/navapbc/strata-paidleave/pull/59/changes).
1 parent 6ab2466 commit 327f129

18 files changed

Lines changed: 495 additions & 7 deletions
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
//= link_directory ../stylesheets/strata .css
2+
//= link strata/index.js
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<div data-controller="strata--conditional-field"
2+
data-strata--conditional-field-source-value="<%= @source %>"
3+
data-strata--conditional-field-match-value="<%= @match %>"
4+
data-strata--conditional-field-clear-value="<%= @clear %>"
5+
<%= "hidden" unless @initially_visible %>>
6+
<%= content %>
7+
</div>
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# frozen_string_literal: true
2+
3+
module Strata
4+
# ConditionalFieldComponent wraps content that should be shown or hidden
5+
# based on a radio button's selected value.
6+
#
7+
# @example Basic usage via FormBuilder
8+
# <%= f.conditional(:has_employer, eq: "true") do %>
9+
# <%= f.text_field :employer_name %>
10+
# <% end %>
11+
#
12+
# @example Direct component usage
13+
# <%= render Strata::ConditionalFieldComponent.new(
14+
# source: "form[has_employer]",
15+
# match: "true"
16+
# ) do %>
17+
# <p>Shown when has_employer is true</p>
18+
# <% end %>
19+
#
20+
class ConditionalFieldComponent < ViewComponent::Base
21+
def initialize(source:, match:, initially_visible: false, clear: false)
22+
@source = source
23+
@match = Array(match).map(&:to_s).join(",")
24+
@initially_visible = initially_visible
25+
@clear = clear
26+
end
27+
end
28+
end
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { Controller } from "@hotwired/stimulus"
2+
3+
// Conditionally shows/hides form fields based on a radio button's selected value.
4+
//
5+
// Usage:
6+
// <div data-controller="strata--conditional-field"
7+
// data-strata--conditional-field-source-value="form_model[field_name]"
8+
// data-strata--conditional-field-match-value="true"
9+
// hidden>
10+
// <!-- conditional content -->
11+
// </div>
12+
//
13+
// The `source` value is the `name` attribute of the radio button group to observe.
14+
// The `match` value is a comma-separated list of values that make this section visible.
15+
export default class extends Controller {
16+
static values = {
17+
source: String,
18+
match: String,
19+
clear: { type: Boolean, default: false }
20+
}
21+
22+
connect() {
23+
this.radioButtons = document.querySelectorAll(`input[type="radio"][name="${this.sourceValue}"]`)
24+
this.boundToggle = this.toggle.bind(this)
25+
26+
this.radioButtons.forEach((radio) => {
27+
radio.addEventListener("change", this.boundToggle)
28+
})
29+
30+
this.toggle()
31+
}
32+
33+
disconnect() {
34+
this.radioButtons.forEach((radio) => {
35+
radio.removeEventListener("change", this.boundToggle)
36+
})
37+
}
38+
39+
toggle() {
40+
const selected = document.querySelector(`input[type="radio"][name="${this.sourceValue}"]:checked`)
41+
const selectedValue = selected ? selected.value : null
42+
const matchValues = this.matchValue.split(",")
43+
44+
if (selectedValue && matchValues.includes(selectedValue)) {
45+
this.show()
46+
} else {
47+
this.hide()
48+
}
49+
}
50+
51+
show() {
52+
this.element.hidden = false
53+
this.element.removeAttribute("aria-hidden")
54+
this.enableInputs()
55+
}
56+
57+
hide() {
58+
this.element.hidden = true
59+
this.element.setAttribute("aria-hidden", "true")
60+
this.disableInputs()
61+
62+
if (this.clearValue) {
63+
this.clearInputs()
64+
}
65+
}
66+
67+
enableInputs() {
68+
this.element.querySelectorAll("input, select, textarea").forEach((input) => {
69+
input.disabled = false
70+
})
71+
}
72+
73+
disableInputs() {
74+
this.element.querySelectorAll("input, select, textarea").forEach((input) => {
75+
input.disabled = true
76+
})
77+
}
78+
79+
clearInputs() {
80+
this.element.querySelectorAll("input, select, textarea").forEach((input) => {
81+
if (input.type === "radio" || input.type === "checkbox") {
82+
input.checked = false
83+
} else {
84+
input.value = ""
85+
}
86+
})
87+
}
88+
}

app/components/strata/index.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import ConditionalFieldComponentController from "./conditional_field_component_controller"
2+
3+
export { ConditionalFieldComponentController }
4+
5+
// As we add more components with Stimulus, add the Controller to this function to make
6+
// importing easier
7+
export function registerControllers(application) {
8+
application.register("strata--conditional-field", ConditionalFieldComponentController)
9+
}

app/helpers/strata/form_builder.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,24 @@ def money_field(attribute, options = {})
526526
end
527527
end
528528

529+
def conditional(attribute, eq:, clear: false, &block)
530+
match_values = Array(eq).map(&:to_s)
531+
source_name = "#{@object_name}[#{attribute}]"
532+
533+
current_value = object&.send(attribute)
534+
initially_visible = current_value.present? && match_values.include?(current_value.to_s)
535+
536+
@template.render(
537+
Strata::ConditionalFieldComponent.new(
538+
source: source_name,
539+
match: match_values,
540+
initially_visible: initially_visible,
541+
clear: clear
542+
),
543+
&block
544+
)
545+
end
546+
529547
def us_states_and_territories
530548
[
531549
[ "", "" ],
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# frozen_string_literal: true
2+
3+
module Strata
4+
# ConditionalFieldPreview provides preview examples for the conditional form field helper.
5+
# It demonstrates showing/hiding form fields based on radio button selections.
6+
#
7+
# @example Viewing the yes_no preview
8+
# # In Lookbook UI
9+
# # Navigate to Strata > ConditionalFieldPreview > yes_no
10+
#
11+
class ConditionalFieldPreview < Lookbook::Preview
12+
layout "strata/component_preview"
13+
14+
# @label Yes/No conditional
15+
def yes_no
16+
render template: "strata/previews/_conditional_field_yes_no", locals: { model: new_model }
17+
end
18+
19+
# @label Yes/No with pre-selected value
20+
def yes_no_prefilled
21+
model = new_model
22+
model.has_employer = "true"
23+
model.employer_name = "Acme Corp"
24+
render template: "strata/previews/_conditional_field_yes_no", locals: { model: model }
25+
end
26+
27+
# @label Multiple radio options
28+
def radio_options
29+
render template: "strata/previews/_conditional_field_radio_options", locals: { model: new_model }
30+
end
31+
32+
private
33+
34+
def new_model
35+
Class.new do
36+
include ActiveModel::Model
37+
include ActiveModel::Attributes
38+
39+
attribute :has_employer, :string
40+
attribute :employer_name, :string
41+
attribute :leave_type, :string
42+
attribute :medical_provider, :string
43+
attribute :other_reason, :string
44+
45+
def self.model_name
46+
ActiveModel::Name.new(self, nil, "TestModel")
47+
end
48+
end.new
49+
end
50+
end
51+
end

app/views/layouts/strata/component_preview.html.erb

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,6 @@
55
<meta name="viewport" content="width=device-width,initial-scale=1">
66
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
77
<%= javascript_include_tag '@uswds/uswds/dist/js/uswds-init.min.js' %>
8-
9-
<% # Temporary workaround in dummy app to get around SASS asset pipeline issues %>
10-
<link rel="stylesheet" href="https://demo.pfml-accelerator-dev.navateam.com/assets/application-b918bc024f45f36022bb3ea3dd6cade6160147564a9d646d7f36271b0da9c25a.css">
118
</head>
129

1310
<body>
@@ -20,6 +17,21 @@
2017
</div>
2118
</main>
2219
</div>
20+
<script type="importmap">
21+
{
22+
"imports": {
23+
"@hotwired/stimulus": "https://unpkg.com/@hotwired/stimulus@3.2.2/dist/stimulus.js"
24+
}
25+
}
26+
</script>
27+
28+
<script type="module">
29+
import { Application } from "@hotwired/stimulus"
30+
import { registerControllers } from "<%= asset_path('strata/index.js') %>"
31+
32+
const application = Application.start()
33+
registerControllers(application)
34+
</script>
2335

2436
<%= javascript_include_tag '@uswds/uswds/dist/js/uswds.min.js' %>
2537
</body>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<%= strata_form_with(model: model, url: false) do |f| %>
2+
<%= f.fieldset "What type of leave are you requesting?", attribute: :leave_type do %>
3+
<%= f.radio_button :leave_type, "medical", label: "Medical" %>
4+
<%= f.radio_button :leave_type, "family", label: "Family" %>
5+
<%= f.radio_button :leave_type, "other", label: "Other" %>
6+
<% end %>
7+
8+
<%= f.conditional(:leave_type, eq: "medical") do %>
9+
<%= f.text_field :medical_provider, label: "Medical provider", hint: "Name of your healthcare provider" %>
10+
<% end %>
11+
12+
<%= f.conditional(:leave_type, eq: "other") do %>
13+
<%= f.text_field :other_reason, label: "Please describe your reason for leave" %>
14+
<% end %>
15+
<% end %>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<%= strata_form_with(model: model, url: false) do |f| %>
2+
<%= f.yes_no :has_employer, legend: "Do you currently have an employer?" %>
3+
4+
<%= f.conditional(:has_employer, eq: "true") do %>
5+
<%= f.text_field :employer_name, label: "Employer name", hint: "Enter the legal name of your employer" %>
6+
<% end %>
7+
<% end %>

0 commit comments

Comments
 (0)