superform 0.5.0 → 0.6.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 +4 -4
- data/CHANGELOG.md +131 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +131 -29
- data/README.md +200 -66
- data/SPEC_STYLE_GUIDE.md +146 -0
- data/lib/generators/superform/install/USAGE +6 -2
- data/lib/generators/superform/install/install_generator.rb +11 -22
- data/lib/generators/superform/install/templates/base.rb +33 -0
- data/lib/superform/dom.rb +51 -0
- data/lib/superform/field.rb +121 -0
- data/lib/superform/field_collection.rb +33 -0
- data/lib/superform/form.rb +34 -0
- data/lib/superform/namespace.rb +134 -0
- data/lib/superform/namespace_collection.rb +51 -0
- data/lib/superform/node.rb +12 -0
- data/lib/superform/rails/components/base.rb +31 -0
- data/lib/superform/rails/components/button.rb +20 -0
- data/lib/superform/rails/components/checkbox.rb +19 -0
- data/lib/superform/rails/components/field.rb +11 -0
- data/lib/superform/rails/components/input.rb +59 -0
- data/lib/superform/rails/components/label.rb +20 -0
- data/lib/superform/rails/components/select.rb +43 -0
- data/lib/superform/rails/components/textarea.rb +12 -0
- data/lib/superform/rails/form.rb +240 -0
- data/lib/superform/rails/option_mapper.rb +36 -0
- data/lib/superform/rails/strong_parameters.rb +73 -0
- data/lib/superform/rails.rb +17 -368
- data/lib/superform/version.rb +1 -1
- data/lib/superform.rb +3 -294
- metadata +25 -9
- data/lib/generators/superform/install/templates/application_form.rb +0 -31
@@ -0,0 +1,240 @@
|
|
1
|
+
module Superform
|
2
|
+
module Rails
|
3
|
+
# A Phlex::HTML view module that accepts a model and sets a `Superform::Namespace`
|
4
|
+
# with the `Object#model_name` as the key and maps the object to form fields
|
5
|
+
# and namespaces.
|
6
|
+
#
|
7
|
+
# The `Form::Field` is a class that's meant to be extended so you can customize the `Form` inputs
|
8
|
+
# to your applications needs. Defaults for the `input`, `button`, `label`, and `textarea` tags
|
9
|
+
# are provided.
|
10
|
+
#
|
11
|
+
# The `Form` component also handles Rails authenticity tokens via the `authenticity_toklen_field`
|
12
|
+
# method and the HTTP verb via the `_method_field`.
|
13
|
+
class Form < Component
|
14
|
+
include Phlex::Rails::Helpers::FormAuthenticityToken
|
15
|
+
include Phlex::Rails::Helpers::URLFor
|
16
|
+
|
17
|
+
attr_accessor :model
|
18
|
+
|
19
|
+
delegate \
|
20
|
+
:Field,
|
21
|
+
:field,
|
22
|
+
:collection,
|
23
|
+
:namespace,
|
24
|
+
:assign,
|
25
|
+
:serialize,
|
26
|
+
to: :@namespace
|
27
|
+
|
28
|
+
# The Field class is designed to be extended to create custom forms. To override,
|
29
|
+
# in your subclass you may have something like this:
|
30
|
+
#
|
31
|
+
# ```ruby
|
32
|
+
# class MyForm < Superform::Rails::Form
|
33
|
+
# class MyLabel < Superform::Rails::Components::Label
|
34
|
+
# def view_template(&content)
|
35
|
+
# label(form: @field.dom.name, class: "text-bold", &content)
|
36
|
+
# end
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# class Field < Field
|
40
|
+
# def label(**attributes)
|
41
|
+
# MyLabel.new(self, **attributes)
|
42
|
+
# end
|
43
|
+
# end
|
44
|
+
# end
|
45
|
+
# ```
|
46
|
+
#
|
47
|
+
# Now all calls to `label` will have the `text-bold` class applied to it.
|
48
|
+
class Field < Superform::Field
|
49
|
+
def button(**attributes)
|
50
|
+
Components::Button.new(self, attributes:)
|
51
|
+
end
|
52
|
+
|
53
|
+
def input(**attributes)
|
54
|
+
Components::Input.new(self, attributes:)
|
55
|
+
end
|
56
|
+
|
57
|
+
def text(*, **, &)
|
58
|
+
input(*, **, type: :text, &)
|
59
|
+
end
|
60
|
+
|
61
|
+
def checkbox(**attributes)
|
62
|
+
Components::Checkbox.new(self, attributes:)
|
63
|
+
end
|
64
|
+
|
65
|
+
def label(**attributes, &)
|
66
|
+
Components::Label.new(self, attributes:, &)
|
67
|
+
end
|
68
|
+
|
69
|
+
def textarea(**attributes)
|
70
|
+
Components::Textarea.new(self, attributes:)
|
71
|
+
end
|
72
|
+
|
73
|
+
def select(*collection, **attributes, &)
|
74
|
+
Components::Select.new(self, attributes:, collection:, &)
|
75
|
+
end
|
76
|
+
|
77
|
+
# HTML5 input type convenience methods - clean API without _field suffix
|
78
|
+
# Examples:
|
79
|
+
# field(:email).email(class: "form-input")
|
80
|
+
# field(:age).number(min: 18, max: 99)
|
81
|
+
# field(:birthday).date
|
82
|
+
# field(:secret).hidden(value: "token123")
|
83
|
+
# field(:gender).radio("male", id: "user_gender_male")
|
84
|
+
def hidden(*, **, &)
|
85
|
+
input(*, **, type: :hidden, &)
|
86
|
+
end
|
87
|
+
|
88
|
+
def password(*, **, &)
|
89
|
+
input(*, **, type: :password, &)
|
90
|
+
end
|
91
|
+
|
92
|
+
def email(*, **, &)
|
93
|
+
input(*, **, type: :email, &)
|
94
|
+
end
|
95
|
+
|
96
|
+
def url(*, **, &)
|
97
|
+
input(*, **, type: :url, &)
|
98
|
+
end
|
99
|
+
|
100
|
+
def tel(*, **, &)
|
101
|
+
input(*, **, type: :tel, &)
|
102
|
+
end
|
103
|
+
alias_method :phone, :tel
|
104
|
+
|
105
|
+
def number(*, **, &)
|
106
|
+
input(*, **, type: :number, &)
|
107
|
+
end
|
108
|
+
|
109
|
+
def range(*, **, &)
|
110
|
+
input(*, **, type: :range, &)
|
111
|
+
end
|
112
|
+
|
113
|
+
def date(*, **, &)
|
114
|
+
input(*, **, type: :date, &)
|
115
|
+
end
|
116
|
+
|
117
|
+
def time(*, **, &)
|
118
|
+
input(*, **, type: :time, &)
|
119
|
+
end
|
120
|
+
|
121
|
+
def datetime(*, **, &)
|
122
|
+
input(*, **, type: :"datetime-local", &)
|
123
|
+
end
|
124
|
+
|
125
|
+
def month(*, **, &)
|
126
|
+
input(*, **, type: :month, &)
|
127
|
+
end
|
128
|
+
|
129
|
+
def week(*, **, &)
|
130
|
+
input(*, **, type: :week, &)
|
131
|
+
end
|
132
|
+
|
133
|
+
def color(*, **, &)
|
134
|
+
input(*, **, type: :color, &)
|
135
|
+
end
|
136
|
+
|
137
|
+
def search(*, **, &)
|
138
|
+
input(*, **, type: :search, &)
|
139
|
+
end
|
140
|
+
|
141
|
+
def file(*, **, &)
|
142
|
+
input(*, **, type: :file, &)
|
143
|
+
end
|
144
|
+
|
145
|
+
def radio(value, *, **, &)
|
146
|
+
input(*, **, type: :radio, value: value, &)
|
147
|
+
end
|
148
|
+
|
149
|
+
# Rails compatibility aliases
|
150
|
+
alias_method :check_box, :checkbox
|
151
|
+
alias_method :text_area, :textarea
|
152
|
+
|
153
|
+
def title
|
154
|
+
key.to_s.titleize
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def build_field(...)
|
159
|
+
self.class::Field.new(...)
|
160
|
+
end
|
161
|
+
|
162
|
+
def initialize(model, action: nil, method: nil, **attributes)
|
163
|
+
@model = model
|
164
|
+
@action = action
|
165
|
+
@method = method
|
166
|
+
@attributes = attributes
|
167
|
+
@namespace = Namespace.root(key, object: model, form: self)
|
168
|
+
end
|
169
|
+
|
170
|
+
def around_template(&)
|
171
|
+
form_tag do
|
172
|
+
authenticity_token_field
|
173
|
+
_method_field
|
174
|
+
super
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def form_tag(&)
|
179
|
+
form action: form_action, method: form_method, **@attributes, &
|
180
|
+
end
|
181
|
+
|
182
|
+
def view_template(&block)
|
183
|
+
yield self if block_given?
|
184
|
+
end
|
185
|
+
|
186
|
+
def submit(value = submit_value, **attributes)
|
187
|
+
input **attributes.merge(
|
188
|
+
name: "commit",
|
189
|
+
type: "submit",
|
190
|
+
value: value
|
191
|
+
)
|
192
|
+
end
|
193
|
+
|
194
|
+
def key
|
195
|
+
@model.model_name.param_key
|
196
|
+
end
|
197
|
+
|
198
|
+
protected
|
199
|
+
def authenticity_token_field
|
200
|
+
input(
|
201
|
+
name: "authenticity_token",
|
202
|
+
type: "hidden",
|
203
|
+
value: form_authenticity_token
|
204
|
+
)
|
205
|
+
end
|
206
|
+
|
207
|
+
def _method_field
|
208
|
+
input(
|
209
|
+
name: "_method",
|
210
|
+
type: "hidden",
|
211
|
+
value: _method_field_value
|
212
|
+
)
|
213
|
+
end
|
214
|
+
|
215
|
+
def _method_field_value
|
216
|
+
@method || resource_method_field_value
|
217
|
+
end
|
218
|
+
|
219
|
+
def resource_method_field_value
|
220
|
+
@model.persisted? ? "patch" : "post"
|
221
|
+
end
|
222
|
+
|
223
|
+
def submit_value
|
224
|
+
"#{resource_action.to_s.capitalize} #{@model.model_name}"
|
225
|
+
end
|
226
|
+
|
227
|
+
def resource_action
|
228
|
+
@model.persisted? ? :update : :create
|
229
|
+
end
|
230
|
+
|
231
|
+
def form_action
|
232
|
+
@action ||= url_for(action: resource_action)
|
233
|
+
end
|
234
|
+
|
235
|
+
def form_method
|
236
|
+
@method.to_s.downcase == "get" ? "get" : "post"
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Superform
|
2
|
+
module Rails
|
3
|
+
# Accept a collection of objects and map them to options suitable for form controls, like `select > options`
|
4
|
+
class OptionMapper
|
5
|
+
include Enumerable
|
6
|
+
|
7
|
+
def initialize(collection)
|
8
|
+
@collection = collection
|
9
|
+
end
|
10
|
+
|
11
|
+
def each(&options)
|
12
|
+
@collection.each do |object|
|
13
|
+
case object
|
14
|
+
in ActiveRecord::Relation => relation
|
15
|
+
active_record_relation_options_enumerable(relation).each(&options)
|
16
|
+
in id, value
|
17
|
+
options.call id, value
|
18
|
+
in value
|
19
|
+
options.call value, value.to_s
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def active_record_relation_options_enumerable(relation)
|
25
|
+
Enumerator.new do |collection|
|
26
|
+
relation.each do |object|
|
27
|
+
attributes = object.attributes
|
28
|
+
id = attributes.delete(relation.primary_key)
|
29
|
+
value = attributes.values.join(" ")
|
30
|
+
collection << [ id, value ]
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module Superform
|
2
|
+
module Rails
|
3
|
+
module StrongParameters
|
4
|
+
protected
|
5
|
+
# Assigns permitted params to the given form and returns the model.
|
6
|
+
# Usage in a controller when you want to build/validate without saving:
|
7
|
+
#
|
8
|
+
# def preview
|
9
|
+
# @post = Post.new
|
10
|
+
# post = permit PostForm.new(@post)
|
11
|
+
# # post now has attributes from params, but is not persisted
|
12
|
+
# render :preview
|
13
|
+
# end
|
14
|
+
def permit(form)
|
15
|
+
form_params = params.require(form.key)
|
16
|
+
assign(form_params, to: form).model
|
17
|
+
end
|
18
|
+
|
19
|
+
# Saves the form's underlying model after assigning permitted params.
|
20
|
+
# Typical Rails controller usage (create):
|
21
|
+
#
|
22
|
+
# def create
|
23
|
+
# @post = Post.new
|
24
|
+
# if save PostForm.new(@post)
|
25
|
+
# redirect_to @post
|
26
|
+
# else
|
27
|
+
# render :new, status: :unprocessable_entity
|
28
|
+
# end
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# Typical Rails controller usage (update):
|
32
|
+
#
|
33
|
+
# def update
|
34
|
+
# @post = Post.find(params[:id])
|
35
|
+
# if save PostForm.new(@post)
|
36
|
+
# redirect_to @post
|
37
|
+
# else
|
38
|
+
# render :edit, status: :unprocessable_entity
|
39
|
+
# end
|
40
|
+
# end
|
41
|
+
def save(form)
|
42
|
+
permit(form).save
|
43
|
+
end
|
44
|
+
|
45
|
+
# Bang version that raises on validation failure.
|
46
|
+
# Useful when you prefer exceptions or are in a transaction:
|
47
|
+
#
|
48
|
+
# def create
|
49
|
+
# @post = Post.new
|
50
|
+
# save! PostForm.new(@post)
|
51
|
+
# redirect_to @post
|
52
|
+
# rescue ActiveRecord::RecordInvalid
|
53
|
+
# render :new, status: :unprocessable_entity
|
54
|
+
# end
|
55
|
+
def save!(form)
|
56
|
+
permit(form).save!
|
57
|
+
end
|
58
|
+
|
59
|
+
# Assigns params to the form and returns the form.
|
60
|
+
def assign(params, to:)
|
61
|
+
to.tap do |form|
|
62
|
+
# This output of this string goes nowhere since it likely
|
63
|
+
# won't be used. I'm not sure if I'm right about this though,
|
64
|
+
# If I'm wrong, then I think I need to encapsulate this module
|
65
|
+
# into a class that can store the rendered HTML that can be
|
66
|
+
# rendered later.
|
67
|
+
render_to_string form
|
68
|
+
form.assign params
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|