Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/assets/stylesheets/components/_memorable-date.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.usa-memorable-date input[type='text'] {
@include u-padding-x(1);
}
1 change: 1 addition & 0 deletions app/assets/stylesheets/components/all.scss
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@
@import 'validated-checkbox';
@import 'i18n-dropdown';
@import 'location-collection-item';
@import 'memorable-date';
92 changes: 92 additions & 0 deletions app/components/memorable_date_component.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<label class="usa-label">
<%= label %>
</label>
<span class="usa-hint">
<%= hint %>
</span>
<%= content_tag :'lg-memorable-date', min: min, max: max, **tag_options do -%>
<div class="usa-memorable-date">
<%= content_tag(
:script,
error_messages.to_json,
{
type: 'application/json',
class: 'memorable-date__error-strings',
},
false,
) %>
<lg-validated-field>
<%= f.simple_fields_for name do |p| %>
<%= p.input(
:month,
pattern: '(1[0-2])|(0?[1-9])',
wrapper_html: { class: 'usa-form-group usa-form-group--month margin-bottom-0' },
label: t('components.memorable_date.month'),
label_html: { class: 'usa-label' },
input_html: {
type: 'text',
class: 'validated-field__input memorable-date__month',
minLength: 1,
maxLength: 2,
aria: {
invalid: false,
describedby: "validated-field-error-#{unique_id}",
},
value: month,
},
error: { id: "validated-field-error-#{unique_id}" },
required: required,
) %>
<% end %>
</lg-validated-field>
<lg-validated-field>
<%= f.simple_fields_for name do |p| %>
<%= p.input(
:day,
pattern: '(3[01])|([12][0-9])|(0?[1-9])',
wrapper_html: { class: 'usa-form-group usa-form-group--day margin-bottom-0' },
label: t('components.memorable_date.day'),
label_html: { class: 'usa-label' },
input_html: {
type: 'text',
class: 'validated-field__input memorable-date__day',
minLength: 1,
maxLength: 2,
aria: {
invalid: false,
describedby: "validated-field-error-#{unique_id}",
},
value: day,
},
error: { id: "validated-field-error-#{unique_id}" },
required: required,
) %>
<% end %>
</lg-validated-field>
<lg-validated-field>
<%= f.simple_fields_for name do |p| %>
<%= p.input(
:year,
pattern: '\d{4}',
wrapper_html: { class: 'usa-form-group usa-form-group--year margin-bottom-0' },
label: t('components.memorable_date.year'),
label_html: { class: 'usa-label' },
input_html: {
type: 'text',
class: 'validated-field__input memorable-date__year',
minLength: 4,
maxLength: 4,
aria: {
invalid: false,
describedby: "validated-field-error-#{unique_id}",
},
value: year,
},
error: { id: "validated-field-error-#{unique_id}" },
required: required,
) %>
<% end %>
</lg-validated-field>
</div>
<% end -%>
<div class="usa-error-message" id="validated-field-error-<%= unique_id %>" style="display:none;"></div>
175 changes: 175 additions & 0 deletions app/components/memorable_date_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
##
# Provides a component that accepts a date using the inputs specified
# by USWDS here: https://designsystem.digital.gov/components/memorable-date/
#
# This treats the month, day, and year as nested fields to allow for various
# uses of the component. Translate the fields in the controller (or equivalent)
# if you need them to represent a single value.
#
# The errors for this component are configurable via +error_messages+, and you may
# include custom min/max validations via +range_errors+.
class MemorableDateComponent < BaseComponent
attr_reader :name, :month, :day, :year, :required, :hint, :label, :form, :tag_options

alias_method :f, :form

