view_partial_form_builder 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []