turbo_form 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f7a0e172eb5fd1fceae6c061a577eb424636d03b818b4ac5468e2d917e40955b
4
+ data.tar.gz: 82872425eec773e4ed97db3e91c8935a6c125eeca95fd6f6214303ce86ddb0dd
5
+ SHA512:
6
+ metadata.gz: ede619403754616875c64b0cd57f3a56b6bea8f4d2f4a4844ebecacef86fdce3ae16ea2f21022e0ea146c2f092ba69b43783a39ec0c8cba3f25e393b5dad97cd
7
+ data.tar.gz: 2a236bdc2ce28e7fbb0f7005b65b1472954d90a2581619bb311b2bf1b059e7d595afdcdbf2eefe38d3bb79cf4429104b994f6c0ded268499f86897ffa9cf4b17
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright Andy Cohen
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,173 @@
1
+ # turbo_form
2
+
3
+ Turbo-Stream backed dynamic forms for Rails, by convention.
4
+
5
+ A form that needs to change as it is filled in — a dependent dropdown, a section
6
+ that appears once you pick a type — usually costs you a route, a controller
7
+ action, a Stimulus controller and some wiring. This gem asks for two words
8
+ instead:
9
+
10
+ ```erb
11
+ <%= form_for @widget, dynamic: true do |f| %>
12
+ <%= f.select :category, @widget.categories, {}, dynamic_trigger: true %>
13
+
14
+ <div id="flavor-field">
15
+ <%= f.select :flavor, @widget.flavors %>
16
+ </div>
17
+ <% end %>
18
+ ```
19
+
20
+ Pick a category and the form is sent to the server as it currently stands. The
21
+ server rebuilds `@resource` from what you typed and renders
22
+ `app/views/widgets/dynamic_form.turbo_stream.erb`:
23
+
24
+ ```erb
25
+ <%= fields model: @resource do |f| %>
26
+ <%= turbo_stream.update "flavor-field" do %>
27
+ <%= f.select :flavor, @resource.flavors %>
28
+ <% end %>
29
+ <% end %>
30
+ ```
31
+
32
+ That's the whole feature. No route, no controller, no JavaScript.
33
+
34
+ ## Installation
35
+
36
+ ```ruby
37
+ gem "turbo_form"
38
+ ```
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.
44
+
45
+ ## The two options
46
+
47
+ ### `dynamic:` on the form
48
+
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`.
52
+
53
+ Pass a string to render something else instead:
54
+
55
+ ```erb
56
+ <%= form_for @widget, dynamic: "shared/refresh_widget" %>
57
+ ```
58
+
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
+ ### `dynamic_trigger:` on a field
64
+
65
+ ```erb
66
+ <%= f.text_field :name, dynamic_trigger: true %> <%# on the default event %>
67
+ <%= f.text_field :name, dynamic_trigger: :blur %> <%# on a named event %>
68
+ ```
69
+
70
+ `true` lets Stimulus pick the element's natural event: `change` for a select or
71
+ checkbox, `input` for a text field, `click` for a button. Name an event when you
72
+ want something else — `:blur` on text fields is usually what you want, since the
73
+ default fires on every keystroke.
74
+
75
+ Works on every Rails field helper, including the select and date families where
76
+ Rails keeps HTML attributes in a separate hash:
77
+
78
+ ```erb
79
+ <%= f.collection_select :category_id, Category.all, :id, :name, dynamic_trigger: true %>
80
+ ```
81
+
82
+ ### With SimpleForm
83
+
84
+ turbo_form builds on `ActionView::Helpers::FormBuilder`, which SimpleForm
85
+ inherits from, so it works without SimpleForm being involved at all:
86
+
87
+ ```slim
88
+ = simple_form_for @widget, dynamic: true do |f|
89
+ = f.input :category, input_html: { dynamic_trigger: true }
90
+ ```
91
+
92
+ ## What the endpoint does
93
+
94
+ A `dynamic: true` form carries a signed description of itself to a single
95
+ endpoint the gem draws into your routes. Given a valid signature it:
96
+
97
+ 1. permits the submitted parameters wholesale,
98
+ 2. builds a **new, unsaved** instance of the form's class from them,
99
+ 3. assigns it to `@resource`,
100
+ 4. renders the turbo_stream template.
101
+
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.
105
+
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.
109
+
110
+ ## Configuration
111
+
112
+ ```ruby
113
+ # config/initializers/turbo_form.rb
114
+
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.
118
+ TurboForm.parent_controller = "ApplicationController"
119
+
120
+ # Draw the route yourself instead.
121
+ TurboForm.draw_routes = false
122
+ ```
123
+
124
+ ## JavaScript
125
+
126
+ **importmap-rails** — nothing to do. The engine pins its controller as
127
+ `controllers/turbo_form_controller`, which the `eagerLoadControllersFrom` /
128
+ `lazyLoadControllersFrom` in a stock `app/javascript/controllers/index.js`
129
+ registers as `turbo-form` on its own.
130
+
131
+ If you hand-wrote that file, register it yourself:
132
+
133
+ ```js
134
+ import TurboFormController from "controllers/turbo_form_controller"
135
+ application.register("turbo-form", TurboFormController)
136
+ ```
137
+
138
+ **esbuild, Vite, Bun, webpack** — install the npm package alongside the gem, at
139
+ the same version, and register it:
140
+
141
+ ```bash
142
+ yarn add turbo_form
143
+ ```
144
+ ```js
145
+ import TurboFormController from "turbo_form"
146
+ application.register("turbo-form", TurboFormController)
147
+ ```
148
+
149
+ ### Testing against it
150
+
151
+ The controller counts completed round trips, so a system test can wait on one
152
+ instead of sleeping:
153
+
154
+ ```ruby
155
+ def expect_dynamic_form_request
156
+ form = find("[data-controller~='turbo-form']")
157
+ before = form["data-turbo-form-requests-value"].to_i
158
+ yield
159
+ assert_selector "[data-turbo-form-requests-value='#{before + 1}']"
160
+ end
161
+ ```
162
+
163
+ ## What this deliberately doesn't do
164
+
165
+ It handles the common case well and gets out of the way otherwise. There is no
166
+ debouncing, no request cancellation, no loading state, no per-element URL
167
+ override, and no hook for loading an existing record instead of building a new
168
+ one. When you need those, write the action by hand — that path is still open,
169
+ and this gem doesn't stand in front of it.
170
+
171
+ ## License
172
+
173
+ MIT.
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/setup"
2
+
3
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
4
+ load "rails/tasks/engine.rake"
5
+
6
+ require "bundler/gem_tasks"
@@ -0,0 +1,37 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+ import { Turbo } from "@hotwired/turbo-rails"
3
+
4
+ // Re-renders the form it is attached to, from the server, as a Turbo Stream.
5
+ //
6
+ // The whole form is submitted so the server sees exactly the state the user is
7
+ // looking at; nothing is saved, and the response only describes what should
8
+ // change on screen.
9
+ export default class extends Controller {
10
+ static values = { url: String, requests: Number }
11
+
12
+ async submit() {
13
+ const response = await fetch(this.urlValue, {
14
+ method: "PATCH",
15
+ headers: this.#headers,
16
+ body: new FormData(this.element)
17
+ })
18
+
19
+ if (!response.ok) return
20
+
21
+ Turbo.renderStreamMessage(await response.text())
22
+
23
+ // Lets a system test wait on a completed round trip instead of sleeping.
24
+ this.requestsValue++
25
+ }
26
+
27
+ get #headers() {
28
+ const headers = { Accept: "text/vnd.turbo-stream.html" }
29
+ const token = document.querySelector("meta[name=csrf-token]")?.content
30
+
31
+ // The form's own authenticity_token rides along in the body; this covers
32
+ // the forms that don't carry one.
33
+ if (token) headers["X-CSRF-Token"] = token
34
+
35
+ return headers
36
+ }
37
+ }
@@ -0,0 +1,28 @@
1
+ module TurboForm
2
+ # Renders a form again from the state the browser currently has it in.
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.
8
+ class DynamicFormsController < TurboForm.parent_controller.constantize
9
+ rescue_from TurboForm::Signature::Invalid do
10
+ head :bad_request
11
+ end
12
+
13
+ def update
14
+ @resource = signature.model.new(form_params)
15
+
16
+ render template: signature.template_for(@resource), formats: :turbo_stream
17
+ end
18
+
19
+ private
20
+ def signature
21
+ @signature ||= TurboForm::Signature.verify(params[:signature])
22
+ end
23
+
24
+ def form_params
25
+ params.fetch(signature.scope, {}).permit!
26
+ end
27
+ end
28
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,10 @@
1
+ # Drawn straight into the host's route set so that installing the gem is the
2
+ # whole installation -- there is nothing to mount. Set `TurboForm.draw_routes`
3
+ # to false to take that over.
4
+ #
5
+ # PATCH rather than POST for two reasons: whole forms outgrow a query string,
6
+ # and an edit form's `_method=patch` hidden field would make Rack rewrite a POST
7
+ # out from under us.
8
+ Rails.application.routes.draw do
9
+ patch "/turbo_form/:signature" => "turbo_form/dynamic_forms#update", as: :turbo_form
10
+ end if TurboForm.draw_routes
@@ -0,0 +1,5 @@
1
+ # Keyed as `controllers/..._controller` so the stock
2
+ # `eagerLoadControllersFrom("controllers", application)` in a Rails app finds it
3
+ # and registers it as `turbo-form`, with nothing asked of the host. The asset
4
+ # name is distinct so the host's own app/javascript/controllers can't shadow it.
5
+ pin "controllers/turbo_form_controller", to: "turbo_form.js"
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :turbo_form do
3
+ # # Task goes here
4
+ # end
@@ -0,0 +1,44 @@
1
+ require "turbo-rails"
2
+ require "turbo_form/form_helper"
3
+ require "turbo_form/form_builder"
4
+ require "turbo_form/signature"
5
+
6
+ module TurboForm
7
+ # Deliberately *not* `isolate_namespace`: the template this engine renders
8
+ # belongs to the host, and isolating would point its route helpers at this
9
+ # engine's own (empty) route set -- so `widgets_path` in a host's
10
+ # dynamic_form template would raise.
11
+ class Engine < ::Rails::Engine
12
+ # importmap-rails reads config.importmap.paths exactly once, in its own
13
+ # `importmap` initializer, and draws the host's pins last -- so appending
14
+ # here both registers ours and leaves the host able to override them.
15
+ # The guard keeps esbuild/vite/bun hosts booting; a `before:` naming an
16
+ # initializer that doesn't exist is itself harmless.
17
+ initializer "turbo_form.importmap", before: "importmap" do |app|
18
+ next unless app.config.respond_to?(:importmap)
19
+
20
+ app.config.importmap.paths << root.join("config/turbo_form_importmap.rb")
21
+ app.config.importmap.cache_sweepers << root.join("app/assets/javascripts")
22
+ end
23
+
24
+ # Propshaft puts every engine's app/assets/* on the load path by itself.
25
+ # Sprockets additionally wants the asset named, or the pin above silently
26
+ # resolves to nothing.
27
+ initializer "turbo_form.assets" do |app|
28
+ next unless app.config.respond_to?(:assets)
29
+
30
+ app.config.assets.precompile << "turbo_form.js"
31
+ end
32
+
33
+ # Deliberately eager rather than `ActiveSupport.on_load(:action_view)`: that
34
+ # hook doesn't fire until Action View is first loaded, which in an app that
35
+ # isn't eager loading is partway through rendering the first view -- late
36
+ # enough that the first form on the first request can miss the patch.
37
+ initializer "turbo_form.form_helpers" do
38
+ require "action_view"
39
+
40
+ ActionView::Helpers::FormHelper.prepend(TurboForm::FormHelper)
41
+ ActionView::Helpers::FormBuilder.prepend(TurboForm::FormBuilder)
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,90 @@
1
+ module TurboForm
2
+ # Prepended onto ActionView::Helpers::FormBuilder.
3
+ #
4
+ # f.text_field :name, dynamic_trigger: true # re-render on the default event
5
+ # f.select :category, categories, {}, dynamic_trigger: :blur
6
+ #
7
+ # `dynamic_trigger:` always has to end up as a `data-action`, but where it
8
+ # *arrives* depends on the helper. Most treat their `options` hash as the tag's
9
+ # HTML attributes; the select and date families keep those in a trailing
10
+ # `html_options` instead, which is where these overrides earn their keep.
11
+ module FormBuilder
12
+ def select(method, choices = nil, options = {}, html_options = {}, &block)
13
+ super(method, choices, *hoist_trigger(options, html_options), &block)
14
+ end
15
+
16
+ def collection_select(method, collection, value_method, text_method, options = {}, html_options = {})
17
+ super(method, collection, value_method, text_method, *hoist_trigger(options, html_options))
18
+ end
19
+
20
+ def grouped_collection_select(method, collection, group_method, group_label_method, option_key_method, option_value_method, options = {}, html_options = {})
21
+ super(method, collection, group_method, group_label_method, option_key_method, option_value_method, *hoist_trigger(options, html_options))
22
+ end
23
+
24
+ def collection_checkboxes(method, collection, value_method, text_method, options = {}, html_options = {}, &block)
25
+ super(method, collection, value_method, text_method, *hoist_trigger(options, html_options), &block)
26
+ end
27
+
28
+ def collection_radio_buttons(method, collection, value_method, text_method, options = {}, html_options = {}, &block)
29
+ super(method, collection, value_method, text_method, *hoist_trigger(options, html_options), &block)
30
+ end
31
+
32
+ def time_zone_select(method, priority_zones = nil, options = {}, html_options = {})
33
+ super(method, priority_zones, *hoist_trigger(options, html_options))
34
+ end
35
+
36
+ def weekday_select(method, options = {}, html_options = {})
37
+ super(method, *hoist_trigger(options, html_options))
38
+ end
39
+
40
+ def date_select(method, options = {}, html_options = {})
41
+ super(method, *hoist_trigger(options, html_options))
42
+ end
43
+
44
+ def time_select(method, options = {}, html_options = {})
45
+ super(method, *hoist_trigger(options, html_options))
46
+ end
47
+
48
+ def datetime_select(method, options = {}, html_options = {})
49
+ super(method, *hoist_trigger(options, html_options))
50
+ end
51
+
52
+ private
53
+ # Every other field helper -- generated and hand-written alike -- passes its
54
+ # attributes through here on the way to the tag.
55
+ def objectify_options(options)
56
+ super(absorb_trigger(options))
57
+ end
58
+
59
+ # A trailing `dynamic_trigger:` binds to `options` because Ruby folds bare
60
+ # keywords into the first optional positional hash -- but for these helpers
61
+ # that hash is the *select's* options, not the tag's. Carry it across.
62
+ # SimpleForm arrives on the other side, via `input_html:`, so take it from
63
+ # either.
64
+ def hoist_trigger(options, html_options)
65
+ return [ options, absorb_trigger(html_options) ] unless options.key?(:dynamic_trigger)
66
+
67
+ trigger = options[:dynamic_trigger]
68
+ [ options.except(:dynamic_trigger), absorb_trigger(html_options.merge(dynamic_trigger: trigger)) ]
69
+ end
70
+
71
+ def absorb_trigger(attributes)
72
+ return attributes unless attributes.key?(:dynamic_trigger)
73
+
74
+ trigger = attributes[:dynamic_trigger]
75
+ attributes = attributes.except(:dynamic_trigger)
76
+ return attributes unless trigger
77
+
78
+ data = (attributes[:data] || {}).dup
79
+ data[:action] = [ data[:action], stimulus_action_for(trigger) ].compact.join(" ")
80
+ attributes.merge(data: data)
81
+ end
82
+
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#submit" : "#{trigger}->turbo-form#submit"
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,38 @@
1
+ module TurboForm
2
+ # Prepended onto ActionView::Helpers::FormHelper.
3
+ #
4
+ # form_for @widget, dynamic: true
5
+ # form_for @widget, dynamic: "shared/refresh" # render this template instead
6
+ module FormHelper
7
+ # `form_for` ends in `form_with`, so overriding one covers both.
8
+ def form_with(model: false, scope: nil, url: nil, format: nil, **options, &block)
9
+ dynamic = options.delete(:dynamic)
10
+ return super unless dynamic
11
+
12
+ object = _object_for_form_builder(model)
13
+ raise ArgumentError, "form_for/form_with needs a :model to build a dynamic form from" unless object
14
+
15
+ scope ||= model_name_from_record_or_class(object).param_key
16
+ signature = TurboForm::Signature.new(
17
+ model_name: object.class.name,
18
+ scope: scope.to_s,
19
+ template: (dynamic unless dynamic == true)
20
+ )
21
+
22
+ wire_dynamic_form(options, signature)
23
+ super(model:, scope:, url:, format:, **options, &block)
24
+ end
25
+
26
+ private
27
+ # `form_for` funnels HTML attributes through options[:html] while `form_with`
28
+ # takes them at the top level. Write wherever the caller already is, and sit
29
+ # alongside any Stimulus controller they asked for rather than replacing it.
30
+ def wire_dynamic_form(options, signature)
31
+ attributes = options.key?(:html) ? (options[:html] ||= {}) : options
32
+ data = attributes[:data] ||= {}
33
+
34
+ data[:controller] = [ data[:controller], "turbo-form" ].compact.join(" ")
35
+ data[:turbo_form_url_value] = turbo_form_path(signature)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,43 @@
1
+ module TurboForm
2
+ # A tamper-proof description of a dynamic form, carried in the URL the browser
3
+ # posts back to.
4
+ #
5
+ # It is signed because the endpoint acts on every word of it: `model_name` gets
6
+ # constantized and instantiated, `template` gets rendered. Neither may come
7
+ # from the client unverified.
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.
12
+ class Signature
13
+ class Invalid < StandardError; end
14
+
15
+ def self.verify(token)
16
+ payload = TurboForm.verifier.verified(token.to_s)
17
+ raise Invalid, "not a signature this application generated" unless payload
18
+
19
+ new(**payload.symbolize_keys)
20
+ end
21
+
22
+ attr_reader :model_name, :scope, :template
23
+
24
+ def initialize(model_name:, scope:, template: nil)
25
+ @model_name = model_name
26
+ @scope = scope
27
+ @template = template
28
+ end
29
+
30
+ def model = model_name.constantize
31
+
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")
36
+ end
37
+
38
+ def to_s
39
+ TurboForm.verifier.generate({ model_name:, scope:, template: })
40
+ end
41
+ alias to_param to_s
42
+ end
43
+ end
@@ -0,0 +1,3 @@
1
+ module TurboForm
2
+ VERSION = "0.0.1"
3
+ end
data/lib/turbo_form.rb ADDED
@@ -0,0 +1,31 @@
1
+ require "active_support/core_ext/module/attribute_accessors"
2
+ require "turbo_form/version"
3
+ require "turbo_form/engine"
4
+
5
+ module TurboForm
6
+ # The controller the engine's endpoint inherits from. Defaulting to the host's
7
+ # own `ApplicationController` means its `before_action`s -- authentication
8
+ # above all -- apply to dynamic renders for free. Point it elsewhere when that
9
+ # inheritance brings something the endpoint shouldn't have.
10
+ mattr_accessor :parent_controller, default: "ApplicationController"
11
+
12
+ # Set to false to keep the engine out of the host's route set and draw its
13
+ # route yourself.
14
+ mattr_accessor :draw_routes, default: true
15
+
16
+ class << self
17
+ attr_writer :verifier
18
+
19
+ # Signs the description of a form so the endpoint can trust the class it is
20
+ # about to instantiate and the template it is about to render. Derived from
21
+ # the application's own key generator, the way SignedGlobalID is. `url_safe`
22
+ # so the token can live in a path segment; JSON so nothing Marshalled ever
23
+ # crosses the wire.
24
+ def verifier
25
+ @verifier ||= ActiveSupport::MessageVerifier.new(
26
+ Rails.application.key_generator.generate_key("turbo_form"),
27
+ url_safe: true, serializer: JSON
28
+ )
29
+ end
30
+ end
31
+ end
metadata ADDED
@@ -0,0 +1,113 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: turbo_form
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Andy Cohen
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: actionview
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: 7.1.0
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: 7.1.0
26
+ - !ruby/object:Gem::Dependency
27
+ name: activesupport
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 7.1.0
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: 7.1.0
40
+ - !ruby/object:Gem::Dependency
41
+ name: railties
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: 7.1.0
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: 7.1.0
54
+ - !ruby/object:Gem::Dependency
55
+ name: turbo-rails
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: 2.0.0
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: 2.0.0
68
+ description: Zero-configuration Turbo-Stream backed dynamic forms for Rails.
69
+ email:
70
+ - outlawandy@gmail.com
71
+ executables: []
72
+ extensions: []
73
+ extra_rdoc_files: []
74
+ files:
75
+ - MIT-LICENSE
76
+ - README.md
77
+ - Rakefile
78
+ - app/assets/javascripts/turbo_form.js
79
+ - app/controllers/turbo_form/dynamic_forms_controller.rb
80
+ - config/routes.rb
81
+ - config/turbo_form_importmap.rb
82
+ - lib/tasks/turbo_form_tasks.rake
83
+ - lib/turbo_form.rb
84
+ - lib/turbo_form/engine.rb
85
+ - lib/turbo_form/form_builder.rb
86
+ - lib/turbo_form/form_helper.rb
87
+ - lib/turbo_form/signature.rb
88
+ - lib/turbo_form/version.rb
89
+ homepage: https://github.com/outlawandy/turbo_form
90
+ licenses:
91
+ - MIT
92
+ metadata:
93
+ homepage_uri: https://github.com/outlawandy/turbo_form
94
+ source_code_uri: https://github.com/outlawandy/turbo_form
95
+ changelog_uri: https://github.com/outlawandy/turbo_form/blob/main/CHANGELOG.md
96
+ rdoc_options: []
97
+ require_paths:
98
+ - lib
99
+ required_ruby_version: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '3.1'
104
+ required_rubygems_version: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ version: '0'
109
+ requirements: []
110
+ rubygems_version: 4.0.20
111
+ specification_version: 4
112
+ summary: Turbo-Stream backed dynamic forms, by convention.
113
+ test_files: []