Skip to content

Commit 85a45e2

Browse files
authored
Add enhancements for USDS form builder (#96)
- Add tax_id_field to us form builder - Add tests to catch missing content - Add hint content for date_picker and tax_id_field
1 parent 61c5ee7 commit 85a45e2

File tree

3 files changed

+390
-33
lines changed

3 files changed

+390
-33
lines changed

template/{{app_name}}/app/helpers/uswds_form_builder.rb

Lines changed: 110 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
# additional helpers like fieldset and hint.
55
# https://api.rubyonrails.org/classes/ActionView/Helpers/FormBuilder.html
66
class UswdsFormBuilder < ActionView::Helpers::FormBuilder
7+
standard_helpers = %i[email_field file_field password_field text_area text_field]
8+
79
def initialize(*args)
810
super
911
self.options[:html] ||= {}
@@ -19,25 +21,33 @@ def initialize(*args)
1921
#
2022
# Example usage:
2123
# <%= f.text_field :foobar, { label: "Custom label text", hint: "Some hint text" } %>
22-
%i[email_field file_field password_field text_area text_field].each do |field_type|
24+
standard_helpers.each do |field_type|
2325
define_method(field_type) do |attribute, options = {}|
2426
classes = us_class_for_field_type(field_type, options[:width])
2527
classes += " usa-input--error" if has_error?(attribute)
26-
27-
options[:class] ||= ""
28-
options[:class].prepend("#{classes} ")
28+
append_to_option(options, :class, " #{classes}")
2929

3030
label_text = options.delete(:label)
31+
label_class = options.delete(:label_class) || ""
32+
33+
label_options = options.except(:width, :class, :id).merge({
34+
class: label_class,
35+
for: options[:id]
36+
})
37+
field_options = options.except(:label, :hint, :large_label, :label_class)
3138

32-
us_form_group(attribute: attribute) do
33-
us_text_field_label(attribute, label_text, options) + super(attribute, options)
39+
if options[:hint]
40+
field_options[:aria_describedby] = hint_id(attribute)
41+
end
42+
43+
form_group(attribute, options[:group_options] || {}) do
44+
us_text_field_label(attribute, label_text, label_options) + super(attribute, field_options)
3445
end
3546
end
3647
end
3748

3849
def check_box(attribute, options = {}, *args)
39-
options[:class] ||= ""
40-
options[:class].prepend(us_class_for_field_type(:check_box))
50+
append_to_option(options, :class, " #{us_class_for_field_type(:check_box)}")
4151

4252
label_text = options.delete(:label)
4353

@@ -47,8 +57,7 @@ def check_box(attribute, options = {}, *args)
4757
end
4858

4959
def radio_button(attribute, tag_value, options = {})
50-
options[:class] ||= ""
51-
options[:class].prepend(us_class_for_field_type(:radio_button))
60+
append_to_option(options, :class, " #{us_class_for_field_type(:radio_button)}")
5261

5362
label_text = options.delete(:label)
5463
label_options = { for: field_id(attribute, tag_value) }.merge(options)
@@ -59,21 +68,21 @@ def radio_button(attribute, tag_value, options = {})
5968
end
6069

6170
def select(attribute, choices, options = {}, html_options = {})
62-
classes = "usa-select"
63-
64-
html_options[:class] ||= ""
65-
html_options[:class].prepend("#{classes} ")
71+
append_to_option(html_options, :class, " usa-select")
6672

6773
label_text = options.delete(:label)
6874

69-
us_form_group(attribute: attribute) do
75+
form_group(attribute) do
7076
us_text_field_label(attribute, label_text, options) + super(attribute, choices, options, html_options)
7177
end
7278
end
7379

7480
def submit(value = nil, options = {})
75-
options[:class] ||= ""
76-
options[:class].prepend("usa-button ")
81+
append_to_option(options, :class, " usa-button")
82+
83+
if options[:big]
84+
append_to_option(options, :class, " usa-button--big margin-y-6")
85+
end
7786

7887
super(value, options)
7988
end
@@ -92,16 +101,49 @@ def honeypot_field
92101
# Custom helpers
93102
########################################
94103

104+
def tax_id_field(attribute, options = {})
105+
options[:inputmode] = "numeric"
106+
options[:placeholder] = "_________"
107+
options[:width] = "md"
108+
109+
append_to_option(options, :class, " usa-masked")
110+
append_to_option(options, :hint, @template.content_tag(:p, I18n.t("us_form_with.tax_id_format")))
111+
112+
text_field(attribute, options)
113+
end
114+
115+
def date_picker(attribute, options = {})
116+
raw_value = object.send(attribute) if object
117+
118+
append_to_option(options, :hint, @template.content_tag(:p, I18n.t("us_form_with.date_picker_format")))
119+
120+
group_options = options[:group_options] || {}
121+
append_to_option(group_options, :class, " usa-date-picker")
122+
123+
if raw_value.is_a?(Date)
124+
append_to_option(group_options, :"data-default-value", raw_value.strftime("%Y-%m-%d"))
125+
value = raw_value.strftime("%m/%d/%Y") if raw_value.is_a?(Date)
126+
end
127+
128+
text_field(attribute, options.merge(value: value, group_options: group_options))
129+
end
130+
95131
def field_error(attribute)
96132
return unless has_error?(attribute)
97133

98134
@template.content_tag(:span, object.errors[attribute].to_sentence, class: "usa-error-message")
99135
end
100136

101-
def fieldset(legend, attribute = nil, &block)
102-
us_form_group(attribute: attribute) do
137+
def fieldset(legend, options = {}, &block)
138+
legend_classes = "usa-legend"
139+
140+
if options[:large_legend]
141+
legend_classes += " usa-legend--large"
142+
end
143+
144+
form_group(options[:attribute]) do
103145
@template.content_tag(:fieldset, class: "usa-fieldset") do
104-
@template.content_tag(:legend, legend, class: "usa-legend") + @template.capture(&block)
146+
@template.content_tag(:legend, legend, class: legend_classes) + @template.capture(&block)
105147
end
106148
end
107149
end
@@ -121,6 +163,17 @@ def hint(text)
121163
@template.content_tag(:div, @template.raw(text), class: "usa-hint")
122164
end
123165

166+
def form_group(attribute = nil, options = {}, &block)
167+
append_to_option(options, :class, " usa-form-group")
168+
children = @template.capture(&block)
169+
170+
if options[:show_error] or (attribute and has_error?(attribute))
171+
append_to_option(options, :class, " usa-form-group--error")
172+
end
173+
174+
@template.content_tag(:div, children, options)
175+
end
176+
124177
def yes_no(attribute, options = {})
125178
yes_options = options[:yes_options] || {}
126179
no_options = options[:no_options] || {}
@@ -132,7 +185,7 @@ def yes_no(attribute, options = {})
132185
@template.capture do
133186
# Hidden field included for same reason as radio button collections (https://api.rubyonrails.org/classes/ActionView/Helpers/FormOptionsHelper.html#method-i-collection_radio_buttons)
134187
hidden_field(attribute, value: "") +
135-
fieldset(options[:legend] || human_name(attribute), attribute) do
188+
fieldset(options[:legend] || human_name(attribute), { attribute: attribute }) do
136189
buttons =
137190
radio_button(attribute, true, yes_options) +
138191
radio_button(attribute, false, no_options)
@@ -147,6 +200,16 @@ def yes_no(attribute, options = {})
147200
end
148201

149202
private
203+
def append_to_option(options, key, value)
204+
current_value = options[key] || ""
205+
206+
if current_value.is_a?(Proc)
207+
options[key] = -> { current_value.call + value }
208+
else
209+
options[key] = current_value + value
210+
end
211+
end
212+
150213
def us_class_for_field_type(field_type, width = nil)
151214
case field_type
152215
when :check_box
@@ -167,15 +230,33 @@ def us_class_for_field_type(field_type, width = nil)
167230

168231
# Render the label, hint text, and error message for a form field
169232
def us_text_field_label(attribute, text = nil, options = {})
170-
hint_text = options.delete(:hint)
233+
hint_option = options.delete(:hint)
234+
classes = "usa-label"
235+
for_attr = options[:for] || field_id(attribute)
171236

172-
if hint_text
173-
hint_id = "#{attribute}_hint"
174-
options[:aria_describedby] = hint_id
175-
hint = @template.content_tag(:div, @template.raw(hint_text), id: hint_id, class: "usa-hint")
237+
if options[:class]
238+
classes += " #{options[:class]}"
239+
end
240+
241+
unless text
242+
text = human_name(attribute)
176243
end
177244

178-
label(attribute, text, { class: "usa-label" }) + hint + field_error(attribute)
245+
if options[:optional]
246+
text += @template.content_tag(:span, " (#{I18n.t('us_form_with.optional').downcase})", class: "usa-hint")
247+
end
248+
249+
if hint_option
250+
if hint_option.is_a?(Proc)
251+
hint_content = @template.capture(&hint_option)
252+
else
253+
hint_content = @template.raw(hint_option)
254+
end
255+
256+
hint = @template.content_tag(:div, hint_content, id: hint_id(attribute), class: "usa-hint")
257+
end
258+
259+
label(attribute, @template.raw(text), { class: classes, for: for_attr }) + field_error(attribute) + hint
179260
end
180261

181262
# Label for a checkbox or radio
@@ -192,11 +273,7 @@ def us_toggle_label(type, attribute, text = nil, options = {})
192273
label(attribute, label_text, options)
193274
end
194275

195-
def us_form_group(attribute: nil, show_error: nil, &block)
196-
children = @template.capture(&block)
197-
classes = "usa-form-group"
198-
classes += " usa-form-group--error" if show_error or (attribute and has_error?(attribute))
199-
200-
@template.content_tag(:div, children, class: classes)
276+
def hint_id(attribute)
277+
"#{attribute}_hint"
201278
end
202279
end

template/{{app_name}}/config/locales/defaults/en.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,6 @@ en:
6161
us_form_with:
6262
boolean_true: "Yes"
6363
boolean_false: "No"
64+
date_picker_format: "Format: mm/dd/yyyy"
65+
optional: "Optional"
66+
tax_id_format: "For example, 123456789"

0 commit comments

Comments
 (0)