Class: CafeCar::FormBuilder
- Inherits:
-
ActionView::Helpers::FormBuilder
- Object
- ActionView::Helpers::FormBuilder
- CafeCar::FormBuilder
- Includes:
- Resolver
- Defined in:
- lib/cafe_car/form_builder.rb
Instance Method Summary collapse
- #association(method, collection: nil, multiple: false, **options) ⇒ Object
- #association_accessible?(info) ⇒ Boolean
-
#association_collection(info) ⇒ Object
Association choices obey the associated model's Pundit scope.
-
#enum(method, choices = nil, **options) ⇒ Object
An ActiveRecord enum renders as a plain
- #error(method, text = error_text(method)) ⇒ Object
- #error_text(method) ⇒ Object
- #errors(method) ⇒ Object
- #field(method) ⇒ Object
- #fields_for(method, object = object_for(method)) ⇒ Object
- #hidden(*methods) ⇒ Object
- #hint(method, text = info(method).hint) ⇒ Object
- #info(method) ⇒ Object
-
#initialize ⇒ FormBuilder
constructor
A new instance of FormBuilder.
- #input(method, *args, as: nil, **options) ⇒ Object
- #label(method, text = info(method).label, required: info(method).required?) ⇒ Object
- #model ⇒ Object
- #object_for(method) ⇒ Object
-
#options_url(klass) ⇒ Object
URL of the associated model's typeahead feed in the current namespace, or nil when no such route exists.
- #policy ⇒ Object
-
#preserved_association(info) ⇒ Object
A persisted association can sit outside the viewer's current list boundary (or listing can be denied wholesale).
- #remaining_attributes ⇒ Object
- #remaining_fields(&block) ⇒ Object
-
#searchable_select(info) ⇒ Object
HTML options that flag an association
- #show ⇒ Object
- #submit(value = nil, **options) ⇒ Object
-
#with_selected(info, collection) ⇒ Object
The capped option collection, guaranteeing the currently-associated record is among the options even when it sorts past the cap — otherwise editing a record whose association is beyond
max_collection_optionswould silently drop the value.
Methods included from Resolver
Constructor Details
#initialize ⇒ FormBuilder
Returns a new instance of FormBuilder.
7 8 9 10 |
# File 'lib/cafe_car/form_builder.rb', line 7 def initialize(...) super @fields = {} end |
Instance Method Details
#association(method, collection: nil, multiple: false, **options) ⇒ Object
16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
# File 'lib/cafe_car/form_builder.rb', line 16 def association(method, collection: nil, multiple: false, **) info = info(method) return show(info.input_key) if info.polymorphic? and object.persisted? return hidden(*info.polymorphic_methods) if info.polymorphic? return preserved_association(info) unless association_accessible?(info) collection ||= with_selected(info, association_collection(info)) # A multi-select filters by a set (`author_id[]`), so no blank "any" option — # an empty selection already means "any". A single select keeps its prompt. [:include_blank] ||= info.prompt unless multiple html = searchable_select(info) html[:multiple] = true if multiple collection_select(info.input_key, collection, :id, -> { @template.present(_1).title }, , html) end |
#association_accessible?(info) ⇒ Boolean
45 46 47 |
# File 'lib/cafe_car/form_builder.rb', line 45 def association_accessible?(info) @template.policy(info.reflection.klass).index? end |
#association_collection(info) ⇒ Object
Association choices obey the associated model's Pundit scope. The remote typeahead endpoint already applies this boundary; using the same boundary for the initial HTML options keeps the progressive-enhancement path from leaking rows the user cannot reach through that endpoint.
39 40 41 42 43 |
# File 'lib/cafe_car/form_builder.rb', line 39 def association_collection(info) klass = info.reflection.klass scope = association_accessible?(info) ? @template.policy_scope(klass) : klass.none info.collection(scope) end |
#enum(method, choices = nil, **options) ⇒ Object
An ActiveRecord enum renders as a plain
99 100 101 102 103 104 105 106 |
# File 'lib/cafe_car/form_builder.rb', line 99 def enum(method, choices = nil, **) info = info(method) choices ||= info.values [:include_blank] ||= info.prompt select(method, choices, ) end |
#error(method, text = error_text(method)) ⇒ Object
158 159 160 |
# File 'lib/cafe_car/form_builder.rb', line 158 def error(method, text = error_text(method), **) @template.tag.span(text, **) if text.present? end |
#error_text(method) ⇒ Object
154 155 156 |
# File 'lib/cafe_car/form_builder.rb', line 154 def error_text(method) errors(method).to_sentence.presence end |
#errors(method) ⇒ Object
148 149 150 151 152 |
# File 'lib/cafe_car/form_builder.rb', line 148 def errors(method) errors = object.try(:errors) associated = info(method).reflection&.then { errors[_1.name] } || [] errors[method] | associated end |
#field(method) ⇒ Object
113 114 115 |
# File 'lib/cafe_car/form_builder.rb', line 113 def field(method, **, &) @fields[method] ||= const(:FieldBuilder).new(method:, form: self, template: @template, **, &) end |
#fields_for(method, object = object_for(method)) ⇒ Object
162 163 164 165 166 167 168 169 170 171 |
# File 'lib/cafe_car/form_builder.rb', line 162 def fields_for(method, object = object_for(method), **, &) method = method.to_s.chomp("_attributes").to_sym if block_given? super else super(method, **) do |f| f.remaining_fields end end end |
#hidden(*methods) ⇒ Object
108 109 110 111 |
# File 'lib/cafe_car/form_builder.rb', line 108 def hidden(*methods, **, &) methods.map { input(_1, as: :hidden_field, **, &) } .then { @template.safe_join(_1) } end |
#hint(method, text = info(method).hint) ⇒ Object
144 145 146 |
# File 'lib/cafe_car/form_builder.rb', line 144 def hint(method, text = info(method).hint, **) @template.tag.small(text, **) if text.present? end |
#info(method) ⇒ Object
126 |
# File 'lib/cafe_car/form_builder.rb', line 126 def info(method) = model.info.field(method) |
#input(method, *args, as: nil, **options) ⇒ Object
128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 |
# File 'lib/cafe_car/form_builder.rb', line 128 def input(method, *args, as: nil, **) info = info(method) as ||= info.input [:placeholder] = info.placeholder unless .key?(:placeholder) [:autocomplete] = info.autocomplete unless .key?(:autocomplete) [:multiple] = true if as == :file_field && info.multiple? && !.key?(:multiple) # Field-typed inputs render through their component object; explicit `as:` # overrides the family doesn't own (e.g. `hidden_field`) fall through to the # helper directly, preserving `#input`'s "render via any form helper" contract. return public_send(as, method, *args, **) unless Inputs::BaseInput.classes.key?(as) Inputs::BaseInput.build(as, form: self, method:, template: @template, args:, **).to_html end |
#label(method, text = info(method).label, required: info(method).required?) ⇒ Object
117 118 119 |
# File 'lib/cafe_car/form_builder.rb', line 117 def label(method, text = info(method).label, required: info(method).required?, **, &) super(method, @template.safe_join([ text, required ? "*" : "" ]), required:, **, &) end |
#model ⇒ Object
12 |
# File 'lib/cafe_car/form_builder.rb', line 12 def model = @object.is_a?(Class) ? @object : @object.class |
#object_for(method) ⇒ Object
173 174 175 176 177 178 179 180 |
# File 'lib/cafe_car/form_builder.rb', line 173 def object_for(method) method = method.to_s.chomp("_attributes").to_sym if info(method).reflection.collection? object.try(method) else object.try(method) || object.try("build_#{method}") end end |
#options_url(klass) ⇒ Object
URL of the associated model's typeahead feed in the current namespace, or nil
when no such route exists. e.g. belongs_to :owner -> options_admin_users_path.
62 63 64 65 66 |
# File 'lib/cafe_car/form_builder.rb', line 62 def (klass) return unless klass helper = [ :options, *@template.namespace, klass.model_name.route_key, :path ].join("_") @template.public_send(helper) if @template.respond_to?(helper) end |
#policy ⇒ Object
13 |
# File 'lib/cafe_car/form_builder.rb', line 13 def policy = @template.policy(@object) |
#preserved_association(info) ⇒ Object
A persisted association can sit outside the viewer's current list boundary
(or listing can be denied wholesale). Keep that foreign key through an
unrelated edit without exposing the associated record as a labelled option.
The controller accepts only this unchanged value; any reassignment still
requires index? and membership in the associated policy scope.
90 91 92 93 94 95 |
# File 'lib/cafe_car/form_builder.rb', line 90 def preserved_association(info) return @template.safe_join([]) unless object.respond_to?(:persisted?) && object.persisted? return @template.safe_join([]) unless info.reflection.belongs_to? hidden(info.input_key) end |
#remaining_attributes ⇒ Object
182 |
# File 'lib/cafe_car/form_builder.rb', line 182 def remaining_attributes = policy.attributes.editable - @fields.keys |
#remaining_fields(&block) ⇒ Object
184 185 186 187 188 |
# File 'lib/cafe_car/form_builder.rb', line 184 def remaining_fields(**, &block) block ||= proc { field(_1, **) } fields = remaining_attributes.map(&block) @template.safe_join(fields) end |
#searchable_select(info) ⇒ Object
HTML options that flag an association
53 54 55 56 57 58 |
# File 'lib/cafe_car/form_builder.rb', line 53 def searchable_select(info) data = { "searchable-select": "" } url = (info.reflection&.klass) data["searchable-select-url"] = url if url { data: } end |
#show ⇒ Object
14 |
# File 'lib/cafe_car/form_builder.rb', line 14 def show(...) = ui.Input { @template.present(@object).show(...) } |
#submit(value = nil, **options) ⇒ Object
121 122 123 124 |
# File 'lib/cafe_car/form_builder.rb', line 121 def submit(value = nil, **) [:class] ||= ui.class(:button, :primary) super(value, ) end |
#with_selected(info, collection) ⇒ Object
The capped option collection, guaranteeing the currently-associated record is
among the options even when it sorts past the cap — otherwise editing a record
whose association is beyond max_collection_options would silently drop the value.
71 72 73 74 75 76 77 78 79 80 81 82 83 |
# File 'lib/cafe_car/form_builder.rb', line 71 def with_selected(info, collection) return collection unless info.reflection&.belongs_to? return collection unless association_accessible?(info) selected = object.try(info.reflection.name) return collection unless selected records = collection.to_a return records if records.include?(selected) scope = @template.policy_scope(info.reflection.klass) scope.where(id: selected.id).exists? ? [ selected, *records ] : records end |