turbo_form 0.3.0 → 0.4.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 +35 -27
- data/app/controllers/turbo_form/dynamic_forms_controller.rb +30 -7
- data/lib/turbo_form/form_helper.rb +14 -2
- data/lib/turbo_form/signature.rb +31 -10
- data/lib/turbo_form/version.rb +1 -1
- data/lib/turbo_form.rb +6 -6
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e9cee9bf341b5d0a6bf59ac5334065a7c71f66c0cfe99636471af207054c6484
|
|
4
|
+
data.tar.gz: 6d60f46ff0e7e74e43ba80a6db207a7fc5d30cf79afb0a7baf5ca8fa3a08fad5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 87318b7b8397c9e71d1754cea8b527f7d813865b6ad8d4101183494f8788815b1f77ea3a22053b0cfe7ec2418a9f644b21380bde114839ba98adf6b38d155241
|
|
7
|
+
data.tar.gz: b62767b018f4c05fce71f7af5060d68526d0c4bfb3f47d6b872405f89c879489f4d4320b93a09d2e55f452b3c158b3ac0af4520d8567788e6d32ee3548d5dbdd
|
data/README.md
CHANGED
|
@@ -51,20 +51,23 @@ See [JavaScript](#javascript) for what that does and how to do it by hand.
|
|
|
51
51
|
|
|
52
52
|
### `dynamic:` on the form
|
|
53
53
|
|
|
54
|
-
`dynamic: true`
|
|
55
|
-
`
|
|
56
|
-
|
|
54
|
+
`dynamic: true` makes the form dynamic. When a trigger fires, the gem renders a
|
|
55
|
+
template named `dynamic_form`, looked up just as the controller that rendered
|
|
56
|
+
the form would look up one of its own views:
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
- A form rendered by `WidgetsController` gets
|
|
59
|
+
`widgets/dynamic_form.turbo_stream.erb`.
|
|
60
|
+
- If that doesn't exist, lookup follows controller inheritance, ending at
|
|
61
|
+
`application/dynamic_form.turbo_stream.erb`.
|
|
62
|
+
- Partials resolve the same way, so `render "fields"` inside the template finds
|
|
63
|
+
`widgets/_fields`, just as it would in the form itself.
|
|
64
|
+
|
|
65
|
+
To render a different template, pass its name instead of `true`:
|
|
59
66
|
|
|
60
67
|
```erb
|
|
61
68
|
<%= form_for @widget, dynamic: "shared/refresh_widget" %>
|
|
62
69
|
```
|
|
63
70
|
|
|
64
|
-
Works on `form_for` and `form_with` alike. Any template engine works — the
|
|
65
|
-
template is resolved the way every other Rails template is, so `.slim` and
|
|
66
|
-
`.haml` are fine.
|
|
67
|
-
|
|
68
71
|
### `dynamic_trigger:` on a field
|
|
69
72
|
|
|
70
73
|
```erb
|
|
@@ -124,18 +127,24 @@ A `dynamic: true` form carries a signed description of itself to a single
|
|
|
124
127
|
endpoint the gem draws into your routes. Given a valid signature it:
|
|
125
128
|
|
|
126
129
|
1. permits the submitted parameters wholesale,
|
|
127
|
-
2.
|
|
130
|
+
2. rebuilds the form's object and assigns them to it — an edit form's record is
|
|
131
|
+
found again by id, and a new form's object is rebuilt from the attributes it
|
|
132
|
+
was built with (a parent's foreign key from `@order.line_items.new`, say) before
|
|
133
|
+
the submitted ones are applied,
|
|
128
134
|
3. assigns it to `@resource`,
|
|
129
|
-
4.
|
|
135
|
+
4. runs `TurboForm.before_render` inside the controller with the resource, if one is set,
|
|
130
136
|
5. renders the turbo_stream template.
|
|
131
137
|
|
|
132
|
-
Nothing is persisted
|
|
133
|
-
|
|
134
|
-
|
|
138
|
+
Nothing is persisted. `@resource` exists to be asked what the form should now
|
|
139
|
+
look like, not to be saved — and since Active Record saves some assignments on
|
|
140
|
+
the spot (`has_many` writers, `*_ids=`), an Active Record resource is rebuilt
|
|
141
|
+
inside a transaction that is always rolled back.
|
|
135
142
|
|
|
136
|
-
The signature covers the class name, the parameter scope
|
|
137
|
-
|
|
138
|
-
may come from the
|
|
143
|
+
The signature covers the class name, the parameter scope, the template, the view
|
|
144
|
+
paths the form was rendered under, and the record's id or starting attributes. It is signed because the endpoint
|
|
145
|
+
constantizes, loads and renders what it names; none of that may come from the
|
|
146
|
+
browser unverified. It is not encrypted, so those starting attributes are
|
|
147
|
+
readable in the page, as the form itself is.
|
|
139
148
|
|
|
140
149
|
## Configuration
|
|
141
150
|
|
|
@@ -151,7 +160,7 @@ TurboForm.parent_controller = "ApplicationController"
|
|
|
151
160
|
TurboForm.draw_routes = false
|
|
152
161
|
|
|
153
162
|
# Run something before the render. See below.
|
|
154
|
-
TurboForm.before_render = ->(
|
|
163
|
+
TurboForm.before_render = ->(resource) { }
|
|
155
164
|
```
|
|
156
165
|
|
|
157
166
|
### `before_render`
|
|
@@ -159,17 +168,18 @@ TurboForm.before_render = ->(controller, resource) { }
|
|
|
159
168
|
The endpoint is a controller you never wrote, inheriting filters written for
|
|
160
169
|
controllers that save things. `before_render` is where you get to treat it like
|
|
161
170
|
one of your own: it runs in a `before_action`, after the signature is verified
|
|
162
|
-
and the resource is built, and is handed
|
|
171
|
+
and the resource is built, and is handed that resource. It runs *inside* the
|
|
172
|
+
controller, so its protected helpers — Pundit's among them — are in reach.
|
|
163
173
|
|
|
164
174
|
Satisfy a filter the endpoint can't — the most common need, since nothing here
|
|
165
175
|
is authorized because nothing here is saved:
|
|
166
176
|
|
|
167
177
|
```ruby
|
|
168
178
|
# Pundit's after_action :verify_authorized
|
|
169
|
-
TurboForm.before_render = ->(
|
|
179
|
+
TurboForm.before_render = ->(resource) { skip_authorization }
|
|
170
180
|
|
|
171
181
|
# Action Policy's verify_authorized
|
|
172
|
-
TurboForm.before_render = ->(
|
|
182
|
+
TurboForm.before_render = ->(resource) { skip_verify_authorized! }
|
|
173
183
|
```
|
|
174
184
|
|
|
175
185
|
Or authorize it for real, if re-rendering a form is something you'd rather gate.
|
|
@@ -178,18 +188,18 @@ permission you mean.
|
|
|
178
188
|
|
|
179
189
|
```ruby
|
|
180
190
|
# Pundit
|
|
181
|
-
TurboForm.before_render = ->(
|
|
191
|
+
TurboForm.before_render = ->(resource) { authorize resource, :edit? }
|
|
182
192
|
|
|
183
193
|
# Action Policy
|
|
184
|
-
TurboForm.before_render = ->(
|
|
194
|
+
TurboForm.before_render = ->(resource) { authorize! resource, to: :edit? }
|
|
185
195
|
|
|
186
196
|
# CanCanCan -- this also satisfies `check_authorization`, which has no runtime skip
|
|
187
|
-
TurboForm.before_render = ->(
|
|
197
|
+
TurboForm.before_render = ->(resource) { authorize! :edit, resource }
|
|
188
198
|
```
|
|
189
199
|
|
|
190
200
|
Raising works the way it does in any `before_action`: your
|
|
191
201
|
`rescue_from`s catch it, since the endpoint inherits them too. Rendering or
|
|
192
|
-
redirecting
|
|
202
|
+
redirecting halts the chain as usual.
|
|
193
203
|
|
|
194
204
|
## JavaScript
|
|
195
205
|
|
|
@@ -245,9 +255,7 @@ a page carries more than one dynamic form.
|
|
|
245
255
|
## What this deliberately doesn't do
|
|
246
256
|
|
|
247
257
|
It handles the common case well and gets out of the way otherwise. There is no
|
|
248
|
-
debouncing, no request cancellation
|
|
249
|
-
override, and no hook for loading an existing record instead of building a new
|
|
250
|
-
one. When you need those, write the action by hand — that path is still open,
|
|
258
|
+
debouncing, no request cancellation and no loading state. When you need those, write the action by hand — that path is still open,
|
|
251
259
|
and this gem doesn't stand in front of it.
|
|
252
260
|
|
|
253
261
|
## License
|
|
@@ -1,25 +1,48 @@
|
|
|
1
1
|
module TurboForm
|
|
2
2
|
# Renders a form again from the state the browser currently has it in.
|
|
3
3
|
#
|
|
4
|
-
# Nothing here is saved. The
|
|
5
|
-
# it what the form should now look like, which is why the parameters are
|
|
6
|
-
# whole: narrowing them would only make the re-rendered form disagree
|
|
7
|
-
# the user actually typed.
|
|
4
|
+
# Nothing here is saved. The form's object is rebuilt only so the template can
|
|
5
|
+
# ask it what the form should now look like, which is why the parameters are
|
|
6
|
+
# taken whole: narrowing them would only make the re-rendered form disagree
|
|
7
|
+
# with what the user actually typed.
|
|
8
8
|
class DynamicFormsController < TurboForm.parent_controller.constantize
|
|
9
9
|
rescue_from TurboForm::Signature::Invalid do
|
|
10
10
|
head :bad_request
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
+
around_action :discard_writes
|
|
13
14
|
before_action :build_resource
|
|
14
|
-
before_action { TurboForm.before_render
|
|
15
|
+
before_action { instance_exec(@resource, &TurboForm.before_render) if TurboForm.before_render }
|
|
15
16
|
|
|
16
17
|
def update
|
|
17
|
-
render template
|
|
18
|
+
render signature.template || "dynamic_form", formats: :turbo_stream
|
|
18
19
|
end
|
|
19
20
|
|
|
20
21
|
private
|
|
21
22
|
def build_resource
|
|
22
|
-
@resource = signature.
|
|
23
|
+
@resource = signature.rebuild(form_params)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Assigning to a saved record is not always inert: Active Record saves
|
|
27
|
+
# `has_many` writers and `*_ids=` on the spot.
|
|
28
|
+
def discard_writes
|
|
29
|
+
return yield unless defined?(ActiveRecord::Base) && signature.model < ActiveRecord::Base
|
|
30
|
+
|
|
31
|
+
signature.model.transaction do
|
|
32
|
+
yield
|
|
33
|
+
raise ActiveRecord::Rollback
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Renders as if it were the controller that rendered the form, so the
|
|
38
|
+
# template's relative partials resolve the way the form's own did.
|
|
39
|
+
#
|
|
40
|
+
# Read while the lookup context is built, before `rescue_from` is in play,
|
|
41
|
+
# so a bad signature falls through here and is answered by the action.
|
|
42
|
+
def _prefixes
|
|
43
|
+
signature.prefixes
|
|
44
|
+
rescue TurboForm::Signature::Invalid
|
|
45
|
+
super
|
|
23
46
|
end
|
|
24
47
|
|
|
25
48
|
def signature
|
|
@@ -14,9 +14,11 @@ module TurboForm
|
|
|
14
14
|
|
|
15
15
|
scope ||= model_name_from_record_or_class(object).param_key
|
|
16
16
|
signature = TurboForm::Signature.new(
|
|
17
|
-
model_name: object.class.name,
|
|
17
|
+
model_name: (object.class.try(:base_class) || object.class).name,
|
|
18
18
|
scope: scope.to_s,
|
|
19
|
-
template: (dynamic unless dynamic == true)
|
|
19
|
+
template: (dynamic unless dynamic == true),
|
|
20
|
+
prefixes: lookup_context.prefixes,
|
|
21
|
+
**origin_of(object)
|
|
20
22
|
)
|
|
21
23
|
|
|
22
24
|
wire_dynamic_form(options, signature)
|
|
@@ -24,6 +26,16 @@ module TurboForm
|
|
|
24
26
|
end
|
|
25
27
|
|
|
26
28
|
private
|
|
29
|
+
# A saved record is found again by id. An unsaved one is rebuilt from what
|
|
30
|
+
# it had already been given -- a parent's foreign key, say -- since the
|
|
31
|
+
# form may not submit that back. A plain form object with no dirty
|
|
32
|
+
# tracking has nothing to carry.
|
|
33
|
+
def origin_of(object)
|
|
34
|
+
return { id: object.id } if object.try(:persisted?)
|
|
35
|
+
|
|
36
|
+
{ seed: object.try(:changes).to_h.transform_values(&:last) }
|
|
37
|
+
end
|
|
38
|
+
|
|
27
39
|
# `form_for` funnels HTML attributes through options[:html] while `form_with`
|
|
28
40
|
# takes them at the top level. Write wherever the caller already is, and sit
|
|
29
41
|
# alongside any Stimulus controller they asked for rather than replacing it.
|
data/lib/turbo_form/signature.rb
CHANGED
|
@@ -6,9 +6,13 @@ module TurboForm
|
|
|
6
6
|
# constantized and instantiated, `template` gets rendered. Neither may come
|
|
7
7
|
# from the client unverified.
|
|
8
8
|
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
9
|
+
# `prefixes` are the view paths the form was rendered under, so the endpoint
|
|
10
|
+
# can find its template and partials the way the form's own controller would.
|
|
11
|
+
#
|
|
12
|
+
# It also says where the form's object came from, so the endpoint can rebuild
|
|
13
|
+
# that object rather than a blank one: the `id` of a saved record, or the
|
|
14
|
+
# `seed` of attributes an unsaved one already had -- the `tank_id` of
|
|
15
|
+
# `@tank.rings.new` -- which the form itself may never submit.
|
|
12
16
|
class Signature
|
|
13
17
|
class Invalid < StandardError; end
|
|
14
18
|
|
|
@@ -19,25 +23,42 @@ module TurboForm
|
|
|
19
23
|
new(**payload.symbolize_keys)
|
|
20
24
|
end
|
|
21
25
|
|
|
22
|
-
attr_reader :model_name, :scope, :template
|
|
26
|
+
attr_reader :model_name, :scope, :template, :prefixes, :id, :seed
|
|
23
27
|
|
|
24
|
-
def initialize(model_name:, scope:, template: nil)
|
|
28
|
+
def initialize(model_name:, scope:, template: nil, prefixes: [], id: nil, seed: {})
|
|
25
29
|
@model_name = model_name
|
|
26
30
|
@scope = scope
|
|
27
31
|
@template = template
|
|
32
|
+
@prefixes = prefixes
|
|
33
|
+
@id = id
|
|
34
|
+
@seed = seed
|
|
28
35
|
end
|
|
29
36
|
|
|
30
37
|
def model = model_name.constantize
|
|
31
38
|
|
|
32
|
-
#
|
|
33
|
-
# `
|
|
34
|
-
def
|
|
35
|
-
|
|
39
|
+
# The form's object as the user now has it. A new one is built in a single
|
|
40
|
+
# step so the submitted `type` picks its STI subclass.
|
|
41
|
+
def rebuild(params)
|
|
42
|
+
return model.new(seed.merge(params.to_h)) unless id
|
|
43
|
+
|
|
44
|
+
retype(model.find(id), params).tap { |resource| resource.assign_attributes(params) }
|
|
36
45
|
end
|
|
37
46
|
|
|
38
47
|
def to_s
|
|
39
|
-
TurboForm.verifier.generate({ model_name:, scope:, template: })
|
|
48
|
+
TurboForm.verifier.generate({ model_name:, scope:, template:, prefixes:, id:, seed: })
|
|
40
49
|
end
|
|
41
50
|
alias to_param to_s
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
# `find` answers with the subclass that was saved, not the one just picked.
|
|
54
|
+
# Let `new` choose by Active Record's own STI rules, and switch before
|
|
55
|
+
# assigning so nested records land on the object that is kept.
|
|
56
|
+
def retype(record, params)
|
|
57
|
+
type = params.to_h.slice(model.try(:inheritance_column).to_s)
|
|
58
|
+
return record if type.empty?
|
|
59
|
+
|
|
60
|
+
subclass = model.new(type).class
|
|
61
|
+
record.instance_of?(subclass) ? record : record.becomes(subclass)
|
|
62
|
+
end
|
|
42
63
|
end
|
|
43
64
|
end
|
data/lib/turbo_form/version.rb
CHANGED
data/lib/turbo_form.rb
CHANGED
|
@@ -13,12 +13,12 @@ module TurboForm
|
|
|
13
13
|
# route yourself.
|
|
14
14
|
mattr_accessor :draw_routes, default: true
|
|
15
15
|
|
|
16
|
-
#
|
|
17
|
-
# was rebuilt into. The endpoint is an inherited controller
|
|
18
|
-
# wrote, so this is where it gets to treat it like one of its
|
|
19
|
-
# the render, skip a filter the endpoint can't satisfy
|
|
20
|
-
# (`
|
|
21
|
-
#
|
|
16
|
+
# Run as the body of a `before_action`, inside the endpoint, and handed the
|
|
17
|
+
# resource the form was rebuilt into. The endpoint is an inherited controller
|
|
18
|
+
# the host never wrote, so this is where it gets to treat it like one of its
|
|
19
|
+
# own: authorize the render, skip a filter the endpoint can't satisfy
|
|
20
|
+
# (`skip_authorization`), set something the template needs. Left alone, only
|
|
21
|
+
# the host's own inherited filters apply.
|
|
22
22
|
mattr_accessor :before_render
|
|
23
23
|
|
|
24
24
|
class << self
|