turbo_reflex 0.0.1 → 0.0.2

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.
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
+ }