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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1f11fa90121336cea5cba3e376930c7c03c59f1a4cad8383de46482921651a94
4
- data.tar.gz: cfec05650064dcb07e6eadb0b78c501317cace17bd7062c666ad3972379e4c1b
3
+ metadata.gz: e755c08731146b7a40c49b9eeaee5b267b56476764ce97fdb68ae8392e5cad0a
4
+ data.tar.gz: b369765812de40bf81c672ee7f131e4bc8c0a4c234baf210096a41c175f02505
5
5
  SHA512:
6
- metadata.gz: ec98e31913b9ca5f3aac6c75952b2ee51a19c01c82368f2dc5c87f58c91eb6dd0f41dfae969165f6be7fc1ac2e87595f85c30d19ef43938e3c95a58e2cd1c7ba
7
- data.tar.gz: b138902a808ecbff61fbc8b3ae37a34e576895972512a7c0c5d78e8c318130fa567d53570d9163210d2015141ea47a38e3c0b021082be936341d9f5c951a4b25
6
+ metadata.gz: 014cdef2d3639b063ab13f6d757f68c2eb4b14d3f5c89b51301deae77302c3e6ad11a38c21cd974a2fc4ae22d5f4db57d6b698ff1042ac495aaa13c6bccdaff1
7
+ data.tar.gz: ad6fd81bc9e597d14b63e2915b0c9ba820da4fa71b7db2f129db25ebee8ded0bc836e2353f1799d8565db689f77a0e2dccc73c9a875336a0dedcf5548b8bf169
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,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
- * **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
12
 
13
13
  ## Installation
14
14
 
15
- Install the gem and add to the Rails application's Gemfile by executing:
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
- ## Usage
22
+ Then install it.
20
23
 
21
- Super Forms streamlines the development of forms on Rails applications by making everything a component.
24
+ $ rails g superform:install
22
25
 
23
- Here's what a Superform looks in your Erb files.
26
+ This will install both Phlex Rails and Superform.
24
27
 
25
- ```erb
26
- <%= render ApplicationForm.new model: @user do
27
- render field(:email).input(type: :email)
28
- render field(:name).input
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
- button(type: :submit) { "Sign up" }
31
- end %>
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
- That's very spartan form! Let's add labels and HTML between each form row so we have something to work with.
44
+ Then render it in your templates. Here's what it looks like from an Erb file.
35
45
 
36
46
  ```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 %>
47
+ <h1>New post</h1>
48
+ <%= render Posts::Form.new model: @post %>
49
49
  ```
50
50
 
51
- Jumpin' Jimmidy! That's starting to get purty verbose. Let's add some helpers to `ApplicationForm` and tighten things up.
51
+ ## Customization
52
52
 
53
- ## Customizing Look & Feel
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
- class ApplicationForm < Superform::Base
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
- ```erb
92
- <%= render ApplicationForm.new model: @user do
93
- labeled field(:name).input
94
- labeled field(:email).input(type: :email)
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
- submit "Sign up"
97
- end %>
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 Forms
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
- ```erb
126
- <%= render AdminForm.new model: @user do
127
- labeled field(:name).tooltip_input
128
- labeled field(:email).tooltip_input(type: :email)
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
- submit "Save"
131
- end %>
139
+ submit "Save"
140
+ end
141
+ end
132
142
  ```
133
143
 
134
- ### Self-permitting Parameters
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
- Guess what? It also permits form fields for you in your controller, like this:
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 UserController < ApplicationController
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,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,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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Superform
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.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.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-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