view_partial_form_builder 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e30eab928ee84a6217afbabc1c746aa7a7906efa5417fc323f6b78e476aa3fbb
4
+ data.tar.gz: 21af6978419ac00cebba9989b86943eb2d4d102397d880cee50acb8a7ef11c7c
5
+ SHA512:
6
+ metadata.gz: 848d4ddf155cb750a63400e9a5aaa3ea062a4d01b3041294c6ddbf64c3570882e28052d6b55066c731320daae6b43e7fa53c47f1a89924603bab50c3ba3885be
7
+ data.tar.gz: 05e69629609a81b0563bc5a234c3305dc0355e6f0b458d6aea41e75d49009d26a1e274a9ce7e4ccf6b50aa4ff90afa79c30220f44d97a4c754d9988fd9476046
@@ -0,0 +1,20 @@
1
+ Copyright 2020 Sean Doyle
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,277 @@
1
+ # ViewPartialFormBuilder
2
+
3
+ Construct `<form>` elements and their fields by combining
4
+ [`ActionView::Helpers::FormBuilder`][FormBuilder] with [Rails View
5
+ Partials][partials].
6
+
7
+ [FormBuilder]: https://api.rubyonrails.org/classes/ActionView/Helpers/FormBuilder.html
8
+ [partials]: https://api.rubyonrails.org/classes/ActionView/PartialRenderer.html
9
+
10
+ ## Usage
11
+
12
+ ### Building the Form
13
+
14
+ First, render a `<form>` element with [`form_with`][form_with] the necessary
15
+ fields:
16
+
17
+ ```html+erb
18
+ <%# app/views/users/new.html.erb %>
19
+
20
+ <%= form_with(model: user) do |form| %>
21
+ <%= form.label(:name) %>
22
+ <%= form.text_field(:name, class: "text-field", required: true) %>
23
+
24
+ <%= form.label(:email) %>
25
+ <%= form.email_field(:email, class: "text-field text-field--large", required: true) %>
26
+
27
+ <%= form.label(:password) %>
28
+ <%= form.password_field(:email, class: "text-field", required: true) %>
29
+
30
+ <%= form.button(class: "button button--primary") %>
31
+ <% end %>
32
+ ```
33
+
34
+ ### Declaring the Fields' View Partials
35
+
36
+ Next, declare view partials that correspond to the [`FormBuilder`][FormBuilder]
37
+ helper method you'd like to have more control over:
38
+
39
+ ```html+erb
40
+ <%# app/views/form_builder/_text_field.html.erb %>
41
+
42
+ <input
43
+ type="text"
44
+ name="<%= form.object_name %>[<%= method %>]"
45
+ class="text-field"
46
+ <% options.each do |attribute, value| %>
47
+ <%= attribute %>="<%= value %>"
48
+ <% end %>
49
+ >
50
+
51
+ <%# app/views/form_builder/_email_field.html.erb %>
52
+
53
+ <input
54
+ type="email"
55
+ name="<%= form.object_name %>[<%= method %>]"
56
+ class="text-field text-field--large"
57
+ <% options.each do |attribute, value| %>
58
+ <%= attribute %>="<%= value %>"
59
+ <% end %>
60
+ >
61
+
62
+ <%# app/views/form_builder/_button.html.erb %>
63
+
64
+ <button
65
+ class="button button--primary"
66
+ <% options.each do |attribute, value| %>
67
+ <%= attribute %>="<%= value %>"
68
+ <% end %>
69
+ >
70
+ <%= value %>
71
+ </button>
72
+ ```
73
+
74
+ You'll have local access to the `FormBuilder` instance as the template-local
75
+ `form` variable. You can mix and match between declaring HTML elements, and
76
+ generating HTML through Rails' helpers:
77
+
78
+ ```html+erb
79
+ <%# app/views/form_builder/_email_field.html.erb %>
80
+
81
+ <div class="email-field-wrapper">
82
+ <%= form.email_field(method, required: true, **options)) %>
83
+ </div>
84
+ ```
85
+
86
+ ```html+erb
87
+ <%# app/views/form_builder/_button.html.erb %>
88
+
89
+ <div class="button-wrapper">
90
+ <%= form.button(*arguments, **options, &block) %>
91
+ </div>
92
+ ```
93
+
94
+ Templates with calls to [`FormBuilder#fields`][fields] and
95
+ [`FormBuilder::fields_for`][fields_for] will yield instances of
96
+ `ViewPartialFormBuilder` as block arguments.
97
+
98
+ With the exception of `fields` and `fields_for`, view partials for all other
99
+ [`FormBuilder` field methods][FormBuilder] can be declared.
100
+
101
+ When a partial for a helper method is not declared, `ViewPartialFormBuilder`
102
+ will fall back to the default helper method's behavior.
103
+
104
+ [fields]: https://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-fields
105
+ [fields_for]: https://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-fields_for
106
+
107
+ ### Arguments
108
+
109
+ Every view partial has access to the arguments it was invoked with. For example,
110
+ the [`FormBuilder#button`][button] accepts two arguments: `method` and `value`.
111
+ Arguments are made available as partial-local variables (along with key-value
112
+ pairs in the [`local_assigns`][local_assigns]).
113
+
114
+ In addition, each view partial receives:
115
+
116
+ * `form` - a reference to the instance of `ViewPartialFormBuilder`, which is a
117
+ descendant of [`ActionView::Helpers::FormBuilder`][FormBuilder]
118
+
119
+ * `arguments` - an Array containing the arguments the helper received, in the
120
+ order they were received. This can be useful to pass to the view partial's
121
+ helper by [splatting them][splat] out.
122
+
123
+ * `&block` - a callable, `yield`-able block if the helper method was passed one
124
+
125
+ In cases when a [`ActionView::Helpers::FormBuilder` helper
126
+ method][FormBuilder]'s last arguments are options (either `Hash` instances or
127
+ [keyword arguments][]), they're omitted from the `arguments` array.
128
+
129
+ If you want to pass-through all arguments, options, and block parameters, you
130
+ can splat them out:
131
+
132
+ ```html+erb
133
+ <%# app/views/form_builder/_label.html.erb %>
134
+
135
+ <%= form.label(*arguments, **options, &block) %>
136
+ ```
137
+
138
+ ```html+erb
139
+ <%# app/views/form_builder/_select.html.erb %>
140
+
141
+ <%= form.select(*arguments, **html_options, &block) %>
142
+ ```
143
+
144
+ #### Handling DOMTokenList attributes
145
+
146
+ An [HTML element's `class` attribute][mdn-class] is treated by browsers as a
147
+ [`DOMTokenList`][DOMTokenList]:
148
+
149
+ > set of space-separated tokens. Such a set is returned by
150
+ > [`Element.classList`][classList], ...
151
+ > [`HTMLAnchorElement.relList`][relList]...
152
+ >
153
+ > It is indexed beginning with `0` as with JavaScript Array objects.
154
+ > `DOMTokenList` is always case-sensitive.
155
+
156
+ When rendering a field's DOMTokenList-backed attributes (like `class` or
157
+ [`"data-controller"` when specifying StimulusJS
158
+ controllers][stimulus-controller]), transforming and combining singular `String`
159
+ instances into lists of token can be very useful.
160
+
161
+ To simplify those scenarios, a partial's template-local optional attributes are
162
+ made available with the `#merge_token_lists` method.
163
+
164
+ These optional attributes are available through the `options` or `html_options`
165
+ partial-local variables. Their name will depend on the partial's corresponding
166
+ [`ActionView::Helpers::FormBuilder`][FormBuilder] interface.
167
+
168
+ Calls to `#merge_token_lists` will merge the key-value pairs and return a new
169
+ Hash-like structure. The attribute's value will be transformed into an `Array`.
170
+ Given call to `form.text_field` and a corresponding partial declaration:
171
+
172
+ ```html+erb
173
+ <%# app/views/users/new.html.erb %>
174
+ <%= form_with(model: post) do |form| %>
175
+ <%= form.text_field(:name, class: "text-field--modifier") %>
176
+ <% end %>
177
+
178
+ <# app/views/form_builder/_text_field.html.erb %>
179
+
180
+ <%= form.text_field(*arguments, **options.merge_token_lists(class: "text-field")) %>
181
+ ```
182
+
183
+ The resulting HTML `<input>` element will merge have its [`class`
184
+ attribute][mdn-class] set to a list containing both sets of ERB-side `class:`
185
+ values:
186
+
187
+ ```html
188
+ <input type="text" name="post[name]" class="text-field text-field--modifier">
189
+ ```
190
+
191
+ [form_with]: https://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-form_with
192
+ [button]: https://api.rubyonrails.org/classes/ActionView/Helpers/FormBuilder.html#method-i-button
193
+ [local_assigns]: https://api.rubyonrails.org/classes/ActionView/Template.html#method-i-local_assigns
194
+ [splat]: https://ruby-doc.org/core-2.2.0/doc/syntax/calling_methods_rdoc.html#label-Array+to+Arguments+Conversion
195
+ [keyword arguments]: https://thoughtbot.com/blog/ruby-2-keyword-arguments
196
+ [mdn-class]: https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/class
197
+ [DOMTokenList]: https://developer.mozilla.org/en-US/docs/Web/API/DOMTokenList
198
+ [classList]: https://developer.mozilla.org/en-US/docs/Web/API/Element/classList
199
+ [relList]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLAnchorElement/relList
200
+ [stimulus-controller]: https://stimulusjs.org/reference/controllers#identifiers
201
+
202
+ ### Rendering the Fields
203
+
204
+ The fields' view partial files behave like any other: their contents will be
205
+ used to populate the original call-site.
206
+
207
+ To opt-out of view partial rendering for a field, first call `#default` on the
208
+ block-local `form` variable:
209
+
210
+ ```erb
211
+ <%# app/views/users/form_builder/_email_field.html.erb %>
212
+
213
+ <%= form.default.email_field(*arguments, **options) %>
214
+ ```
215
+
216
+ When passing a `model:` or `scope:` to calls to [`form_with`][form_with],
217
+ a pluralized version of the FormBuilder's object name will be prepended to the
218
+ look up path.
219
+
220
+ For example, when calling `form_with(model: User.new)`, a partial declared in
221
+ `app/views/users/form_builder/` would take precedent over a partial declared in
222
+ `app/views/form_builder/`.
223
+
224
+ ```erb
225
+ <%# app/views/users/form_builder/_password_field.html.erb %>
226
+
227
+ <div class="password-field-wrapper">
228
+ <%= form.password_field(*arguments, **options) %>
229
+ </div>
230
+ ```
231
+
232
+ If you'd like to render a specific partial for a field, you can declare the name
233
+ as the `partial:` option:
234
+
235
+ ```erb
236
+ <%# app/views/users/new.html.erb %>
237
+
238
+ <%= form_with(model: User.new) do |form| %>
239
+ <%= form.email_field(:email, partial: "emails/my_special_text_field") %>
240
+ <% end %>
241
+ ```
242
+
243
+ ### Configuration
244
+
245
+ View partials lookup and resolution will be scoped to the
246
+ `app/views/form_builder` directory.
247
+
248
+ To override this destination to another directory (for example,
249
+ `app/views/fields`, or `app/views/users/fields`), set
250
+ `ViewPartialFormBuilder.view_partial_directory`:
251
+
252
+ ```ruby
253
+ # config/initializers/view_partial_form_builder.rb
254
+ ViewPartialFormBuilder.view_partial_directory = "fields"
255
+ ```
256
+
257
+ ## Installation
258
+
259
+ Add this line to your application's Gemfile:
260
+
261
+ ```ruby
262
+ gem 'view_partial_form_builder'
263
+ ```
264
+
265
+ And then execute:
266
+
267
+ ```bash
268
+ $ bundle
269
+ ```
270
+
271
+ ## Contributing
272
+
273
+ See [CONTRIBUTING.md](CONTRIBUTING.md).
274
+
275
+ ## License
276
+
277
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,32 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'ViewPartialFormBuilder'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.md')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
18
+ load 'rails/tasks/engine.rake'
19
+
20
+ load 'rails/tasks/statistics.rake'
21
+
22
+ require 'bundler/gem_tasks'
23
+
24
+ require 'rake/testtask'
25
+
26
+ Rake::TestTask.new(:test) do |t|
27
+ t.libs << 'test'
28
+ t.pattern = 'test/**/*_test.rb'
29
+ t.verbose = false
30
+ end
31
+
32
+ task default: :test
@@ -0,0 +1,5 @@
1
+ require "view_partial_form_builder/engine"
2
+
3
+ module ViewPartialFormBuilder
4
+ mattr_accessor :view_partial_directory, default: "form_builder"
5
+ end
@@ -0,0 +1,9 @@
1
+ require "view_partial_form_builder/form_builder"
2
+
3
+ module ViewPartialFormBuilder
4
+ class Engine < ::Rails::Engine
5
+ ActiveSupport.on_load(:action_controller_base) do
6
+ default_form_builder ViewPartialFormBuilder::FormBuilder
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,296 @@
1
+ require "view_partial_form_builder/lookup_context"
2
+ require "view_partial_form_builder/html_attributes"
3
+
4
+ module ViewPartialFormBuilder
5
+ class FormBuilder < ActionView::Helpers::FormBuilder
6
+ attr_reader :default
7
+
8
+ def initialize(*)
9
+ super
10
+
11
+ @default = ActionView::Helpers::FormBuilder.new(
12
+ object_name,
13
+ object,
14
+ @template,
15
+ options,
16
+ )
17
+ @lookup_context = LookupContext.new(
18
+ overridden_context: @template.lookup_context,
19
+ object_name: object_name,
20
+ view_partial_directory: ViewPartialFormBuilder.view_partial_directory,
21
+ )
22
+ end
23
+
24
+ def label(method, text = nil, **options, &block)
25
+ locals = {
26
+ method: method,
27
+ text: text,
28
+ options: HtmlAttributes.new(options),
29
+ block: block,
30
+ arguments: [method, text],
31
+ }
32
+
33
+ render_partial("label", locals, fallback: -> { super }, &block)
34
+ end
35
+
36
+ def check_box(method, options = {}, checked_value = "1", unchecked_value = "0")
37
+ locals = {
38
+ method: method,
39
+ options: HtmlAttributes.new(options),
40
+ checked_value: checked_value,
41
+ unchecked_value: unchecked_value,
42
+ arguments: [method, options, checked_value, unchecked_value],
43
+ }
44
+
45
+ render_partial("check_box", locals, fallback: -> { super })
46
+ end
47
+
48
+ def radio_button(method, tag_value, **options)
49
+ locals = {
50
+ method: method,
51
+ tag_value: tag_value,
52
+ options: HtmlAttributes.new(options),
53
+ arguments: [method, tag_value],
54
+ }
55
+
56
+ render_partial("radio_button", locals, fallback: -> { super })
57
+ end
58
+
59
+ def select(method, choices = nil, options = {}, **html_options, &block)
60
+ html_options = @default_html_options.merge(html_options)
61
+
62
+ locals = {
63
+ method: method,
64
+ choices: choices,
65
+ options: options,
66
+ html_options: HtmlAttributes.new(html_options),
67
+ block: block,
68
+ arguments: [method, choices, options],
69
+ }
70
+
71
+ render_partial("select", locals, fallback: -> { super }, &block)
72
+ end
73
+
74
+ def collection_select(method, collection, value_method, text_method, options = {}, **html_options)
75
+ html_options = @default_html_options.merge(html_options)
76
+
77
+ locals = {
78
+ method: method,
79
+ collection: collection,
80
+ value_method: value_method,
81
+ text_method: text_method,
82
+ options: options,
83
+ html_options: html_options,
84
+ arguments: [method, collection, value_method, text_method, options],
85
+ }
86
+
87
+ render_partial("collection_select", locals, fallback: -> { super })
88
+ end
89
+
90
+ def collection_check_boxes(method, collection, value_method, text_method, options = {}, **html_options, &block)
91
+ html_options = @default_html_options.merge(html_options)
92
+
93
+ locals = {
94
+ method: method,
95
+ collection: collection,
96
+ value_method: value_method,
97
+ text_method: text_method,
98
+ options: options,
99
+ html_options: HtmlAttributes.new(html_options),
100
+ block: block,
101
+ arguments: [
102
+ method,
103
+ collection,
104
+ value_method,
105
+ text_method,
106
+ options,
107
+ ],
108
+ }
109
+
110
+ render_partial("collection_check_boxes", locals, fallback: -> { super }, &block)
111
+ end
112
+
113
+ def collection_radio_buttons(method, collection, value_method, text_method, options = {}, **html_options, &block)
114
+ html_options = @default_html_options.merge(html_options)
115
+
116
+ locals = {
117
+ method: method,
118
+ collection: collection,
119
+ value_method: value_method,
120
+ text_method: text_method,
121
+ options: options,
122
+ html_options: HtmlAttributes.new(html_options),
123
+ block: block,
124
+ arguments: [
125
+ method,
126
+ collection,
127
+ value_method,
128
+ text_method,
129
+ options,
130
+ ],
131
+ }
132
+
133
+ render_partial("collection_radio_buttons", locals, fallback: -> { super }, &block)
134
+ end
135
+
136
+ def grouped_collection_select(method, collection, group_method, group_label_method, option_key_method, option_value_method, options = {}, **html_options)
137
+ html_options = @default_html_options.merge(html_options)
138
+
139
+ locals = {
140
+ method: method,
141
+ collection: collection,
142
+ group_method: group_method,
143
+ group_label_method: group_label_method,
144
+ option_key_method: option_key_method,
145
+ option_value_method: option_value_method,
146
+ html_options: HtmlAttributes.new(html_options),
147
+ options: options,
148
+ arguments: [
149
+ method,
150
+ collection,
151
+ group_method,
152
+ group_label_method,
153
+ option_key_method,
154
+ option_value_method,
155
+ options,
156
+ ],
157
+ }
158
+
159
+ render_partial("grouped_collection_select", locals, fallback: -> { super })
160
+ end
161
+
162
+ def time_zone_select(method, priority_zones = nil, options = {}, **html_options)
163
+ html_options = @default_html_options.merge(html_options)
164
+
165
+ locals = {
166
+ method: method,
167
+ priority_zones: priority_zones,
168
+ html_options: HtmlAttributes.new(html_options),
169
+ options: options,
170
+ arguments: [method, priority_zones, options],
171
+ }
172
+
173
+ render_partial("time_zone_select", locals, fallback: -> { super })
174
+ end
175
+
176
+ def hidden_field(method, **options)
177
+ @emitted_hidden_id = true if method == :id
178
+
179
+ locals = {
180
+ method: method,
181
+ options: HtmlAttributes.new(options),
182
+ arguments: [method],
183
+ }
184
+
185
+ render_partial("hidden_field", locals, fallback: -> { super })
186
+ end
187
+
188
+ def file_field(method, **options)
189
+ self.multipart = true
190
+
191
+ locals = {
192
+ method: method,
193
+ options: HtmlAttributes.new(options),
194
+ arguments: [method],
195
+ }
196
+
197
+ render_partial("file_field", locals, fallback: -> { super })
198
+ end
199
+
200
+ def submit(value = nil, **options)
201
+ value, options = nil, value if value.is_a?(Hash)
202
+ value ||= submit_default_value
203
+
204
+ locals = {
205
+ value: value,
206
+ options: HtmlAttributes.new(options),
207
+ arguments: [value],
208
+ }
209
+
210
+ render_partial("submit", locals, fallback: -> { super })
211
+ end
212
+
213
+ def button(value = nil, **options)
214
+ value, options = nil, value if value.is_a?(Hash)
215
+ value ||= submit_default_value
216
+
217
+ locals = {
218
+ value: value,
219
+ options: HtmlAttributes.new(options),
220
+ arguments: [value],
221
+ }
222
+
223
+ render_partial("button", locals, fallback: -> { super })
224
+ end
225
+
226
+ DYNAMICALLY_DECLARED = (
227
+ field_helpers +
228
+ [:rich_text_area] -
229
+ [:label, :check_box, :radio_button, :fields_for, :fields, :hidden_field, :file_field]
230
+ )
231
+
232
+ DYNAMICALLY_DECLARED.each do |selector|
233
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
234
+ def #{selector}(method, **options)
235
+ render_partial(
236
+ "#{selector}",
237
+ {
238
+ method: method,
239
+ options: HtmlAttributes.new(options),
240
+ arguments: [method],
241
+ },
242
+ fallback: -> { super },
243
+ )
244
+ end
245
+ RUBY
246
+ end
247
+
248
+ private
249
+
250
+ attr_reader :lookup_context
251
+
252
+ def render_partial(field, locals, fallback:, &block)
253
+ return fallback.call if about_to_recurse_infinitely?(field)
254
+
255
+ options = locals.fetch(:options, {})
256
+ partial_override = options.delete(:partial)
257
+ locals = objectify_options(options).merge(locals, form: self)
258
+
259
+ lookup_context.override do
260
+ if partial_override.present?
261
+ render(partial_override, locals, &block)
262
+ elsif partial_exists?(field)
263
+ render(field, locals, &block)
264
+ else
265
+ fallback.call
266
+ end
267
+ end
268
+ end
269
+
270
+ def partial_exists?(template_name)
271
+ template_is_partial = true
272
+
273
+ lookup_context.template_exists?(
274
+ template_name,
275
+ lookup_context.prefixes,
276
+ template_is_partial,
277
+ )
278
+ end
279
+
280
+ def render(partial_name, locals, &block)
281
+ if block.present?
282
+ @template.render(layout: partial_name, locals: locals, &block)
283
+ else
284
+ @template.render(partial: partial_name, locals: locals)
285
+ end
286
+ end
287
+
288
+ def about_to_recurse_infinitely?(field)
289
+ @template.instance_eval do
290
+ *, partial = @virtual_path.split("/")
291
+
292
+ partial == "_#{field}"
293
+ end
294
+ end
295
+ end
296
+ end
@@ -0,0 +1,47 @@
1
+ module ViewPartialFormBuilder
2
+ class HtmlAttributes
3
+ def initialize(**attributes)
4
+ @attributes = attributes
5
+ end
6
+
7
+ def merge_token_lists(**token_list_attributes)
8
+ merge(
9
+ token_list_attributes.reduce({}) do |merged, (key, value)|
10
+ token_list = Array(attributes.delete(key)).unshift(value)
11
+
12
+ merged.merge(key => token_list.flatten.uniq)
13
+ end
14
+ )
15
+ end
16
+
17
+ def to_h
18
+ attributes.to_h
19
+ end
20
+
21
+ def to_hash
22
+ attributes.to_hash
23
+ end
24
+
25
+ def method_missing(method_name, *arguments, &block)
26
+ if attributes.respond_to?(method_name)
27
+ return_value = attributes.public_send(method_name, *arguments, &block)
28
+
29
+ if return_value.kind_of?(Hash)
30
+ HtmlAttributes.new(return_value)
31
+ else
32
+ return_value
33
+ end
34
+ else
35
+ super
36
+ end
37
+ end
38
+
39
+ def respond_to_missing?(method_name, include_private = false)
40
+ attributes.respond_to?(method_name) || super
41
+ end
42
+
43
+ private
44
+
45
+ attr_reader :attributes
46
+ end
47
+ end
@@ -0,0 +1,53 @@
1
+ module ViewPartialFormBuilder
2
+ class LookupContext
3
+ delegate_missing_to :overridden_context
4
+
5
+ def initialize(overridden_context:, object_name:, view_partial_directory:)
6
+ @object_name = object_name.to_s.pluralize
7
+ @overridden_context = overridden_context
8
+ @prefixes = overridden_context.prefixes.dup.freeze
9
+ @view_partial_directory = view_partial_directory
10
+ end
11
+
12
+ def override(&block)
13
+ previous_prefixes = overridden_context.prefixes
14
+
15
+ overridden_context.prefixes = prefix_overrides
16
+
17
+ yield
18
+ ensure
19
+ overridden_context.prefixes = previous_prefixes
20
+ end
21
+
22
+ private
23
+
24
+ attr_reader :overridden_context, :object_name, :view_partial_directory
25
+
26
+ def prefix_overrides
27
+ *overridden_prefixes, root_prefix = @prefixes.dup
28
+
29
+ prefixes = [
30
+ "#{object_name}/#{view_partial_directory}",
31
+ object_name,
32
+ view_partial_directory,
33
+ "#{root_prefix}/#{view_partial_directory}",
34
+ root_prefix,
35
+ ]
36
+
37
+ overridden_prefixes.reverse_each do |prefix|
38
+ namespace, *files = prefix.split("/")
39
+
40
+ prefixes.unshift(prefix)
41
+
42
+ if namespace.present?
43
+ prefixes.unshift("#{namespace}/#{view_partial_directory}")
44
+ end
45
+
46
+ prefixes.unshift("#{prefix}/#{view_partial_directory}")
47
+ end
48
+
49
+
50
+ prefixes.uniq
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,3 @@
1
+ module ViewPartialFormBuilder
2
+ VERSION = '0.1.0'
3
+ end
metadata ADDED
@@ -0,0 +1,123 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: view_partial_form_builder
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Sean Doyle
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-04-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: actionview
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 4.0.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 4.0.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: railties
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 4.0.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 4.0.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest-around
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: activemodel
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 4.0.0
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: 4.0.0
69
+ - !ruby/object:Gem::Dependency
70
+ name: activerecord-nulldb-adapter
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description: A Rails form builder where all designer-facing configuration is via templates.
84
+ email:
85
+ - sean.p.doyle24@gmail.com
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - MIT-LICENSE
91
+ - README.md
92
+ - Rakefile
93
+ - lib/view_partial_form_builder.rb
94
+ - lib/view_partial_form_builder/engine.rb
95
+ - lib/view_partial_form_builder/form_builder.rb
96
+ - lib/view_partial_form_builder/html_attributes.rb
97
+ - lib/view_partial_form_builder/lookup_context.rb
98
+ - lib/view_partial_form_builder/version.rb
99
+ homepage: https://github.com/seanpdoyle/view_partial_form_builder
100
+ licenses:
101
+ - MIT
102
+ metadata: {}
103
+ post_install_message:
104
+ rdoc_options: []
105
+ require_paths:
106
+ - lib
107
+ required_ruby_version: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ required_rubygems_version: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ requirements: []
118
+ rubygems_version: 3.0.3
119
+ signing_key:
120
+ specification_version: 4
121
+ summary: Construct <form> element fields by combining ActionView::Helpers::FormBuilder
122
+ with Rails View Partials
123
+ test_files: []