module GovUkDateFields class FormFields include ActionView::Context include ActionView::Helpers::TagHelper delegate :concat, to: :@output_buffer DATE_SEGMENTS = { day: '_dd', month: '_mm', year: '_yyyy', }.freeze def initialize(form, object_name, attribute, options={}) @form = form @object = form.object @object_name = object_name @attribute = attribute @options = options @day_value = @object.send("#{@attribute}_dd")&.gsub(/\D/, '') @month_value = @object.send("#{@attribute}_mm")&.gsub(/\D/, '') @year_value = @object.send("#{@attribute}_yyyy")&.gsub(/\D/, '') end def raw_output generate_input_fields end def output raw_output.html_safe end private def generate_input_fields content_tag(:div, class: form_group_classes) do content_tag(:fieldset, fieldset_options(@attribute, @options)) do concat fieldset_legend(@attribute, @options) concat hint(@attribute) concat error(@attribute) concat input_fields_div end end end def generate_input_for(name, value, width: 2) css_class = "govuk-input govuk-date-input__input govuk-input--width-#{width}" css_class += " govuk-input--error" if error_for_attr? content_tag(:div, class: 'govuk-date-input__item') do content_tag(:div, class: 'govuk-form-group') do %Q| |.html_safe end end end def error_for_attr? @object.errors.keys.include?(@attribute) && @object.errors[@attribute].any? end def html_id(date_segment) brackets2underscore(html_name(date_segment)) end def html_name(date_segment) "#{@object_name}[#{@attribute}#{DATE_SEGMENTS[date_segment]}]" end def brackets2underscore(string) string.tr('[','_').tr(']', '_').gsub('__', '_').gsub(/_$/, '') end def attribute_prefix brackets2underscore(@object_name.to_s) end def form_group_id [attribute_prefix, @attribute].join('_') end def form_group_hint_id [form_group_id, 'hint'].join('_') end def form_group_classes classes = ['govuk-form-group'] classes << 'govuk-form-group--error' if error_for_attr? classes end def fieldset_options(attribute, options) defaults = { class: 'govuk-fieldset', role: 'group' } aria_ids = [] aria_ids << id_for(attribute, 'hint') if hint(attribute) aria_ids << id_for(attribute, 'error') if error_for_attr? # If the array is empty, `#presence` will return nil defaults['aria-describedby'] = aria_ids.presence merge_attributes( options[:fieldset_options], default: defaults ) end def fieldset_legend(attribute, options) default_attrs = { class: 'govuk-fieldset__legend' }.freeze default_opts = { visually_hidden: false, page_heading: true, size: 'xl' }.freeze legend_options = merge_attributes( options[:legend_options], default: default_attrs ).reverse_merge( default_opts ) opts = legend_options.extract!(*default_opts.keys) legend_options[:class] << " govuk-fieldset__legend--#{opts[:size]}" legend_options[:class] << " govuk-visually-hidden" if opts[:visually_hidden] # The `page_heading` option can be false to disable "Legends as page headings" # https://design-system.service.gov.uk/get-started/labels-legends-headings/ # if opts[:page_heading] content_tag(:legend, legend_options) do content_tag(:h1, fieldset_text(attribute), class: 'govuk-fieldset__heading') end else content_tag(:legend, fieldset_text(attribute), legend_options) end end def input_fields_div content_tag(:div, class: 'govuk-date-input', id: form_group_id) do concat generate_input_for(:day, @day_value) concat generate_input_for(:month, @month_value) concat generate_input_for(:year, @year_value, width: 4) end end def hint(attribute) return unless hint_text(attribute) content_tag(:span, hint_text(attribute), class: 'govuk-hint', id: id_for(attribute, 'hint')) end def error(attribute) return unless error_for_attr? text = error_full_message_for(attribute) content_tag(:span, text, class: 'govuk-error-message', id: id_for(attribute, 'error')) end def id_for(attribute, suffix) [attribute_prefix, attribute, suffix].join('_') end def default_label attribute attribute.to_s.split('.').last.humanize.capitalize end def fieldset_text attribute localized 'helpers.fieldset', attribute, default_label(attribute) end def hint_text attribute localized 'helpers.hint', attribute, '' end def localized_label attribute localized 'helpers.label', attribute, default_label(attribute) end def error_full_message_for attribute message = @object.errors.full_messages_for(attribute).first message&.sub default_label(attribute), localized_label(attribute) end # If a form view is reused but the attribute doesn't change (for example in # partials) an `i18n_attribute` can be used to lookup the legend or hint locales # based on this, instead of the original attribute. # # We prioritise the `i18n_attribute` if provided, and if no locale is found, # we try the 'real' attribute as a fallback and finally the default value. # def localized scope, attribute, default found = if @options[:i18n_attribute] key = "#{@object_name}.#{@options[:i18n_attribute]}" I18n.translate(key, default: '', scope: scope).presence || I18n.translate("#{key}_html", default: '', scope: scope).html_safe.presence end return found if found key = "#{@object_name}.#{attribute}" # Passes blank String as default because nil is interpreted as no default I18n.translate(key, default: '', scope: scope).presence || I18n.translate("#{key}_html", default: default, scope: scope).html_safe.presence end # Given an attributes hash that could include any number of arbitrary keys, this method # ensure we merge one or more 'default' attributes into the hash, creating the keys if # don't exist, or merging the defaults if the keys already exists. # It supports strings or arrays as values. # def merge_attributes attributes, default: hash = attributes || {} hash.merge(default) { |_key, oldval, newval| Array(newval) + Array(oldval) } end end end