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.
@@ -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