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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1f11fa90121336cea5cba3e376930c7c03c59f1a4cad8383de46482921651a94
4
- data.tar.gz: cfec05650064dcb07e6eadb0b78c501317cace17bd7062c666ad3972379e4c1b
3
+ metadata.gz: b87df41698b91049bed9e30f976d56b92c5c694762a63803453d29fa47c025a9
4
+ data.tar.gz: bd9ec4dd8499de8303411c3ee88f7a2888ce285b5592eb32829db6043782a4f9
5
5
  SHA512:
6
- metadata.gz: ec98e31913b9ca5f3aac6c75952b2ee51a19c01c82368f2dc5c87f58c91eb6dd0f41dfae969165f6be7fc1ac2e87595f85c30d19ef43938e3c95a58e2cd1c7ba
7
- data.tar.gz: b138902a808ecbff61fbc8b3ae37a34e576895972512a7c0c5d78e8c318130fa567d53570d9163210d2015141ea47a38e3c0b021082be936341d9f5c951a4b25
6
+ metadata.gz: 5f945ca1d2a57d3b4ac69d064c72f701cd7d3d524a64dd241c80a23b3fd53cecd572800f2ebbe760321348d0f770b60d60be5acb4a4482e007d9040d21b3898f
7
+ data.tar.gz: a47df25dcc990f3162b874f85999df79e0acf170643395071aaa2b2c29fde23ff302a055a9ed132927945cc53c30376f645846370f5e323eb89115dc740136ce
data/Gemfile CHANGED
@@ -7,4 +7,6 @@ gemspec
7
7
 
8
8
  gem "rake", "~> 13.0"
9
9
 
10
+ # Run tests
10
11
  gem "rspec", "~> 3.0"
12
+ gem "guard-rspec", "~> 4.7"
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
- * **Strong Params are built in.** Superform automatically permits the form fields for you. How many times have you changed the form and forgot to permit a param from the controller? No more! Superform has you covered.
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**. Superform is built on top of POROs, so you can easily compose forms together to create complex forms. You can even extend forms to create new forms with a different look and feel.
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 system. [Chris McCord said it very eloquently in a love letter to react](https://fly.io/blog/love-letter-react/). This aspires to be that, but in Ruby.
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
- Install the gem and add to the Rails application's Gemfile by executing:
17
+ Add to the Rails application's Gemfile by executing:
16
18
 
17
19
  $ bundle add superform
18
20
 
19
- ## Usage
21
+ Then install it.
20
22
 
21
- Super Forms streamlines the development of forms on Rails applications by making everything a component.
23
+ $ rails g superform:install
22
24
 
23
- Here's what a Superform looks in your Erb files.
25
+ This will install both Phlex Rails and Superform.
24
26
 
25
- ```erb
26
- <%= render ApplicationForm.new model: @user do
27
- render field(:email).input(type: :email)
28
- render field(:name).input
27
+ ## Usage
29
28
 
30
- button(type: :submit) { "Sign up" }
31
- end %>
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
- That's very spartan form! Let's add labels and HTML between each form row so we have something to work with.
43
+ Then render it in your templates. Here's what it looks like from an Erb file.
35
44
 
36
45
  ```erb
37
- <%= render ApplicationForm.new do
38
- div class: "form-row" do
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
- Jumpin' Jimmidy! That's starting to get purty verbose. Let's add some helpers to `ApplicationForm` and tighten things up.
52
-
53
- ## Customizing Look & Feel
50
+ ## Customization
54
51
 
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.
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
- class ApplicationForm < Superform::Base
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
- ```erb
92
- <%= render ApplicationForm.new model: @user do
93
- labeled field(:name).input
94
- labeled field(:email).input(type: :email)
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
- submit "Sign up"
97
- end %>
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
- ### Extending Forms
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
- ```erb
126
- <%= render AdminForm.new model: @user do
127
- labeled field(:name).tooltip_input
128
- labeled field(:email).tooltip_input(type: :email)
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
- submit "Save"
131
- end %>
138
+ submit "Save"
139
+ end
140
+ end
132
141
  ```
133
142
 
134
- ### Self-permitting Parameters
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? It also permits form fields for you in your controller, like this:
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 UserController < ApplicationController
140
- # Your actions
150
+ class PostsController < ApplicationController
151
+ include Superform::Rails::StrongParameters
141
152
 
142
- private
153
+ def create
154
+ @post = assign params.require(:post), to: Post.new
143
155
 
144
- def permitted_params
145
- @form.permit params
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
- To do that though you need to move the form into your controller, which is pretty easy:
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
- ```ruby
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
- button(type: :submit) { "Sign up" }
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
- before_action :assign_form
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
- # Your actions
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
- private
195
+ ### Rails form helpers
166
196
 
167
- def assign_form
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
- def permitted_params
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
- Then render it from your Erb in less lines, like this:
201
+ ### Simple Form
178
202
 
179
- ```
180
- <%= render @form %>
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,8 @@
1
+ Description:
2
+ Installs Phlex Rails and Superform
3
+
4
+ Example:
5
+ bin/rails generate superform:install
6
+
7
+ This will create:
8
+ app/views/forms/application_form.rb
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Superform
4
- VERSION = "0.1.0"
4
+ VERSION = "0.3.0"
5
5
  end
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
- # Your code goes here...
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.1.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-06-24 00:00:00.000000000 Z
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