Module: Hecks::Forms::FieldRenderer
- Defined in:
- lib/hecks/forms/field_renderer.rb
Overview
A Field (field_shape.rb) -> the <div class="field">...</div> or
<fieldset>...</fieldset> markup for it. One renderer, called
recursively for a group's children, shared by CommandFormRenderer
(POST) and QueryFormRenderer (GET) — the same attribute shape asks
for the same input either way; only the surrounding <form>'s
method differs.
Class Method Summary collapse
- .aria(field) ⇒ Object
- .aria_attrs(field) ⇒ Object
- .checkbox(field, value) ⇒ Object
-
.dig(values, path) ⇒ Object
valuesis a nested hash;patha dotted string using the SAME segment spelling the hash keys are built from (leaf_keyin params.rb) — symbols one level down from a group, strings at the flat top when a sticky POST re-render hands raw params back untouched. - .dom_id(path) ⇒ Object
- .group(field, values, errors, reference_options, tag:, css: nil) ⇒ Object
- .input(field, value) ⇒ Object
- .leaf(field, values, errors, reference_options) ⇒ Object
- .leaf_pattern(field) ⇒ Object
- .list(field, values, _errors) ⇒ Object
- .money_preview_target(field) ⇒ Object
- .radio_group(field, value) ⇒ Object
- .reference_select(field, value, options) ⇒ Object
-
.render(field, values: {}, errors: nil, reference_options: {}) ⇒ Object
valuesis the nested hash a sticky re-render (a rejected command, a submitted query) carries back — read with the SAME dotted path a Field already carries, viadig. - .required_mark(field) ⇒ Object
- .select(field, value) ⇒ Object
- .textarea(field, value) ⇒ Object
- .wrap(field, body, error) ⇒ Object
Class Method Details
.aria(field) ⇒ Object
138 139 140 |
# File 'lib/hecks/forms/field_renderer.rb', line 138 def self.aria(field) Tag.attrs(required: field.required?, aria_describedby: field.help ? "#{dom_id(field.path)}-help" : nil) end |
.aria_attrs(field) ⇒ Object
142 143 144 |
# File 'lib/hecks/forms/field_renderer.rb', line 142 def self.aria_attrs(field) { required: field.required?, aria_describedby: field.help ? "#{dom_id(field.path)}-help" : nil } end |
.checkbox(field, value) ⇒ Object
91 92 93 94 95 96 97 98 99 100 |
# File 'lib/hecks/forms/field_renderer.rb', line 91 def self.checkbox(field, value) checked = value.nil? ? field.default == true : [true, "true", "on", "1"].include?(value) <<~HTML <div class="checkbox-row"> <input type="hidden" name="#{Escape.attr(field.path)}" value="0"> #{Tag.void('input', id: dom_id(field.path), name: field.path, type: 'checkbox', value: '1', checked: checked)} <label for="#{dom_id(field.path)}">#{Escape.html(field.label)}</label> </div> HTML end |
.dig(values, path) ⇒ Object
values is a nested hash; path a dotted string using the SAME
segment spelling the hash keys are built from (leaf_key in
params.rb) — symbols one level down from a group, strings at the
flat top when a sticky POST re-render hands raw params back untouched.
150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 |
# File 'lib/hecks/forms/field_renderer.rb', line 150 def self.dig(values, path) return nil unless values.is_a?(Hash) # A sticky re-render after a refused submission hands back the RAW # flat params (`{"amount.cents"=>"1050"}` — the same shape the form # posted, string values and all, dotted key intact) ; a prefill from # an existing record's own state hands back a NESTED hash instead # (`{amount: {cents: 1050}}`). Flat wins when both would answer, # since only the raw form is ever what the caller actually typed. flat = values[path.to_s] || values[path.to_sym] return flat unless flat.nil? path.to_s.split(".").reduce(values) do |acc, segment| break nil unless acc.is_a?(Hash) acc[segment.to_sym] || acc[segment] end end |
.dom_id(path) ⇒ Object
134 |
# File 'lib/hecks/forms/field_renderer.rb', line 134 def self.dom_id(path) = "f-#{path.to_s.tr('.', '-')}" |
.group(field, values, errors, reference_options, tag:, css: nil) ⇒ Object
28 29 30 31 32 33 34 35 36 |
# File 'lib/hecks/forms/field_renderer.rb', line 28 def self.group(field, values, errors, , tag:, css: nil) inner = field.children.map { |child| render(child, values: values, errors: errors, reference_options: ) } <<~HTML <#{tag}#{css ? %( class="#{css}") : ""}> <legend>#{Escape.html(field.label)}#{required_mark(field)}</legend> #{inner.join("\n")} </#{tag}> HTML end |
.input(field, value) ⇒ Object
76 77 78 79 80 81 82 83 84 85 |
# File 'lib/hecks/forms/field_renderer.rb', line 76 def self.input(field, value) money = field.kind == :number && field.help.to_s.include?("cents") tag = Tag.void("input", id: dom_id(field.path), name: field.path, type: field.html_type, value: value || field.default, step: field.step, pattern: leaf_pattern(field), placeholder: field.default, **aria_attrs(field), **(money ? { data_money_cents: money_preview_target(field) } : {})) return tag unless money %(#{tag} <span id="#{dom_id(field.path)}-preview" class="mono help" aria-live="polite"></span>) end |
.leaf(field, values, errors, reference_options) ⇒ Object
51 52 53 54 55 56 57 58 59 60 61 62 63 |
# File 'lib/hecks/forms/field_renderer.rb', line 51 def self.leaf(field, values, errors, ) value = dig(values, field.path) error = errors && errors[field.path] body = case field.kind when :boolean then checkbox(field, value) when :radio then radio_group(field, value) when :select then select(field, value) when :textarea then textarea(field, value) when :reference then reference_select(field, value, [field.path]) else input(field, value) end wrap(field, body, error) end |
.leaf_pattern(field) ⇒ Object
135 |
# File 'lib/hecks/forms/field_renderer.rb', line 135 def self.leaf_pattern(field) = field.kind == :reference ? nil : field.pattern |
.list(field, values, _errors) ⇒ Object
38 39 40 41 42 43 44 45 46 47 48 49 |
# File 'lib/hecks/forms/field_renderer.rb', line 38 def self.list(field, values, _errors) item = field.children.first current = Array(dig(values, field.path)).join("\n") <<~HTML <div class="field"> <label for="#{dom_id(field.path)}">#{Escape.html(field.label)}#{required_mark(field)}</label> <textarea id="#{dom_id(field.path)}" name="#{Escape.attr(field.path)}" #{aria(field)} placeholder="one #{Escape.attr(item.label.downcase)} per line">#{Escape.html(current)}</textarea> <span class="help" id="#{dom_id(field.path)}-help">One #{Escape.html(item.label.downcase)} per line#{item.leaf? ? '' : ' — each line is one JSON object'}.</span> </div> HTML end |
.money_preview_target(field) ⇒ Object
136 |
# File 'lib/hecks/forms/field_renderer.rb', line 136 def self.money_preview_target(field) = "##{dom_id(field.path)}-preview" |
.radio_group(field, value) ⇒ Object
102 103 104 105 106 107 108 109 110 111 112 |
# File 'lib/hecks/forms/field_renderer.rb', line 102 def self.radio_group(field, value) selected = value || field.default = field..map do |option_value, option_label| checked = option_value.to_s == selected.to_s id = "#{dom_id(field.path)}-#{Naming.snake(option_value)}" <<~HTML <label>#{Tag.void('input', id: id, type: 'radio', name: field.path, value: option_value, checked: checked)} #{Escape.html(option_label)}</label> HTML end %(<div class="radio-group" role="radiogroup">#{.join}</div>) end |
.reference_select(field, value, options) ⇒ Object
123 124 125 126 127 128 129 130 131 |
# File 'lib/hecks/forms/field_renderer.rb', line 123 def self.reference_select(field, value, ) return input(field.tap { |f| f.html_type = "text" }, value) unless && !.empty? rendered = .map do |id, label| %(<option value="#{Escape.attr(id)}"#{id.to_s == value.to_s ? ' selected' : ''}>#{Escape.html(label)}</option>) end blank = field.optional? ? %(<option value="">—</option>) : %(<option value="" disabled#{value ? '' : ' selected'}>choose one…</option>) %(<select id="#{dom_id(field.path)}" name="#{Escape.attr(field.path)}" #{aria(field)}>#{blank}#{rendered.join}</select>) end |
.render(field, values: {}, errors: nil, reference_options: {}) ⇒ Object
values is the nested hash a sticky re-render (a rejected command,
a submitted query) carries back — read with the SAME dotted path a
Field already carries, via dig. reference_options maps a
:reference field's own path to [[id, label], ...], built by the
caller (it needs a repository; this module stays pure markup).
19 20 21 22 23 24 25 26 |
# File 'lib/hecks/forms/field_renderer.rb', line 19 def self.render(field, values: {}, errors: nil, reference_options: {}) case field.kind when :group then group(field, values, errors, , tag: "fieldset") when :money then group(field, values, errors, , tag: "fieldset", css: "money") when :list then list(field, values, errors) else leaf(field, values, errors, ) end end |
.required_mark(field) ⇒ Object
133 |
# File 'lib/hecks/forms/field_renderer.rb', line 133 def self.required_mark(field) = field.required? ? %(<span class="required-mark" title="required">*</span>) : "" |
.select(field, value) ⇒ Object
114 115 116 117 118 119 120 121 |
# File 'lib/hecks/forms/field_renderer.rb', line 114 def self.select(field, value) selected = value || field.default = field..map do |option_value, option_label| %(<option value="#{Escape.attr(option_value)}"#{option_value.to_s == selected.to_s ? ' selected' : ''}>#{Escape.html(option_label)}</option>) end blank = field.optional? ? %(<option value="">—</option>) : "" %(<select id="#{dom_id(field.path)}" name="#{Escape.attr(field.path)}" #{aria(field)}>#{blank}#{.join}</select>) end |
.textarea(field, value) ⇒ Object
87 88 89 |
# File 'lib/hecks/forms/field_renderer.rb', line 87 def self.textarea(field, value) %(<textarea id="#{dom_id(field.path)}" name="#{Escape.attr(field.path)}" #{aria(field)}>#{Escape.html(value)}</textarea>) end |
.wrap(field, body, error) ⇒ Object
65 66 67 68 69 70 71 72 73 74 |
# File 'lib/hecks/forms/field_renderer.rb', line 65 def self.wrap(field, body, error) <<~HTML <div class="field#{error ? ' has-error' : ''}"> #{field.kind == :boolean ? '' : %(<label for="#{dom_id(field.path)}">#{Escape.html(field.label)}#{required_mark(field)}</label>)} #{body} #{field.help ? %(<span class="help" id="#{dom_id(field.path)}-help">#{Escape.html(field.help)}</span>) : ''} #{error ? %(<span class="help" role="alert">#{Escape.html(error)}</span>) : ''} </div> HTML end |