superform 0.1.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +2 -0
- data/Gemfile.lock +220 -0
- data/Guardfile +70 -0
- data/README.md +114 -74
- data/lib/generators/superform/install/USAGE +8 -0
- data/lib/generators/superform/install/install_generator.rb +24 -0
- data/lib/generators/superform/install/templates/application_form.rb +31 -0
- data/lib/superform/rails.rb +209 -0
- data/lib/superform/version.rb +1 -1
- data/lib/superform.rb +220 -5
- metadata +23 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b87df41698b91049bed9e30f976d56b92c5c694762a63803453d29fa47c025a9
|
4
|
+
data.tar.gz: bd9ec4dd8499de8303411c3ee88f7a2888ce285b5592eb32829db6043782a4f9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5f945ca1d2a57d3b4ac69d064c72f701cd7d3d524a64dd241c80a23b3fd53cecd572800f2ebbe760321348d0f770b60d60be5acb4a4482e007d9040d21b3898f
|
7
|
+
data.tar.gz: a47df25dcc990f3162b874f85999df79e0acf170643395071aaa2b2c29fde23ff302a055a9ed132927945cc53c30376f645846370f5e323eb89115dc740136ce
|
data/Gemfile
CHANGED
data/Gemfile.lock
ADDED
@@ -0,0 +1,220 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
superform (0.2.0)
|
5
|
+
phlex-rails (~> 1.0)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
actioncable (7.0.6)
|
11
|
+
actionpack (= 7.0.6)
|
12
|
+
activesupport (= 7.0.6)
|
13
|
+
nio4r (~> 2.0)
|
14
|
+
websocket-driver (>= 0.6.1)
|
15
|
+
actionmailbox (7.0.6)
|
16
|
+
actionpack (= 7.0.6)
|
17
|
+
activejob (= 7.0.6)
|
18
|
+
activerecord (= 7.0.6)
|
19
|
+
activestorage (= 7.0.6)
|
20
|
+
activesupport (= 7.0.6)
|
21
|
+
mail (>= 2.7.1)
|
22
|
+
net-imap
|
23
|
+
net-pop
|
24
|
+
net-smtp
|
25
|
+
actionmailer (7.0.6)
|
26
|
+
actionpack (= 7.0.6)
|
27
|
+
actionview (= 7.0.6)
|
28
|
+
activejob (= 7.0.6)
|
29
|
+
activesupport (= 7.0.6)
|
30
|
+
mail (~> 2.5, >= 2.5.4)
|
31
|
+
net-imap
|
32
|
+
net-pop
|
33
|
+
net-smtp
|
34
|
+
rails-dom-testing (~> 2.0)
|
35
|
+
actionpack (7.0.6)
|
36
|
+
actionview (= 7.0.6)
|
37
|
+
activesupport (= 7.0.6)
|
38
|
+
rack (~> 2.0, >= 2.2.4)
|
39
|
+
rack-test (>= 0.6.3)
|
40
|
+
rails-dom-testing (~> 2.0)
|
41
|
+
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
42
|
+
actiontext (7.0.6)
|
43
|
+
actionpack (= 7.0.6)
|
44
|
+
activerecord (= 7.0.6)
|
45
|
+
activestorage (= 7.0.6)
|
46
|
+
activesupport (= 7.0.6)
|
47
|
+
globalid (>= 0.6.0)
|
48
|
+
nokogiri (>= 1.8.5)
|
49
|
+
actionview (7.0.6)
|
50
|
+
activesupport (= 7.0.6)
|
51
|
+
builder (~> 3.1)
|
52
|
+
erubi (~> 1.4)
|
53
|
+
rails-dom-testing (~> 2.0)
|
54
|
+
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
55
|
+
activejob (7.0.6)
|
56
|
+
activesupport (= 7.0.6)
|
57
|
+
globalid (>= 0.3.6)
|
58
|
+
activemodel (7.0.6)
|
59
|
+
activesupport (= 7.0.6)
|
60
|
+
activerecord (7.0.6)
|
61
|
+
activemodel (= 7.0.6)
|
62
|
+
activesupport (= 7.0.6)
|
63
|
+
activestorage (7.0.6)
|
64
|
+
actionpack (= 7.0.6)
|
65
|
+
activejob (= 7.0.6)
|
66
|
+
activerecord (= 7.0.6)
|
67
|
+
activesupport (= 7.0.6)
|
68
|
+
marcel (~> 1.0)
|
69
|
+
mini_mime (>= 1.1.0)
|
70
|
+
activesupport (7.0.6)
|
71
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
72
|
+
i18n (>= 1.6, < 2)
|
73
|
+
minitest (>= 5.1)
|
74
|
+
tzinfo (~> 2.0)
|
75
|
+
builder (3.2.4)
|
76
|
+
cgi (0.3.6)
|
77
|
+
coderay (1.1.3)
|
78
|
+
concurrent-ruby (1.2.2)
|
79
|
+
crass (1.0.6)
|
80
|
+
date (3.3.3)
|
81
|
+
diff-lcs (1.5.0)
|
82
|
+
erb (4.0.2)
|
83
|
+
cgi (>= 0.3.3)
|
84
|
+
erubi (1.12.0)
|
85
|
+
ffi (1.15.5)
|
86
|
+
formatador (1.1.0)
|
87
|
+
globalid (1.1.0)
|
88
|
+
activesupport (>= 5.0)
|
89
|
+
guard (2.18.0)
|
90
|
+
formatador (>= 0.2.4)
|
91
|
+
listen (>= 2.7, < 4.0)
|
92
|
+
lumberjack (>= 1.0.12, < 2.0)
|
93
|
+
nenv (~> 0.1)
|
94
|
+
notiffany (~> 0.0)
|
95
|
+
pry (>= 0.13.0)
|
96
|
+
shellany (~> 0.0)
|
97
|
+
thor (>= 0.18.1)
|
98
|
+
guard-compat (1.2.1)
|
99
|
+
guard-rspec (4.7.3)
|
100
|
+
guard (~> 2.1)
|
101
|
+
guard-compat (~> 1.1)
|
102
|
+
rspec (>= 2.99.0, < 4.0)
|
103
|
+
i18n (1.14.1)
|
104
|
+
concurrent-ruby (~> 1.0)
|
105
|
+
listen (3.8.0)
|
106
|
+
rb-fsevent (~> 0.10, >= 0.10.3)
|
107
|
+
rb-inotify (~> 0.9, >= 0.9.10)
|
108
|
+
loofah (2.21.3)
|
109
|
+
crass (~> 1.0.2)
|
110
|
+
nokogiri (>= 1.12.0)
|
111
|
+
lumberjack (1.2.8)
|
112
|
+
mail (2.8.1)
|
113
|
+
mini_mime (>= 0.1.1)
|
114
|
+
net-imap
|
115
|
+
net-pop
|
116
|
+
net-smtp
|
117
|
+
marcel (1.0.2)
|
118
|
+
method_source (1.0.0)
|
119
|
+
mini_mime (1.1.2)
|
120
|
+
minitest (5.18.1)
|
121
|
+
nenv (0.3.0)
|
122
|
+
net-imap (0.3.6)
|
123
|
+
date
|
124
|
+
net-protocol
|
125
|
+
net-pop (0.1.2)
|
126
|
+
net-protocol
|
127
|
+
net-protocol (0.2.1)
|
128
|
+
timeout
|
129
|
+
net-smtp (0.3.3)
|
130
|
+
net-protocol
|
131
|
+
nio4r (2.5.9)
|
132
|
+
nokogiri (1.15.3-arm64-darwin)
|
133
|
+
racc (~> 1.4)
|
134
|
+
nokogiri (1.15.3-x86_64-linux)
|
135
|
+
racc (~> 1.4)
|
136
|
+
notiffany (0.1.3)
|
137
|
+
nenv (~> 0.1)
|
138
|
+
shellany (~> 0.0)
|
139
|
+
phlex (1.8.1)
|
140
|
+
concurrent-ruby (~> 1.2)
|
141
|
+
erb (>= 4)
|
142
|
+
zeitwerk (~> 2.6)
|
143
|
+
phlex-rails (1.0.0)
|
144
|
+
phlex (~> 1.7)
|
145
|
+
rails (>= 6.1, < 8)
|
146
|
+
zeitwerk (~> 2.6)
|
147
|
+
pry (0.14.2)
|
148
|
+
coderay (~> 1.1)
|
149
|
+
method_source (~> 1.0)
|
150
|
+
racc (1.7.1)
|
151
|
+
rack (2.2.7)
|
152
|
+
rack-test (2.1.0)
|
153
|
+
rack (>= 1.3)
|
154
|
+
rails (7.0.6)
|
155
|
+
actioncable (= 7.0.6)
|
156
|
+
actionmailbox (= 7.0.6)
|
157
|
+
actionmailer (= 7.0.6)
|
158
|
+
actionpack (= 7.0.6)
|
159
|
+
actiontext (= 7.0.6)
|
160
|
+
actionview (= 7.0.6)
|
161
|
+
activejob (= 7.0.6)
|
162
|
+
activemodel (= 7.0.6)
|
163
|
+
activerecord (= 7.0.6)
|
164
|
+
activestorage (= 7.0.6)
|
165
|
+
activesupport (= 7.0.6)
|
166
|
+
bundler (>= 1.15.0)
|
167
|
+
railties (= 7.0.6)
|
168
|
+
rails-dom-testing (2.1.1)
|
169
|
+
activesupport (>= 5.0.0)
|
170
|
+
minitest
|
171
|
+
nokogiri (>= 1.6)
|
172
|
+
rails-html-sanitizer (1.6.0)
|
173
|
+
loofah (~> 2.21)
|
174
|
+
nokogiri (~> 1.14)
|
175
|
+
railties (7.0.6)
|
176
|
+
actionpack (= 7.0.6)
|
177
|
+
activesupport (= 7.0.6)
|
178
|
+
method_source
|
179
|
+
rake (>= 12.2)
|
180
|
+
thor (~> 1.0)
|
181
|
+
zeitwerk (~> 2.5)
|
182
|
+
rake (13.0.6)
|
183
|
+
rb-fsevent (0.11.2)
|
184
|
+
rb-inotify (0.10.1)
|
185
|
+
ffi (~> 1.0)
|
186
|
+
rspec (3.12.0)
|
187
|
+
rspec-core (~> 3.12.0)
|
188
|
+
rspec-expectations (~> 3.12.0)
|
189
|
+
rspec-mocks (~> 3.12.0)
|
190
|
+
rspec-core (3.12.2)
|
191
|
+
rspec-support (~> 3.12.0)
|
192
|
+
rspec-expectations (3.12.3)
|
193
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
194
|
+
rspec-support (~> 3.12.0)
|
195
|
+
rspec-mocks (3.12.5)
|
196
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
197
|
+
rspec-support (~> 3.12.0)
|
198
|
+
rspec-support (3.12.0)
|
199
|
+
shellany (0.0.1)
|
200
|
+
thor (1.2.2)
|
201
|
+
timeout (0.4.0)
|
202
|
+
tzinfo (2.0.6)
|
203
|
+
concurrent-ruby (~> 1.0)
|
204
|
+
websocket-driver (0.7.5)
|
205
|
+
websocket-extensions (>= 0.1.0)
|
206
|
+
websocket-extensions (0.1.5)
|
207
|
+
zeitwerk (2.6.8)
|
208
|
+
|
209
|
+
PLATFORMS
|
210
|
+
arm64-darwin-22
|
211
|
+
x86_64-linux
|
212
|
+
|
213
|
+
DEPENDENCIES
|
214
|
+
guard-rspec (~> 4.7)
|
215
|
+
rake (~> 13.0)
|
216
|
+
rspec (~> 3.0)
|
217
|
+
superform!
|
218
|
+
|
219
|
+
BUNDLED WITH
|
220
|
+
2.4.8
|
data/Guardfile
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
# A sample Guardfile
|
2
|
+
# More info at https://github.com/guard/guard#readme
|
3
|
+
|
4
|
+
## Uncomment and set this to only include directories you want to watch
|
5
|
+
# directories %w(app lib config test spec features) \
|
6
|
+
# .select{|d| Dir.exist?(d) ? d : UI.warning("Directory #{d} does not exist")}
|
7
|
+
|
8
|
+
## Note: if you are using the `directories` clause above and you are not
|
9
|
+
## watching the project directory ('.'), then you will want to move
|
10
|
+
## the Guardfile to a watched dir and symlink it back, e.g.
|
11
|
+
#
|
12
|
+
# $ mkdir config
|
13
|
+
# $ mv Guardfile config/
|
14
|
+
# $ ln -s config/Guardfile .
|
15
|
+
#
|
16
|
+
# and, you'll have to watch "config/Guardfile" instead of "Guardfile"
|
17
|
+
|
18
|
+
# Note: The cmd option is now required due to the increasing number of ways
|
19
|
+
# rspec may be run, below are examples of the most common uses.
|
20
|
+
# * bundler: 'bundle exec rspec'
|
21
|
+
# * bundler binstubs: 'bin/rspec'
|
22
|
+
# * spring: 'bin/rspec' (This will use spring if running and you have
|
23
|
+
# installed the spring binstubs per the docs)
|
24
|
+
# * zeus: 'zeus rspec' (requires the server to be started separately)
|
25
|
+
# * 'just' rspec: 'rspec'
|
26
|
+
|
27
|
+
guard :rspec, cmd: "bundle exec rspec" do
|
28
|
+
require "guard/rspec/dsl"
|
29
|
+
dsl = Guard::RSpec::Dsl.new(self)
|
30
|
+
|
31
|
+
# Feel free to open issues for suggestions and improvements
|
32
|
+
|
33
|
+
# RSpec files
|
34
|
+
rspec = dsl.rspec
|
35
|
+
watch(rspec.spec_helper) { rspec.spec_dir }
|
36
|
+
watch(rspec.spec_support) { rspec.spec_dir }
|
37
|
+
watch(rspec.spec_files)
|
38
|
+
|
39
|
+
# Ruby files
|
40
|
+
ruby = dsl.ruby
|
41
|
+
dsl.watch_spec_files_for(ruby.lib_files)
|
42
|
+
|
43
|
+
# Rails files
|
44
|
+
rails = dsl.rails(view_extensions: %w(erb haml slim))
|
45
|
+
dsl.watch_spec_files_for(rails.app_files)
|
46
|
+
dsl.watch_spec_files_for(rails.views)
|
47
|
+
|
48
|
+
watch(rails.controllers) do |m|
|
49
|
+
[
|
50
|
+
rspec.spec.call("routing/#{m[1]}_routing"),
|
51
|
+
rspec.spec.call("controllers/#{m[1]}_controller"),
|
52
|
+
rspec.spec.call("acceptance/#{m[1]}")
|
53
|
+
]
|
54
|
+
end
|
55
|
+
|
56
|
+
# Rails config changes
|
57
|
+
watch(rails.spec_helper) { rspec.spec_dir }
|
58
|
+
watch(rails.routes) { "#{rspec.spec_dir}/routing" }
|
59
|
+
watch(rails.app_controller) { "#{rspec.spec_dir}/controllers" }
|
60
|
+
|
61
|
+
# Capybara features specs
|
62
|
+
watch(rails.view_dirs) { |m| rspec.spec.call("features/#{m[1]}") }
|
63
|
+
watch(rails.layouts) { |m| rspec.spec.call("features/#{m[1]}") }
|
64
|
+
|
65
|
+
# Turnip features and steps
|
66
|
+
watch(%r{^spec/acceptance/(.+)\.feature$})
|
67
|
+
watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) do |m|
|
68
|
+
Dir[File.join("**/#{m[1]}.feature")][0] || "spec/acceptance"
|
69
|
+
end
|
70
|
+
end
|
data/README.md
CHANGED
@@ -4,58 +4,56 @@ Superform aims to be the best way to build forms in Rails applications. Here's w
|
|
4
4
|
|
5
5
|
* **Everything is a component.** Superform is built on top of [Phlex](https://phlex.fun), so every bit of HTML in the form can be customized to your precise needs. Use it with your own CSS Framework or go crazy customizing every last bit of TailwindCSS.
|
6
6
|
|
7
|
-
* **
|
7
|
+
* **Automatic strong parameters.** Superform automatically permits form fields so you don't have to facepalm yourself after adding a field, wondering why it doesn't persist, only to realize you forgot to add the parameter to your controller. No more! Superform was architected with safety & security in mind, meaning it can automatically permit your form parameters.
|
8
8
|
|
9
|
-
* **Compose forms with Plain 'ol Ruby Objects
|
9
|
+
* **Compose complex forms with Plain 'ol Ruby Objects.** Superform is built on top of POROs, so you can easily compose classes, modules, & ruby code together to create complex forms. You can even extend forms to create new forms with a different look and feel.
|
10
10
|
|
11
|
-
It's a complete rewrite of Rails form's internals that's inspired by Reactive component
|
11
|
+
It's a complete rewrite of Rails form's internals that's inspired by Reactive component design patterns.
|
12
|
+
|
13
|
+
[![Maintainability](https://api.codeclimate.com/v1/badges/0e4dfe2a1ece26e3a59e/maintainability)](https://codeclimate.com/github/rubymonolith/superform/maintainability) [![Ruby](https://github.com/rubymonolith/superform/actions/workflows/main.yml/badge.svg)](https://github.com/rubymonolith/superform/actions/workflows/main.yml)
|
12
14
|
|
13
15
|
## Installation
|
14
16
|
|
15
|
-
|
17
|
+
Add to the Rails application's Gemfile by executing:
|
16
18
|
|
17
19
|
$ bundle add superform
|
18
20
|
|
19
|
-
|
21
|
+
Then install it.
|
20
22
|
|
21
|
-
|
23
|
+
$ rails g superform:install
|
22
24
|
|
23
|
-
|
25
|
+
This will install both Phlex Rails and Superform.
|
24
26
|
|
25
|
-
|
26
|
-
<%= render ApplicationForm.new model: @user do
|
27
|
-
render field(:email).input(type: :email)
|
28
|
-
render field(:name).input
|
27
|
+
## Usage
|
29
28
|
|
30
|
-
|
31
|
-
|
29
|
+
Superform streamlines the development of forms on Rails applications by making everything a component.
|
30
|
+
|
31
|
+
After installing, create a form in `app/views/*/form.rb`. For example, a form for a `Post` resource might look like this.
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
# ./app/views/posts/form.rb
|
35
|
+
class Posts::Form < ApplicationForm
|
36
|
+
def template(&)
|
37
|
+
row field(:title).input
|
38
|
+
row field(:body).textarea
|
39
|
+
end
|
40
|
+
end
|
32
41
|
```
|
33
42
|
|
34
|
-
|
43
|
+
Then render it in your templates. Here's what it looks like from an Erb file.
|
35
44
|
|
36
45
|
```erb
|
37
|
-
|
38
|
-
|
39
|
-
render field(:email).label
|
40
|
-
render field(:email).input(type: :email)
|
41
|
-
end
|
42
|
-
div class: "form-row" do
|
43
|
-
render field(:name).label
|
44
|
-
render field(:name).input
|
45
|
-
end
|
46
|
-
|
47
|
-
button(type: :submit) { "Sign up" }
|
48
|
-
end %>
|
46
|
+
<h1>New post</h1>
|
47
|
+
<%= render Posts::Form.new @post %>
|
49
48
|
```
|
50
49
|
|
51
|
-
|
52
|
-
|
53
|
-
## Customizing Look & Feel
|
50
|
+
## Customization
|
54
51
|
|
55
|
-
Superforms are built
|
52
|
+
Superforms are built out of [Phlex components](https://www.phlex.fun/html/components/). The method names correspeond with the HTML tag, its arguments are attributes, and the blocks are the contents of the tag.
|
56
53
|
|
57
54
|
```ruby
|
58
|
-
|
55
|
+
# ./app/views/forms/application_form.rb
|
56
|
+
class ApplicationForm < ApplicationForm
|
59
57
|
class MyInputComponent < ApplicationComponent
|
60
58
|
def template(&)
|
61
59
|
div class: "form-field" do
|
@@ -88,18 +86,27 @@ end
|
|
88
86
|
|
89
87
|
That looks like a LOT of code, and it is, but look at how easy it is to create forms.
|
90
88
|
|
91
|
-
```
|
92
|
-
|
93
|
-
|
94
|
-
|
89
|
+
```ruby
|
90
|
+
# ./app/views/users/form.rb
|
91
|
+
class Users::Form < ApplicationForm
|
92
|
+
def template(&)
|
93
|
+
labeled field(:name).input
|
94
|
+
labeled field(:email).input(type: :email)
|
95
95
|
|
96
|
-
|
97
|
-
|
96
|
+
submit "Sign up"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
```
|
100
|
+
|
101
|
+
Then render it from Erb.
|
102
|
+
|
103
|
+
```erb
|
104
|
+
<%= render Users::Form.new @user %>
|
98
105
|
```
|
99
106
|
|
100
107
|
Much better!
|
101
108
|
|
102
|
-
|
109
|
+
## Extending Superforms
|
103
110
|
|
104
111
|
The best part? If you have forms with a completely different look and feel, you can extend the forms just like you would a Ruby class:
|
105
112
|
|
@@ -122,63 +129,96 @@ end
|
|
122
129
|
|
123
130
|
Then, just like you did in your Erb, you create the form:
|
124
131
|
|
125
|
-
```
|
126
|
-
|
127
|
-
|
128
|
-
|
132
|
+
```ruby
|
133
|
+
class Admin::Users::Form < AdminForm
|
134
|
+
def template(&)
|
135
|
+
labeled field(:name).tooltip_input
|
136
|
+
labeled field(:email).tooltip_input(type: :email)
|
129
137
|
|
130
|
-
|
131
|
-
|
138
|
+
submit "Save"
|
139
|
+
end
|
140
|
+
end
|
132
141
|
```
|
133
142
|
|
134
|
-
|
143
|
+
Since Superforms are just Ruby objects, you can organize them however you want. You can keep your view component classes embedded in your Superform file if you prefer for everythign to be in one place, keep the forms in the `app/views/forms/*.rb` folder and the components in `app/views/forms/**/*_component.rb`, use Ruby's `include` and `extend` features to modify different form classes, or put them in a gem and share them with an entire organization or open source community. It's just Ruby code!
|
144
|
+
|
145
|
+
## Automatic strong parameters
|
135
146
|
|
136
|
-
Guess what?
|
147
|
+
Guess what? Superform eliminates then need for Strong Parameters in Rails by assigning the values of the `params` hash _through_ your form. Here's what it looks like.
|
137
148
|
|
138
149
|
```ruby
|
139
|
-
class
|
140
|
-
|
150
|
+
class PostsController < ApplicationController
|
151
|
+
include Superform::Rails::StrongParameters
|
141
152
|
|
142
|
-
|
153
|
+
def create
|
154
|
+
@post = assign params.require(:post), to: Post.new
|
143
155
|
|
144
|
-
|
145
|
-
|
156
|
+
if @post.save
|
157
|
+
# Success path
|
158
|
+
else
|
159
|
+
# Error path
|
160
|
+
end
|
146
161
|
end
|
162
|
+
|
163
|
+
def update
|
164
|
+
@post = Post.find(params[:id])
|
165
|
+
|
166
|
+
assign params.require(:post), to: @post
|
167
|
+
|
168
|
+
if @post.save
|
169
|
+
# Success path
|
170
|
+
else
|
171
|
+
# Error path
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
private
|
176
|
+
# Defaults to `Posts::Form`, but you can override it here if
|
177
|
+
# you uncomment and add your own class. You could also pass the
|
178
|
+
# `form: FormClass` into the `assign` method.
|
179
|
+
#
|
180
|
+
# def form_class
|
181
|
+
# end
|
147
182
|
end
|
148
183
|
```
|
149
184
|
|
150
|
-
|
185
|
+
How does it work? An instance of the form is created, then the hash is assigned to it. If the params include data outside of what a form accepts, it will be ignored.
|
151
186
|
|
152
|
-
|
153
|
-
class UserController < ApplicationController
|
154
|
-
class Form < ApplicationForm
|
155
|
-
render field(:email).input(type: :email)
|
156
|
-
render field(:name).input
|
187
|
+
## Comparisons
|
157
188
|
|
158
|
-
|
159
|
-
end
|
189
|
+
Rails ships with a lot of great options to make forms. Many of these inspired Superform. The tl;dr:
|
160
190
|
|
161
|
-
|
191
|
+
1. Rails has a lot of great form helpers. Simple Form and Formtastic both have concise ways of defining HTML forms, but do require frequently opening and closing Erb tags.
|
162
192
|
|
163
|
-
|
193
|
+
2. Superform is uniquely capable of permitting its own controller parameters, leaving you with one less thing to worry about and test. Additionally it can be extended, shared, and modularized since its Plain' 'ol Ruby, which opens up a world of TailwindCSS form libraries and proprietary form libraries developed internally by organizations.
|
164
194
|
|
165
|
-
|
195
|
+
### Rails form helpers
|
166
196
|
|
167
|
-
|
168
|
-
@form = Form.new(model: @user)
|
169
|
-
end
|
197
|
+
Rails form helpers have lasted for almost 20 years and are super solid, but things get tricky when your application starts to take on different styles of forms. To manage it all you have to cobble together helper methods, partials, and templates. Additionally, the structure of the form then has to be expressed to the controller as strong params, forcing you to repeat yourself.
|
170
198
|
|
171
|
-
|
172
|
-
@form.permit params
|
173
|
-
end
|
174
|
-
end
|
175
|
-
```
|
199
|
+
With Simpleform, you build the entire form with Ruby code, so you avoid the Erb gymnastics and helper method soup that it takes in Rails to scale up forms in an organization.
|
176
200
|
|
177
|
-
|
201
|
+
### Simple Form
|
178
202
|
|
179
|
-
|
180
|
-
|
181
|
-
|
203
|
+
I built some pretty amazing applications with Simple Form and admire its syntax. It requires "Erb soup", which is an opening and closing line of Erb per line. If you follow a specific directory structure or use their component framework, you can get pretty far, but you'll hit a wall when you need to start putting wrappers around forms or inputs.
|
204
|
+
|
205
|
+
https://github.com/heartcombo/simple_form#the-wrappers-api
|
206
|
+
|
207
|
+
The API is there, but when you change the syntax, you have to reboot the server to see the changes. UI development should be reflected immediately when the page is reloaded, which is what Superforms can do.
|
208
|
+
|
209
|
+
Like Rails form helpers, it doesn't self-permit parameters.
|
210
|
+
|
211
|
+
https://www.ruby-toolbox.com/projects/simple_form
|
212
|
+
|
213
|
+
### Formtastic
|
214
|
+
|
215
|
+
Formtastic gives us a nice DSL inside of Erb that we can use to create forms, but like Simple Form, there's a lot of opening and closing Erb tags that make the syntax clunky.
|
216
|
+
|
217
|
+
It has generators that give you Ruby objects that represent HTML form inputs that you can customize, but its limited to very specific parts of the HTML components. Superform lets you customize every aspect of the HTML in your form elements.
|
218
|
+
|
219
|
+
It also does not permit its own parameters.
|
220
|
+
|
221
|
+
https://www.ruby-toolbox.com/projects/formtastic
|
182
222
|
|
183
223
|
## Development
|
184
224
|
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class Superform::InstallGenerator < Rails::Generators::Base
|
2
|
+
source_root File.expand_path("templates", __dir__)
|
3
|
+
|
4
|
+
APPLICATION_CONFIGURATION_PATH = Rails.root.join("config/application.rb")
|
5
|
+
|
6
|
+
def install_phlex_rails
|
7
|
+
gem "phlex-rails"
|
8
|
+
generate "phlex:install"
|
9
|
+
end
|
10
|
+
|
11
|
+
def autoload_components
|
12
|
+
return unless APPLICATION_CONFIGURATION_PATH.exist?
|
13
|
+
|
14
|
+
inject_into_class(
|
15
|
+
APPLICATION_CONFIGURATION_PATH,
|
16
|
+
"Application",
|
17
|
+
%( config.autoload_paths << "\#{root}/app/views/forms"\n)
|
18
|
+
)
|
19
|
+
end
|
20
|
+
|
21
|
+
def create_application_form
|
22
|
+
template "application_form.rb", Rails.root.join("app/views/forms/application_form.rb")
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
class ApplicationForm < Superform::Rails::Form
|
2
|
+
include Phlex::Rails::Helpers::Pluralize
|
3
|
+
|
4
|
+
def row(component)
|
5
|
+
div do
|
6
|
+
render component.field.label(style: "display: block;")
|
7
|
+
render component
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def around_template(&)
|
12
|
+
super do
|
13
|
+
error_messages
|
14
|
+
yield
|
15
|
+
submit
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def error_messages
|
20
|
+
if model.errors.any?
|
21
|
+
div(style: "color: red;") do
|
22
|
+
h2 { "#{pluralize model.errors.count, "error"} prohibited this post from being saved:" }
|
23
|
+
ul do
|
24
|
+
model.errors.each do |error|
|
25
|
+
li { error.full_message }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,209 @@
|
|
1
|
+
module Superform
|
2
|
+
module Rails
|
3
|
+
class Form < Phlex::HTML
|
4
|
+
attr_reader :model
|
5
|
+
|
6
|
+
delegate \
|
7
|
+
:field,
|
8
|
+
:collection,
|
9
|
+
:namespace,
|
10
|
+
:key,
|
11
|
+
:assign,
|
12
|
+
:serialize,
|
13
|
+
to: :@namespace
|
14
|
+
|
15
|
+
class Field < Superform::Field
|
16
|
+
def button(**attributes)
|
17
|
+
Components::ButtonComponent.new(self, attributes: attributes)
|
18
|
+
end
|
19
|
+
|
20
|
+
def input(**attributes)
|
21
|
+
Components::InputComponent.new(self, attributes: attributes)
|
22
|
+
end
|
23
|
+
|
24
|
+
def label(**attributes)
|
25
|
+
Components::LabelComponent.new(self, attributes: attributes)
|
26
|
+
end
|
27
|
+
|
28
|
+
def textarea(**attributes)
|
29
|
+
Components::TextareaComponent.new(self, attributes: attributes)
|
30
|
+
end
|
31
|
+
|
32
|
+
def title
|
33
|
+
key.to_s.titleize
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def initialize(model, action: nil, method: nil)
|
38
|
+
@model = model
|
39
|
+
@action = action
|
40
|
+
@method = method
|
41
|
+
@namespace = Namespace.root(model.model_name.param_key, object: model, field_class: self.class::Field)
|
42
|
+
end
|
43
|
+
|
44
|
+
def around_template(&)
|
45
|
+
form action: form_action, method: form_method do
|
46
|
+
authenticity_token_field
|
47
|
+
_method_field
|
48
|
+
super
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def template(&block)
|
53
|
+
yield_content(&block)
|
54
|
+
end
|
55
|
+
|
56
|
+
def submit(value = submit_value)
|
57
|
+
input(
|
58
|
+
name: "commit",
|
59
|
+
type: "submit",
|
60
|
+
value: value
|
61
|
+
)
|
62
|
+
end
|
63
|
+
|
64
|
+
protected
|
65
|
+
|
66
|
+
def authenticity_token_field
|
67
|
+
input(
|
68
|
+
name: "authenticity_token",
|
69
|
+
type: "hidden",
|
70
|
+
value: helpers.form_authenticity_token
|
71
|
+
)
|
72
|
+
end
|
73
|
+
|
74
|
+
def _method_field
|
75
|
+
input(
|
76
|
+
name: "_method",
|
77
|
+
type: "hidden",
|
78
|
+
value: _method_field_value
|
79
|
+
)
|
80
|
+
end
|
81
|
+
|
82
|
+
def _method_field_value
|
83
|
+
@method || @model.persisted? ? "patch" : "post"
|
84
|
+
end
|
85
|
+
|
86
|
+
def submit_value
|
87
|
+
"#{resource_action.to_s.capitalize} #{@model.model_name}"
|
88
|
+
end
|
89
|
+
|
90
|
+
def resource_action
|
91
|
+
@model.persisted? ? :update : :create
|
92
|
+
end
|
93
|
+
|
94
|
+
def form_action
|
95
|
+
@action ||= helpers.url_for(action: resource_action)
|
96
|
+
end
|
97
|
+
|
98
|
+
def form_method
|
99
|
+
@method.to_s.downcase == "get" ? "get" : "post"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
module StrongParameters
|
104
|
+
protected
|
105
|
+
# Assigns params to the form model.
|
106
|
+
def assign(params, to:, form: form_class)
|
107
|
+
to.tap do |model|
|
108
|
+
form.new(model).tap do |form|
|
109
|
+
# TODO: Figure out how to render this in a way that doesn't concat a string; just throw everything away.
|
110
|
+
render_to_string form
|
111
|
+
form.assign params
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Defaults to the form defined in `./app/views/*/form.rb`.
|
117
|
+
def form_class
|
118
|
+
self.controller_name.camelize.constantize::Form
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
module Components
|
123
|
+
class FieldComponent < ApplicationComponent
|
124
|
+
attr_reader :field, :dom
|
125
|
+
|
126
|
+
delegate :dom, to: :field
|
127
|
+
|
128
|
+
def initialize(field, attributes: {})
|
129
|
+
@field = field
|
130
|
+
@attributes = attributes
|
131
|
+
end
|
132
|
+
|
133
|
+
def field_attributes
|
134
|
+
{}
|
135
|
+
end
|
136
|
+
|
137
|
+
def focus(value = true)
|
138
|
+
@attributes[:autofocus] = value
|
139
|
+
self
|
140
|
+
end
|
141
|
+
|
142
|
+
private
|
143
|
+
|
144
|
+
def attributes
|
145
|
+
field_attributes.merge(@attributes)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
class LabelComponent < FieldComponent
|
150
|
+
def template(&)
|
151
|
+
label(**attributes) { field.key.to_s.titleize }
|
152
|
+
end
|
153
|
+
|
154
|
+
def field_attributes
|
155
|
+
{ for: dom.id }
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
class ButtonComponent < FieldComponent
|
160
|
+
def template(&block)
|
161
|
+
button(**attributes) { button_text }
|
162
|
+
end
|
163
|
+
|
164
|
+
def button_text
|
165
|
+
@attributes.fetch(:value, dom.value).titleize
|
166
|
+
end
|
167
|
+
|
168
|
+
def field_attributes
|
169
|
+
{ id: dom.id, name: dom.name, value: dom.value }
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
class InputComponent < FieldComponent
|
174
|
+
def template(&)
|
175
|
+
input(**attributes)
|
176
|
+
end
|
177
|
+
|
178
|
+
def field_attributes
|
179
|
+
{ id: dom.id, name: dom.name, value: dom.value, type: type }
|
180
|
+
end
|
181
|
+
|
182
|
+
def type
|
183
|
+
case field.value
|
184
|
+
when URI
|
185
|
+
"url"
|
186
|
+
when Integer
|
187
|
+
"number"
|
188
|
+
when Date, DateTime
|
189
|
+
"date"
|
190
|
+
when Time
|
191
|
+
"time"
|
192
|
+
else
|
193
|
+
"text"
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
class TextareaComponent < FieldComponent
|
199
|
+
def template(&)
|
200
|
+
textarea(**attributes) { dom.value }
|
201
|
+
end
|
202
|
+
|
203
|
+
def field_attributes
|
204
|
+
{ id: dom.id, name: dom.name }
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
data/lib/superform/version.rb
CHANGED
data/lib/superform.rb
CHANGED
@@ -1,8 +1,223 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative "superform/version"
|
4
|
-
|
5
1
|
module Superform
|
6
2
|
class Error < StandardError; end
|
7
|
-
|
3
|
+
|
4
|
+
autoload :Rails, "superform/rails"
|
5
|
+
|
6
|
+
class DOM
|
7
|
+
def initialize(field:)
|
8
|
+
@field = field
|
9
|
+
end
|
10
|
+
|
11
|
+
def value
|
12
|
+
@field.value.to_s
|
13
|
+
end
|
14
|
+
|
15
|
+
def id
|
16
|
+
lineage.map(&:key).join("_")
|
17
|
+
end
|
18
|
+
|
19
|
+
def name
|
20
|
+
root, *names = keys
|
21
|
+
names.map { |name| "[#{name}]" }.unshift(root).join
|
22
|
+
end
|
23
|
+
|
24
|
+
def inspect
|
25
|
+
"<id=#{id.inspect} name=#{name.inspect} value=#{value.inspect}/>"
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def keys
|
31
|
+
lineage.map do |node|
|
32
|
+
# If the parent of a field is a field, the name should be nil.
|
33
|
+
node.key unless node.parent.is_a? Field
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def lineage
|
38
|
+
Enumerator.produce(@field, &:parent).take_while(&:itself).reverse
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class Node
|
43
|
+
attr_reader :key, :parent
|
44
|
+
|
45
|
+
def initialize(key, parent:)
|
46
|
+
@key = key
|
47
|
+
@parent = parent
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class Namespace < Node
|
52
|
+
include Enumerable
|
53
|
+
|
54
|
+
attr_reader :object
|
55
|
+
|
56
|
+
def initialize(key, parent:, object: nil, field_class: Field)
|
57
|
+
super(key, parent: parent)
|
58
|
+
@object = object
|
59
|
+
@field_class = field_class
|
60
|
+
@children = Hash.new
|
61
|
+
yield self if block_given?
|
62
|
+
end
|
63
|
+
|
64
|
+
def namespace(key, &block)
|
65
|
+
create_child(key, self.class, object: object_for(key: key), &block)
|
66
|
+
end
|
67
|
+
|
68
|
+
def field(key)
|
69
|
+
create_child(key, @field_class, object: object)
|
70
|
+
end
|
71
|
+
|
72
|
+
def collection(key, &block)
|
73
|
+
create_child(key, NamespaceCollection, &block)
|
74
|
+
end
|
75
|
+
|
76
|
+
def serialize
|
77
|
+
each_with_object Hash.new do |child, hash|
|
78
|
+
hash[child.key] = child.serialize
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def each(&)
|
83
|
+
@children.values.each(&)
|
84
|
+
end
|
85
|
+
|
86
|
+
def assign(hash)
|
87
|
+
each do |child|
|
88
|
+
child.assign hash[child.key]
|
89
|
+
end
|
90
|
+
self
|
91
|
+
end
|
92
|
+
|
93
|
+
def self.root(*args, **kwargs, &block)
|
94
|
+
new(*args, parent: nil, **kwargs, &block)
|
95
|
+
end
|
96
|
+
private
|
97
|
+
|
98
|
+
def create_child(key, child_class, **options, &block)
|
99
|
+
fetch(key) { child_class.new(key, parent: self, **options, &block) }
|
100
|
+
end
|
101
|
+
|
102
|
+
def fetch(key, &build)
|
103
|
+
@children[key] ||= build.call
|
104
|
+
end
|
105
|
+
|
106
|
+
def object_for(key:)
|
107
|
+
@object.send(key) if @object.respond_to? key
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
class Field < Node
|
112
|
+
attr_reader :dom
|
113
|
+
|
114
|
+
def initialize(key, parent:, object: nil, value: nil)
|
115
|
+
super key, parent: parent
|
116
|
+
@object = object
|
117
|
+
@value = value
|
118
|
+
@dom = DOM.new(field: self)
|
119
|
+
end
|
120
|
+
|
121
|
+
def value
|
122
|
+
if @object and @object.respond_to? @key
|
123
|
+
@object.send @key
|
124
|
+
else
|
125
|
+
@value
|
126
|
+
end
|
127
|
+
end
|
128
|
+
alias :serialize :value
|
129
|
+
|
130
|
+
def assign(value)
|
131
|
+
if @object and @object.respond_to? "#{@key}="
|
132
|
+
@object.send "#{@key}=", value
|
133
|
+
else
|
134
|
+
@value = value
|
135
|
+
end
|
136
|
+
end
|
137
|
+
alias :value= :assign
|
138
|
+
|
139
|
+
# Wraps a field that's an array of values with a bunch of fields
|
140
|
+
# that are indexed with the array's index.
|
141
|
+
def collection(&)
|
142
|
+
@collection ||= FieldCollection.new(field: self, &)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
class FieldCollection
|
147
|
+
include Enumerable
|
148
|
+
|
149
|
+
def initialize(field:, &)
|
150
|
+
@field = field
|
151
|
+
@index = 0
|
152
|
+
each(&) if block_given?
|
153
|
+
end
|
154
|
+
|
155
|
+
def each(&)
|
156
|
+
values.each do |value|
|
157
|
+
yield build_field(value: value)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def field
|
162
|
+
build_field
|
163
|
+
end
|
164
|
+
|
165
|
+
def values
|
166
|
+
Array(@field.value)
|
167
|
+
end
|
168
|
+
|
169
|
+
private
|
170
|
+
|
171
|
+
def build_field(**kwargs)
|
172
|
+
@field.class.new(@index += 1, parent: @field, **kwargs)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
class NamespaceCollection < Node
|
177
|
+
include Enumerable
|
178
|
+
|
179
|
+
def initialize(key, parent:, &template)
|
180
|
+
super(key, parent: parent)
|
181
|
+
@template = template
|
182
|
+
@namespaces = enumerate(parent_collection)
|
183
|
+
end
|
184
|
+
|
185
|
+
def serialize
|
186
|
+
map(&:serialize)
|
187
|
+
end
|
188
|
+
|
189
|
+
def assign(array)
|
190
|
+
# The problem with zip-ing the array is if I need to add new
|
191
|
+
# elements to it and wrap it in the namespace.
|
192
|
+
zip(array) do |namespace, hash|
|
193
|
+
namespace.assign hash
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
def each(&)
|
198
|
+
@namespaces.each(&)
|
199
|
+
end
|
200
|
+
|
201
|
+
private
|
202
|
+
|
203
|
+
def enumerate(enumerator)
|
204
|
+
Enumerator.new do |y|
|
205
|
+
enumerator.each.with_index do |object, key|
|
206
|
+
y << build_namespace(key, object: object)
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
def build_namespace(index, **kwargs)
|
212
|
+
parent.class.new(index, parent: self, **kwargs, &@template)
|
213
|
+
end
|
214
|
+
|
215
|
+
def parent_collection
|
216
|
+
@parent.object.send @key
|
217
|
+
end
|
218
|
+
end
|
8
219
|
end
|
220
|
+
|
221
|
+
def Superform(...)
|
222
|
+
Superform::Namespace.root(...)
|
223
|
+
end
|
metadata
CHANGED
@@ -1,15 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: superform
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Brad Gessler
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
12
|
-
dependencies:
|
11
|
+
date: 2023-07-21 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: phlex-rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.0'
|
13
27
|
description: A better way to customize and build forms for your Rails application
|
14
28
|
email:
|
15
29
|
- bradgessler@gmail.com
|
@@ -21,10 +35,16 @@ files:
|
|
21
35
|
- CHANGELOG.md
|
22
36
|
- CODE_OF_CONDUCT.md
|
23
37
|
- Gemfile
|
38
|
+
- Gemfile.lock
|
39
|
+
- Guardfile
|
24
40
|
- LICENSE.txt
|
25
41
|
- README.md
|
26
42
|
- Rakefile
|
43
|
+
- lib/generators/superform/install/USAGE
|
44
|
+
- lib/generators/superform/install/install_generator.rb
|
45
|
+
- lib/generators/superform/install/templates/application_form.rb
|
27
46
|
- lib/superform.rb
|
47
|
+
- lib/superform/rails.rb
|
28
48
|
- lib/superform/version.rb
|
29
49
|
- sig/superform.rbs
|
30
50
|
homepage: https://github.com/rubymonolith/superform
|