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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +277 -0
- data/Rakefile +32 -0
- data/lib/view_partial_form_builder.rb +5 -0
- data/lib/view_partial_form_builder/engine.rb +9 -0
- data/lib/view_partial_form_builder/form_builder.rb +296 -0
- data/lib/view_partial_form_builder/html_attributes.rb +47 -0
- data/lib/view_partial_form_builder/lookup_context.rb +53 -0
- data/lib/view_partial_form_builder/version.rb +3 -0
- metadata +123 -0
checksums.yaml
ADDED
@@ -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
|
data/MIT-LICENSE
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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).
|
data/Rakefile
ADDED
@@ -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,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
|
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: []
|