superform 0.1.0 → 0.2.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 +100 -48
- 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 +191 -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: e755c08731146b7a40c49b9eeaee5b267b56476764ce97fdb68ae8392e5cad0a
|
4
|
+
data.tar.gz: b369765812de40bf81c672ee7f131e4bc8c0a4c234baf210096a41c175f02505
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 014cdef2d3639b063ab13f6d757f68c2eb4b14d3f5c89b51301deae77302c3e6ad11a38c21cd974a2fc4ae22d5f4db57d6b698ff1042ac495aaa13c6bccdaff1
|
7
|
+
data.tar.gz: ad6fd81bc9e597d14b63e2915b0c9ba820da4fa71b7db2f129db25ebee8ded0bc836e2353f1799d8565db689f77a0e2dccc73c9a875336a0dedcf5548b8bf169
|
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,57 @@ 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
12
|
|
13
13
|
## Installation
|
14
14
|
|
15
|
-
|
15
|
+
> **Note**
|
16
|
+
> This doesn't actually work yet. There is working source code at https://github.com/rubymonolith/demo/tree/main/app/views/superform that's being extracted into a gem. This repo and README exist to validate some ideas before the gem is finalized and published.
|
17
|
+
|
18
|
+
Add to the Rails application's Gemfile by executing:
|
16
19
|
|
17
20
|
$ bundle add superform
|
18
21
|
|
19
|
-
|
22
|
+
Then install it.
|
20
23
|
|
21
|
-
|
24
|
+
$ rails g superform:install
|
22
25
|
|
23
|
-
|
26
|
+
This will install both Phlex Rails and Superform.
|
24
27
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
28
|
+
## Usage
|
29
|
+
|
30
|
+
Superform streamlines the development of forms on Rails applications by making everything a component.
|
31
|
+
|
32
|
+
After installing, create a form in `app/views/*/form.rb`. For example, a form for a `Post` resource might look like this.
|
29
33
|
|
30
|
-
|
31
|
-
|
34
|
+
```ruby
|
35
|
+
# ./app/views/posts/form.rb
|
36
|
+
class Posts::Form < ApplicationForm
|
37
|
+
def template(&)
|
38
|
+
row field(:title).input
|
39
|
+
row field(:body).textarea
|
40
|
+
end
|
41
|
+
end
|
32
42
|
```
|
33
43
|
|
34
|
-
|
44
|
+
Then render it in your templates. Here's what it looks like from an Erb file.
|
35
45
|
|
36
46
|
```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 %>
|
47
|
+
<h1>New post</h1>
|
48
|
+
<%= render Posts::Form.new model: @post %>
|
49
49
|
```
|
50
50
|
|
51
|
-
|
51
|
+
## Customization
|
52
52
|
|
53
|
-
|
54
|
-
|
55
|
-
Superforms are built entirely out of Phlex components. The method names correspeond with the tag, its arguments are attributes, and the blocks are the contents of the element.
|
53
|
+
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
54
|
|
57
55
|
```ruby
|
58
|
-
|
56
|
+
# ./app/views/forms/application_form.rb
|
57
|
+
class ApplicationForm < ApplicationForm
|
59
58
|
class MyInputComponent < ApplicationComponent
|
60
59
|
def template(&)
|
61
60
|
div class: "form-field" do
|
@@ -88,18 +87,27 @@ end
|
|
88
87
|
|
89
88
|
That looks like a LOT of code, and it is, but look at how easy it is to create forms.
|
90
89
|
|
91
|
-
```
|
92
|
-
|
93
|
-
|
94
|
-
|
90
|
+
```ruby
|
91
|
+
# ./app/views/users/form.rb
|
92
|
+
class Users::Form < ApplicationForm
|
93
|
+
def template(&)
|
94
|
+
labeled field(:name).input
|
95
|
+
labeled field(:email).input(type: :email)
|
95
96
|
|
96
|
-
|
97
|
-
|
97
|
+
submit "Sign up"
|
98
|
+
end
|
99
|
+
end
|
100
|
+
```
|
101
|
+
|
102
|
+
Then render it from Erb.
|
103
|
+
|
104
|
+
```erb
|
105
|
+
<%= render Users::Form.new model: @user %>
|
98
106
|
```
|
99
107
|
|
100
108
|
Much better!
|
101
109
|
|
102
|
-
### Extending
|
110
|
+
### Extending Superforms
|
103
111
|
|
104
112
|
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
113
|
|
@@ -122,18 +130,25 @@ end
|
|
122
130
|
|
123
131
|
Then, just like you did in your Erb, you create the form:
|
124
132
|
|
125
|
-
```
|
126
|
-
|
127
|
-
|
128
|
-
|
133
|
+
```ruby
|
134
|
+
class Admin::Users::Form < AdminForm
|
135
|
+
def template(&)
|
136
|
+
labeled field(:name).tooltip_input
|
137
|
+
labeled field(:email).tooltip_input(type: :email)
|
129
138
|
|
130
|
-
|
131
|
-
|
139
|
+
submit "Save"
|
140
|
+
end
|
141
|
+
end
|
132
142
|
```
|
133
143
|
|
134
|
-
|
144
|
+
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!
|
145
|
+
|
146
|
+
### Automatic strong parameters
|
135
147
|
|
136
|
-
|
148
|
+
> **Note**
|
149
|
+
> THese docs are a work in progress. Strong params do work, but not as documented below. Stay tuned for updates.
|
150
|
+
|
151
|
+
Guess what? Superform also permits form fields for you in your controller, like this:
|
137
152
|
|
138
153
|
```ruby
|
139
154
|
class UserController < ApplicationController
|
@@ -147,10 +162,11 @@ class UserController < ApplicationController
|
|
147
162
|
end
|
148
163
|
```
|
149
164
|
|
150
|
-
To do that though you need to move the form into your controller, which is pretty easy:
|
165
|
+
To do that though you need to move the form as an inline class into your controller or `app/views` folder, which is pretty easy:
|
151
166
|
|
152
167
|
```ruby
|
153
|
-
class
|
168
|
+
class UsersController < ApplicationController
|
169
|
+
# You could also put this in `./app/views/users/form.rb`
|
154
170
|
class Form < ApplicationForm
|
155
171
|
render field(:email).input(type: :email)
|
156
172
|
render field(:name).input
|
@@ -180,6 +196,42 @@ Then render it from your Erb in less lines, like this:
|
|
180
196
|
<%= render @form %>
|
181
197
|
```
|
182
198
|
|
199
|
+
## Comparisons
|
200
|
+
|
201
|
+
Rails ships with a lot of great options to make forms. Many of these inspired Superform. The tl;dr:
|
202
|
+
|
203
|
+
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.
|
204
|
+
|
205
|
+
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.
|
206
|
+
|
207
|
+
### Rails form helpers
|
208
|
+
|
209
|
+
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.
|
210
|
+
|
211
|
+
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.
|
212
|
+
|
213
|
+
### Simple Form
|
214
|
+
|
215
|
+
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.
|
216
|
+
|
217
|
+
https://github.com/heartcombo/simple_form#the-wrappers-api
|
218
|
+
|
219
|
+
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.
|
220
|
+
|
221
|
+
Like Rails form helpers, it doesn't self-permit parameters.
|
222
|
+
|
223
|
+
https://www.ruby-toolbox.com/projects/simple_form
|
224
|
+
|
225
|
+
### Formtastic
|
226
|
+
|
227
|
+
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.
|
228
|
+
|
229
|
+
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.
|
230
|
+
|
231
|
+
It also does not permit its own parameters.
|
232
|
+
|
233
|
+
https://www.ruby-toolbox.com/projects/formtastic
|
234
|
+
|
183
235
|
## Development
|
184
236
|
|
185
237
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
@@ -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,191 @@
|
|
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
|
+
|
104
|
+
module Components
|
105
|
+
class FieldComponent < ApplicationComponent
|
106
|
+
attr_reader :field, :dom
|
107
|
+
|
108
|
+
delegate :dom, to: :field
|
109
|
+
|
110
|
+
def initialize(field, attributes: {})
|
111
|
+
@field = field
|
112
|
+
@attributes = attributes
|
113
|
+
end
|
114
|
+
|
115
|
+
def field_attributes
|
116
|
+
{}
|
117
|
+
end
|
118
|
+
|
119
|
+
def focus(value = true)
|
120
|
+
@attributes[:autofocus] = value
|
121
|
+
self
|
122
|
+
end
|
123
|
+
|
124
|
+
private
|
125
|
+
|
126
|
+
def attributes
|
127
|
+
field_attributes.merge(@attributes)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
class LabelComponent < FieldComponent
|
132
|
+
def template(&)
|
133
|
+
label(**attributes) { field.key.to_s.titleize }
|
134
|
+
end
|
135
|
+
|
136
|
+
def field_attributes
|
137
|
+
{ for: dom.id }
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
class ButtonComponent < FieldComponent
|
142
|
+
def template(&block)
|
143
|
+
button(**attributes) { button_text }
|
144
|
+
end
|
145
|
+
|
146
|
+
def button_text
|
147
|
+
@attributes.fetch(:value, dom.value).titleize
|
148
|
+
end
|
149
|
+
|
150
|
+
def field_attributes
|
151
|
+
{ id: dom.id, name: dom.name, value: dom.value }
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
class InputComponent < FieldComponent
|
156
|
+
def template(&)
|
157
|
+
input(**attributes)
|
158
|
+
end
|
159
|
+
|
160
|
+
def field_attributes
|
161
|
+
{ id: dom.id, name: dom.name, value: dom.value, type: type }
|
162
|
+
end
|
163
|
+
|
164
|
+
def type
|
165
|
+
case field.value
|
166
|
+
when URI
|
167
|
+
"url"
|
168
|
+
when Integer
|
169
|
+
"number"
|
170
|
+
when Date, DateTime
|
171
|
+
"date"
|
172
|
+
when Time
|
173
|
+
"time"
|
174
|
+
else
|
175
|
+
"text"
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
class TextareaComponent < FieldComponent
|
181
|
+
def template(&)
|
182
|
+
textarea(**attributes) { dom.value }
|
183
|
+
end
|
184
|
+
|
185
|
+
def field_attributes
|
186
|
+
{ id: dom.id, name: dom.name }
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
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.2.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
|