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 168 169 170 171 172 173 174 |
# 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. # `key?` decides which spelling answers, at every step below — # never `||`, which would treat a genuinely-held `false` the # same as an absent key and fall through to `nil`. str = path.to_s return values[str] if values.key?(str) sym = path.to_sym return values[sym] if values.key?(sym) str.split(".").reduce(values) do |acc, segment| break nil unless acc.is_a?(Hash) seg_sym = segment.to_sym acc.key?(seg_sym) ? acc[seg_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 |