turbo_form 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 887edabc903374a74393c5a9f2b04e70c78d421a17e893355f9b2f985404a7da
4
- data.tar.gz: 11cbdfb726a72675d6980e373001ca40de5a9258f2329b2391992779a5eebfa7
3
+ metadata.gz: e9cee9bf341b5d0a6bf59ac5334065a7c71f66c0cfe99636471af207054c6484
4
+ data.tar.gz: 6d60f46ff0e7e74e43ba80a6db207a7fc5d30cf79afb0a7baf5ca8fa3a08fad5
5
5
  SHA512:
6
- metadata.gz: 0431310fcc0bb70e96b71336275a63889b7746b79d4ba4ca64fb36358beff818d56d5b7b05f50663875371771a8e33c85fdb6b1a68e60228a7fcfeaf8a0760e9
7
- data.tar.gz: 56831a247fe03fd5bc540a2a44c6fde0a8a4dc2ac64a4f8b8a9dca712ba5134fd8ac79cced5b8e12e78e93e7daa7f427bad683fa56e1729bd59fbe212eb55197
6
+ metadata.gz: 87318b7b8397c9e71d1754cea8b527f7d813865b6ad8d4101183494f8788815b1f77ea3a22053b0cfe7ec2418a9f644b21380bde114839ba98adf6b38d155241
7
+ data.tar.gz: b62767b018f4c05fce71f7af5060d68526d0c4bfb3f47d6b872405f89c879489f4d4320b93a09d2e55f452b3c158b3ac0af4520d8567788e6d32ee3548d5dbdd
data/README.md CHANGED
@@ -37,29 +37,37 @@ 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 and nothing to generate. On a stock Rails app —
41
- Propshaft, importmap-rails, stimulus-rails — the Stimulus controller registers
42
- itself. See [JavaScript](#javascript) if your app bundles with esbuild, Vite,
43
- Bun or webpack.
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
 
47
52
  ### `dynamic:` on the form
48
53
 
49
- `dynamic: true` wires the form up and points it at
50
- `app/views/<resource dir>/dynamic_form.turbo_stream.*` — alongside the
51
- resource's own partial, so `widgets/_widget` gets `widgets/dynamic_form`.
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
+
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.
52
64
 
53
- Pass a string to render something else instead:
65
+ To render a different template, pass its name instead of `true`:
54
66
 
55
67
  ```erb
56
68
  <%= form_for @widget, dynamic: "shared/refresh_widget" %>
57
69
  ```
58
70
 
59
- Works on `form_for` and `form_with` alike. Any template engine works — the
60
- template is resolved the way every other Rails template is, so `.slim` and
61
- `.haml` are fine.
62
-
63
71
  ### `dynamic_trigger:` on a field
64
72
 
65
73
  ```erb
@@ -72,6 +80,30 @@ checkbox, `input` for a text field, `click` for a button. Name an event when you
72
80
  want something else — `:blur` on text fields is usually what you want, since the
73
81
  default fires on every keystroke.
74
82
 
83
+ #### Sending the form somewhere else
84
+
85
+ A trigger can answer to a different endpoint than its own form, and can add to
86
+ what the form sends:
87
+
88
+ ```erb
89
+ <%= f.button "Run", type: "button", dynamic_trigger: {
90
+ event: :click,
91
+ url: formula_preview_widgets_path,
92
+ params: { attribute: :rafter_count }
93
+ } %>
94
+ ```
95
+
96
+ `url:` replaces the form's own endpoint for that one trigger; everything else on
97
+ the form keeps using the form's. `params:` are appended to the submitted form
98
+ data, so one endpoint can tell which of several triggers asked. Both are
99
+ optional, and either can be given without the other.
100
+
101
+ That endpoint is yours, not the gem's — it's an ordinary action rendering an
102
+ ordinary turbo_stream template, so the whole form arrives as `params` under its
103
+ usual scope. Use it when one form feeds several actions: a dozen fields that
104
+ each preview themselves, or a select that belongs to a nested form with its own
105
+ controller.
106
+
75
107
  Works on every Rails field helper, including the select and date families where
76
108
  Rails keeps HTML attributes in a separate hash:
77
109
 
@@ -95,32 +127,80 @@ A `dynamic: true` form carries a signed description of itself to a single
95
127
  endpoint the gem draws into your routes. Given a valid signature it:
96
128
 
97
129
  1. permits the submitted parameters wholesale,
98
- 2. builds a **new, unsaved** instance of the form's class from them,
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,
99
134
  3. assigns it to `@resource`,
100
- 4. renders the turbo_stream template.
135
+ 4. runs `TurboForm.before_render` inside the controller with the resource, if one is set,
136
+ 5. renders the turbo_stream template.
101
137
 
102
- Nothing is persisted, and the resource is always freshly instantiated — even for
103
- an edit form. `@resource` exists to be asked what the form should now look like,
104
- not to be saved.
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.
105
142
 
106
- The signature covers the class name, the parameter scope and the template. It is
107
- signed because the endpoint constantizes and renders what it names; none of that
108
- may come from the browser unverified.
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.
109
148
 
110
149
  ## Configuration
111
150
 
112
151
  ```ruby
113
152
  # config/initializers/turbo_form.rb
114
153
 
115
- # The endpoint inherits from this, so your authentication applies to it. If your
116
- # ApplicationController enforces something the endpoint can't satisfy -- Pundit's
117
- # `verify_authorized`, say -- point this at a controller that doesn't.
154
+ # The endpoint inherits from this, so your authentication applies to it. Point
155
+ # it at a narrower controller when that inheritance brings something the
156
+ # endpoint shouldn't have.
118
157
  TurboForm.parent_controller = "ApplicationController"
119
158
 
120
159
  # Draw the route yourself instead.
121
160
  TurboForm.draw_routes = false
161
+
162
+ # Run something before the render. See below.
163
+ TurboForm.before_render = ->(resource) { }
164
+ ```
165
+
166
+ ### `before_render`
167
+
168
+ The endpoint is a controller you never wrote, inheriting filters written for
169
+ controllers that save things. `before_render` is where you get to treat it like
170
+ one of your own: it runs in a `before_action`, after the signature is verified
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.
173
+
174
+ Satisfy a filter the endpoint can't — the most common need, since nothing here
175
+ is authorized because nothing here is saved:
176
+
177
+ ```ruby
178
+ # Pundit's after_action :verify_authorized
179
+ TurboForm.before_render = ->(resource) { skip_authorization }
180
+
181
+ # Action Policy's verify_authorized
182
+ TurboForm.before_render = ->(resource) { skip_verify_authorized! }
122
183
  ```
123
184
 
185
+ Or authorize it for real, if re-rendering a form is something you'd rather gate.
186
+ Name the query explicitly: the endpoint's action is `update`, which is not the
187
+ permission you mean.
188
+
189
+ ```ruby
190
+ # Pundit
191
+ TurboForm.before_render = ->(resource) { authorize resource, :edit? }
192
+
193
+ # Action Policy
194
+ TurboForm.before_render = ->(resource) { authorize! resource, to: :edit? }
195
+
196
+ # CanCanCan -- this also satisfies `check_authorization`, which has no runtime skip
197
+ TurboForm.before_render = ->(resource) { authorize! :edit, resource }
198
+ ```
199
+
200
+ Raising works the way it does in any `before_action`: your
201
+ `rescue_from`s catch it, since the endpoint inherits them too. Rendering or
202
+ redirecting halts the chain as usual.
203
+
124
204
  ## JavaScript
125
205
 
126
206
  **importmap-rails** — nothing to do. The engine pins its controller as
@@ -135,17 +215,27 @@ import TurboFormController from "controllers/turbo_form_controller"
135
215
  application.register("turbo-form", TurboFormController)
136
216
  ```
137
217
 
138
- **esbuild, Vite, Bun, webpack** — install the npm package alongside the gem, at
139
- the same version, and register it:
218
+ **esbuild, Vite, Bun, webpack** — there is no sweep to pick the controller up,
219
+ so the package has to be installed alongside the gem, at the same version, and
220
+ registered. `rails generate turbo_form:install` does both: it adds the package
221
+ with whichever manager your lockfile names, and writes
140
222
 
141
- ```bash
142
- yarn add @rolemodel/turbo_form
143
- ```
144
223
  ```js
145
- import TurboFormController from "@rolemodel/turbo_form"
146
- application.register("turbo-form", TurboFormController)
224
+ // app/javascript/controllers/turbo_form_controller.js
225
+ import TurboFormController from "@rolemodel/turbo-form"
226
+
227
+ export default TurboFormController
147
228
  ```
148
229
 
230
+ Registering by *filename* rather than by appending to
231
+ `app/javascript/controllers/index.js` is deliberate. That file is regenerated
232
+ from the contents of the directory every time `rails generate stimulus` runs, so
233
+ a registration appended to it lasts until the next controller you generate. A
234
+ file named `turbo_form_controller.js` is picked up by that same regeneration and
235
+ registered as `turbo-form`, every time.
236
+
237
+ To do it by hand, write that file yourself and run `rails stimulus:manifest:update`.
238
+
149
239
  ### Testing against it
150
240
 
151
241
  A re-render is a round trip, so the line after a trigger is racing it. Wrap the
@@ -165,9 +255,7 @@ a page carries more than one dynamic form.
165
255
  ## What this deliberately doesn't do
166
256
 
167
257
  It handles the common case well and gets out of the way otherwise. There is no
168
- debouncing, no request cancellation, no loading state, no per-element URL
169
- override, and no hook for loading an existing record instead of building a new
170
- 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,
171
259
  and this gem doesn't stand in front of it.
172
260
 
173
261
  ## License
@@ -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
- async perform() {
13
- this.trigger
14
- const response = await fetch(this.urlValue, {
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: new FormData(this.element)
23
+ body
18
24
  })
19
25
 
20
26
  if (!response.ok) return
@@ -1,22 +1,50 @@
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 resource is rebuilt only so the template can ask
5
- # it what the form should now look like, which is why the parameters are taken
6
- # whole: narrowing them would only make the re-rendered form disagree with what
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
- def update
14
- @resource = signature.model.new(form_params)
13
+ around_action :discard_writes
14
+ before_action :build_resource
15
+ before_action { instance_exec(@resource, &TurboForm.before_render) if TurboForm.before_render }
15
16
 
16
- render template: signature.template_for(@resource), formats: :turbo_stream
17
+ def update
18
+ render signature.template || "dynamic_form", formats: :turbo_stream
17
19
  end
18
20
 
19
21
  private
22
+ def build_resource
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
46
+ end
47
+
20
48
  def signature
21
49
  @signature ||= TurboForm::Signature.verify(params[:signature])
22
50
  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
@@ -0,0 +1,4 @@
1
+ // Registered as `turbo-form` by the Stimulus manifest, which reads this filename.
2
+ import TurboFormController from "@rolemodel/turbo-form"
3
+
4
+ export default TurboFormController
@@ -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(trigger) ].compact.join(" ")
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
- # `true` leaves the event off the descriptor so Stimulus binds the element's
84
- # own default: `change` for a select, `input` for a text field, `click` for
85
- # a button. Anything else is taken as the event name.
86
- def stimulus_action_for(trigger)
87
- trigger == true ? "turbo-form#perform" : "#{trigger}->turbo-form#perform"
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
@@ -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.
@@ -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
- # Note that this identifies a *class*, not a record. Dynamic forms are usually
10
- # editing something unsaved, which is also why GlobalID -- which requires a
11
- # persisted record -- can't express it.
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
- # By convention the template sits alongside the resource's own partial:
33
- # `widgets/_widget` gets `widgets/dynamic_form`.
34
- def template_for(resource)
35
- template || File.join(File.dirname(resource.to_partial_path), "dynamic_form")
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
@@ -1,3 +1,3 @@
1
1
  module TurboForm
2
- VERSION = "0.2.0"
2
+ VERSION = "0.4.0"
3
3
  end
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
- # mattr_accessor :authorize, default: ->(controller) { true }
15
+
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
+ 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.2.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Cohen
@@ -79,6 +79,8 @@ 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