@hotwired/turbo-rails 7.1.0 → 7.1.3
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.
- package/README.md +44 -11
- package/app/assets/javascripts/turbo.js +91 -18
- package/app/javascript/turbo/cable_stream_source_element.js +2 -1
- package/app/javascript/turbo/form_submissions.js +5 -0
- package/app/javascript/turbo/index.js +3 -0
- package/app/javascript/turbo/snakeize.js +31 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
# Turbo
|
|
1
|
+
# <img src="assets/logo.png?sanitize=true" width="24" height="24" alt="Turbo"> Turbo
|
|
2
2
|
|
|
3
3
|
[Turbo](https://turbo.hotwired.dev) gives you the speed of a single-page web application without having to write any JavaScript. Turbo accelerates links and form submissions without requiring you to change your server-side generated HTML. It lets you carve up a page into independent frames, which can be lazy-loaded and operate as independent components. And finally, helps you make partial page updates using just HTML and a set of CRUD-like container tags. These three techniques reduce the amount of custom JavaScript that many web applications need to write by an order of magnitude. And for the few dynamic bits that are left, you're invited to finish the job with [Stimulus](https://github.com/hotwired/stimulus).
|
|
4
4
|
|
|
5
|
-
On top of accelerating web applications, Turbo was built from the ground-up to form the foundation of hybrid native applications. Write the navigational shell of your Android or iOS app using the standard platform tooling, then seamlessly fill in features from the web, following native navigation patterns. Not every mobile screen needs to be written in Swift or Kotlin to feel native. With Turbo, you spend less time wrangling JSON, waiting on app stores to approve updates, or reimplementing features you've already created in HTML.
|
|
5
|
+
On top of accelerating web applications, Turbo was built from the ground-up to form the foundation of hybrid native applications. Write the navigational shell of your [Android](https://github.com/hotwired/turbo-android) or [iOS](https://github.com/hotwired/turbo-ios) app using the standard platform tooling, then seamlessly fill in features from the web, following native navigation patterns. Not every mobile screen needs to be written in Swift or Kotlin to feel native. With Turbo, you spend less time wrangling JSON, waiting on app stores to approve updates, or reimplementing features you've already created in HTML.
|
|
6
6
|
|
|
7
7
|
Turbo is a language-agnostic framework written in TypeScript, but this gem builds on top of those basics to make the integration with Rails as smooth as possible. You can deliver turbo updates via model callbacks over Action Cable, respond to controller actions with native navigation or standard redirects, and render turbo frames with helpers and layout-free responses.
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
## Turbo Drive
|
|
10
|
+
## Navigate with Turbo Drive
|
|
11
11
|
|
|
12
|
-
Turbo is a continuation of the ideas from the previous Turbolinks framework, and the heart of that past approach lives on as Turbo Drive. When installed, Turbo automatically intercepts all clicks on `<a href>` links to the same domain. When you click an eligible link, Turbo prevents the browser from following it. Instead, Turbo changes the browser’s URL using the History API, requests the new page using `fetch`, and then renders the HTML response.
|
|
12
|
+
Turbo is a continuation of the ideas from the previous [Turbolinks](https://github.com/turbolinks/turbolinks) framework, and the heart of that past approach lives on as Turbo Drive. When installed, Turbo automatically intercepts all clicks on `<a href>` links to the same domain. When you click an eligible link, Turbo prevents the browser from following it. Instead, Turbo changes the browser’s URL using the History API, requests the new page using `fetch`, and then renders the HTML response.
|
|
13
13
|
|
|
14
14
|
During rendering, Turbo replaces the current `<body>` element outright and merges the contents of the `<head>` element. The JavaScript window and document objects, and the HTML `<html>` element, persist from one rendering to the next.
|
|
15
15
|
|
|
@@ -24,31 +24,64 @@ Turbo.session.drive = false
|
|
|
24
24
|
|
|
25
25
|
Then you can use `data-turbo="true"` to enable Drive on a per-element basis.
|
|
26
26
|
|
|
27
|
+
[See documentation](https://turbo.hotwired.dev/handbook/drive).
|
|
27
28
|
|
|
28
|
-
## Turbo Frames
|
|
29
|
+
## Decompose with Turbo Frames
|
|
29
30
|
|
|
30
|
-
Turbo reinvents the old HTML technique of frames without any of the drawbacks that lead to developers abandoning it. With Turbo Frames, you can treat a subset of the page as its own component
|
|
31
|
+
Turbo reinvents the old HTML technique of frames without any of the drawbacks that lead to developers abandoning it. With Turbo Frames, **you can treat a subset of the page as its own component**, where links and form submissions **replace only that part**. This removes an entire class of problems around partial interactivity that before would have required custom JavaScript.
|
|
31
32
|
|
|
32
|
-
It also makes it dead easy to carve a single page into smaller pieces that can all live on their own cache timeline. While the bulk of the page might easily be cached between users, a small personalized toolbar perhaps cannot. With Turbo::Frames, you can designate the toolbar as a frame, which will be lazy-loaded automatically by the publicly-cached root page. This means simpler pages, easier caching schemes with fewer dependent keys, and all without needing to write a lick of custom JavaScript.
|
|
33
|
+
It also makes it dead easy to carve a single page into smaller pieces that can all live on their own cache timeline. While the bulk of the page might easily be cached between users, a small personalized toolbar perhaps cannot. With Turbo::Frames, you can designate the toolbar as a frame, which will be **lazy-loaded automatically** by the publicly-cached root page. This means simpler pages, easier caching schemes with fewer dependent keys, and all without needing to write a lick of custom JavaScript.
|
|
33
34
|
|
|
35
|
+
This gem provides a `turbo_frame_tag` helper to create those frame.
|
|
34
36
|
|
|
35
|
-
|
|
37
|
+
For instance:
|
|
38
|
+
```erb
|
|
39
|
+
<%# app/views/todos/show.html.erb %>
|
|
40
|
+
<%= turbo_frame_tag @todo do %>
|
|
41
|
+
<p><%= @todo.description %></p>
|
|
36
42
|
|
|
37
|
-
|
|
43
|
+
<%= link_to 'Edit this todo', edit_todo_path(@todo) %>
|
|
44
|
+
<% end %>
|
|
45
|
+
|
|
46
|
+
<%# app/views/todos/edit.html.erb %>
|
|
47
|
+
<%= turbo_frame_tag @todo do %>
|
|
48
|
+
<%= render "form" %>
|
|
49
|
+
|
|
50
|
+
<%= link_to 'Cancel', todo_path(@todo) %>
|
|
51
|
+
<% end %>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
When the user will click on the `Edit this todo` link, as direct response to this direct user interaction, the turbo frame will be replaced with the one in the `edit.html.erb` page automatically.
|
|
55
|
+
|
|
56
|
+
[See documentation](https://turbo.hotwired.dev/handbook/frames).
|
|
57
|
+
|
|
58
|
+
## Come Alive with Turbo Streams
|
|
59
|
+
|
|
60
|
+
Partial page updates that are **delivered asynchronously over a web socket connection** is the hallmark of modern, reactive web applications. With Turbo Streams, you can get all of that modern goodness using the existing server-side HTML you're already rendering to deliver the first page load. With a set of simple CRUD container tags, you can send HTML fragments over the web socket (or in response to direct interactions), and see the page change in response to new data. Again, **no need to construct an entirely separate API**, **no need to wrangle JSON**, **no need to reimplement the HTML construction in JavaScript**. Take the HTML you're already making, wrap it in an update tag, and, voila, your page comes alive.
|
|
38
61
|
|
|
39
62
|
With this Rails integration, you can create these asynchronous updates directly in response to your model changes. Turbo uses Active Jobs to provide asynchronous partial rendering and Action Cable to deliver those updates to subscribers.
|
|
40
63
|
|
|
64
|
+
This gem provides a `turbo_stream_from` helper to create a turbo stream.
|
|
65
|
+
|
|
66
|
+
```erb
|
|
67
|
+
<%# app/views/todos/show.html.erb %>
|
|
68
|
+
<%= turbo_stream_from dom_id(@todo) %>
|
|
69
|
+
|
|
70
|
+
<%# Rest of show here %>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
[See documentation](https://turbo.hotwired.dev/handbook/streams).
|
|
41
74
|
|
|
42
75
|
## Installation
|
|
43
76
|
|
|
44
|
-
|
|
77
|
+
This gem is automatically configured for applications made with Rails 7+ (unless --skip-hotwire is passed to the generator). But if you're on Rails 6, you can install it manually:
|
|
45
78
|
|
|
46
79
|
1. Add the `turbo-rails` gem to your Gemfile: `gem 'turbo-rails'`
|
|
47
80
|
2. Run `./bin/bundle install`
|
|
48
81
|
3. Run `./bin/rails turbo:install`
|
|
49
82
|
4. Run `./bin/rails turbo:install:redis` to change the development Action Cable adapter from Async (the default one) to Redis. The Async adapter does not support Turbo Stream broadcasting.
|
|
50
83
|
|
|
51
|
-
Running `turbo:install` will install through NPM if
|
|
84
|
+
Running `turbo:install` will install through NPM if Node.js is used in the application. Otherwise the asset pipeline version is used. To use the asset pipeline version, you must have `importmap-rails` installed first and listed higher in the Gemfile.
|
|
52
85
|
|
|
53
86
|
If you're using node and need to use the cable consumer, you can import [`cable`](https://github.com/hotwired/turbo-rails/blob/main/app/javascript/turbo/cable.js) (`import { cable } from "@hotwired/turbo-rails"`), but ensure that your application actually *uses* the members it `import`s when using this style (see [turbo-rails#48](https://github.com/hotwired/turbo-rails/issues/48)).
|
|
54
87
|
|
|
@@ -3486,6 +3486,19 @@ var cable = Object.freeze({
|
|
|
3486
3486
|
subscribeTo: subscribeTo
|
|
3487
3487
|
});
|
|
3488
3488
|
|
|
3489
|
+
function walk(obj) {
|
|
3490
|
+
if (!obj || typeof obj !== "object") return obj;
|
|
3491
|
+
if (obj instanceof Date || obj instanceof RegExp) return obj;
|
|
3492
|
+
if (Array.isArray(obj)) return obj.map(walk);
|
|
3493
|
+
return Object.keys(obj).reduce((function(acc, key) {
|
|
3494
|
+
var camel = key[0].toLowerCase() + key.slice(1).replace(/([A-Z]+)/g, (function(m, x) {
|
|
3495
|
+
return "_" + x.toLowerCase();
|
|
3496
|
+
}));
|
|
3497
|
+
acc[camel] = walk(obj[key]);
|
|
3498
|
+
return acc;
|
|
3499
|
+
}), {});
|
|
3500
|
+
}
|
|
3501
|
+
|
|
3489
3502
|
class TurboCableStreamSourceElement extends HTMLElement {
|
|
3490
3503
|
async connectedCallback() {
|
|
3491
3504
|
connectStreamSource(this);
|
|
@@ -3508,13 +3521,24 @@ class TurboCableStreamSourceElement extends HTMLElement {
|
|
|
3508
3521
|
const signed_stream_name = this.getAttribute("signed-stream-name");
|
|
3509
3522
|
return {
|
|
3510
3523
|
channel: channel,
|
|
3511
|
-
signed_stream_name: signed_stream_name
|
|
3524
|
+
signed_stream_name: signed_stream_name,
|
|
3525
|
+
...walk({
|
|
3526
|
+
...this.dataset
|
|
3527
|
+
})
|
|
3512
3528
|
};
|
|
3513
3529
|
}
|
|
3514
3530
|
}
|
|
3515
3531
|
|
|
3516
3532
|
customElements.define("turbo-cable-stream-source", TurboCableStreamSourceElement);
|
|
3517
3533
|
|
|
3534
|
+
function overrideMethodWithFormmethod({detail: {formSubmission: {fetchRequest: fetchRequest, submitter: submitter}}}) {
|
|
3535
|
+
if (submitter && submitter.formMethod && fetchRequest.body.has("_method")) {
|
|
3536
|
+
fetchRequest.body.set("_method", submitter.formMethod);
|
|
3537
|
+
}
|
|
3538
|
+
}
|
|
3539
|
+
|
|
3540
|
+
addEventListener("turbo:submit-start", overrideMethodWithFormmethod);
|
|
3541
|
+
|
|
3518
3542
|
var adapters = {
|
|
3519
3543
|
logger: self.console,
|
|
3520
3544
|
WebSocket: self.WebSocket
|
|
@@ -3533,8 +3557,6 @@ const now = () => (new Date).getTime();
|
|
|
3533
3557
|
|
|
3534
3558
|
const secondsSince = time => (now() - time) / 1e3;
|
|
3535
3559
|
|
|
3536
|
-
const clamp = (number, min, max) => Math.max(min, Math.min(max, number));
|
|
3537
|
-
|
|
3538
3560
|
class ConnectionMonitor {
|
|
3539
3561
|
constructor(connection) {
|
|
3540
3562
|
this.visibilityDidChange = this.visibilityDidChange.bind(this);
|
|
@@ -3547,7 +3569,7 @@ class ConnectionMonitor {
|
|
|
3547
3569
|
delete this.stoppedAt;
|
|
3548
3570
|
this.startPolling();
|
|
3549
3571
|
addEventListener("visibilitychange", this.visibilityDidChange);
|
|
3550
|
-
logger.log(`ConnectionMonitor started.
|
|
3572
|
+
logger.log(`ConnectionMonitor started. stale threshold = ${this.constructor.staleThreshold} s`);
|
|
3551
3573
|
}
|
|
3552
3574
|
}
|
|
3553
3575
|
stop() {
|
|
@@ -3588,24 +3610,29 @@ class ConnectionMonitor {
|
|
|
3588
3610
|
}), this.getPollInterval());
|
|
3589
3611
|
}
|
|
3590
3612
|
getPollInterval() {
|
|
3591
|
-
const {
|
|
3592
|
-
const
|
|
3593
|
-
|
|
3613
|
+
const {staleThreshold: staleThreshold, reconnectionBackoffRate: reconnectionBackoffRate} = this.constructor;
|
|
3614
|
+
const backoff = Math.pow(1 + reconnectionBackoffRate, Math.min(this.reconnectAttempts, 10));
|
|
3615
|
+
const jitterMax = this.reconnectAttempts === 0 ? 1 : reconnectionBackoffRate;
|
|
3616
|
+
const jitter = jitterMax * Math.random();
|
|
3617
|
+
return staleThreshold * 1e3 * backoff * (1 + jitter);
|
|
3594
3618
|
}
|
|
3595
3619
|
reconnectIfStale() {
|
|
3596
3620
|
if (this.connectionIsStale()) {
|
|
3597
|
-
logger.log(`ConnectionMonitor detected stale connection. reconnectAttempts = ${this.reconnectAttempts},
|
|
3621
|
+
logger.log(`ConnectionMonitor detected stale connection. reconnectAttempts = ${this.reconnectAttempts}, time stale = ${secondsSince(this.refreshedAt)} s, stale threshold = ${this.constructor.staleThreshold} s`);
|
|
3598
3622
|
this.reconnectAttempts++;
|
|
3599
3623
|
if (this.disconnectedRecently()) {
|
|
3600
|
-
logger.log(
|
|
3624
|
+
logger.log(`ConnectionMonitor skipping reopening recent disconnect. time disconnected = ${secondsSince(this.disconnectedAt)} s`);
|
|
3601
3625
|
} else {
|
|
3602
3626
|
logger.log("ConnectionMonitor reopening");
|
|
3603
3627
|
this.connection.reopen();
|
|
3604
3628
|
}
|
|
3605
3629
|
}
|
|
3606
3630
|
}
|
|
3631
|
+
get refreshedAt() {
|
|
3632
|
+
return this.pingedAt ? this.pingedAt : this.startedAt;
|
|
3633
|
+
}
|
|
3607
3634
|
connectionIsStale() {
|
|
3608
|
-
return secondsSince(this.
|
|
3635
|
+
return secondsSince(this.refreshedAt) > this.constructor.staleThreshold;
|
|
3609
3636
|
}
|
|
3610
3637
|
disconnectedRecently() {
|
|
3611
3638
|
return this.disconnectedAt && secondsSince(this.disconnectedAt) < this.constructor.staleThreshold;
|
|
@@ -3622,14 +3649,10 @@ class ConnectionMonitor {
|
|
|
3622
3649
|
}
|
|
3623
3650
|
}
|
|
3624
3651
|
|
|
3625
|
-
ConnectionMonitor.pollInterval = {
|
|
3626
|
-
min: 3,
|
|
3627
|
-
max: 30,
|
|
3628
|
-
multiplier: 5
|
|
3629
|
-
};
|
|
3630
|
-
|
|
3631
3652
|
ConnectionMonitor.staleThreshold = 6;
|
|
3632
3653
|
|
|
3654
|
+
ConnectionMonitor.reconnectionBackoffRate = .15;
|
|
3655
|
+
|
|
3633
3656
|
var INTERNAL = {
|
|
3634
3657
|
message_types: {
|
|
3635
3658
|
welcome: "welcome",
|
|
@@ -3772,6 +3795,7 @@ Connection.prototype.events = {
|
|
|
3772
3795
|
return this.monitor.recordPing();
|
|
3773
3796
|
|
|
3774
3797
|
case message_types.confirmation:
|
|
3798
|
+
this.subscriptions.confirmSubscription(identifier);
|
|
3775
3799
|
return this.subscriptions.notify(identifier, "connected");
|
|
3776
3800
|
|
|
3777
3801
|
case message_types.rejection:
|
|
@@ -3839,9 +3863,47 @@ class Subscription {
|
|
|
3839
3863
|
}
|
|
3840
3864
|
}
|
|
3841
3865
|
|
|
3866
|
+
class SubscriptionGuarantor {
|
|
3867
|
+
constructor(subscriptions) {
|
|
3868
|
+
this.subscriptions = subscriptions;
|
|
3869
|
+
this.pendingSubscriptions = [];
|
|
3870
|
+
}
|
|
3871
|
+
guarantee(subscription) {
|
|
3872
|
+
if (this.pendingSubscriptions.indexOf(subscription) == -1) {
|
|
3873
|
+
logger.log(`SubscriptionGuarantor guaranteeing ${subscription.identifier}`);
|
|
3874
|
+
this.pendingSubscriptions.push(subscription);
|
|
3875
|
+
} else {
|
|
3876
|
+
logger.log(`SubscriptionGuarantor already guaranteeing ${subscription.identifier}`);
|
|
3877
|
+
}
|
|
3878
|
+
this.startGuaranteeing();
|
|
3879
|
+
}
|
|
3880
|
+
forget(subscription) {
|
|
3881
|
+
logger.log(`SubscriptionGuarantor forgetting ${subscription.identifier}`);
|
|
3882
|
+
this.pendingSubscriptions = this.pendingSubscriptions.filter((s => s !== subscription));
|
|
3883
|
+
}
|
|
3884
|
+
startGuaranteeing() {
|
|
3885
|
+
this.stopGuaranteeing();
|
|
3886
|
+
this.retrySubscribing();
|
|
3887
|
+
}
|
|
3888
|
+
stopGuaranteeing() {
|
|
3889
|
+
clearTimeout(this.retryTimeout);
|
|
3890
|
+
}
|
|
3891
|
+
retrySubscribing() {
|
|
3892
|
+
this.retryTimeout = setTimeout((() => {
|
|
3893
|
+
if (this.subscriptions && typeof this.subscriptions.subscribe === "function") {
|
|
3894
|
+
this.pendingSubscriptions.map((subscription => {
|
|
3895
|
+
logger.log(`SubscriptionGuarantor resubscribing ${subscription.identifier}`);
|
|
3896
|
+
this.subscriptions.subscribe(subscription);
|
|
3897
|
+
}));
|
|
3898
|
+
}
|
|
3899
|
+
}), 500);
|
|
3900
|
+
}
|
|
3901
|
+
}
|
|
3902
|
+
|
|
3842
3903
|
class Subscriptions {
|
|
3843
3904
|
constructor(consumer) {
|
|
3844
3905
|
this.consumer = consumer;
|
|
3906
|
+
this.guarantor = new SubscriptionGuarantor(this);
|
|
3845
3907
|
this.subscriptions = [];
|
|
3846
3908
|
}
|
|
3847
3909
|
create(channelName, mixin) {
|
|
@@ -3856,7 +3918,7 @@ class Subscriptions {
|
|
|
3856
3918
|
this.subscriptions.push(subscription);
|
|
3857
3919
|
this.consumer.ensureActiveConnection();
|
|
3858
3920
|
this.notify(subscription, "initialized");
|
|
3859
|
-
this.
|
|
3921
|
+
this.subscribe(subscription);
|
|
3860
3922
|
return subscription;
|
|
3861
3923
|
}
|
|
3862
3924
|
remove(subscription) {
|
|
@@ -3874,6 +3936,7 @@ class Subscriptions {
|
|
|
3874
3936
|
}));
|
|
3875
3937
|
}
|
|
3876
3938
|
forget(subscription) {
|
|
3939
|
+
this.guarantor.forget(subscription);
|
|
3877
3940
|
this.subscriptions = this.subscriptions.filter((s => s !== subscription));
|
|
3878
3941
|
return subscription;
|
|
3879
3942
|
}
|
|
@@ -3881,7 +3944,7 @@ class Subscriptions {
|
|
|
3881
3944
|
return this.subscriptions.filter((s => s.identifier === identifier));
|
|
3882
3945
|
}
|
|
3883
3946
|
reload() {
|
|
3884
|
-
return this.subscriptions.map((subscription => this.
|
|
3947
|
+
return this.subscriptions.map((subscription => this.subscribe(subscription)));
|
|
3885
3948
|
}
|
|
3886
3949
|
notifyAll(callbackName, ...args) {
|
|
3887
3950
|
return this.subscriptions.map((subscription => this.notify(subscription, callbackName, ...args)));
|
|
@@ -3895,6 +3958,15 @@ class Subscriptions {
|
|
|
3895
3958
|
}
|
|
3896
3959
|
return subscriptions.map((subscription => typeof subscription[callbackName] === "function" ? subscription[callbackName](...args) : undefined));
|
|
3897
3960
|
}
|
|
3961
|
+
subscribe(subscription) {
|
|
3962
|
+
if (this.sendCommand(subscription, "subscribe")) {
|
|
3963
|
+
this.guarantor.guarantee(subscription);
|
|
3964
|
+
}
|
|
3965
|
+
}
|
|
3966
|
+
confirmSubscription(identifier) {
|
|
3967
|
+
logger.log(`Subscription confirmed ${identifier}`);
|
|
3968
|
+
this.findAll(identifier).map((subscription => this.guarantor.forget(subscription)));
|
|
3969
|
+
}
|
|
3898
3970
|
sendCommand(subscription, command) {
|
|
3899
3971
|
const {identifier: identifier} = subscription;
|
|
3900
3972
|
return this.consumer.send({
|
|
@@ -3965,6 +4037,7 @@ var index = Object.freeze({
|
|
|
3965
4037
|
INTERNAL: INTERNAL,
|
|
3966
4038
|
Subscription: Subscription,
|
|
3967
4039
|
Subscriptions: Subscriptions,
|
|
4040
|
+
SubscriptionGuarantor: SubscriptionGuarantor,
|
|
3968
4041
|
adapters: adapters,
|
|
3969
4042
|
createWebSocketURL: createWebSocketURL,
|
|
3970
4043
|
logger: logger,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { connectStreamSource, disconnectStreamSource } from "@hotwired/turbo"
|
|
2
2
|
import { subscribeTo } from "./cable"
|
|
3
|
+
import snakeize from "./snakeize"
|
|
3
4
|
|
|
4
5
|
class TurboCableStreamSourceElement extends HTMLElement {
|
|
5
6
|
async connectedCallback() {
|
|
@@ -20,7 +21,7 @@ class TurboCableStreamSourceElement extends HTMLElement {
|
|
|
20
21
|
get channel() {
|
|
21
22
|
const channel = this.getAttribute("channel")
|
|
22
23
|
const signed_stream_name = this.getAttribute("signed-stream-name")
|
|
23
|
-
return { channel, signed_stream_name }
|
|
24
|
+
return { channel, signed_stream_name, ...snakeize({ ...this.dataset }) }
|
|
24
25
|
}
|
|
25
26
|
}
|
|
26
27
|
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import "./cable_stream_source_element"
|
|
2
|
+
import { overrideMethodWithFormmethod } from "./form_submissions"
|
|
2
3
|
|
|
3
4
|
import * as Turbo from "@hotwired/turbo"
|
|
4
5
|
export { Turbo }
|
|
5
6
|
|
|
6
7
|
import * as cable from "./cable"
|
|
7
8
|
export { cable }
|
|
9
|
+
|
|
10
|
+
addEventListener("turbo:submit-start", overrideMethodWithFormmethod)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Based on https://github.com/nathan7/snakeize
|
|
2
|
+
//
|
|
3
|
+
// This software is released under the MIT license:
|
|
4
|
+
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
5
|
+
// this software and associated documentation files (the "Software"), to deal in
|
|
6
|
+
// the Software without restriction, including without limitation the rights to
|
|
7
|
+
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
8
|
+
// the Software, and to permit persons to whom the Software is furnished to do so,
|
|
9
|
+
// subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
// The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
// copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
16
|
+
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
17
|
+
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
18
|
+
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
19
|
+
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
20
|
+
export default function walk (obj) {
|
|
21
|
+
if (!obj || typeof obj !== 'object') return obj;
|
|
22
|
+
if (obj instanceof Date || obj instanceof RegExp) return obj;
|
|
23
|
+
if (Array.isArray(obj)) return obj.map(walk);
|
|
24
|
+
return Object.keys(obj).reduce(function (acc, key) {
|
|
25
|
+
var camel = key[0].toLowerCase() + key.slice(1).replace(/([A-Z]+)/g, function (m, x) {
|
|
26
|
+
return '_' + x.toLowerCase();
|
|
27
|
+
});
|
|
28
|
+
acc[camel] = walk(obj[key]);
|
|
29
|
+
return acc;
|
|
30
|
+
}, {});
|
|
31
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hotwired/turbo-rails",
|
|
3
|
-
"version": "7.1.
|
|
3
|
+
"version": "7.1.3",
|
|
4
4
|
"description": "The speed of a single-page web application without having to write any JavaScript",
|
|
5
5
|
"module": "app/javascript/turbo/index.js",
|
|
6
6
|
"main": "app/assets/javascripts/turbo.js",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"@hotwired/turbo": "^7.1.0",
|
|
17
|
-
"@rails/actioncable": "^
|
|
17
|
+
"@rails/actioncable": "^7.0"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"@rollup/plugin-node-resolve": "^11.0.1",
|