turbo_reflex 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,28 +1,483 @@
1
- # TurboReflex
2
- Short description and motivation.
1
+ <p align="center">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="https://ik.imagekit.io/hopsoft/turbo-reflex-logo-light_2cG9cdQd1.webp?ik-sdk-version=javascript-1.4.3&updatedAt=1663075749336">
4
+ <img width="320" src="https://ik.imagekit.io/hopsoft/turbo-reflex-logo-dark_kSmo1eDLm.webp?ik-sdk-version=javascript-1.4.3&updatedAt=1663075749241" />
5
+ </picture>
6
+ <h1 align="center">
7
+ Welcome to TurboReflex 👋<br />
8
+ </h1>
9
+ <p align="center">
10
+ <a href="http://blog.codinghorror.com/the-best-code-is-no-code-at-all/">
11
+ <img alt="Lines of Code" src="https://img.shields.io/badge/loc-400-47d299.svg" />
12
+ </a>
13
+ <a href="https://codeclimate.com/github/hopsoft/turbo_reflex/maintainability">
14
+ <img src="https://api.codeclimate.com/v1/badges/fe1162a742fe83a4fdfd/maintainability" />
15
+ </a>
16
+ <a href="https://rubygems.org/gems/turbo_reflex">
17
+ <img alt="GEM Version" src="https://img.shields.io/gem/v/turbo_reflex?color=168AFE&include_prereleases&logo=ruby&logoColor=FE1616">
18
+ </a>
19
+ <a href="https://rubygems.org/gems/turbo_reflex">
20
+ <img alt="GEM Downloads" src="https://img.shields.io/gem/dt/turbo_reflex?color=168AFE&logo=ruby&logoColor=FE1616">
21
+ </a>
22
+ <a href="https://github.com/testdouble/standard">
23
+ <img alt="Ruby Style" src="https://img.shields.io/badge/style-standard-168AFE?logo=ruby&logoColor=FE1616" />
24
+ </a>
25
+ <a href="https://www.npmjs.com/package/turbo_reflex">
26
+ <img alt="NPM Version" src="https://img.shields.io/npm/v/turbo_reflex?color=168AFE&logo=npm">
27
+ </a>
28
+ <a href="https://www.npmjs.com/package/turbo_reflex">
29
+ <img alt="NPM Downloads" src="https://img.shields.io/npm/dm/turbo_reflex?color=168AFE&logo=npm">
30
+ </a>
31
+ <a href="https://bundlephobia.com/package/turbo_reflex@">
32
+ <img alt="NPM Bundle Size" src="https://img.shields.io/bundlephobia/minzip/turbo_reflex?label=bundle%20size&logo=npm&color=47d299">
33
+ </a>
34
+ <a href="https://github.com/sheerun/prettier-standard">
35
+ <img alt="JavaScript Style" src="https://img.shields.io/badge/style-prettier--standard-168AFE?logo=javascript&logoColor=f4e137" />
36
+ </a>
37
+ <a href="https://github.com/hopsoft/turbo_reflex/actions/workflows/tests.yml">
38
+ <img alt="Tests" src="https://github.com/hopsoft/turbo_reflex/actions/workflows/tests.yml/badge.svg" />
39
+ </a>
40
+ <a href="https://twitter.com/hopsoft">
41
+ <img alt="Twitter Follow" src="https://img.shields.io/twitter/follow/hopsoft?logo=twitter&style=social">
42
+ </a>
43
+ </p>
44
+ </p>
45
+
46
+ #### TurboReflex enhances the [reactive programming](https://en.wikipedia.org/wiki/Reactive_programming) model for [Turbo Frames](https://turbo.hotwired.dev/reference/frames).
47
+
48
+ <!-- Tocer[start]: Auto-generated, don't remove. -->
49
+
50
+ ## Table of Contents
51
+
52
+ - [Why TurboReflex?](#why-turboreflex)
53
+ - [Sponsors](#sponsors)
54
+ - [Dependencies](#dependencies)
55
+ - [Setup](#setup)
56
+ - [Usage](#usage)
57
+ - [Reflex Triggers](#reflex-triggers)
58
+ - [Lifecycle Events](#lifecycle-events)
59
+ - [Targeting Frames](#targeting-frames)
60
+ - [Working with Forms](#working-with-forms)
61
+ - [Server Side Reflexes](#server-side-reflexes)
62
+ - [Appending Turbo Streams](#appending-turbo-streams)
63
+ - [Setting Instance Variables](#setting-instance-variables)
64
+ - [Hijacking the Response](#hijacking-the-response)
65
+ - [Broadcasting Turbo Streams](#broadcasting-turbo-streams)
66
+ - [Putting it All Together](#putting-it-all-together)
67
+ - [License](#license)
68
+ - [Todos](#todos)
69
+ - [Releasing](#releasing)
70
+
71
+ <!-- Tocer[finish]: Auto-generated, don't remove. -->
72
+
73
+ ## Why TurboReflex?
74
+
75
+ [Turbo Frames](https://turbo.hotwired.dev/reference/frames) are a terrific technology that can help you build modern reactive web applications.
76
+ They are similar to [iframes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe) in that they focus on features like
77
+ discrete isolated content, browser history, and scoped navigation... *with the caveat that they share their parent's DOM tree.*
78
+
79
+ **TurboReflex** extends Turbo Frames and adds support for client triggered reflexes [*(think RPC)*](https://en.wikipedia.org/wiki/Remote_procedure_call).
80
+ Reflexes let you *sprinkle* ✨ in functionality and skip the ceremony of typical [REST](https://en.wikipedia.org/wiki/Representational_state_transfer) boilerplate *(routes, controllers, actions, etc...)*.
81
+ Reflexes are great for features that ride atop RESTful resources. Things like making selections, toggling switches, adding filters, etc...
82
+ **Basically any feature where you've been tempted to create a non-RESTful action in a controller.**
83
+
84
+ Reflexes improve the developer experience (DX) of creating modern reactive applications.
85
+ They share the same mental model as React and other client side frameworks.
86
+
87
+ 1. **Trigger an event**
88
+ 2. **Change state**
89
+ 3. **(Re)render to reflect the new state**
90
+ 4. *repeat...*
91
+
92
+ *The primary distinction being that __state is wholly managed by the server__.*
93
+
94
+ TurboReflex is a lightweight Turbo Frame extension... which means that reactivity runs over HTTP.
95
+ **Web sockets are NOT used for the reactive critical path!** 🎉
96
+
97
+ ## Sponsors
98
+
99
+ <p align="center">
100
+ <em>Proudly sponsored by</em>
101
+ </p>
102
+ <p align="center">
103
+ <a href="https://www.clickfunnels.com?utm_source=hopsoft&utm_medium=open-source&utm_campaign=turbo_reflex">
104
+ <img src="https://images.clickfunnel.com/uploads/digital_asset/file/176632/clickfunnels-dark-logo.svg" width="575" />
105
+ </a>
106
+ </p>
107
+
108
+ ## Dependencies
109
+
110
+ - [rails](https://rubygems.org/gems/rails) `>=6.1`
111
+ - [turbo-rails](https://rubygems.org/gems/turbo-rails) `>=1.1`
112
+ - [@hotwired/turbo-rails](https://yarnpkg.com/package/@hotwired/turbo-rails) `>=7.1`
113
+
114
+ ## Setup
115
+
116
+ 1. Add the TurboReflex dependencies
117
+
118
+ ```diff
119
+ # Gemfile
120
+ +gem "turbo_reflex", "~> 0.0.2"
121
+ ```
122
+
123
+ ```diff
124
+ # package.json
125
+ "dependencies": {
126
+ "@hotwired/turbo-rails": ">=7.1",
127
+ + "turbo_reflex": "^0.0.2"
128
+ ```
129
+
130
+ *Be sure to install the __same version__ of the Ruby and JavaScript libraries.*
131
+
132
+ 2. Import TurboReflex in your JavaScript app
133
+
134
+ ```diff
135
+ # app/javascript/application.js
136
+ +import 'turbo_reflex'
137
+ ```
138
+
139
+ 2. Add TurboReflex behavior to the Rails app
140
+
141
+ ```diff
142
+ # app/views/layouts/application.html.erb
143
+ <html>
144
+ <head>
145
+ ...
146
+ + <%= turbo_reflex_meta_tag %>
147
+ ...
148
+ ```
149
+
150
+ ```diff
151
+ # /app/controllers/application_controller.rb
152
+ class ApplicationController < ActionController::Base
153
+ + include TurboReflex::Controller
154
+ end
155
+ ```
3
156
 
4
157
  ## Usage
5
- How to use my plugin.
6
158
 
7
- ## Installation
8
- Add this line to your application's Gemfile:
159
+ This example illustrates how to use TurboReflex to manage upvotes on a Post.
160
+
161
+ 1. **Trigger an event** - *register an element to listen for events that trigger reflexes*
162
+
163
+ ```erb
164
+ <!-- app/views/posts/show.html.erb -->
165
+ <%= turbo_frame_tag dom_id(@post) do %>
166
+ <a href="#" data-turbo-reflex="VotesReflex#upvote">Upvote</a>
167
+ Upvote Count: <%= @post.votes >
168
+ <% end %>
169
+ ```
170
+
171
+ 2. **Change state** - *create a server side reflex that modifies state*
172
+
173
+ ```ruby
174
+ # app/reflexes/posts_reflex.rb
175
+ class PostsReflex < TurboReflex::Base
176
+ def upvote
177
+ Post.find(controller.params[:id]).increment! :votes
178
+ end
179
+ end
180
+ ```
181
+
182
+ 3. **(Re)render to reflect the new state** - *normal Rails / Turbo Frame behavior runs and (re)renders the frame*
183
+
184
+ ### Reflex Triggers
185
+
186
+ TurboReady uses [event delegation](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events#event_delegation) to capture events that can trigger reflexes.
187
+
188
+ Here is the list of default events and respective elements that TurboReflex monitors.
189
+
190
+ - **`change`** - `<input>`, `<select>`, `<textarea>`
191
+ - **`submit`** - `<form>`
192
+ - **`click`** - `*` *all other elements*
193
+
194
+ It's possible to override these defaults like so.
195
+
196
+ ```js
197
+ import TurboReflex from 'turbo_reflex'
198
+
199
+ // restrict `click` monitoring to <a> and <button> elements
200
+ TurboReflex.registerEvent('click', ['a', 'button'])
201
+ ```
202
+
203
+ You can also register custom events and elements.
204
+ Here's an example that sets up monitoring for the `sl-change` event on the `sl-switch` element from the [Shoelace web component library](https://shoelace.style/).
205
+
206
+ ```js
207
+ TurboReflex.registerEvent('sl-change', ['sl-switch'])
208
+ ```
209
+
210
+ ### Lifecycle Events
211
+
212
+ TurboReflex supports the following lifecycle events.
213
+
214
+ - `turbo-reflex:before-start` - fires before reflex processing starts
215
+ - `turbo-reflex:start` - fires before the reflex is sent to the server
216
+ - `turbo-reflex:finish` - fires after the server has processed the reflex and responded
217
+ - `turbo-reflex:missing-frame-id` - fires if the reflex cannot determine the target frame id
218
+ - `turbo-reflex:missing-frame` - fires if the the reflex cannot locate the frame element
219
+ - `turbo-reflex:missing-frame-src` - fires if the reflex cannot determine the frame's `src`
220
+ - `turbo-reflex:error` - fires if an unexpected error occurs
221
+
222
+ ### Targeting Frames
223
+
224
+ By default TurboReflex targets the [`closest`](https://developer.mozilla.org/en-US/docs/Web/API/Element/closest) `<turbo-frame>` element,
225
+ but you can also explicitly target other frames.
226
+
227
+ 1. Look for `data-turbo-reflex-frame` on the reflex elemnt
228
+
229
+ ```erb
230
+ <input type="checkbox"
231
+ data-turbo-reflex="ExampleReflex#work"
232
+ data-turbo-reflex-frame="some-frame-id">
233
+ ```
234
+
235
+ 2. Look for `data-turbo-frame` on the reflex element
236
+
237
+ ```erb
238
+ <input type="checkbox"
239
+ data-turbo-reflex="ExampleReflex#work"
240
+ data-turbo-frame="some-frame-id">
241
+ ```
242
+
243
+ 3. Find the closest `<turbo-frame>` to the reflex element
244
+
245
+ ```erb
246
+ <turbo-frame id="example-frame">
247
+ <input type="checkbox" data-turbo-reflex="ExampleReflex#work">
248
+ </turbo-frame>
249
+ ```
250
+
251
+ ### Working with Forms
252
+
253
+ TurboReflex works great with Rails forms.
254
+ Just specify the `data-turbo-reflex` attribute on the form.
255
+
256
+ ```erb
257
+ # app/views/posts/post.html.erb
258
+ <%= turbo_frame_tag dom_id(@post) do %>
259
+ <%= form_with model: @post, html: { turbo_reflex: "ExampleReflex#work" } do |form| %>
260
+ ...
261
+ <% end %>
262
+ <% end %>
263
+
264
+ <%= turbo_frame_tag dom_id(@post) do %>
265
+ <%= form_for @post, remote: true, html: { turbo_reflex: "ExampleReflex#work" } do |form| %>
266
+ ...
267
+ <% end %>
268
+ <% end %>
269
+
270
+ <%= form_with model: @post,
271
+ html: { turbo_frame: dom_id(@post), turbo_reflex: "ExampleReflex#work" } do |form| %>
272
+ ...
273
+ <% end %>
274
+ ```
275
+
276
+ ### Server Side Reflexes
277
+
278
+ The client side DOM attribute `data-turbo-reflex` is indicates what reflex *(Ruby class and method)* to invoke.
279
+ The attribute value is specified with RDoc notation. i.e. `ClassName#method_name`
280
+
281
+ Here's an example.
282
+
283
+ ```erb
284
+ <a data-turbo-reflex="DemoReflex#example">
285
+ ```
286
+
287
+ Server side reflexes can live anywhere in your app; however, we recommend you keep them in the `app` directory.
288
+
289
+ ```diff
290
+ |- app
291
+ | |...
292
+ | |- models
293
+ +| |- reflexes
294
+ | |- views
295
+ ```
296
+
297
+ Reflexes are simple Ruby classes that inherit from `TurboReflex::Base`.
298
+ They expose the following instance methods and properties.
299
+
300
+ - `element` - a struct that represents the DOM element that triggered the reflex
301
+ - `controller` - the Rails controller processing the HTTP request
302
+ - `turbo_stream` - a Turbo Stream [`TagBuilder`](https://github.com/hotwired/turbo-rails/blob/main/app/models/turbo/streams/tag_builder.rb)
303
+ - `turbo_streams` - a list of Turbo Streams to append to the response
9
304
 
10
305
  ```ruby
11
- gem "turbo_reflex"
306
+ # app/reflexes/demo_reflex.rb
307
+ class DemoReflex < TurboReflex::Base
308
+ # The reflex method is invoked by an ActionController before filter.
309
+ # Standard Rails behavior takes over after the reflex method completes.
310
+ def example
311
+ # - execute business logic
312
+ # - update state
313
+ # - append additional Turbo Streams
314
+ end
315
+ end
12
316
  ```
13
317
 
14
- And then execute:
15
- ```bash
16
- $ bundle
318
+ ### Appending Turbo Streams
319
+
320
+ It's possible to append additional Turbo Streams to the response in a reflex.
321
+ Appended streams are added to the response body **after** the Rails controller action has completed and rendered the view template.
322
+
323
+ ```ruby
324
+ # app/reflexes/demo_reflex.rb
325
+ class DemoReflex < TurboReflex::Base
326
+ def example
327
+ # logic...
328
+ turbo_streams << turbo_stream.append("dom_id", "CONTENT")
329
+ turbo_streams << turbo_stream.prepend("dom_id", "CONTENT")
330
+ turbo_streams << turbo_stream.replace("dom_id", "CONTENT")
331
+ turbo_streams << turbo_stream.update("dom_id", "CONTENT")
332
+ turbo_streams << turbo_stream.remove("dom_id")
333
+ turbo_streams << turbo_stream.before("dom_id", "CONTENT")
334
+ turbo_streams << turbo_stream.after("dom_id", "CONTENT")
335
+ turbo_streams << turbo_stream.invoke("console.log", args: ["Whoa! 🤯"])
336
+ end
337
+ end
17
338
  ```
18
339
 
19
- Or install it yourself as:
20
- ```bash
21
- $ gem install turbo_reflex
340
+ *This proves especially powerful when paired with [TurboReady](https://github.com/hopsoft/turbo_ready).*
341
+
342
+ > 📘 **NOTE:** `turbo_stream.invoke` is a [TurboReady](https://github.com/hopsoft/turbo_ready#usage) feature.
343
+
344
+ ### Setting Instance Variables
345
+
346
+ It can be useful to set instance variables on the Rails controller from the reflex.
347
+
348
+ Here's an example that shows how to do this.
349
+
350
+ ```erb
351
+ <!-- app/views/posts/index.html.erb -->
352
+ <%= turbo_frame_tag dom_id(@posts) do %>
353
+ <%= check_box_tag :all, :all, @all, data: { turbo_reflex: "PostsReflex#toggle_all" } %>
354
+ View All
355
+
356
+ <% @posts.each do |post| %>
357
+ ...
358
+ <% end %>
359
+ <% end %>
360
+ ```
361
+
362
+ ```ruby
363
+ # app/reflexes/posts_reflex.rb
364
+ class PostsReflex < TurboReflex::Reflex
365
+ def toggle_all
366
+ posts = element.checked ? Post.all : Post.unread
367
+ controller.instance_variable_set(:@all, element.checked)
368
+ controller.instance_variable_set(:@posts, posts)
369
+ end
370
+ end
22
371
  ```
23
372
 
24
- ## Contributing
25
- Contribution directions go here.
373
+ ```ruby
374
+ # app/controllers/posts_controller.rb
375
+ class PostsController < ApplicationController
376
+ def index
377
+ @posts ||= Post.unread
378
+ end
379
+ end
380
+ ```
381
+
382
+ ### Hijacking the Response
383
+
384
+ Sometimes you may want to hijack the normal Rails response from within a reflex.
385
+
386
+ For example, consider the need for a related but separate form that updates a subset of user attributes.
387
+ We'd like to avoid creating a non RESTful route,
388
+ but aren't thrilled at the prospect of adding REST boilerplate for a new route, controller, action, etc...
389
+
390
+ In that scenario we can reuse an existing route and hijack the response handling with a reflex.
391
+
392
+ Here's how to do it.
393
+
394
+ ```erb
395
+ <!-- app/views/users/show.html.erb -->
396
+ <%= turbo_frame_tag "user-alt" do %>
397
+ <%= form_with model: @user, data: { turbo_reflex: "UserReflex#example" } do |form| %>
398
+ ...
399
+ <% end %>
400
+ <% end %>
401
+ ```
402
+
403
+ The form above will send a `PATCH` request to `users#update`,
404
+ but we'll hijack the handling in the reflex so we never hit `users#update`.
405
+
406
+ ```ruby
407
+ # app/reflexes/user_reflex.html.erb
408
+ class UserReflex < TurboReflex::Base
409
+ def example
410
+ # business logic, save record, etc...
411
+ controller.render html: "<turbo-frame id='user-alt'>We Hijacked the response!</turbo-frame>".html_safe
412
+ end
413
+ end
414
+ ```
415
+
416
+ Remember that reflexes are invoked by a controller [before filter](https://guides.rubyonrails.org/action_controller_overview.html#filters).
417
+ That means rendering from inside a reflex halts the standard request cycle.
418
+
419
+ ### Broadcasting Turbo Streams
420
+
421
+ You can also broadcast Turbo Streams to subscribed users from a reflex.
422
+
423
+ ```ruby
424
+ # app/reflexes/demo_reflex.rb
425
+ class DemoReflex < TurboReflex::Base
426
+ def example
427
+ # logic...
428
+ Turbo::StreamsChannel
429
+ .broadcast_invoke_later_to "some-subscription", "console.log", args: ["Whoa! 🤯"]
430
+ end
431
+ end
432
+ ```
433
+
434
+ *Learn more about Turbo Stream broadcasting by reading through the
435
+ [hotwired/turbo-rails](https://github.com/hotwired/turbo-rails/blob/main/app/models/concerns/turbo/broadcastable.rb) source code.*
436
+
437
+ > 📘 **NOTE:** `broadcast_invoke_later_to` is a [TurboReady](https://github.com/hopsoft/turbo_ready#broadcasting) feature.
438
+
439
+ ### Putting it All Together
440
+
441
+ The best way to learn this stuff is from working examples.
442
+ Be sure to clone the library and run the test application.
443
+ Then dig into the internals.
444
+
445
+ ```sh
446
+ git clone https://github.com/hopsoft/turbo_reflex.git
447
+ cd turbo_reflex
448
+ bundle
449
+ cd test/dummy
450
+ bin/rails s
451
+ # View the app in a browser at http://localhost:3000
452
+ ```
453
+
454
+ You can review the implementation in [`test/dummy/app`](https://github.com/hopsoft/turbo_reflex/tree/main/test/dummy).
455
+ *Feel free to add some demos and submit a pull request while you're in there.*
456
+
457
+ ![TurboReflex Demos](https://ik.imagekit.io/hopsoft/turbo-reflex-demos_EP54JuWt5.webp?ik-sdk-version=javascript-1.4.3&updatedAt=1663083040904)
26
458
 
27
459
  ## License
460
+
28
461
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
462
+
463
+ ## Todos
464
+
465
+ - [ ] Add tests for lifecycle events
466
+ - [ ] Add tests for select elements
467
+ - [ ] Add tests for checkbox elements
468
+ - [ ] Add controller tests
469
+ - [ ] Add tests for all variants of frame targeting
470
+
471
+
472
+ ## Releasing
473
+
474
+ 1. Run `yarn upgrade` and `bundle update` to pick up the latest
475
+ 1. Bump version number at `lib/turbo_reflex/version.rb`. Pre-release versions use `.preN`
476
+ 1. Run `bin/standardize`
477
+ 1. Run `rake build` and `yarn build`
478
+ 1. Commit and push changes to GitHub
479
+ 1. Run `rake release`
480
+ 1. Run `yarn publish --no-git-tag-version`
481
+ 1. Yarn will prompt you for the new version. Pre-release versions use `-preN`
482
+ 1. Commit and push any changes to GitHub
483
+ 1. Create a new release on GitHub ([here](https://github.com/hopsoft/turbo_reflex/releases)) and generate the changelog for the stable release for it
data/Rakefile CHANGED
@@ -1,3 +1,17 @@
1
- require "bundler/setup"
1
+ # frozen_string_literal: true
2
2
 
3
+ require "bundler/setup"
3
4
  require "bundler/gem_tasks"
5
+ require "rake/testtask"
6
+
7
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
8
+ load "rails/tasks/engine.rake"
9
+ load "rails/tasks/statistics.rake"
10
+
11
+ Rake::TestTask.new do |test|
12
+ test.libs << "test"
13
+ test.test_files = FileList["test/**/*_test.rb"]
14
+ test.warning = false
15
+ end
16
+
17
+ task default: :test
@@ -0,0 +1,2 @@
1
+ var l={beforeStart:"turbo-reflex:before-start",start:"turbo-reflex:start",finish:"turbo-reflex:finish",error:"turbo-reflex:error",missingFrameId:"turbo-reflex:missing-frame-id",missingFrame:"turbo-reflex:missing-frame",missingFrameSrc:"turbo-reflex:missing-frame-src"};function y(t,e=document,r={}){let n=new CustomEvent(t,{detail:r,cancelable:!0,bubbles:!0});e.dispatchEvent(n)}function F(){Object.values(l).forEach(t=>console.log(t))}var o={...l,dispatch:y,logEventNames:F};var u={};addEventListener("turbo:before-fetch-response",t=>{let e=t.target;u[e.id]=e.src;let{turboReflexActive:r,turboReflexElementId:n}=e.dataset;if(!r)return;let s=document.getElementById(n);delete e.dataset.turboReflexActive,delete e.dataset.turboReflexElementId,o.dispatch(o.finish,s||document,{frame:e,element:s||"Unknown! Missing id attribute."})});addEventListener("turbo:frame-load",t=>{let e=t.target;e.dataset.turboReflexSrc=u[e.id]||e.src||e.dataset.turboReflexSrc,delete u[e.id]});var L={get token(){return document.getElementById("turbo-reflex-token").getAttribute("content")}},c=L;function m(t){return t.closest("[data-turbo-reflex]")}function S(t){return t.closest("turbo-frame")}function b(t){let e=t.dataset.turboReflexFrame||t.dataset.turboFrame;if(!e){let r=S(t);r&&(e=r.id)}return e||(console.error("The reflex element does not specify a frame!","Please move the reflex element inside a <turbo-frame> or set the 'data-turbo-reflex-frame' or 'data-turbo-frame' attribute.",t),o.dispatch(o.missingFrameId,t,{element:t})),e}function g(t){let e=document.getElementById(t);return e||(console.error(`The frame '${t}' does not exist!`),o.dispatch(o.missingFrame,document,{id:t})),e}function x(t){let e=t.dataset.turboReflexSrc||t.src;return e||(console.error(`The the 'src' for <turbo-frame id='${t.id}'> is unknown!`,"TurboReflex uses 'src' to (re)render frame content after the reflex is invoked.","Please set the 'src' or 'data-turbo-reflex-src' attribute on the <turbo-frame> element.",t),o.dispatch(o.missingFrameSrc,t,{frame:t})),e}function k(t,e={}){if(t.tagName.toLowerCase()!=="select")return e.value=t.value;if(!t.multiple)return e.value=t.options[t.selectedIndex].value;e.values=Array.from(t.options).reduce((r,n)=>(n.selected&&r.push(n.value),r),[])}function v(t){let e=Array.from(t.attributes).reduce((r,n)=>(r[n.name]=n.value,r),{});return e.tag=t.tagName,e.checked=t.checked,e.disabled=t.disabled,k(t,e),e}var a={},h;function p(t){h=t}function f(t,e){a[t]=e,document.addEventListener(t,h,!0)}function E(t,e){return e=e.toLowerCase(),a[t].includes(e)||!Object.values(a).flat().includes(e)&&a[t].includes("*")}function R(){console.log(a)}addEventListener("turbo:before-fetch-request",t=>{let e=t.target,{turboReflexActive:r}=e.dataset;if(!r)return;let{fetchOptions:n}=t.detail;n.headers["Turbo-Reflex"]=c.token});function I(t){let e=document.createElement("a");return e.href=t,new URL(e)}function A(t,e={}){e.token=c.token;let r=document.createElement("input");r.type="hidden",r.name="turbo_reflex",r.value=JSON.stringify(e),t.appendChild(r)}function C(t){let e,r,n,s;try{if(e=m(t.target),!e||!E(t.type,e.tagName)||(o.dispatch(o.beforeStart,e,{element:e}),r=b(e),!r)||(n=g(r),!n)||(s=x(n),!s))return;let i={frameId:r,element:v(e)};if(o.dispatch(o.start,e,{element:e,frameId:r,frame:n,frameSrc:s,payload:i}),n.dataset.turboReflexActive=!0,n.dataset.turboReflexElementId=e.id,e.tagName.toLowerCase()==="form")return A(e,i);t.preventDefault();let d=I(s);d.searchParams.set("turbo_reflex",JSON.stringify(i)),n.src=d.toString()}catch(i){console.error("TurboReflex encountered an unexpected error!",{element:e,frameId:r,frame:n,frameSrc:s,target:t.target},i),o.dispatch(o.error,e||document,{element:e,frameId:r,frame:n,frameSrc:s,error:i})}}p(C);f("change",["input","select","textarea"]);f("submit",["form"]);f("click",["*"]);var M={registerEvent:f,logRegisteredEvents:R,logLifecycleEventNames:o.logEventNames};export{M as default};
2
+ //# sourceMappingURL=turbo_reflex.min.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../javascript/lifecycle_events.js", "../../javascript/frame_sources.js", "../../javascript/security.js", "../../javascript/elements.js", "../../javascript/event_registry.js", "../../javascript/turbo_reflex.js"],
4
+ "sourcesContent": ["const events = {\n beforeStart: 'turbo-reflex:before-start',\n start: 'turbo-reflex:start',\n finish: 'turbo-reflex:finish',\n error: 'turbo-reflex:error',\n missingFrameId: 'turbo-reflex:missing-frame-id',\n missingFrame: 'turbo-reflex:missing-frame',\n missingFrameSrc: 'turbo-reflex:missing-frame-src'\n}\n\nfunction dispatch (name, target = document, detail = {}) {\n const event = new CustomEvent(name, {\n detail,\n cancelable: true,\n bubbles: true\n })\n target.dispatchEvent(event)\n}\n\nfunction logEventNames () {\n Object.values(events).forEach(name => console.log(name))\n}\n\nexport default { ...events, dispatch, logEventNames }\n", "import LifecycleEvents from './lifecycle_events'\nconst frameSources = {}\n\n// fires after receiving a turbo HTTP response\naddEventListener('turbo:before-fetch-response', event => {\n const frame = event.target\n frameSources[frame.id] = frame.src\n\n const { turboReflexActive, turboReflexElementId } = frame.dataset\n if (!turboReflexActive) return\n\n const element = document.getElementById(turboReflexElementId)\n delete frame.dataset.turboReflexActive\n delete frame.dataset.turboReflexElementId\n\n LifecycleEvents.dispatch(LifecycleEvents.finish, element || document, {\n frame,\n element: element || 'Unknown! Missing id attribute.'\n })\n})\n\n// fires when a frame element is navigated and finishes loading\naddEventListener('turbo:frame-load', event => {\n const frame = event.target\n frame.dataset.turboReflexSrc =\n frameSources[frame.id] || frame.src || frame.dataset.turboReflexSrc\n delete frameSources[frame.id]\n})\n", "const Security = {\n get token () {\n return document.getElementById('turbo-reflex-token').getAttribute('content')\n }\n}\n\nexport default Security\n", "import LifecycleEvents from './lifecycle_events'\n\nfunction findClosestReflex (element) {\n return element.closest('[data-turbo-reflex]')\n}\n\nfunction findClosestFrame (element) {\n return element.closest('turbo-frame')\n}\n\nfunction findFrameId (element) {\n let id = element.dataset.turboReflexFrame || element.dataset.turboFrame\n if (!id) {\n const frame = findClosestFrame(element)\n if (frame) id = frame.id\n }\n if (!id) {\n console.error(\n `The reflex element does not specify a frame!`,\n `Please move the reflex element inside a <turbo-frame> or set the 'data-turbo-reflex-frame' or 'data-turbo-frame' attribute.`,\n element\n )\n LifecycleEvents.dispatch(LifecycleEvents.missingFrameId, element, {\n element\n })\n }\n return id\n}\n\nfunction findFrame (id) {\n const frame = document.getElementById(id)\n if (!frame) {\n console.error(`The frame '${id}' does not exist!`)\n LifecycleEvents.dispatch(LifecycleEvents.missingFrame, document, { id })\n }\n return frame\n}\n\nfunction findFrameSrc (frame) {\n const frameSrc = frame.dataset.turboReflexSrc || frame.src\n if (!frameSrc) {\n console.error(\n `The the 'src' for <turbo-frame id='${frame.id}'> is unknown!`,\n `TurboReflex uses 'src' to (re)render frame content after the reflex is invoked.`,\n `Please set the 'src' or 'data-turbo-reflex-src' attribute on the <turbo-frame> element.`,\n frame\n )\n LifecycleEvents.dispatch(LifecycleEvents.missingFrameSrc, frame, { frame })\n }\n return frameSrc\n}\n\nfunction assignElementValueToPayload (element, payload = {}) {\n if (element.tagName.toLowerCase() !== 'select')\n return (payload.value = element.value)\n\n if (!element.multiple)\n return (payload.value = element.options[element.selectedIndex].value)\n\n payload.values = Array.from(element.options).reduce((memo, option) => {\n if (option.selected) memo.push(option.value)\n return memo\n }, [])\n}\n\nfunction buildAttributePayload (element) {\n const payload = Array.from(element.attributes).reduce((memo, attr) => {\n memo[attr.name] = attr.value\n return memo\n }, {})\n\n payload.tag = element.tagName\n payload.checked = element.checked\n payload.disabled = element.disabled\n assignElementValueToPayload(element, payload)\n\n return payload\n}\n\nexport {\n findClosestReflex,\n findClosestFrame,\n findFrameId,\n findFrame,\n findFrameSrc,\n buildAttributePayload\n}\n", "const registeredEvents = {}\nlet eventListener\n\nfunction registerEventListener (fn) {\n eventListener = fn\n}\n\nfunction registerEvent (eventName, tagNames) {\n registeredEvents[eventName] = tagNames\n document.addEventListener(eventName, eventListener, true)\n}\n\nfunction isRegisteredEvent (eventName, tagName) {\n tagName = tagName.toLowerCase()\n return (\n registeredEvents[eventName].includes(tagName) ||\n (!Object.values(registeredEvents)\n .flat()\n .includes(tagName) &&\n registeredEvents[eventName].includes('*'))\n )\n}\n\nfunction logRegisteredEvents () {\n console.log(registeredEvents)\n}\n\nexport {\n registerEventListener,\n registerEvent,\n registeredEvents,\n isRegisteredEvent,\n logRegisteredEvents\n}\n", "import './frame_sources'\nimport Security from './security'\nimport LifecycleEvents from './lifecycle_events'\nimport {\n findClosestReflex,\n findClosestFrame,\n findFrameId,\n findFrame,\n findFrameSrc,\n buildAttributePayload\n} from './elements'\nimport {\n registerEventListener,\n registerEvent,\n registeredEvents,\n isRegisteredEvent,\n logRegisteredEvents\n} from './event_registry'\n\n// fires before making a turbo HTTP request\naddEventListener('turbo:before-fetch-request', event => {\n const frame = event.target\n const { turboReflexActive } = frame.dataset\n if (!turboReflexActive) return\n const { fetchOptions } = event.detail\n fetchOptions.headers['Turbo-Reflex'] = Security.token\n})\n\nfunction buildURL (urlString) {\n const a = document.createElement('a')\n a.href = urlString\n return new URL(a)\n}\n\nfunction invokeFormReflex (form, payload = {}) {\n payload.token = Security.token\n const input = document.createElement('input')\n input.type = 'hidden'\n input.name = 'turbo_reflex'\n input.value = JSON.stringify(payload)\n form.appendChild(input)\n}\n\nfunction invokeReflex (event) {\n let element, frameId, frame, frameSrc\n try {\n element = findClosestReflex(event.target)\n if (!element) return\n\n if (!isRegisteredEvent(event.type, element.tagName)) return\n\n LifecycleEvents.dispatch(LifecycleEvents.beforeStart, element, { element })\n\n frameId = findFrameId(element)\n if (!frameId) return\n\n frame = findFrame(frameId)\n if (!frame) return\n\n frameSrc = findFrameSrc(frame)\n if (!frameSrc) return\n\n const payload = {\n frameId: frameId,\n element: buildAttributePayload(element)\n }\n\n LifecycleEvents.dispatch(LifecycleEvents.start, element, {\n element,\n frameId,\n frame,\n frameSrc,\n payload\n })\n frame.dataset.turboReflexActive = true\n frame.dataset.turboReflexElementId = element.id\n\n if (element.tagName.toLowerCase() === 'form')\n return invokeFormReflex(element, payload)\n\n event.preventDefault()\n const frameURL = buildURL(frameSrc)\n frameURL.searchParams.set('turbo_reflex', JSON.stringify(payload))\n frame.src = frameURL.toString()\n } catch (error) {\n console.error(\n `TurboReflex encountered an unexpected error!`,\n { element, frameId, frame, frameSrc, target: event.target },\n error\n )\n LifecycleEvents.dispatch(LifecycleEvents.error, element || document, {\n element,\n frameId,\n frame,\n frameSrc,\n error\n })\n }\n}\n\n// wire things up and setup default events\nregisterEventListener(invokeReflex)\nregisterEvent('change', ['input', 'select', 'textarea'])\nregisterEvent('submit', ['form'])\nregisterEvent('click', ['*'])\n\nexport default {\n registerEvent,\n logRegisteredEvents,\n logLifecycleEventNames: LifecycleEvents.logEventNames\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["events", "dispatch", "name", "target", "detail", "event", "logEventNames", "lifecycle_events_default", "frameSources", "event", "frame", "turboReflexActive", "turboReflexElementId", "element", "lifecycle_events_default", "Security", "security_default", "findClosestReflex", "element", "findClosestFrame", "findFrameId", "id", "frame", "lifecycle_events_default", "findFrame", "findFrameSrc", "frameSrc", "assignElementValueToPayload", "payload", "memo", "option", "buildAttributePayload", "attr", "registeredEvents", "eventListener", "registerEventListener", "fn", "registerEvent", "eventName", "tagNames", "isRegisteredEvent", "tagName", "logRegisteredEvents", "event", "frame", "turboReflexActive", "fetchOptions", "security_default", "buildURL", "urlString", "a", "invokeFormReflex", "form", "payload", "input", "invokeReflex", "element", "frameId", "frameSrc", "findClosestReflex", "isRegisteredEvent", "lifecycle_events_default", "findFrameId", "findFrame", "findFrameSrc", "buildAttributePayload", "frameURL", "error", "registerEventListener", "registerEvent", "turbo_reflex_default", "logRegisteredEvents"]
7
+ }