##
# @param [String] name Field name for hash containing month, day, and year
# @param [String] hint Additional Hint to show to the user
# @param [String] label Label for field
# @param [String] form Form that this field belongs to
# @param [String] month Starting value for month
# @param [String] day Starting value for day
# @param [String] year Starting value for year
# @param [Boolean] required Whether this field is required
# @param [Date,#to_date] min Minimum allowed date, inclusive
# @param [Date,#to_date] max Maximum allowed date, inclusive
# @param [Hash<Symbol,String>] error_messages Array of mappings of error states to messages
# @param [Array<Hash>] range_errors Array of custom range errors
# @option range_errors [Date,#to_date] :min Minimum value for range check
# @option range_errors [Date,#to_date] :max Maximum value for range check
# @option range_errors [String] :message Error message to display if range check fails
def initialize(
name:, hint:, label:, form:,
month: nil,
day: nil,
year: nil,
required: false,
min: nil,
max: nil,
error_messages: {},
range_errors: [],
**tag_options
)
@name = name
@month = month
@day = day
@year = year
@required = required
@min = min
@max = max
@hint = hint
@label = label
@form = form
@tag_options = tag_options
@error_messages = error_messages
@range_errors = range_errors
end

# Get error messages to be provided to the component.
# Includes both a hash lookup for general error messages
# and an array lookup for custom range error messages.
def error_messages
{
'error_messages' => generate_error_messages(label, @min, @max, @error_messages),
'range_errors' => @range_errors.map do |err|
new_err = {
message: err[:message],
}
new_err[:min] = convert_date err[:min] if !err[:min].blank?
new_err[:max] = convert_date err[:max] if !err[:max].blank?
new_err
end,
}
end

# Get min date as a string like 1892-01-23
def min
convert_date @min
end

# Get max date as a string like 1892-01-23
def max
convert_date @max
end

# Extract a memorable date param from a submitted form value
#
# @param [Hash] date
# @option date [String] month
# @option date [String] day
# @option date [String] year
# @return [String,nil] The formatted date, or nil if the param cannot be converted
def self.extract_date_param(date)
if date.instance_of?(String) || date.empty?
nil
else
formatted_date = [
date&.[](:year),
date&.[](:month)&.rjust(2, '0'),
date&.[](:day)&.rjust(2, '0'),
].join '-'
formatted_date if /^\d{4}-\d{2}-\d{2}$/.match? formatted_date
end
end

private

# Convert a Date or date-like value to a string like 1892-01-23
def convert_date(date)
date.to_date.to_s if date.respond_to?(:to_date)
end

# Convert a Date or date-like value to a long-form localized date string
def i18n_long_format(date)
if date.respond_to?(:to_date)
# i18n-tasks-use t('date.formats.long')
I18n.l(date.to_date, format: :long)
end
end

# Configure default generic error messages for component,
# then integrate any overrides
def generate_error_messages(label, min, max, override_error_messages)
base_error_messages = {
missing_month_day_year: t(
'components.memorable_date.errors.missing_month_day_year',
label: label,
),
missing_month_day: t('components.memorable_date.errors.missing_month_day'),
missing_month_year: t('components.memorable_date.errors.missing_month_year'),
missing_day_year: t('components.memorable_date.errors.missing_day_year'),
missing_month: t('components.memorable_date.errors.missing_month'),
missing_day: t('components.memorable_date.errors.missing_day'),
missing_year: t('components.memorable_date.errors.missing_year'),
invalid_month: t('components.memorable_date.errors.invalid_month'),
invalid_day: t('components.memorable_date.errors.invalid_day'),
invalid_year: t('components.memorable_date.errors.invalid_year'),
invalid_date: t('components.memorable_date.errors.invalid_date'),
}
if label && min
base_error_messages[:range_underflow] =
t(
'components.memorable_date.errors.range_underflow', label: label,
date: i18n_long_format(min)
)
end

if label && max
base_error_messages[:range_overflow] =
t(
'components.memorable_date.errors.range_overflow', label: label,
date: i18n_long_format(max)
)
end

if label && min && max
base_error_messages[:outside_date_range] =
t(
'components.memorable_date.errors.outside_date_range',
label: label,
min: i18n_long_format(min),
max: i18n_long_format(max),
)
end

if override_error_messages
{
**base_error_messages,
**override_error_messages,
}
else
base_error_messages
end
end
end
1 change: 1 addition & 0 deletions app/components/memorable_date_component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import '@18f/identity-memorable-date';
Loading