turbo-rails 0.7.2 → 0.7.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +11 -0
- data/app/assets/javascripts/turbo.js +180 -99
- data/app/channels/turbo/streams/broadcasts.rb +30 -32
- data/app/helpers/turbo/streams/action_helper.rb +2 -3
- data/app/models/turbo/streams/tag_builder.rb +2 -12
- data/lib/install/turbo_needs_redis.rb +9 -0
- data/lib/install/turbo_with_asset_pipeline.rb +20 -12
- data/lib/install/turbo_with_webpacker.rb +3 -23
- data/lib/tasks/turbo_tasks.rake +5 -0
- data/lib/turbo/engine.rb +0 -6
- data/lib/turbo/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4f96104fa6d1282a5f3845a119e748b04a819078840d3750604ebebeac081871
|
4
|
+
data.tar.gz: 0fdbf552964ff5ada90d746a32ec5575181c010471f4dcd921c2b443bfc732af
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 662af2c0c6187537aafc70747ed3f66215bc65bde320475d12d10aeb30d2aca17ed1a3fa1403033a73f51323969782e3e482d5c145a3d0382921045eff22f2cb
|
7
|
+
data.tar.gz: 23a86030ac88022ba35c3b9369e1cf73bc2803e3c6106abb22ac9b179bb170b78e732fbab873eb56698f7a4c262298f2ab962f89359bd2369ac71820205df513
|
data/README.md
CHANGED
@@ -15,6 +15,15 @@ During rendering, Turbo replaces the current `<body>` element outright and merge
|
|
15
15
|
|
16
16
|
Whereas Turbolinks previously just dealt with links, Turbo can now also process form submissions and responses. This means the entire flow in the web application is wrapped into Turbo, making all the parts fast. No more need for `data-remote=true`.
|
17
17
|
|
18
|
+
Turbo Drive can be disabled on a per-element basis by annotating the element or any of its ancestors with `data-turbo="false"`. If you want Turbo Drive to be disabled by default, then you can adjust your import like this:
|
19
|
+
|
20
|
+
```js
|
21
|
+
import { Turbo } from "@hotwired/turbo-rails"
|
22
|
+
Turbo.session.drive = false
|
23
|
+
```
|
24
|
+
|
25
|
+
Then you can use `data-turbo="true"` to enable Drive on a per-element basis.
|
26
|
+
|
18
27
|
|
19
28
|
## Turbo Frames
|
20
29
|
|
@@ -37,6 +46,7 @@ The JavaScript for Turbo can either be run through the asset pipeline, which is
|
|
37
46
|
1. Add the `turbo-rails` gem to your Gemfile: `gem 'turbo-rails'`
|
38
47
|
2. Run `./bin/bundle install`
|
39
48
|
3. Run `./bin/rails turbo:install`
|
49
|
+
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.
|
40
50
|
|
41
51
|
Running `turbo:install` will install through NPM if Webpacker is installed 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.
|
42
52
|
|
@@ -48,6 +58,7 @@ The `Turbo` instance is automatically assigned to `window.Turbo` upon import:
|
|
48
58
|
import "@hotwired/turbo-rails"
|
49
59
|
```
|
50
60
|
|
61
|
+
|
51
62
|
## Usage
|
52
63
|
|
53
64
|
You can watch [the video introduction to Hotwire](https://hotwired.dev/#screencast), which focuses extensively on demonstration Turbo in a Rails demo. Then you should familiarize yourself with [Turbo handbook](https://turbo.hotwired.dev/handbook/introduction) to understand Drive, Frames, and Streams in-depth. Finally, dive into the code documentation by starting with [`Turbo::FramesHelper`](https://github.com/hotwired/turbo-rails/blob/main/app/helpers/turbo/frames_helper.rb), [`Turbo::StreamsHelper`](https://github.com/hotwired/turbo-rails/blob/main/app/helpers/turbo/streams_helper.rb), [`Turbo::Streams::TagBuilder`](https://github.com/hotwired/turbo-rails/blob/main/app/models/turbo/streams/tag_builder.rb), and [`Turbo::Broadcastable`](https://github.com/hotwired/turbo-rails/blob/main/app/models/concerns/turbo/broadcastable.rb).
|
@@ -361,8 +361,10 @@ class FetchRequest {
|
|
361
361
|
const response = await fetch(this.url.href, fetchOptions);
|
362
362
|
return await this.receive(response);
|
363
363
|
} catch (error) {
|
364
|
-
|
365
|
-
|
364
|
+
if (error.name !== "AbortError") {
|
365
|
+
this.delegate.requestErrored(this, error);
|
366
|
+
throw error;
|
367
|
+
}
|
366
368
|
} finally {
|
367
369
|
this.delegate.requestFinished(this);
|
368
370
|
}
|
@@ -385,13 +387,15 @@ class FetchRequest {
|
|
385
387
|
return fetchResponse;
|
386
388
|
}
|
387
389
|
get fetchOptions() {
|
390
|
+
var _a;
|
388
391
|
return {
|
389
392
|
method: FetchMethod[this.method].toUpperCase(),
|
390
393
|
credentials: "same-origin",
|
391
394
|
headers: this.headers,
|
392
395
|
redirect: "follow",
|
393
396
|
body: this.body,
|
394
|
-
signal: this.abortSignal
|
397
|
+
signal: this.abortSignal,
|
398
|
+
referrer: (_a = this.delegate.referrer) === null || _a === void 0 ? void 0 : _a.href
|
395
399
|
};
|
396
400
|
}
|
397
401
|
get defaultHeaders() {
|
@@ -544,7 +548,8 @@ class FormSubmission {
|
|
544
548
|
}
|
545
549
|
get action() {
|
546
550
|
var _a;
|
547
|
-
|
551
|
+
const formElementAction = typeof this.formElement.action === "string" ? this.formElement.action : null;
|
552
|
+
return ((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formaction")) || this.formElement.getAttribute("action") || formElementAction || "";
|
548
553
|
}
|
549
554
|
get location() {
|
550
555
|
return expandURL(this.action);
|
@@ -691,11 +696,7 @@ class Snapshot {
|
|
691
696
|
return this.getElementForAnchor(anchor) != null;
|
692
697
|
}
|
693
698
|
getElementForAnchor(anchor) {
|
694
|
-
|
695
|
-
return this.element.querySelector(`[id='${anchor}'], a[name='${anchor}']`);
|
696
|
-
} catch (_a) {
|
697
|
-
return null;
|
698
|
-
}
|
699
|
+
return anchor ? this.element.querySelector(`[id='${anchor}'], a[name='${anchor}']`) : null;
|
699
700
|
}
|
700
701
|
get isConnected() {
|
701
702
|
return this.element.isConnected;
|
@@ -785,6 +786,12 @@ class View {
|
|
785
786
|
scrollToPosition({x: x, y: y}) {
|
786
787
|
this.scrollRoot.scrollTo(x, y);
|
787
788
|
}
|
789
|
+
scrollToTop() {
|
790
|
+
this.scrollToPosition({
|
791
|
+
x: 0,
|
792
|
+
y: 0
|
793
|
+
});
|
794
|
+
}
|
788
795
|
get scrollRoot() {
|
789
796
|
return window;
|
790
797
|
}
|
@@ -1354,6 +1361,9 @@ class Visit {
|
|
1354
1361
|
get restorationData() {
|
1355
1362
|
return this.history.getRestorationDataForIdentifier(this.restorationIdentifier);
|
1356
1363
|
}
|
1364
|
+
get silent() {
|
1365
|
+
return this.isSamePage;
|
1366
|
+
}
|
1357
1367
|
start() {
|
1358
1368
|
if (this.state == VisitState.initialized) {
|
1359
1369
|
this.recordTimingMetric(TimingMetric.visitStart);
|
@@ -1377,6 +1387,7 @@ class Visit {
|
|
1377
1387
|
this.state = VisitState.completed;
|
1378
1388
|
this.adapter.visitCompleted(this);
|
1379
1389
|
this.delegate.visitCompleted(this);
|
1390
|
+
this.followRedirect();
|
1380
1391
|
}
|
1381
1392
|
}
|
1382
1393
|
fail() {
|
@@ -1483,8 +1494,10 @@ class Visit {
|
|
1483
1494
|
}
|
1484
1495
|
followRedirect() {
|
1485
1496
|
if (this.redirectedToLocation && !this.followedRedirect) {
|
1486
|
-
this.
|
1487
|
-
|
1497
|
+
this.adapter.visitProposedToLocation(this.redirectedToLocation, {
|
1498
|
+
action: "replace",
|
1499
|
+
response: this.response
|
1500
|
+
});
|
1488
1501
|
this.followedRedirect = true;
|
1489
1502
|
}
|
1490
1503
|
}
|
@@ -1538,9 +1551,9 @@ class Visit {
|
|
1538
1551
|
performScroll() {
|
1539
1552
|
if (!this.scrolled) {
|
1540
1553
|
if (this.action == "restore") {
|
1541
|
-
this.scrollToRestoredPosition() || this.scrollToAnchor() || this.scrollToTop();
|
1554
|
+
this.scrollToRestoredPosition() || this.scrollToAnchor() || this.view.scrollToTop();
|
1542
1555
|
} else {
|
1543
|
-
this.scrollToAnchor() || this.scrollToTop();
|
1556
|
+
this.scrollToAnchor() || this.view.scrollToTop();
|
1544
1557
|
}
|
1545
1558
|
if (this.isSamePage) {
|
1546
1559
|
this.delegate.visitScrolledToSamePageLocation(this.view.lastRenderedLocation, this.location);
|
@@ -1562,12 +1575,6 @@ class Visit {
|
|
1562
1575
|
return true;
|
1563
1576
|
}
|
1564
1577
|
}
|
1565
|
-
scrollToTop() {
|
1566
|
-
this.view.scrollToPosition({
|
1567
|
-
x: 0,
|
1568
|
-
y: 0
|
1569
|
-
});
|
1570
|
-
}
|
1571
1578
|
recordTimingMetric(metric) {
|
1572
1579
|
this.timingMetrics[metric] = (new Date).getTime();
|
1573
1580
|
}
|
@@ -1666,14 +1673,20 @@ class BrowserAdapter {
|
|
1666
1673
|
this.progressBar.setValue(1);
|
1667
1674
|
this.hideProgressBar();
|
1668
1675
|
}
|
1669
|
-
visitCompleted(visit) {
|
1670
|
-
visit.followRedirect();
|
1671
|
-
}
|
1676
|
+
visitCompleted(visit) {}
|
1672
1677
|
pageInvalidated() {
|
1673
1678
|
this.reload();
|
1674
1679
|
}
|
1675
1680
|
visitFailed(visit) {}
|
1676
1681
|
visitRendered(visit) {}
|
1682
|
+
formSubmissionStarted(formSubmission) {
|
1683
|
+
this.progressBar.setValue(0);
|
1684
|
+
this.showProgressBarAfterDelay();
|
1685
|
+
}
|
1686
|
+
formSubmissionFinished(formSubmission) {
|
1687
|
+
this.progressBar.setValue(1);
|
1688
|
+
this.hideProgressBar();
|
1689
|
+
}
|
1677
1690
|
showProgressBarAfterDelay() {
|
1678
1691
|
this.progressBarTimeout = window.setTimeout(this.showProgressBar, this.session.progressBarDelay);
|
1679
1692
|
}
|
@@ -1891,7 +1904,8 @@ class LinkClickObserver {
|
|
1891
1904
|
};
|
1892
1905
|
this.clickBubbled = event => {
|
1893
1906
|
if (this.clickEventIsSignificant(event)) {
|
1894
|
-
const
|
1907
|
+
const target = event.composedPath && event.composedPath()[0] || event.target;
|
1908
|
+
const link = this.findLinkFromClickTarget(target);
|
1895
1909
|
if (link) {
|
1896
1910
|
const location = this.getLocationForLink(link);
|
1897
1911
|
if (this.delegate.willFollowLinkToLocation(link, location)) {
|
@@ -1937,7 +1951,7 @@ class Navigator {
|
|
1937
1951
|
this.delegate = delegate;
|
1938
1952
|
}
|
1939
1953
|
proposeVisit(location, options = {}) {
|
1940
|
-
if (this.delegate.
|
1954
|
+
if (this.delegate.allowsVisitingLocationWithAction(location, options.action)) {
|
1941
1955
|
this.delegate.visitProposedToLocation(location, options);
|
1942
1956
|
}
|
1943
1957
|
}
|
@@ -1978,7 +1992,11 @@ class Navigator {
|
|
1978
1992
|
get history() {
|
1979
1993
|
return this.delegate.history;
|
1980
1994
|
}
|
1981
|
-
formSubmissionStarted(formSubmission) {
|
1995
|
+
formSubmissionStarted(formSubmission) {
|
1996
|
+
if (typeof this.adapter.formSubmissionStarted === "function") {
|
1997
|
+
this.adapter.formSubmissionStarted(formSubmission);
|
1998
|
+
}
|
1999
|
+
}
|
1982
2000
|
async formSubmissionSucceededWithResponse(formSubmission, fetchResponse) {
|
1983
2001
|
if (formSubmission == this.formSubmission) {
|
1984
2002
|
const responseHTML = await fetchResponse.responseHTML;
|
@@ -2001,14 +2019,23 @@ class Navigator {
|
|
2001
2019
|
const responseHTML = await fetchResponse.responseHTML;
|
2002
2020
|
if (responseHTML) {
|
2003
2021
|
const snapshot = PageSnapshot.fromHTMLString(responseHTML);
|
2004
|
-
|
2022
|
+
if (fetchResponse.serverError) {
|
2023
|
+
await this.view.renderError(snapshot);
|
2024
|
+
} else {
|
2025
|
+
await this.view.renderPage(snapshot);
|
2026
|
+
}
|
2027
|
+
this.view.scrollToTop();
|
2005
2028
|
this.view.clearSnapshotCache();
|
2006
2029
|
}
|
2007
2030
|
}
|
2008
2031
|
formSubmissionErrored(formSubmission, error) {
|
2009
2032
|
console.error(error);
|
2010
2033
|
}
|
2011
|
-
formSubmissionFinished(formSubmission) {
|
2034
|
+
formSubmissionFinished(formSubmission) {
|
2035
|
+
if (typeof this.adapter.formSubmissionFinished === "function") {
|
2036
|
+
this.adapter.formSubmissionFinished(formSubmission);
|
2037
|
+
}
|
2038
|
+
}
|
2012
2039
|
visitStarted(visit) {
|
2013
2040
|
this.delegate.visitStarted(visit);
|
2014
2041
|
}
|
@@ -2016,7 +2043,10 @@ class Navigator {
|
|
2016
2043
|
this.delegate.visitCompleted(visit);
|
2017
2044
|
}
|
2018
2045
|
locationWithActionIsSamePage(location, action) {
|
2019
|
-
|
2046
|
+
const anchor = getAnchor(location);
|
2047
|
+
const currentAnchor = getAnchor(this.view.lastRenderedLocation);
|
2048
|
+
const isRestorationToTop = action === "restore" && typeof anchor === "undefined";
|
2049
|
+
return action !== "replace" && getRequestURL(location) === getRequestURL(this.view.lastRenderedLocation) && (isRestorationToTop || anchor != null && anchor !== currentAnchor);
|
2020
2050
|
}
|
2021
2051
|
visitScrolledToSamePageLocation(oldURL, newURL) {
|
2022
2052
|
this.delegate.visitScrolledToSamePageLocation(oldURL, newURL);
|
@@ -2408,6 +2438,7 @@ class Session {
|
|
2408
2438
|
this.scrollObserver = new ScrollObserver(this);
|
2409
2439
|
this.streamObserver = new StreamObserver(this);
|
2410
2440
|
this.frameRedirector = new FrameRedirector(document.documentElement);
|
2441
|
+
this.drive = true;
|
2411
2442
|
this.enabled = true;
|
2412
2443
|
this.progressBarDelay = 500;
|
2413
2444
|
this.started = false;
|
@@ -2485,7 +2516,7 @@ class Session {
|
|
2485
2516
|
});
|
2486
2517
|
}
|
2487
2518
|
willFollowLinkToLocation(link, location) {
|
2488
|
-
return
|
2519
|
+
return this.elementDriveEnabled(link) && this.locationIsVisitable(location) && this.applicationAllowsFollowingLinkToLocation(link, location);
|
2489
2520
|
}
|
2490
2521
|
followedLinkToLocation(link, location) {
|
2491
2522
|
const action = this.getActionForLink(link);
|
@@ -2494,13 +2525,12 @@ class Session {
|
|
2494
2525
|
});
|
2495
2526
|
}
|
2496
2527
|
convertLinkWithMethodClickToFormSubmission(link) {
|
2497
|
-
var _a;
|
2498
2528
|
const linkMethod = link.getAttribute("data-turbo-method");
|
2499
2529
|
if (linkMethod) {
|
2500
2530
|
const form = document.createElement("form");
|
2501
2531
|
form.method = linkMethod;
|
2502
2532
|
form.action = link.getAttribute("href") || "undefined";
|
2503
|
-
|
2533
|
+
document.body.appendChild(form);
|
2504
2534
|
return dispatch("submit", {
|
2505
2535
|
cancelable: true,
|
2506
2536
|
target: form
|
@@ -2509,8 +2539,8 @@ class Session {
|
|
2509
2539
|
return false;
|
2510
2540
|
}
|
2511
2541
|
}
|
2512
|
-
|
2513
|
-
return this.applicationAllowsVisitingLocation(location);
|
2542
|
+
allowsVisitingLocationWithAction(location, action) {
|
2543
|
+
return this.locationWithActionIsSamePage(location, action) || this.applicationAllowsVisitingLocation(location);
|
2514
2544
|
}
|
2515
2545
|
visitProposedToLocation(location, options) {
|
2516
2546
|
extendURLWithDeprecatedProperties(location);
|
@@ -2518,7 +2548,9 @@ class Session {
|
|
2518
2548
|
}
|
2519
2549
|
visitStarted(visit) {
|
2520
2550
|
extendURLWithDeprecatedProperties(visit.location);
|
2521
|
-
|
2551
|
+
if (!visit.silent) {
|
2552
|
+
this.notifyApplicationAfterVisitingLocation(visit.location, visit.action);
|
2553
|
+
}
|
2522
2554
|
}
|
2523
2555
|
visitCompleted(visit) {
|
2524
2556
|
this.notifyApplicationAfterPageLoad(visit.getTimingMetrics());
|
@@ -2530,7 +2562,7 @@ class Session {
|
|
2530
2562
|
this.notifyApplicationAfterVisitingSamePageLocation(oldURL, newURL);
|
2531
2563
|
}
|
2532
2564
|
willSubmitForm(form, submitter) {
|
2533
|
-
return
|
2565
|
+
return this.elementDriveEnabled(form) && this.elementDriveEnabled(submitter);
|
2534
2566
|
}
|
2535
2567
|
formSubmitted(form, submitter) {
|
2536
2568
|
this.navigator.submitForm(form, submitter);
|
@@ -2549,7 +2581,10 @@ class Session {
|
|
2549
2581
|
this.renderStreamMessage(message);
|
2550
2582
|
}
|
2551
2583
|
viewWillCacheSnapshot() {
|
2552
|
-
|
2584
|
+
var _a;
|
2585
|
+
if (!((_a = this.navigator.currentVisit) === null || _a === void 0 ? void 0 : _a.silent)) {
|
2586
|
+
this.notifyApplicationBeforeCachingSnapshot();
|
2587
|
+
}
|
2553
2588
|
}
|
2554
2589
|
allowsImmediateRender({element: element}, resume) {
|
2555
2590
|
const event = this.notifyApplicationBeforeRender(element, resume);
|
@@ -2562,6 +2597,12 @@ class Session {
|
|
2562
2597
|
viewInvalidated() {
|
2563
2598
|
this.adapter.pageInvalidated();
|
2564
2599
|
}
|
2600
|
+
frameLoaded(frame) {
|
2601
|
+
this.notifyApplicationAfterFrameLoad(frame);
|
2602
|
+
}
|
2603
|
+
frameRendered(fetchResponse, frame) {
|
2604
|
+
this.notifyApplicationAfterFrameRender(fetchResponse, frame);
|
2605
|
+
}
|
2565
2606
|
applicationAllowsFollowingLinkToLocation(link, location) {
|
2566
2607
|
const event = this.notifyApplicationAfterClickingLinkToLocation(link, location);
|
2567
2608
|
return !event.defaultPrevented;
|
@@ -2624,6 +2665,36 @@ class Session {
|
|
2624
2665
|
newURL: newURL.toString()
|
2625
2666
|
}));
|
2626
2667
|
}
|
2668
|
+
notifyApplicationAfterFrameLoad(frame) {
|
2669
|
+
return dispatch("turbo:frame-load", {
|
2670
|
+
target: frame
|
2671
|
+
});
|
2672
|
+
}
|
2673
|
+
notifyApplicationAfterFrameRender(fetchResponse, frame) {
|
2674
|
+
return dispatch("turbo:frame-render", {
|
2675
|
+
detail: {
|
2676
|
+
fetchResponse: fetchResponse
|
2677
|
+
},
|
2678
|
+
target: frame,
|
2679
|
+
cancelable: true
|
2680
|
+
});
|
2681
|
+
}
|
2682
|
+
elementDriveEnabled(element) {
|
2683
|
+
const container = element === null || element === void 0 ? void 0 : element.closest("[data-turbo]");
|
2684
|
+
if (this.drive) {
|
2685
|
+
if (container) {
|
2686
|
+
return container.getAttribute("data-turbo") != "false";
|
2687
|
+
} else {
|
2688
|
+
return true;
|
2689
|
+
}
|
2690
|
+
} else {
|
2691
|
+
if (container) {
|
2692
|
+
return container.getAttribute("data-turbo") == "true";
|
2693
|
+
} else {
|
2694
|
+
return false;
|
2695
|
+
}
|
2696
|
+
}
|
2697
|
+
}
|
2627
2698
|
getActionForLink(link) {
|
2628
2699
|
const action = link.getAttribute("data-turbo-action");
|
2629
2700
|
return isAction(action) ? action : "advance";
|
@@ -2636,15 +2707,6 @@ class Session {
|
|
2636
2707
|
}
|
2637
2708
|
}
|
2638
2709
|
|
2639
|
-
function elementIsNavigable(element) {
|
2640
|
-
const container = element === null || element === void 0 ? void 0 : element.closest("[data-turbo]");
|
2641
|
-
if (container) {
|
2642
|
-
return container.getAttribute("data-turbo") != "false";
|
2643
|
-
} else {
|
2644
|
-
return true;
|
2645
|
-
}
|
2646
|
-
}
|
2647
|
-
|
2648
2710
|
function extendURLWithDeprecatedProperties(url) {
|
2649
2711
|
Object.defineProperties(url, deprecatedLocationPropertyDescriptors);
|
2650
2712
|
}
|
@@ -2657,6 +2719,58 @@ const deprecatedLocationPropertyDescriptors = {
|
|
2657
2719
|
}
|
2658
2720
|
};
|
2659
2721
|
|
2722
|
+
const session = new Session;
|
2723
|
+
|
2724
|
+
const {navigator: navigator} = session;
|
2725
|
+
|
2726
|
+
function start() {
|
2727
|
+
session.start();
|
2728
|
+
}
|
2729
|
+
|
2730
|
+
function registerAdapter(adapter) {
|
2731
|
+
session.registerAdapter(adapter);
|
2732
|
+
}
|
2733
|
+
|
2734
|
+
function visit(location, options) {
|
2735
|
+
session.visit(location, options);
|
2736
|
+
}
|
2737
|
+
|
2738
|
+
function connectStreamSource(source) {
|
2739
|
+
session.connectStreamSource(source);
|
2740
|
+
}
|
2741
|
+
|
2742
|
+
function disconnectStreamSource(source) {
|
2743
|
+
session.disconnectStreamSource(source);
|
2744
|
+
}
|
2745
|
+
|
2746
|
+
function renderStreamMessage(message) {
|
2747
|
+
session.renderStreamMessage(message);
|
2748
|
+
}
|
2749
|
+
|
2750
|
+
function clearCache() {
|
2751
|
+
session.clearCache();
|
2752
|
+
}
|
2753
|
+
|
2754
|
+
function setProgressBarDelay(delay) {
|
2755
|
+
session.setProgressBarDelay(delay);
|
2756
|
+
}
|
2757
|
+
|
2758
|
+
var Turbo = Object.freeze({
|
2759
|
+
__proto__: null,
|
2760
|
+
navigator: navigator,
|
2761
|
+
session: session,
|
2762
|
+
PageRenderer: PageRenderer,
|
2763
|
+
PageSnapshot: PageSnapshot,
|
2764
|
+
start: start,
|
2765
|
+
registerAdapter: registerAdapter,
|
2766
|
+
visit: visit,
|
2767
|
+
connectStreamSource: connectStreamSource,
|
2768
|
+
disconnectStreamSource: disconnectStreamSource,
|
2769
|
+
renderStreamMessage: renderStreamMessage,
|
2770
|
+
clearCache: clearCache,
|
2771
|
+
setProgressBarDelay: setProgressBarDelay
|
2772
|
+
});
|
2773
|
+
|
2660
2774
|
class FrameController {
|
2661
2775
|
constructor(element) {
|
2662
2776
|
this.resolveVisitPromise = () => {};
|
@@ -2672,6 +2786,7 @@ class FrameController {
|
|
2672
2786
|
connect() {
|
2673
2787
|
if (!this.connected) {
|
2674
2788
|
this.connected = true;
|
2789
|
+
this.reloadable = false;
|
2675
2790
|
if (this.loadingStyle == FrameLoadingStyle.lazy) {
|
2676
2791
|
this.appearanceObserver.start();
|
2677
2792
|
}
|
@@ -2707,7 +2822,7 @@ class FrameController {
|
|
2707
2822
|
}
|
2708
2823
|
}
|
2709
2824
|
async loadSourceURL() {
|
2710
|
-
if (!this.settingSourceURL && this.enabled && this.isActive && this.sourceURL != this.currentURL) {
|
2825
|
+
if (!this.settingSourceURL && this.enabled && this.isActive && (this.reloadable || this.sourceURL != this.currentURL)) {
|
2711
2826
|
const previousURL = this.currentURL;
|
2712
2827
|
this.currentURL = this.sourceURL;
|
2713
2828
|
if (this.sourceURL) {
|
@@ -2716,6 +2831,7 @@ class FrameController {
|
|
2716
2831
|
this.appearanceObserver.stop();
|
2717
2832
|
await this.element.loaded;
|
2718
2833
|
this.hasBeenLoaded = true;
|
2834
|
+
session.frameLoaded(this.element);
|
2719
2835
|
} catch (error) {
|
2720
2836
|
this.currentURL = previousURL;
|
2721
2837
|
throw error;
|
@@ -2735,6 +2851,7 @@ class FrameController {
|
|
2735
2851
|
const renderer = new FrameRenderer(this.view.snapshot, snapshot, false);
|
2736
2852
|
if (this.view.renderPromise) await this.view.renderPromise;
|
2737
2853
|
await this.view.render(renderer);
|
2854
|
+
session.frameRendered(fetchResponse, this.element);
|
2738
2855
|
}
|
2739
2856
|
} catch (error) {
|
2740
2857
|
console.error(error);
|
@@ -2752,6 +2869,7 @@ class FrameController {
|
|
2752
2869
|
}
|
2753
2870
|
}
|
2754
2871
|
linkClickIntercepted(element, url) {
|
2872
|
+
this.reloadable = true;
|
2755
2873
|
this.navigateFrame(element, url);
|
2756
2874
|
}
|
2757
2875
|
shouldInterceptFormSubmission(element, submitter) {
|
@@ -2761,6 +2879,7 @@ class FrameController {
|
|
2761
2879
|
if (this.formSubmission) {
|
2762
2880
|
this.formSubmission.stop();
|
2763
2881
|
}
|
2882
|
+
this.reloadable = false;
|
2764
2883
|
this.formSubmission = new FormSubmission(this, element, submitter);
|
2765
2884
|
if (this.formSubmission.fetchRequest.isIdempotent) {
|
2766
2885
|
this.navigateFrame(element, this.formSubmission.fetchRequest.url.href);
|
@@ -2864,10 +2983,10 @@ class FrameController {
|
|
2864
2983
|
return !frameElement.disabled;
|
2865
2984
|
}
|
2866
2985
|
}
|
2867
|
-
if (!
|
2986
|
+
if (!session.elementDriveEnabled(element)) {
|
2868
2987
|
return false;
|
2869
2988
|
}
|
2870
|
-
if (submitter && !
|
2989
|
+
if (submitter && !session.elementDriveEnabled(submitter)) {
|
2871
2990
|
return false;
|
2872
2991
|
}
|
2873
2992
|
return true;
|
@@ -2883,6 +3002,18 @@ class FrameController {
|
|
2883
3002
|
return this.element.src;
|
2884
3003
|
}
|
2885
3004
|
}
|
3005
|
+
get reloadable() {
|
3006
|
+
const frame = this.findFrameElement(this.element);
|
3007
|
+
return frame.hasAttribute("reloadable");
|
3008
|
+
}
|
3009
|
+
set reloadable(value) {
|
3010
|
+
const frame = this.findFrameElement(this.element);
|
3011
|
+
if (value) {
|
3012
|
+
frame.setAttribute("reloadable", "");
|
3013
|
+
} else {
|
3014
|
+
frame.removeAttribute("reloadable");
|
3015
|
+
}
|
3016
|
+
}
|
2886
3017
|
set sourceURL(sourceURL) {
|
2887
3018
|
this.settingSourceURL = true;
|
2888
3019
|
this.element.src = sourceURL !== null && sourceURL !== void 0 ? sourceURL : null;
|
@@ -3089,57 +3220,6 @@ customElements.define("turbo-stream", StreamElement);
|
|
3089
3220
|
}
|
3090
3221
|
})();
|
3091
3222
|
|
3092
|
-
const session = new Session;
|
3093
|
-
|
3094
|
-
const {navigator: navigator} = session;
|
3095
|
-
|
3096
|
-
function start() {
|
3097
|
-
session.start();
|
3098
|
-
}
|
3099
|
-
|
3100
|
-
function registerAdapter(adapter) {
|
3101
|
-
session.registerAdapter(adapter);
|
3102
|
-
}
|
3103
|
-
|
3104
|
-
function visit(location, options) {
|
3105
|
-
session.visit(location, options);
|
3106
|
-
}
|
3107
|
-
|
3108
|
-
function connectStreamSource(source) {
|
3109
|
-
session.connectStreamSource(source);
|
3110
|
-
}
|
3111
|
-
|
3112
|
-
function disconnectStreamSource(source) {
|
3113
|
-
session.disconnectStreamSource(source);
|
3114
|
-
}
|
3115
|
-
|
3116
|
-
function renderStreamMessage(message) {
|
3117
|
-
session.renderStreamMessage(message);
|
3118
|
-
}
|
3119
|
-
|
3120
|
-
function clearCache() {
|
3121
|
-
session.clearCache();
|
3122
|
-
}
|
3123
|
-
|
3124
|
-
function setProgressBarDelay(delay) {
|
3125
|
-
session.setProgressBarDelay(delay);
|
3126
|
-
}
|
3127
|
-
|
3128
|
-
var Turbo = Object.freeze({
|
3129
|
-
__proto__: null,
|
3130
|
-
navigator: navigator,
|
3131
|
-
PageRenderer: PageRenderer,
|
3132
|
-
PageSnapshot: PageSnapshot,
|
3133
|
-
start: start,
|
3134
|
-
registerAdapter: registerAdapter,
|
3135
|
-
visit: visit,
|
3136
|
-
connectStreamSource: connectStreamSource,
|
3137
|
-
disconnectStreamSource: disconnectStreamSource,
|
3138
|
-
renderStreamMessage: renderStreamMessage,
|
3139
|
-
clearCache: clearCache,
|
3140
|
-
setProgressBarDelay: setProgressBarDelay
|
3141
|
-
});
|
3142
|
-
|
3143
3223
|
window.Turbo = Turbo;
|
3144
3224
|
|
3145
3225
|
start();
|
@@ -3154,6 +3234,7 @@ var turbo_es2017Esm = Object.freeze({
|
|
3154
3234
|
navigator: navigator,
|
3155
3235
|
registerAdapter: registerAdapter,
|
3156
3236
|
renderStreamMessage: renderStreamMessage,
|
3237
|
+
session: session,
|
3157
3238
|
setProgressBarDelay: setProgressBarDelay,
|
3158
3239
|
start: start,
|
3159
3240
|
visit: visit
|
@@ -5,71 +5,69 @@
|
|
5
5
|
module Turbo::Streams::Broadcasts
|
6
6
|
include Turbo::Streams::ActionHelper
|
7
7
|
|
8
|
-
def broadcast_remove_to(*streamables,
|
9
|
-
broadcast_action_to *streamables, action: :remove,
|
8
|
+
def broadcast_remove_to(*streamables, **opts)
|
9
|
+
broadcast_action_to *streamables, action: :remove, **opts
|
10
10
|
end
|
11
11
|
|
12
|
-
def broadcast_replace_to(*streamables,
|
13
|
-
broadcast_action_to *streamables, action: :replace,
|
12
|
+
def broadcast_replace_to(*streamables, **opts)
|
13
|
+
broadcast_action_to *streamables, action: :replace, **opts
|
14
14
|
end
|
15
15
|
|
16
|
-
def broadcast_update_to(*streamables,
|
17
|
-
broadcast_action_to *streamables, action: :update,
|
16
|
+
def broadcast_update_to(*streamables, **opts)
|
17
|
+
broadcast_action_to *streamables, action: :update, **opts
|
18
18
|
end
|
19
19
|
|
20
|
-
def broadcast_before_to(*streamables,
|
21
|
-
broadcast_action_to *streamables, action: :before,
|
20
|
+
def broadcast_before_to(*streamables, **opts)
|
21
|
+
broadcast_action_to *streamables, action: :before, **opts
|
22
22
|
end
|
23
23
|
|
24
|
-
def broadcast_after_to(*streamables,
|
25
|
-
broadcast_action_to *streamables, action: :after,
|
24
|
+
def broadcast_after_to(*streamables, **opts)
|
25
|
+
broadcast_action_to *streamables, action: :after, **opts
|
26
26
|
end
|
27
27
|
|
28
|
-
def broadcast_append_to(*streamables,
|
29
|
-
broadcast_action_to *streamables, action: :append,
|
28
|
+
def broadcast_append_to(*streamables, **opts)
|
29
|
+
broadcast_action_to *streamables, action: :append, **opts
|
30
30
|
end
|
31
31
|
|
32
|
-
def broadcast_prepend_to(*streamables,
|
33
|
-
broadcast_action_to *streamables, action: :prepend,
|
32
|
+
def broadcast_prepend_to(*streamables, **opts)
|
33
|
+
broadcast_action_to *streamables, action: :prepend, **opts
|
34
34
|
end
|
35
35
|
|
36
|
-
def broadcast_action_to(*streamables, action:, target
|
37
|
-
broadcast_stream_to *streamables, content: turbo_stream_action_tag(action, target: target, template:
|
36
|
+
def broadcast_action_to(*streamables, action:, target: nil, targets: nil, **rendering)
|
37
|
+
broadcast_stream_to *streamables, content: turbo_stream_action_tag(action, target: target, targets: targets, template:
|
38
38
|
rendering.delete(:content) || (rendering.any? ? render_format(:html, **rendering) : nil)
|
39
39
|
)
|
40
40
|
end
|
41
41
|
|
42
|
-
|
43
|
-
|
44
|
-
broadcast_action_later_to *streamables, action: :replace, target: target, **rendering
|
42
|
+
def broadcast_replace_later_to(*streamables, **opts)
|
43
|
+
broadcast_action_later_to *streamables, action: :replace, **opts
|
45
44
|
end
|
46
45
|
|
47
|
-
def broadcast_update_later_to(*streamables,
|
48
|
-
broadcast_action_later_to *streamables, action: :update,
|
46
|
+
def broadcast_update_later_to(*streamables, **opts)
|
47
|
+
broadcast_action_later_to *streamables, action: :update, **opts
|
49
48
|
end
|
50
49
|
|
51
|
-
def broadcast_before_later_to(*streamables,
|
52
|
-
broadcast_action_later_to *streamables, action: :before,
|
50
|
+
def broadcast_before_later_to(*streamables, **opts)
|
51
|
+
broadcast_action_later_to *streamables, action: :before, **opts
|
53
52
|
end
|
54
53
|
|
55
|
-
def broadcast_after_later_to(*streamables,
|
56
|
-
broadcast_action_later_to *streamables, action: :after,
|
54
|
+
def broadcast_after_later_to(*streamables, **opts)
|
55
|
+
broadcast_action_later_to *streamables, action: :after, **opts
|
57
56
|
end
|
58
57
|
|
59
|
-
def broadcast_append_later_to(*streamables,
|
60
|
-
broadcast_action_later_to *streamables, action: :append,
|
58
|
+
def broadcast_append_later_to(*streamables, **opts)
|
59
|
+
broadcast_action_later_to *streamables, action: :append, **opts
|
61
60
|
end
|
62
61
|
|
63
|
-
def broadcast_prepend_later_to(*streamables,
|
64
|
-
broadcast_action_later_to *streamables, action: :prepend,
|
62
|
+
def broadcast_prepend_later_to(*streamables, **opts)
|
63
|
+
broadcast_action_later_to *streamables, action: :prepend, **opts
|
65
64
|
end
|
66
65
|
|
67
|
-
def broadcast_action_later_to(*streamables, action:, target
|
66
|
+
def broadcast_action_later_to(*streamables, action:, target: nil, targets: nil, **rendering)
|
68
67
|
Turbo::Streams::ActionBroadcastJob.perform_later \
|
69
|
-
stream_name_from(streamables), action: action, target: target, **rendering
|
68
|
+
stream_name_from(streamables), action: action, target: target, targets: targets, **rendering
|
70
69
|
end
|
71
70
|
|
72
|
-
|
73
71
|
def broadcast_render_to(*streamables, **rendering)
|
74
72
|
broadcast_stream_to *streamables, content: render_format(:turbo_stream, **rendering)
|
75
73
|
end
|
@@ -12,10 +12,9 @@ module Turbo::Streams::ActionHelper
|
|
12
12
|
def turbo_stream_action_tag(action, target: nil, targets: nil, template: nil)
|
13
13
|
template = action.to_sym == :remove ? "" : "<template>#{template}</template>"
|
14
14
|
|
15
|
-
if target
|
16
|
-
target = convert_to_turbo_stream_dom_id(target)
|
15
|
+
if target = convert_to_turbo_stream_dom_id(target)
|
17
16
|
%(<turbo-stream action="#{action}" target="#{target}">#{template}</turbo-stream>).html_safe
|
18
|
-
elsif targets
|
17
|
+
elsif targets = convert_to_turbo_stream_dom_id(targets)
|
19
18
|
%(<turbo-stream action="#{action}" targets="#{targets}">#{template}</turbo-stream>).html_safe
|
20
19
|
else
|
21
20
|
raise ArgumentError, "target or targets must be supplied"
|
@@ -212,29 +212,19 @@ class Turbo::Streams::TagBuilder
|
|
212
212
|
|
213
213
|
# Send an action of the type <tt>name</tt> to <tt>target</tt>. Options described in the concrete methods.
|
214
214
|
def action(name, target, content = nil, allow_inferred_rendering: true, **rendering, &block)
|
215
|
-
target_name = extract_target_name_from(target)
|
216
215
|
template = render_template(target, content, allow_inferred_rendering: allow_inferred_rendering, **rendering, &block)
|
217
216
|
|
218
|
-
turbo_stream_action_tag name, target:
|
217
|
+
turbo_stream_action_tag name, target: target, template: template
|
219
218
|
end
|
220
219
|
|
221
220
|
# Send an action of the type <tt>name</tt> to <tt>targets</tt>. Options described in the concrete methods.
|
222
221
|
def action_all(name, targets, content = nil, allow_inferred_rendering: true, **rendering, &block)
|
223
|
-
targets_name = extract_target_name_from(targets)
|
224
222
|
template = render_template(targets, content, allow_inferred_rendering: allow_inferred_rendering, **rendering, &block)
|
225
223
|
|
226
|
-
turbo_stream_action_tag name, targets:
|
224
|
+
turbo_stream_action_tag name, targets: targets, template: template
|
227
225
|
end
|
228
226
|
|
229
227
|
private
|
230
|
-
def extract_target_name_from(target)
|
231
|
-
if target.respond_to?(:to_key)
|
232
|
-
ActionView::RecordIdentifier.dom_id(target)
|
233
|
-
else
|
234
|
-
target
|
235
|
-
end
|
236
|
-
end
|
237
|
-
|
238
228
|
def render_template(target, content = nil, allow_inferred_rendering: true, **rendering, &block)
|
239
229
|
case
|
240
230
|
when content
|
@@ -0,0 +1,9 @@
|
|
1
|
+
if (cable_config_path = Rails.root.join("config/cable.yml")).exist?
|
2
|
+
say "Enable redis in bundle"
|
3
|
+
uncomment_lines "Gemfile", %(gem 'redis')
|
4
|
+
|
5
|
+
say "Switch development cable to use redis"
|
6
|
+
gsub_file cable_config_path.to_s, /development:\n\s+adapter: async/, "development:\n adapter: redis\n url: redis://localhost:6379/1"
|
7
|
+
else
|
8
|
+
say 'ActionCable config file (config/cable.yml) is missing. Uncomment "gem \'redis\'" in your Gemfile and create config/cable.yml to use the Turbo Streams broadcast feature.'
|
9
|
+
end
|
@@ -1,15 +1,23 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
uncomment_lines "Gemfile", %(gem 'redis')
|
1
|
+
if (app_js_path = Rails.root.join("app/javascript/application.js")).exist?
|
2
|
+
say "Import turbo-rails in existing app/javascript/application.js"
|
3
|
+
append_to_file app_js_path, %(import "@hotwired/turbo-rails"\n)
|
4
|
+
else
|
5
|
+
say <<~INSTRUCTIONS, :red
|
6
|
+
You must import @hotwired/turbo-rails in your application.js.
|
7
|
+
INSTRUCTIONS
|
8
|
+
end
|
10
9
|
|
11
|
-
|
12
|
-
|
10
|
+
if (importmap_path = Rails.root.join("config/importmap.rb")).exist?
|
11
|
+
say "Pin @hotwired/turbo-rails in config/importmap.rb"
|
12
|
+
insert_into_file \
|
13
|
+
importmap_path.to_s,
|
14
|
+
%( pin "@hotwired/turbo-rails", to: "turbo.js"\n\n),
|
15
|
+
after: "Rails.application.config.importmap.draw do\n"
|
13
16
|
else
|
14
|
-
say
|
17
|
+
say <<~INSTRUCTIONS, :red
|
18
|
+
You must add @hotwired/turbo-rails to your importmap to reference them via ESM.
|
19
|
+
Example: pin "@hotwired/turbo-rails", to: "turbo.js"
|
20
|
+
INSTRUCTIONS
|
15
21
|
end
|
22
|
+
|
23
|
+
say "Run turbo:install:redis to switch on Redis and use it in development for turbo streams", :red
|
@@ -1,27 +1,7 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
ACTIVE_STORAGE_REGEX = /(import.*ActiveStorage|require.*@rails\/activestorage.*)/.freeze
|
4
|
-
CABLE_CONFIG_PATH = Rails.root.join("config/cable.yml")
|
5
|
-
|
6
|
-
abort "❌ Webpacker not found. Exiting." unless defined?(Webpacker::Engine)
|
1
|
+
say "Appending Turbo setup code to #{Webpacker.config.source_entry_path}/application.js"
|
2
|
+
append_to_file "#{Webpacker.config.source_entry_path}/application.js", %(\nimport "@hotwired/turbo-rails"\n)
|
7
3
|
|
8
4
|
say "Install Turbo"
|
9
5
|
run "yarn add @hotwired/turbo-rails"
|
10
|
-
insert_into_file "#{Webpacker.config.source_entry_path}/application.js", "import \"@hotwired/turbo-rails\"\n", before: ACTIVE_STORAGE_REGEX
|
11
|
-
|
12
|
-
say "Remove Turbolinks"
|
13
|
-
run "#{RbConfig.ruby} bin/bundle remove turbolinks"
|
14
|
-
run "#{RbConfig.ruby} bin/bundle", capture: true
|
15
|
-
run "#{RbConfig.ruby} bin/yarn remove turbolinks"
|
16
|
-
gsub_file "#{Webpacker.config.source_entry_path}/application.js", TURBOLINKS_REGEX, ''
|
17
|
-
gsub_file "#{Webpacker.config.source_entry_path}/application.js", /Turbolinks.start.*\n/, ''
|
18
|
-
|
19
|
-
if CABLE_CONFIG_PATH.exist?
|
20
|
-
say "Enable redis in bundle"
|
21
|
-
uncomment_lines "Gemfile", %(gem 'redis')
|
22
6
|
|
23
|
-
|
24
|
-
gsub_file CABLE_CONFIG_PATH.to_s, /development:\n\s+adapter: async/, "development:\n adapter: redis\n url: redis://localhost:6379/1"
|
25
|
-
else
|
26
|
-
say 'ActionCable config file (config/cable.yml) is missing. Uncomment "gem \'redis\'" in your Gemfile and create config/cable.yml to use the Turbo Streams broadcast feature.'
|
27
|
-
end
|
7
|
+
say "Run turbo:install:redis to switch on Redis and use it in development for turbo streams"
|
data/lib/tasks/turbo_tasks.rake
CHANGED
data/lib/turbo/engine.rb
CHANGED
@@ -26,12 +26,6 @@ module Turbo
|
|
26
26
|
end
|
27
27
|
end
|
28
28
|
|
29
|
-
initializer "turbo.importmap" do
|
30
|
-
if Rails.application.config.respond_to?(:importmap)
|
31
|
-
Rails.application.config.importmap.pin "@hotwired/turbo-rails", to: "turbo.js"
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
35
29
|
initializer "turbo.helpers", before: :load_config_initializers do
|
36
30
|
ActiveSupport.on_load(:action_controller_base) do
|
37
31
|
include Turbo::Streams::TurboStreamsTagBuilder, Turbo::Frames::FrameRequest, Turbo::Native::Navigation
|
data/lib/turbo/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: turbo-rails
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.7.
|
4
|
+
version: 0.7.7
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sam Stephenson
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2021-08-
|
13
|
+
date: 2021-08-26 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: rails
|
@@ -56,6 +56,7 @@ files:
|
|
56
56
|
- app/models/concerns/turbo/broadcastable.rb
|
57
57
|
- app/models/turbo/streams/tag_builder.rb
|
58
58
|
- config/routes.rb
|
59
|
+
- lib/install/turbo_needs_redis.rb
|
59
60
|
- lib/install/turbo_with_asset_pipeline.rb
|
60
61
|
- lib/install/turbo_with_webpacker.rb
|
61
62
|
- lib/tasks/turbo_tasks.rake
|