turbo_form 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +105 -23
- data/app/assets/javascripts/turbo_form.js +10 -4
- data/app/controllers/turbo_form/dynamic_forms_controller.rb +7 -2
- data/lib/generators/turbo_form/install/install_generator.rb +62 -0
- data/lib/generators/turbo_form/install/templates/turbo_form_controller.js +4 -0
- data/lib/turbo_form/engine.rb +23 -0
- data/lib/turbo_form/form_builder.rb +20 -6
- data/lib/turbo_form/system_test_helper.rb +23 -0
- data/lib/turbo_form/version.rb +1 -1
- data/lib/turbo_form.rb +8 -1
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 01bb2ce15b4fc755a0f4b689fa4e11d1a34627a206d95d5b2d008ad3d9e66aa3
|
|
4
|
+
data.tar.gz: 52860d7614b4aa8cd960721ee677854704324d34f0abdcdd938f879546daf7d2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cfee3dbcfb3d319e0e1265735a9dfe88d86a0479f3cfb01b53ad9879ef9f1d7b92eaf8679a9486613ffaba54a57f3f1e2965a66531aa6065557927a629ce39d1
|
|
7
|
+
data.tar.gz: 82b3308ced5f38ace3d7e4f170b1beb77d0559f24a67cbfb91174e39de67ef9e66831a8d1a6a9fa03a2def2772459a821833aa684b45bd6f8a6141443ab36113
|
data/README.md
CHANGED
|
@@ -37,10 +37,15 @@ That's the whole feature. No route, no controller, no JavaScript.
|
|
|
37
37
|
gem "turbo_form"
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
-
There is nothing to mount
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
There is nothing to mount. On a stock Rails app — Propshaft, importmap-rails,
|
|
41
|
+
stimulus-rails — the Stimulus controller registers itself and there is nothing
|
|
42
|
+
to generate either. If your app bundles with esbuild, Vite, Bun or webpack:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
rails generate turbo_form:install
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
See [JavaScript](#javascript) for what that does and how to do it by hand.
|
|
44
49
|
|
|
45
50
|
## The two options
|
|
46
51
|
|
|
@@ -72,6 +77,30 @@ checkbox, `input` for a text field, `click` for a button. Name an event when you
|
|
|
72
77
|
want something else — `:blur` on text fields is usually what you want, since the
|
|
73
78
|
default fires on every keystroke.
|
|
74
79
|
|
|
80
|
+
#### Sending the form somewhere else
|
|
81
|
+
|
|
82
|
+
A trigger can answer to a different endpoint than its own form, and can add to
|
|
83
|
+
what the form sends:
|
|
84
|
+
|
|
85
|
+
```erb
|
|
86
|
+
<%= f.button "Run", type: "button", dynamic_trigger: {
|
|
87
|
+
event: :click,
|
|
88
|
+
url: formula_preview_widgets_path,
|
|
89
|
+
params: { attribute: :rafter_count }
|
|
90
|
+
} %>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`url:` replaces the form's own endpoint for that one trigger; everything else on
|
|
94
|
+
the form keeps using the form's. `params:` are appended to the submitted form
|
|
95
|
+
data, so one endpoint can tell which of several triggers asked. Both are
|
|
96
|
+
optional, and either can be given without the other.
|
|
97
|
+
|
|
98
|
+
That endpoint is yours, not the gem's — it's an ordinary action rendering an
|
|
99
|
+
ordinary turbo_stream template, so the whole form arrives as `params` under its
|
|
100
|
+
usual scope. Use it when one form feeds several actions: a dozen fields that
|
|
101
|
+
each preview themselves, or a select that belongs to a nested form with its own
|
|
102
|
+
controller.
|
|
103
|
+
|
|
75
104
|
Works on every Rails field helper, including the select and date families where
|
|
76
105
|
Rails keeps HTML attributes in a separate hash:
|
|
77
106
|
|
|
@@ -97,7 +126,8 @@ endpoint the gem draws into your routes. Given a valid signature it:
|
|
|
97
126
|
1. permits the submitted parameters wholesale,
|
|
98
127
|
2. builds a **new, unsaved** instance of the form's class from them,
|
|
99
128
|
3. assigns it to `@resource`,
|
|
100
|
-
4.
|
|
129
|
+
4. calls `TurboForm.before_render` with the controller and the resource, if one is set,
|
|
130
|
+
5. renders the turbo_stream template.
|
|
101
131
|
|
|
102
132
|
Nothing is persisted, and the resource is always freshly instantiated — even for
|
|
103
133
|
an edit form. `@resource` exists to be asked what the form should now look like,
|
|
@@ -112,15 +142,55 @@ may come from the browser unverified.
|
|
|
112
142
|
```ruby
|
|
113
143
|
# config/initializers/turbo_form.rb
|
|
114
144
|
|
|
115
|
-
# The endpoint inherits from this, so your authentication applies to it.
|
|
116
|
-
#
|
|
117
|
-
#
|
|
145
|
+
# The endpoint inherits from this, so your authentication applies to it. Point
|
|
146
|
+
# it at a narrower controller when that inheritance brings something the
|
|
147
|
+
# endpoint shouldn't have.
|
|
118
148
|
TurboForm.parent_controller = "ApplicationController"
|
|
119
149
|
|
|
120
150
|
# Draw the route yourself instead.
|
|
121
151
|
TurboForm.draw_routes = false
|
|
152
|
+
|
|
153
|
+
# Run something before the render. See below.
|
|
154
|
+
TurboForm.before_render = ->(controller, resource) { }
|
|
122
155
|
```
|
|
123
156
|
|
|
157
|
+
### `before_render`
|
|
158
|
+
|
|
159
|
+
The endpoint is a controller you never wrote, inheriting filters written for
|
|
160
|
+
controllers that save things. `before_render` is where you get to treat it like
|
|
161
|
+
one of your own: it runs in a `before_action`, after the signature is verified
|
|
162
|
+
and the resource is built, and is handed the controller and that resource.
|
|
163
|
+
|
|
164
|
+
Satisfy a filter the endpoint can't — the most common need, since nothing here
|
|
165
|
+
is authorized because nothing here is saved:
|
|
166
|
+
|
|
167
|
+
```ruby
|
|
168
|
+
# Pundit's after_action :verify_authorized
|
|
169
|
+
TurboForm.before_render = ->(controller, resource) { controller.skip_authorization }
|
|
170
|
+
|
|
171
|
+
# Action Policy's verify_authorized
|
|
172
|
+
TurboForm.before_render = ->(controller, resource) { controller.skip_verify_authorized! }
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Or authorize it for real, if re-rendering a form is something you'd rather gate.
|
|
176
|
+
Name the query explicitly: the endpoint's action is `update`, which is not the
|
|
177
|
+
permission you mean.
|
|
178
|
+
|
|
179
|
+
```ruby
|
|
180
|
+
# Pundit
|
|
181
|
+
TurboForm.before_render = ->(controller, resource) { controller.authorize(resource, :edit?) }
|
|
182
|
+
|
|
183
|
+
# Action Policy
|
|
184
|
+
TurboForm.before_render = ->(controller, resource) { controller.authorize!(resource, to: :edit?) }
|
|
185
|
+
|
|
186
|
+
# CanCanCan -- this also satisfies `check_authorization`, which has no runtime skip
|
|
187
|
+
TurboForm.before_render = ->(controller, resource) { controller.authorize!(:edit, resource) }
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Raising works the way it does in any `before_action`: your
|
|
191
|
+
`rescue_from`s catch it, since the endpoint inherits them too. Rendering or
|
|
192
|
+
redirecting from the controller halts the chain as usual.
|
|
193
|
+
|
|
124
194
|
## JavaScript
|
|
125
195
|
|
|
126
196
|
**importmap-rails** — nothing to do. The engine pins its controller as
|
|
@@ -135,31 +205,43 @@ import TurboFormController from "controllers/turbo_form_controller"
|
|
|
135
205
|
application.register("turbo-form", TurboFormController)
|
|
136
206
|
```
|
|
137
207
|
|
|
138
|
-
**esbuild, Vite, Bun, webpack** —
|
|
139
|
-
the same version, and
|
|
208
|
+
**esbuild, Vite, Bun, webpack** — there is no sweep to pick the controller up,
|
|
209
|
+
so the package has to be installed alongside the gem, at the same version, and
|
|
210
|
+
registered. `rails generate turbo_form:install` does both: it adds the package
|
|
211
|
+
with whichever manager your lockfile names, and writes
|
|
140
212
|
|
|
141
|
-
```bash
|
|
142
|
-
yarn add turbo_form
|
|
143
|
-
```
|
|
144
213
|
```js
|
|
145
|
-
|
|
146
|
-
|
|
214
|
+
// app/javascript/controllers/turbo_form_controller.js
|
|
215
|
+
import TurboFormController from "@rolemodel/turbo-form"
|
|
216
|
+
|
|
217
|
+
export default TurboFormController
|
|
147
218
|
```
|
|
148
219
|
|
|
220
|
+
Registering by *filename* rather than by appending to
|
|
221
|
+
`app/javascript/controllers/index.js` is deliberate. That file is regenerated
|
|
222
|
+
from the contents of the directory every time `rails generate stimulus` runs, so
|
|
223
|
+
a registration appended to it lasts until the next controller you generate. A
|
|
224
|
+
file named `turbo_form_controller.js` is picked up by that same regeneration and
|
|
225
|
+
registered as `turbo-form`, every time.
|
|
226
|
+
|
|
227
|
+
To do it by hand, write that file yourself and run `rails stimulus:manifest:update`.
|
|
228
|
+
|
|
149
229
|
### Testing against it
|
|
150
230
|
|
|
151
|
-
|
|
152
|
-
|
|
231
|
+
A re-render is a round trip, so the line after a trigger is racing it. Wrap the
|
|
232
|
+
trigger and the wait comes with it:
|
|
153
233
|
|
|
154
234
|
```ruby
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
before = form["data-turbo-form-requests-value"].to_i
|
|
158
|
-
yield
|
|
159
|
-
assert_selector "[data-turbo-form-requests-value='#{before + 1}']"
|
|
160
|
-
end
|
|
235
|
+
expect_dynamic_form_request { select "Bourbon", from: "Category" }
|
|
236
|
+
select "Barrel Aged", from: "Flavor"
|
|
161
237
|
```
|
|
162
238
|
|
|
239
|
+
`expect_dynamic_form_request` is available in system tests with nothing to
|
|
240
|
+
require or include — Minitest and RSpec both. It waits on the controller's
|
|
241
|
+
count of completed round trips rather than on the fields that came back, so it
|
|
242
|
+
doesn't care what the response changed. Scope it with Capybara's `within` when
|
|
243
|
+
a page carries more than one dynamic form.
|
|
244
|
+
|
|
163
245
|
## What this deliberately doesn't do
|
|
164
246
|
|
|
165
247
|
It handles the common case well and gets out of the way otherwise. There is no
|
|
@@ -9,12 +9,18 @@ import { Turbo } from "@hotwired/turbo-rails"
|
|
|
9
9
|
export default class extends Controller {
|
|
10
10
|
static values = { url: String, requests: Number }
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
// A trigger can send the form somewhere other than the form's own endpoint,
|
|
13
|
+
// and can add to what it sends. Both ride in as Stimulus action params, so
|
|
14
|
+
// one form can feed several actions without needing several forms.
|
|
15
|
+
async perform({ params: { url, query } }) {
|
|
16
|
+
const body = new FormData(this.element)
|
|
17
|
+
|
|
18
|
+
if (query) Object.entries(query).forEach(([name, value]) => body.append(name, value))
|
|
19
|
+
|
|
20
|
+
const response = await fetch(url || this.urlValue, {
|
|
15
21
|
method: "PATCH",
|
|
16
22
|
headers: this.#headers,
|
|
17
|
-
body
|
|
23
|
+
body
|
|
18
24
|
})
|
|
19
25
|
|
|
20
26
|
if (!response.ok) return
|
|
@@ -10,13 +10,18 @@ module TurboForm
|
|
|
10
10
|
head :bad_request
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
before_action :build_resource
|
|
14
|
+
before_action { TurboForm.before_render&.call(self, @resource) }
|
|
15
15
|
|
|
16
|
+
def update
|
|
16
17
|
render template: signature.template_for(@resource), formats: :turbo_stream
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
private
|
|
21
|
+
def build_resource
|
|
22
|
+
@resource = signature.model.new(form_params)
|
|
23
|
+
end
|
|
24
|
+
|
|
20
25
|
def signature
|
|
21
26
|
@signature ||= TurboForm::Signature.verify(params[:signature])
|
|
22
27
|
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
require "rails/generators"
|
|
2
|
+
|
|
3
|
+
module TurboForm
|
|
4
|
+
module Generators
|
|
5
|
+
# There is nothing to install on importmap-rails: the engine pins its own
|
|
6
|
+
# Stimulus controller and a stock `app/javascript/controllers/index.js`
|
|
7
|
+
# registers it. Bundled apps get no such sweep, so this hands them the two
|
|
8
|
+
# things they'd otherwise write by hand -- the package and the registration.
|
|
9
|
+
class InstallGenerator < Rails::Generators::Base
|
|
10
|
+
source_root File.expand_path("templates", __dir__)
|
|
11
|
+
|
|
12
|
+
LOCKFILES = {
|
|
13
|
+
"yarn.lock" => "yarn add", "pnpm-lock.yaml" => "pnpm add",
|
|
14
|
+
"bun.lock" => "bun add", "bun.lockb" => "bun add"
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
def install
|
|
18
|
+
if importmap?
|
|
19
|
+
say "turbo_form registers its own Stimulus controller on importmap-rails. Nothing to install."
|
|
20
|
+
return
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
install_package
|
|
24
|
+
register_controller
|
|
25
|
+
update_stimulus_manifest
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
def importmap? = Rails.root.join("config/importmap.rb").exist?
|
|
30
|
+
|
|
31
|
+
def install_package
|
|
32
|
+
run "#{package_manager} #{package}"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Named for the controller it is, rather than imported into `index.js`:
|
|
36
|
+
# that file is rewritten wholesale every time `rails generate stimulus`
|
|
37
|
+
# runs, and would take a registration appended to it down with it. The
|
|
38
|
+
# manifest is generated from this directory instead, so a file named
|
|
39
|
+
# this way is picked back up every time and registered as `turbo-form`.
|
|
40
|
+
def register_controller
|
|
41
|
+
copy_file "turbo_form_controller.js", "app/javascript/controllers/turbo_form_controller.js"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Left to Stimulus' own task rather than written here, the way the
|
|
45
|
+
# `stimulus` generator does it: the task is what the app already trusts
|
|
46
|
+
# to regenerate this file, and its internals have moved between releases.
|
|
47
|
+
def update_stimulus_manifest
|
|
48
|
+
return unless Rails.root.join("app/javascript/controllers/index.js").exist?
|
|
49
|
+
|
|
50
|
+
rails_command "stimulus:manifest:update"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def package = "@rolemodel/turbo-form@#{TurboForm::VERSION}"
|
|
54
|
+
|
|
55
|
+
# Matched to the lockfile that is already there, so the generator doesn't
|
|
56
|
+
# introduce a second package manager to an app that settled on one.
|
|
57
|
+
def package_manager
|
|
58
|
+
LOCKFILES.find { |lockfile, _| Rails.root.join(lockfile).exist? }&.last || "npm install"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
data/lib/turbo_form/engine.rb
CHANGED
|
@@ -30,6 +30,29 @@ module TurboForm
|
|
|
30
30
|
app.config.assets.precompile << "turbo_form.js"
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
+
# Minitest's system tests descend from ActionDispatch::SystemTestCase, so
|
|
34
|
+
# the load hook reaches them. RSpec's don't -- RSpec::Rails::SystemExampleGroup
|
|
35
|
+
# assembles itself from Action Dispatch's parts instead of inheriting the
|
|
36
|
+
# case -- so they need telling separately, once RSpec is loaded but before
|
|
37
|
+
# any example group has been defined.
|
|
38
|
+
initializer "turbo_form.system_test_helper" do
|
|
39
|
+
ActiveSupport.on_load(:action_dispatch_system_test_case) do
|
|
40
|
+
require "turbo_form/system_test_helper"
|
|
41
|
+
|
|
42
|
+
include TurboForm::SystemTestHelper
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
config.after_initialize do
|
|
47
|
+
next unless defined?(RSpec.configure)
|
|
48
|
+
|
|
49
|
+
require "turbo_form/system_test_helper"
|
|
50
|
+
|
|
51
|
+
RSpec.configure do |config|
|
|
52
|
+
config.include TurboForm::SystemTestHelper, type: :system
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
33
56
|
# Deliberately eager rather than `ActiveSupport.on_load(:action_view)`: that
|
|
34
57
|
# hook doesn't fire until Action View is first loaded, which in an app that
|
|
35
58
|
# isn't eager loading is partway through rendering the first view -- late
|
|
@@ -75,16 +75,30 @@ module TurboForm
|
|
|
75
75
|
attributes = attributes.except(:dynamic_trigger)
|
|
76
76
|
return attributes unless trigger
|
|
77
77
|
|
|
78
|
+
event, url, query = destructure_trigger(trigger)
|
|
79
|
+
|
|
78
80
|
data = (attributes[:data] || {}).dup
|
|
79
|
-
data[:action] = [ data[:action], stimulus_action_for(
|
|
81
|
+
data[:action] = [ data[:action], stimulus_action_for(event) ].compact.join(" ")
|
|
82
|
+
data[:turbo_form_url_param] = url if url
|
|
83
|
+
data[:turbo_form_query_param] = query if query
|
|
80
84
|
attributes.merge(data: data)
|
|
81
85
|
end
|
|
82
86
|
|
|
83
|
-
#
|
|
84
|
-
#
|
|
85
|
-
#
|
|
86
|
-
def
|
|
87
|
-
trigger
|
|
87
|
+
# The short forms say when to fire and nothing else. The hash form also
|
|
88
|
+
# says where to send the form and what to send with it, which is what a
|
|
89
|
+
# field needs when it answers to a different action than its own form does.
|
|
90
|
+
def destructure_trigger(trigger)
|
|
91
|
+
return trigger.values_at(:event, :url, :params) if trigger.is_a?(Hash)
|
|
92
|
+
|
|
93
|
+
[ trigger, nil, nil ]
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# `true` -- or a hash that names no event -- leaves the event off the
|
|
97
|
+
# descriptor so Stimulus binds the element's own default: `change` for a
|
|
98
|
+
# select, `input` for a text field, `click` for a button. Anything else is
|
|
99
|
+
# taken as the event name.
|
|
100
|
+
def stimulus_action_for(event)
|
|
101
|
+
event.nil? || event == true ? "turbo-form#perform" : "#{event}->turbo-form#perform"
|
|
88
102
|
end
|
|
89
103
|
end
|
|
90
104
|
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module TurboForm::SystemTestHelper
|
|
2
|
+
# Waits for a dynamic form to finish re-rendering itself.
|
|
3
|
+
#
|
|
4
|
+
# A trigger sends the form to the server and swaps part of it out when the
|
|
5
|
+
# response lands, so anything a test does on the next line is racing that
|
|
6
|
+
# response -- and loses often enough to be flaky rather than broken. The
|
|
7
|
+
# Stimulus controller counts completed round trips; waiting on that count is
|
|
8
|
+
# both cheaper and steadier than waiting on whichever field came back.
|
|
9
|
+
#
|
|
10
|
+
# expect_dynamic_form_request { select "Bourbon", from: "Category" }
|
|
11
|
+
# select "Barrel Aged", from: "Flavor"
|
|
12
|
+
#
|
|
13
|
+
# Scope it with Capybara's own `within` when a page carries more than one
|
|
14
|
+
# dynamic form.
|
|
15
|
+
def expect_dynamic_form_request
|
|
16
|
+
form = page.find("[data-controller~='turbo-form']")
|
|
17
|
+
completed = form["data-turbo-form-requests-value"].to_i + 1
|
|
18
|
+
|
|
19
|
+
yield
|
|
20
|
+
|
|
21
|
+
page.assert_selector "[data-turbo-form-requests-value='#{completed}']"
|
|
22
|
+
end
|
|
23
|
+
end
|
data/lib/turbo_form/version.rb
CHANGED
data/lib/turbo_form.rb
CHANGED
|
@@ -12,7 +12,14 @@ module TurboForm
|
|
|
12
12
|
# Set to false to keep the engine out of the host's route set and draw its
|
|
13
13
|
# route yourself.
|
|
14
14
|
mattr_accessor :draw_routes, default: true
|
|
15
|
-
|
|
15
|
+
|
|
16
|
+
# Called from a `before_action` with the controller and the resource the form
|
|
17
|
+
# was rebuilt into. The endpoint is an inherited controller the host never
|
|
18
|
+
# wrote, so this is where it gets to treat it like one of its own: authorize
|
|
19
|
+
# the render, skip a filter the endpoint can't satisfy
|
|
20
|
+
# (`controller.skip_authorization`), set something the template needs. Left
|
|
21
|
+
# alone, only the host's own inherited filters apply.
|
|
22
|
+
mattr_accessor :before_render
|
|
16
23
|
|
|
17
24
|
class << self
|
|
18
25
|
attr_writer :verifier
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: turbo_form
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andy Cohen
|
|
@@ -79,12 +79,15 @@ files:
|
|
|
79
79
|
- app/controllers/turbo_form/dynamic_forms_controller.rb
|
|
80
80
|
- config/routes.rb
|
|
81
81
|
- config/turbo_form_importmap.rb
|
|
82
|
+
- lib/generators/turbo_form/install/install_generator.rb
|
|
83
|
+
- lib/generators/turbo_form/install/templates/turbo_form_controller.js
|
|
82
84
|
- lib/tasks/turbo_form_tasks.rake
|
|
83
85
|
- lib/turbo_form.rb
|
|
84
86
|
- lib/turbo_form/engine.rb
|
|
85
87
|
- lib/turbo_form/form_builder.rb
|
|
86
88
|
- lib/turbo_form/form_helper.rb
|
|
87
89
|
- lib/turbo_form/signature.rb
|
|
90
|
+
- lib/turbo_form/system_test_helper.rb
|
|
88
91
|
- lib/turbo_form/version.rb
|
|
89
92
|
homepage: https://github.com/outlawandy/turbo_form
|
|
90
93
|
licenses:
|