turbo-rails 0.5.12 → 0.7.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +6 -4
- data/app/assets/javascripts/turbo.js +152 -56
- data/app/channels/turbo/streams/broadcasts.rb +8 -0
- data/app/helpers/turbo/streams/action_helper.rb +12 -3
- data/app/models/concerns/turbo/broadcastable.rb +31 -2
- data/app/models/turbo/streams/tag_builder.rb +114 -11
- data/lib/install/turbo_with_asset_pipeline.rb +10 -26
- data/lib/install/turbo_with_webpacker.rb +9 -6
- data/lib/turbo/engine.rb +7 -1
- data/lib/turbo/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e401e540c8c64780f9984ed0d5120511777c47ca3b5d4c023f980469e10085e8
|
4
|
+
data.tar.gz: 44d76bd4452ec01a2580d58e828616f2b634d8cb29a4470f082dd1c4ece899b5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5ff10bfdaecd3c50008a3f9ac63976be979b5a1208c0f2cf39b786889156bdaef33e863ad5e6cdb45e2cf2e61acb8672e42a177326d8b0155e7da7d0bd559006
|
7
|
+
data.tar.gz: 77413a732f4014a257f724c117d47b07ebb3190dc70802348462a9bcd19c6a525e9d18a22c5342bab6f74b70f0f461b871e4c92a5650b5ec10ced95304c6e472
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Turbo
|
2
2
|
|
3
|
-
[Turbo](https://turbo.
|
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
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.
|
6
6
|
|
@@ -38,7 +38,7 @@ The JavaScript for Turbo can either be run through the asset pipeline, which is
|
|
38
38
|
2. Run `./bin/bundle install`
|
39
39
|
3. Run `./bin/rails turbo:install`
|
40
40
|
|
41
|
-
Running `turbo:install` will install through NPM if Webpacker is installed in the application. Otherwise the asset pipeline version is used.
|
41
|
+
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
42
|
|
43
43
|
If you're using Webpack 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)).
|
44
44
|
|
@@ -50,17 +50,19 @@ import "@hotwired/turbo-rails"
|
|
50
50
|
|
51
51
|
## Usage
|
52
52
|
|
53
|
-
You can watch [the video introduction to Hotwire](https://
|
53
|
+
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).
|
54
54
|
|
55
55
|
|
56
56
|
## Compatibility with Rails UJS
|
57
57
|
|
58
|
-
Turbo can coexist with Rails UJS, but you need to take a series of
|
58
|
+
Turbo can coexist with Rails UJS, but you need to take a series of upgrade steps to make it happen. See [the upgrading guide](https://github.com/hotwired/turbo-rails/blob/main/UPGRADING.md).
|
59
59
|
|
60
60
|
|
61
61
|
## Development
|
62
62
|
|
63
63
|
* To run the Rails tests: `bundle exec rake`.
|
64
|
+
* To install dependencies: `bundle install`
|
65
|
+
* To prepare the test database: `cd test/dummy; RAILS_ENV=test ./bin/rails db:migrate`
|
64
66
|
* To compile the JavaScript for the asset pipeline: `yarn build`
|
65
67
|
|
66
68
|
|
@@ -63,6 +63,11 @@ class FrameElement extends HTMLElement {
|
|
63
63
|
disconnectedCallback() {
|
64
64
|
this.delegate.disconnect();
|
65
65
|
}
|
66
|
+
reload() {
|
67
|
+
const {src: src} = this;
|
68
|
+
this.src = null;
|
69
|
+
this.src = src;
|
70
|
+
}
|
66
71
|
attributeChangedCallback(name) {
|
67
72
|
if (name == "loading") {
|
68
73
|
this.delegate.loadingStyleChanged();
|
@@ -144,8 +149,6 @@ function getAnchor(url) {
|
|
144
149
|
return url.hash.slice(1);
|
145
150
|
} else if (anchorMatch = url.href.match(/#(.*)$/)) {
|
146
151
|
return anchorMatch[1];
|
147
|
-
} else {
|
148
|
-
return "";
|
149
152
|
}
|
150
153
|
}
|
151
154
|
|
@@ -162,13 +165,13 @@ function isPrefixedBy(baseURL, url) {
|
|
162
165
|
return baseURL.href === expandURL(prefix).href || baseURL.href.startsWith(prefix);
|
163
166
|
}
|
164
167
|
|
168
|
+
function getRequestURL(url) {
|
169
|
+
const anchor = getAnchor(url);
|
170
|
+
return anchor != null ? url.href.slice(0, -(anchor.length + 1)) : url.href;
|
171
|
+
}
|
172
|
+
|
165
173
|
function toCacheKey(url) {
|
166
|
-
|
167
|
-
if (anchorLength < 2) {
|
168
|
-
return url.href;
|
169
|
-
} else {
|
170
|
-
return url.href.slice(0, -anchorLength);
|
171
|
-
}
|
174
|
+
return getRequestURL(url);
|
172
175
|
}
|
173
176
|
|
174
177
|
function urlsAreEqual(left, right) {
|
@@ -325,6 +328,7 @@ function fetchMethodFromString(method) {
|
|
325
328
|
class FetchRequest {
|
326
329
|
constructor(delegate, method, location, body = new URLSearchParams) {
|
327
330
|
this.abortController = new AbortController;
|
331
|
+
this.resolveRequestPromise = value => {};
|
328
332
|
this.delegate = delegate;
|
329
333
|
this.method = method;
|
330
334
|
this.headers = this.defaultHeaders;
|
@@ -351,11 +355,7 @@ class FetchRequest {
|
|
351
355
|
var _a, _b;
|
352
356
|
const {fetchOptions: fetchOptions} = this;
|
353
357
|
(_b = (_a = this.delegate).prepareHeadersForRequest) === null || _b === void 0 ? void 0 : _b.call(_a, this.headers, this);
|
354
|
-
|
355
|
-
detail: {
|
356
|
-
fetchOptions: fetchOptions
|
357
|
-
}
|
358
|
-
});
|
358
|
+
await this.allowRequestToBeIntercepted(fetchOptions);
|
359
359
|
try {
|
360
360
|
this.delegate.requestStarted(this);
|
361
361
|
const response = await fetch(this.url.href, fetchOptions);
|
@@ -405,6 +405,18 @@ class FetchRequest {
|
|
405
405
|
get abortSignal() {
|
406
406
|
return this.abortController.signal;
|
407
407
|
}
|
408
|
+
async allowRequestToBeIntercepted(fetchOptions) {
|
409
|
+
const requestInterception = new Promise((resolve => this.resolveRequestPromise = resolve));
|
410
|
+
const event = dispatch("turbo:before-fetch-request", {
|
411
|
+
cancelable: true,
|
412
|
+
detail: {
|
413
|
+
fetchOptions: fetchOptions,
|
414
|
+
url: this.url.href,
|
415
|
+
resume: this.resolveRequestPromise
|
416
|
+
}
|
417
|
+
});
|
418
|
+
if (event.defaultPrevented) await requestInterception;
|
419
|
+
}
|
408
420
|
}
|
409
421
|
|
410
422
|
function mergeFormDataEntries(url, entries) {
|
@@ -736,6 +748,8 @@ class FormInterceptor {
|
|
736
748
|
|
737
749
|
class View {
|
738
750
|
constructor(delegate, element) {
|
751
|
+
this.resolveRenderPromise = value => {};
|
752
|
+
this.resolveInterceptionPromise = value => {};
|
739
753
|
this.delegate = delegate;
|
740
754
|
this.element = element;
|
741
755
|
}
|
@@ -743,6 +757,7 @@ class View {
|
|
743
757
|
const element = this.snapshot.getElementForAnchor(anchor);
|
744
758
|
if (element) {
|
745
759
|
this.scrollToElement(element);
|
760
|
+
this.focusElement(element);
|
746
761
|
} else {
|
747
762
|
this.scrollToPosition({
|
748
763
|
x: 0,
|
@@ -750,9 +765,23 @@ class View {
|
|
750
765
|
});
|
751
766
|
}
|
752
767
|
}
|
768
|
+
scrollToAnchorFromLocation(location) {
|
769
|
+
this.scrollToAnchor(getAnchor(location));
|
770
|
+
}
|
753
771
|
scrollToElement(element) {
|
754
772
|
element.scrollIntoView();
|
755
773
|
}
|
774
|
+
focusElement(element) {
|
775
|
+
if (element instanceof HTMLElement) {
|
776
|
+
if (element.hasAttribute("tabindex")) {
|
777
|
+
element.focus();
|
778
|
+
} else {
|
779
|
+
element.setAttribute("tabindex", "-1");
|
780
|
+
element.focus();
|
781
|
+
element.removeAttribute("tabindex");
|
782
|
+
}
|
783
|
+
}
|
784
|
+
}
|
756
785
|
scrollToPosition({x: x, y: y}) {
|
757
786
|
this.scrollRoot.scrollTo(x, y);
|
758
787
|
}
|
@@ -760,20 +789,22 @@ class View {
|
|
760
789
|
return window;
|
761
790
|
}
|
762
791
|
async render(renderer) {
|
763
|
-
if (this.renderer) {
|
764
|
-
throw new Error("rendering is already in progress");
|
765
|
-
}
|
766
792
|
const {isPreview: isPreview, shouldRender: shouldRender, newSnapshot: snapshot} = renderer;
|
767
793
|
if (shouldRender) {
|
768
794
|
try {
|
795
|
+
this.renderPromise = new Promise((resolve => this.resolveRenderPromise = resolve));
|
769
796
|
this.renderer = renderer;
|
770
797
|
this.prepareToRenderSnapshot(renderer);
|
771
|
-
this.
|
798
|
+
const renderInterception = new Promise((resolve => this.resolveInterceptionPromise = resolve));
|
799
|
+
const immediateRender = this.delegate.allowsImmediateRender(snapshot, this.resolveInterceptionPromise);
|
800
|
+
if (!immediateRender) await renderInterception;
|
772
801
|
await this.renderSnapshot(renderer);
|
773
802
|
this.delegate.viewRenderedSnapshot(snapshot, isPreview);
|
774
803
|
this.finishRenderingSnapshot(renderer);
|
775
804
|
} finally {
|
776
805
|
delete this.renderer;
|
806
|
+
this.resolveRenderPromise(undefined);
|
807
|
+
delete this.renderPromise;
|
777
808
|
}
|
778
809
|
} else {
|
779
810
|
this.invalidate();
|
@@ -824,7 +855,7 @@ class LinkInterceptor {
|
|
824
855
|
if (this.delegate.shouldInterceptLinkClick(event.target, event.detail.url)) {
|
825
856
|
this.clickEvent.preventDefault();
|
826
857
|
event.preventDefault();
|
827
|
-
this.
|
858
|
+
this.delegate.linkClickIntercepted(event.target, event.detail.url);
|
828
859
|
}
|
829
860
|
}
|
830
861
|
delete this.clickEvent;
|
@@ -845,21 +876,6 @@ class LinkInterceptor {
|
|
845
876
|
document.removeEventListener("turbo:click", this.linkClicked);
|
846
877
|
document.removeEventListener("turbo:before-visit", this.willVisit);
|
847
878
|
}
|
848
|
-
convertLinkWithMethodClickToFormSubmission(link) {
|
849
|
-
var _a;
|
850
|
-
const linkMethod = link.getAttribute("data-turbo-method") || link.getAttribute("data-method");
|
851
|
-
if (linkMethod) {
|
852
|
-
const form = document.createElement("form");
|
853
|
-
form.method = linkMethod;
|
854
|
-
form.action = link.getAttribute("href") || "undefined";
|
855
|
-
(_a = link.parentNode) === null || _a === void 0 ? void 0 : _a.insertBefore(form, link);
|
856
|
-
return dispatch("submit", {
|
857
|
-
target: form
|
858
|
-
});
|
859
|
-
} else {
|
860
|
-
return false;
|
861
|
-
}
|
862
|
-
}
|
863
879
|
respondsToEventTarget(target) {
|
864
880
|
const element = target instanceof Element ? target : target instanceof Node ? target.parentElement : null;
|
865
881
|
return element && element.closest("turbo-frame, html") == this.element;
|
@@ -1150,7 +1166,7 @@ ProgressBar.animationDuration = 300;
|
|
1150
1166
|
class HeadSnapshot extends Snapshot {
|
1151
1167
|
constructor() {
|
1152
1168
|
super(...arguments);
|
1153
|
-
this.detailsByOuterHTML = this.children.reduce(((result, element) => {
|
1169
|
+
this.detailsByOuterHTML = this.children.filter((element => !elementIsNoscript(element))).reduce(((result, element) => {
|
1154
1170
|
const {outerHTML: outerHTML} = element;
|
1155
1171
|
const details = outerHTML in result ? result[outerHTML] : {
|
1156
1172
|
type: elementType(element),
|
@@ -1217,6 +1233,11 @@ function elementIsScript(element) {
|
|
1217
1233
|
return tagName == "script";
|
1218
1234
|
}
|
1219
1235
|
|
1236
|
+
function elementIsNoscript(element) {
|
1237
|
+
const tagName = element.tagName.toLowerCase();
|
1238
|
+
return tagName == "noscript";
|
1239
|
+
}
|
1240
|
+
|
1220
1241
|
function elementIsStylesheet(element) {
|
1221
1242
|
const tagName = element.tagName.toLowerCase();
|
1222
1243
|
return tagName == "style" || tagName == "link" && element.getAttribute("rel") == "stylesheet";
|
@@ -1319,6 +1340,7 @@ class Visit {
|
|
1319
1340
|
this.referrer = referrer;
|
1320
1341
|
this.snapshotHTML = snapshotHTML;
|
1321
1342
|
this.response = response;
|
1343
|
+
this.isSamePage = this.delegate.locationWithActionIsSamePage(this.location, this.action);
|
1322
1344
|
}
|
1323
1345
|
get adapter() {
|
1324
1346
|
return this.delegate.adapter;
|
@@ -1411,6 +1433,7 @@ class Visit {
|
|
1411
1433
|
const {statusCode: statusCode, responseHTML: responseHTML} = this.response;
|
1412
1434
|
this.render((async () => {
|
1413
1435
|
this.cacheSnapshot();
|
1436
|
+
if (this.view.renderPromise) await this.view.renderPromise;
|
1414
1437
|
if (isSuccessful(statusCode) && responseHTML != null) {
|
1415
1438
|
await this.view.renderPage(PageSnapshot.fromHTMLString(responseHTML));
|
1416
1439
|
this.adapter.visitRendered(this);
|
@@ -1445,10 +1468,15 @@ class Visit {
|
|
1445
1468
|
const isPreview = this.shouldIssueRequest();
|
1446
1469
|
this.render((async () => {
|
1447
1470
|
this.cacheSnapshot();
|
1448
|
-
|
1449
|
-
|
1450
|
-
|
1451
|
-
this.
|
1471
|
+
if (this.isSamePage) {
|
1472
|
+
this.adapter.visitRendered(this);
|
1473
|
+
} else {
|
1474
|
+
if (this.view.renderPromise) await this.view.renderPromise;
|
1475
|
+
await this.view.renderPage(snapshot, isPreview);
|
1476
|
+
this.adapter.visitRendered(this);
|
1477
|
+
if (!isPreview) {
|
1478
|
+
this.complete();
|
1479
|
+
}
|
1452
1480
|
}
|
1453
1481
|
}));
|
1454
1482
|
}
|
@@ -1460,6 +1488,14 @@ class Visit {
|
|
1460
1488
|
this.followedRedirect = true;
|
1461
1489
|
}
|
1462
1490
|
}
|
1491
|
+
goToSamePageAnchor() {
|
1492
|
+
if (this.isSamePage) {
|
1493
|
+
this.render((async () => {
|
1494
|
+
this.cacheSnapshot();
|
1495
|
+
this.adapter.visitRendered(this);
|
1496
|
+
}));
|
1497
|
+
}
|
1498
|
+
}
|
1463
1499
|
requestStarted() {
|
1464
1500
|
this.startRequest();
|
1465
1501
|
}
|
@@ -1502,10 +1538,13 @@ class Visit {
|
|
1502
1538
|
performScroll() {
|
1503
1539
|
if (!this.scrolled) {
|
1504
1540
|
if (this.action == "restore") {
|
1505
|
-
this.scrollToRestoredPosition() || this.scrollToTop();
|
1541
|
+
this.scrollToRestoredPosition() || this.scrollToAnchor() || this.scrollToTop();
|
1506
1542
|
} else {
|
1507
1543
|
this.scrollToAnchor() || this.scrollToTop();
|
1508
1544
|
}
|
1545
|
+
if (this.isSamePage) {
|
1546
|
+
this.delegate.visitScrolledToSamePageLocation(this.view.lastRenderedLocation, this.location);
|
1547
|
+
}
|
1509
1548
|
this.scrolled = true;
|
1510
1549
|
}
|
1511
1550
|
}
|
@@ -1517,8 +1556,9 @@ class Visit {
|
|
1517
1556
|
}
|
1518
1557
|
}
|
1519
1558
|
scrollToAnchor() {
|
1520
|
-
|
1521
|
-
|
1559
|
+
const anchor = getAnchor(this.location);
|
1560
|
+
if (anchor != null) {
|
1561
|
+
this.view.scrollToAnchor(anchor);
|
1522
1562
|
return true;
|
1523
1563
|
}
|
1524
1564
|
}
|
@@ -1548,7 +1588,13 @@ class Visit {
|
|
1548
1588
|
return typeof this.response == "object";
|
1549
1589
|
}
|
1550
1590
|
shouldIssueRequest() {
|
1551
|
-
|
1591
|
+
if (this.isSamePage) {
|
1592
|
+
return false;
|
1593
|
+
} else if (this.action == "restore") {
|
1594
|
+
return !this.hasCachedSnapshot();
|
1595
|
+
} else {
|
1596
|
+
return true;
|
1597
|
+
}
|
1552
1598
|
}
|
1553
1599
|
cacheSnapshot() {
|
1554
1600
|
if (!this.snapshotCached) {
|
@@ -1561,7 +1607,7 @@ class Visit {
|
|
1561
1607
|
await new Promise((resolve => {
|
1562
1608
|
this.frame = requestAnimationFrame((() => resolve()));
|
1563
1609
|
}));
|
1564
|
-
callback();
|
1610
|
+
await callback();
|
1565
1611
|
delete this.frame;
|
1566
1612
|
this.performScroll();
|
1567
1613
|
}
|
@@ -1591,6 +1637,7 @@ class BrowserAdapter {
|
|
1591
1637
|
visitStarted(visit) {
|
1592
1638
|
visit.issueRequest();
|
1593
1639
|
visit.changeHistory();
|
1640
|
+
visit.goToSamePageAnchor();
|
1594
1641
|
visit.loadCachedSnapshot();
|
1595
1642
|
}
|
1596
1643
|
visitRequestStarted(visit) {
|
@@ -1968,6 +2015,12 @@ class Navigator {
|
|
1968
2015
|
visitCompleted(visit) {
|
1969
2016
|
this.delegate.visitCompleted(visit);
|
1970
2017
|
}
|
2018
|
+
locationWithActionIsSamePage(location, action) {
|
2019
|
+
return getRequestURL(location) === getRequestURL(this.view.lastRenderedLocation) && (getAnchor(location) != null || action == "restore");
|
2020
|
+
}
|
2021
|
+
visitScrolledToSamePageLocation(oldURL, newURL) {
|
2022
|
+
this.delegate.visitScrolledToSamePageLocation(oldURL, newURL);
|
2023
|
+
}
|
1971
2024
|
get location() {
|
1972
2025
|
return this.history.location;
|
1973
2026
|
}
|
@@ -2318,7 +2371,7 @@ class PageView extends View {
|
|
2318
2371
|
}
|
2319
2372
|
renderError(snapshot) {
|
2320
2373
|
const renderer = new ErrorRenderer(this.snapshot, snapshot, false);
|
2321
|
-
this.render(renderer);
|
2374
|
+
return this.render(renderer);
|
2322
2375
|
}
|
2323
2376
|
clearSnapshotCache() {
|
2324
2377
|
this.snapshotCache.clear();
|
@@ -2436,10 +2489,26 @@ class Session {
|
|
2436
2489
|
}
|
2437
2490
|
followedLinkToLocation(link, location) {
|
2438
2491
|
const action = this.getActionForLink(link);
|
2439
|
-
this.visit(location.href, {
|
2492
|
+
this.convertLinkWithMethodClickToFormSubmission(link) || this.visit(location.href, {
|
2440
2493
|
action: action
|
2441
2494
|
});
|
2442
2495
|
}
|
2496
|
+
convertLinkWithMethodClickToFormSubmission(link) {
|
2497
|
+
var _a;
|
2498
|
+
const linkMethod = link.getAttribute("data-turbo-method");
|
2499
|
+
if (linkMethod) {
|
2500
|
+
const form = document.createElement("form");
|
2501
|
+
form.method = linkMethod;
|
2502
|
+
form.action = link.getAttribute("href") || "undefined";
|
2503
|
+
(_a = link.parentNode) === null || _a === void 0 ? void 0 : _a.insertBefore(form, link);
|
2504
|
+
return dispatch("submit", {
|
2505
|
+
cancelable: true,
|
2506
|
+
target: form
|
2507
|
+
});
|
2508
|
+
} else {
|
2509
|
+
return false;
|
2510
|
+
}
|
2511
|
+
}
|
2443
2512
|
allowsVisitingLocation(location) {
|
2444
2513
|
return this.applicationAllowsVisitingLocation(location);
|
2445
2514
|
}
|
@@ -2449,11 +2518,17 @@ class Session {
|
|
2449
2518
|
}
|
2450
2519
|
visitStarted(visit) {
|
2451
2520
|
extendURLWithDeprecatedProperties(visit.location);
|
2452
|
-
this.notifyApplicationAfterVisitingLocation(visit.location);
|
2521
|
+
this.notifyApplicationAfterVisitingLocation(visit.location, visit.action);
|
2453
2522
|
}
|
2454
2523
|
visitCompleted(visit) {
|
2455
2524
|
this.notifyApplicationAfterPageLoad(visit.getTimingMetrics());
|
2456
2525
|
}
|
2526
|
+
locationWithActionIsSamePage(location, action) {
|
2527
|
+
return this.navigator.locationWithActionIsSamePage(location, action);
|
2528
|
+
}
|
2529
|
+
visitScrolledToSamePageLocation(oldURL, newURL) {
|
2530
|
+
this.notifyApplicationAfterVisitingSamePageLocation(oldURL, newURL);
|
2531
|
+
}
|
2457
2532
|
willSubmitForm(form, submitter) {
|
2458
2533
|
return elementIsNavigable(form) && elementIsNavigable(submitter);
|
2459
2534
|
}
|
@@ -2476,8 +2551,9 @@ class Session {
|
|
2476
2551
|
viewWillCacheSnapshot() {
|
2477
2552
|
this.notifyApplicationBeforeCachingSnapshot();
|
2478
2553
|
}
|
2479
|
-
|
2480
|
-
this.notifyApplicationBeforeRender(element);
|
2554
|
+
allowsImmediateRender({element: element}, resume) {
|
2555
|
+
const event = this.notifyApplicationBeforeRender(element, resume);
|
2556
|
+
return !event.defaultPrevented;
|
2481
2557
|
}
|
2482
2558
|
viewRenderedSnapshot(snapshot, isPreview) {
|
2483
2559
|
this.view.lastRenderedLocation = this.history.location;
|
@@ -2511,21 +2587,24 @@ class Session {
|
|
2511
2587
|
cancelable: true
|
2512
2588
|
});
|
2513
2589
|
}
|
2514
|
-
notifyApplicationAfterVisitingLocation(location) {
|
2590
|
+
notifyApplicationAfterVisitingLocation(location, action) {
|
2515
2591
|
return dispatch("turbo:visit", {
|
2516
2592
|
detail: {
|
2517
|
-
url: location.href
|
2593
|
+
url: location.href,
|
2594
|
+
action: action
|
2518
2595
|
}
|
2519
2596
|
});
|
2520
2597
|
}
|
2521
2598
|
notifyApplicationBeforeCachingSnapshot() {
|
2522
2599
|
return dispatch("turbo:before-cache");
|
2523
2600
|
}
|
2524
|
-
notifyApplicationBeforeRender(newBody) {
|
2601
|
+
notifyApplicationBeforeRender(newBody, resume) {
|
2525
2602
|
return dispatch("turbo:before-render", {
|
2526
2603
|
detail: {
|
2527
|
-
newBody: newBody
|
2528
|
-
|
2604
|
+
newBody: newBody,
|
2605
|
+
resume: resume
|
2606
|
+
},
|
2607
|
+
cancelable: true
|
2529
2608
|
});
|
2530
2609
|
}
|
2531
2610
|
notifyApplicationAfterRender() {
|
@@ -2539,6 +2618,12 @@ class Session {
|
|
2539
2618
|
}
|
2540
2619
|
});
|
2541
2620
|
}
|
2621
|
+
notifyApplicationAfterVisitingSamePageLocation(oldURL, newURL) {
|
2622
|
+
dispatchEvent(new HashChangeEvent("hashchange", {
|
2623
|
+
oldURL: oldURL.toString(),
|
2624
|
+
newURL: newURL.toString()
|
2625
|
+
}));
|
2626
|
+
}
|
2542
2627
|
getActionForLink(link) {
|
2543
2628
|
const action = link.getAttribute("data-turbo-action");
|
2544
2629
|
return isAction(action) ? action : "advance";
|
@@ -2648,6 +2733,7 @@ class FrameController {
|
|
2648
2733
|
const {body: body} = parseHTMLDocument(html);
|
2649
2734
|
const snapshot = new Snapshot(await this.extractForeignFrameElement(body));
|
2650
2735
|
const renderer = new FrameRenderer(this.view.snapshot, snapshot, false);
|
2736
|
+
if (this.view.renderPromise) await this.view.renderPromise;
|
2651
2737
|
await this.view.render(renderer);
|
2652
2738
|
}
|
2653
2739
|
} catch (error) {
|
@@ -2659,7 +2745,11 @@ class FrameController {
|
|
2659
2745
|
this.loadSourceURL();
|
2660
2746
|
}
|
2661
2747
|
shouldInterceptLinkClick(element, url) {
|
2662
|
-
|
2748
|
+
if (element.hasAttribute("data-turbo-method")) {
|
2749
|
+
return false;
|
2750
|
+
} else {
|
2751
|
+
return this.shouldInterceptNavigation(element);
|
2752
|
+
}
|
2663
2753
|
}
|
2664
2754
|
linkClickIntercepted(element, url) {
|
2665
2755
|
this.navigateFrame(element, url);
|
@@ -2722,7 +2812,9 @@ class FrameController {
|
|
2722
2812
|
const frame = this.findFrameElement(formSubmission.formElement);
|
2723
2813
|
frame.removeAttribute("busy");
|
2724
2814
|
}
|
2725
|
-
|
2815
|
+
allowsImmediateRender(snapshot, resume) {
|
2816
|
+
return true;
|
2817
|
+
}
|
2726
2818
|
viewRenderedSnapshot(snapshot, isPreview) {}
|
2727
2819
|
viewInvalidated() {}
|
2728
2820
|
async visit(url) {
|
@@ -2988,7 +3080,7 @@ customElements.define("turbo-stream", StreamElement);
|
|
2988
3080
|
|
2989
3081
|
Load your application’s JavaScript bundle inside the <head> element instead. <script> elements in <body> are evaluated with each page change.
|
2990
3082
|
|
2991
|
-
For more information, see: https://turbo.
|
3083
|
+
For more information, see: https://turbo.hotwired.dev/handbook/building#working-with-script-elements
|
2992
3084
|
|
2993
3085
|
——
|
2994
3086
|
Suppress this warning by adding a "data-turbo-suppress-warning" attribute to: %s
|
@@ -3036,6 +3128,8 @@ function setProgressBarDelay(delay) {
|
|
3036
3128
|
var Turbo = Object.freeze({
|
3037
3129
|
__proto__: null,
|
3038
3130
|
navigator: navigator,
|
3131
|
+
PageRenderer: PageRenderer,
|
3132
|
+
PageSnapshot: PageSnapshot,
|
3039
3133
|
start: start,
|
3040
3134
|
registerAdapter: registerAdapter,
|
3041
3135
|
visit: visit,
|
@@ -3052,6 +3146,8 @@ start();
|
|
3052
3146
|
|
3053
3147
|
var turbo_es2017Esm = Object.freeze({
|
3054
3148
|
__proto__: null,
|
3149
|
+
PageRenderer: PageRenderer,
|
3150
|
+
PageSnapshot: PageSnapshot,
|
3055
3151
|
clearCache: clearCache,
|
3056
3152
|
connectStreamSource: connectStreamSource,
|
3057
3153
|
disconnectStreamSource: disconnectStreamSource,
|
@@ -13,6 +13,10 @@ module Turbo::Streams::Broadcasts
|
|
13
13
|
broadcast_action_to *streamables, action: :replace, target: target, **rendering
|
14
14
|
end
|
15
15
|
|
16
|
+
def broadcast_update_to(*streamables, target:, **rendering)
|
17
|
+
broadcast_action_to *streamables, action: :update, target: target, **rendering
|
18
|
+
end
|
19
|
+
|
16
20
|
def broadcast_before_to(*streamables, target:, **rendering)
|
17
21
|
broadcast_action_to *streamables, action: :before, target: target, **rendering
|
18
22
|
end
|
@@ -40,6 +44,10 @@ module Turbo::Streams::Broadcasts
|
|
40
44
|
broadcast_action_later_to *streamables, action: :replace, target: target, **rendering
|
41
45
|
end
|
42
46
|
|
47
|
+
def broadcast_update_later_to(*streamables, target:, **rendering)
|
48
|
+
broadcast_action_later_to *streamables, action: :update, target: target, **rendering
|
49
|
+
end
|
50
|
+
|
43
51
|
def broadcast_before_later_to(*streamables, target:, **rendering)
|
44
52
|
broadcast_action_later_to *streamables, action: :before, target: target, **rendering
|
45
53
|
end
|
@@ -6,11 +6,20 @@ module Turbo::Streams::ActionHelper
|
|
6
6
|
#
|
7
7
|
# turbo_stream_action_tag "replace", target: "message_1", template: %(<div id="message_1">Hello!</div>)
|
8
8
|
# # => <turbo-stream action="replace" target="message_1"><template><div id="message_1">Hello!</div></template></turbo-stream>
|
9
|
-
|
10
|
-
|
9
|
+
#
|
10
|
+
# turbo_stream_action_tag "replace", targets: "message_1", template: %(<div id="message_1">Hello!</div>)
|
11
|
+
# # => <turbo-stream action="replace" targets="message_1"><template><div id="message_1">Hello!</div></template></turbo-stream>
|
12
|
+
def turbo_stream_action_tag(action, target: nil, targets: nil, template: nil)
|
11
13
|
template = action.to_sym == :remove ? "" : "<template>#{template}</template>"
|
12
14
|
|
13
|
-
|
15
|
+
if target
|
16
|
+
target = convert_to_turbo_stream_dom_id(target)
|
17
|
+
%(<turbo-stream action="#{action}" target="#{target}">#{template}</turbo-stream>).html_safe
|
18
|
+
elsif targets
|
19
|
+
%(<turbo-stream action="#{action}" targets="#{targets}">#{template}</turbo-stream>).html_safe
|
20
|
+
else
|
21
|
+
raise ArgumentError, "target or targets must be supplied"
|
22
|
+
end
|
14
23
|
end
|
15
24
|
|
16
25
|
private
|
@@ -75,8 +75,8 @@ module Turbo::Broadcastable
|
|
75
75
|
#
|
76
76
|
# # Sends <turbo-stream action="remove" target="clearance_5"></turbo-stream> to the stream named "identity:2:clearances"
|
77
77
|
# clearance.broadcast_remove_to examiner.identity, :clearances
|
78
|
-
def broadcast_remove_to(*streamables)
|
79
|
-
Turbo::StreamsChannel.broadcast_remove_to *streamables, target:
|
78
|
+
def broadcast_remove_to(*streamables, target: self)
|
79
|
+
Turbo::StreamsChannel.broadcast_remove_to *streamables, target: target
|
80
80
|
end
|
81
81
|
|
82
82
|
# Same as <tt>#broadcast_remove_to</tt>, but the designated stream is automatically set to the current model.
|
@@ -103,6 +103,25 @@ module Turbo::Broadcastable
|
|
103
103
|
broadcast_replace_to self, **rendering
|
104
104
|
end
|
105
105
|
|
106
|
+
# Update this broadcastable model in the dom for subscribers of the stream name identified by the passed
|
107
|
+
# <tt>streamables</tt>. The rendering parameters can be set by appending named arguments to the call. Examples:
|
108
|
+
#
|
109
|
+
# # Sends <turbo-stream action="update" target="clearance_5"><template><div id="clearance_5">My Clearance</div></template></turbo-stream>
|
110
|
+
# # to the stream named "identity:2:clearances"
|
111
|
+
# clearance.broadcast_update_to examiner.identity, :clearances
|
112
|
+
#
|
113
|
+
# # Sends <turbo-stream action="update" target="clearance_5"><template><div id="clearance_5">Other partial</div></template></turbo-stream>
|
114
|
+
# # to the stream named "identity:2:clearances"
|
115
|
+
# clearance.broadcast_update_to examiner.identity, :clearances, partial: "clearances/other_partial", locals: { a: 1 }
|
116
|
+
def broadcast_update_to(*streamables, **rendering)
|
117
|
+
Turbo::StreamsChannel.broadcast_update_to *streamables, target: self, **broadcast_rendering_with_defaults(rendering)
|
118
|
+
end
|
119
|
+
|
120
|
+
# Same as <tt>#broadcast_update_to</tt>, but the designated stream is automatically set to the current model.
|
121
|
+
def broadcast_update(**rendering)
|
122
|
+
broadcast_update_to self, **rendering
|
123
|
+
end
|
124
|
+
|
106
125
|
# Insert a rendering of this broadcastable model before the target identified by it's dom id passed as <tt>target</tt>
|
107
126
|
# for subscribers of the stream name identified by the passed <tt>streamables</tt>. The rendering parameters can be set by
|
108
127
|
# appending named arguments to the call. Examples:
|
@@ -202,6 +221,16 @@ module Turbo::Broadcastable
|
|
202
221
|
broadcast_replace_later_to self, **rendering
|
203
222
|
end
|
204
223
|
|
224
|
+
# Same as <tt>broadcast_update_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
|
225
|
+
def broadcast_update_later_to(*streamables, **rendering)
|
226
|
+
Turbo::StreamsChannel.broadcast_update_later_to *streamables, target: self, **broadcast_rendering_with_defaults(rendering)
|
227
|
+
end
|
228
|
+
|
229
|
+
# Same as <tt>#broadcast_update_later_to</tt>, but the designated stream is automatically set to the current model.
|
230
|
+
def broadcast_update_later(**rendering)
|
231
|
+
broadcast_update_later_to self, **rendering
|
232
|
+
end
|
233
|
+
|
205
234
|
# Same as <tt>broadcast_append_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
|
206
235
|
def broadcast_append_later_to(*streamables, target: broadcast_target_default, **rendering)
|
207
236
|
Turbo::StreamsChannel.broadcast_append_later_to *streamables, target: target, **broadcast_rendering_with_defaults(rendering)
|
@@ -40,6 +40,16 @@ class Turbo::Streams::TagBuilder
|
|
40
40
|
action :remove, target, allow_inferred_rendering: false
|
41
41
|
end
|
42
42
|
|
43
|
+
# Removes the <tt>targets</tt> from the dom. The targets can either be a CSS selector string or an object that responds to
|
44
|
+
# <tt>to_key</tt>, which is then called and passed through <tt>ActionView::RecordIdentifier.dom_id</tt> (all Active Records
|
45
|
+
# do). Examples:
|
46
|
+
#
|
47
|
+
# <%= turbo_stream.remove_all ".clearance_item" %>
|
48
|
+
# <%= turbo_stream.remove_all clearance %>
|
49
|
+
def remove_all(targets)
|
50
|
+
action_all :remove, targets, allow_inferred_rendering: false
|
51
|
+
end
|
52
|
+
|
43
53
|
# Replace the <tt>target</tt> in the dom with the either the <tt>content</tt> passed in, a rendering result determined
|
44
54
|
# by the <tt>rendering</tt> keyword arguments, the content in the block, or the rendering of the target as a record. Examples:
|
45
55
|
#
|
@@ -53,6 +63,19 @@ class Turbo::Streams::TagBuilder
|
|
53
63
|
action :replace, target, content, **rendering, &block
|
54
64
|
end
|
55
65
|
|
66
|
+
# Replace the <tt>targets</tt> in the dom with the either the <tt>content</tt> passed in, a rendering result determined
|
67
|
+
# by the <tt>rendering</tt> keyword arguments, the content in the block, or the rendering of the target as a record. Examples:
|
68
|
+
#
|
69
|
+
# <%= turbo_stream.replace_all ".clearance_item", "<div class='clearance_item'>Replace the dom target identified by the class clearance_item</div>" %>
|
70
|
+
# <%= turbo_stream.replace_all clearance %>
|
71
|
+
# <%= turbo_stream.replace_all clearance, partial: "clearances/clearance", locals: { title: "Hello" } %>
|
72
|
+
# <%= turbo_stream.replace_all ".clearance_item" do %>
|
73
|
+
# <div class='.clearance_item'>Replace the dom target identified by the class clearance_item</div>
|
74
|
+
# <% end %>
|
75
|
+
def replace_all(targets, content = nil, **rendering, &block)
|
76
|
+
action_all :replace, targets, content, **rendering, &block
|
77
|
+
end
|
78
|
+
|
56
79
|
# Insert the <tt>content</tt> passed in, a rendering result determined by the <tt>rendering</tt> keyword arguments,
|
57
80
|
# the content in the block, or the rendering of the target as a record before the <tt>target</tt> in the dom. Examples:
|
58
81
|
#
|
@@ -66,6 +89,19 @@ class Turbo::Streams::TagBuilder
|
|
66
89
|
action :before, target, content, **rendering, &block
|
67
90
|
end
|
68
91
|
|
92
|
+
# Insert the <tt>content</tt> passed in, a rendering result determined by the <tt>rendering</tt> keyword arguments,
|
93
|
+
# the content in the block, or the rendering of the target as a record before the <tt>targets</tt> in the dom. Examples:
|
94
|
+
#
|
95
|
+
# <%= turbo_stream.before_all ".clearance_item", "<div class='clearance_item'>Insert before the dom target identified by the class clearance_item</div>" %>
|
96
|
+
# <%= turbo_stream.before_all clearance %>
|
97
|
+
# <%= turbo_stream.before_all clearance, partial: "clearances/clearance", locals: { title: "Hello" } %>
|
98
|
+
# <%= turbo_stream.before_all ".clearance_item" do %>
|
99
|
+
# <div class='clearance_item'>Insert before the dom target identified by clearance_item</div>
|
100
|
+
# <% end %>
|
101
|
+
def before_all(targets, content = nil, **rendering, &block)
|
102
|
+
action_all :before, targets, content, **rendering, &block
|
103
|
+
end
|
104
|
+
|
69
105
|
# Insert the <tt>content</tt> passed in, a rendering result determined by the <tt>rendering</tt> keyword arguments,
|
70
106
|
# the content in the block, or the rendering of the target as a record after the <tt>target</tt> in the dom. Examples:
|
71
107
|
#
|
@@ -79,6 +115,19 @@ class Turbo::Streams::TagBuilder
|
|
79
115
|
action :after, target, content, **rendering, &block
|
80
116
|
end
|
81
117
|
|
118
|
+
# Insert the <tt>content</tt> passed in, a rendering result determined by the <tt>rendering</tt> keyword arguments,
|
119
|
+
# the content in the block, or the rendering of the target as a record after the <tt>targets</tt> in the dom. Examples:
|
120
|
+
#
|
121
|
+
# <%= turbo_stream.after_all ".clearance_item", "<div class='clearance_item'>Insert after the dom target identified by the class clearance_item</div>" %>
|
122
|
+
# <%= turbo_stream.after_all clearance %>
|
123
|
+
# <%= turbo_stream.after_all clearance, partial: "clearances/clearance", locals: { title: "Hello" } %>
|
124
|
+
# <%= turbo_stream.after_all "clearance_item" do %>
|
125
|
+
# <div class='clearance_item'>Insert after the dom target identified by the class clearance_item</div>
|
126
|
+
# <% end %>
|
127
|
+
def after_all(targets, content = nil, **rendering, &block)
|
128
|
+
action_all :after, targets, content, **rendering, &block
|
129
|
+
end
|
130
|
+
|
82
131
|
# Update the <tt>target</tt> in the dom with the either the <tt>content</tt> passed in or a rendering result determined
|
83
132
|
# by the <tt>rendering</tt> keyword arguments, the content in the block, or the rendering of the target as a record. Examples:
|
84
133
|
#
|
@@ -92,6 +141,19 @@ class Turbo::Streams::TagBuilder
|
|
92
141
|
action :update, target, content, **rendering, &block
|
93
142
|
end
|
94
143
|
|
144
|
+
# Update the <tt>targets</tt> in the dom with the either the <tt>content</tt> passed in or a rendering result determined
|
145
|
+
# by the <tt>rendering</tt> keyword arguments, the content in the block, or the rendering of the targets as a record. Examples:
|
146
|
+
#
|
147
|
+
# <%= turbo_stream.update_all "clearance_item", "Update the content of the dom target identified by the class clearance_item" %>
|
148
|
+
# <%= turbo_stream.update_all clearance %>
|
149
|
+
# <%= turbo_stream.update_all clearance, partial: "clearances/new_clearance", locals: { title: "Hello" } %>
|
150
|
+
# <%= turbo_stream.update_all "clearance_item" do %>
|
151
|
+
# Update the content of the dom target identified by the class clearance_item
|
152
|
+
# <% end %>
|
153
|
+
def update_all(targets, content = nil, **rendering, &block)
|
154
|
+
action_all :update, targets, content, **rendering, &block
|
155
|
+
end
|
156
|
+
|
95
157
|
# Append to the target in the dom identified with <tt>target</tt> either the <tt>content</tt> passed in or a
|
96
158
|
# rendering result determined by the <tt>rendering</tt> keyword arguments, the content in the block,
|
97
159
|
# or the rendering of the content as a record. Examples:
|
@@ -106,6 +168,20 @@ class Turbo::Streams::TagBuilder
|
|
106
168
|
action :append, target, content, **rendering, &block
|
107
169
|
end
|
108
170
|
|
171
|
+
# Append to the targets in the dom identified with <tt>targets</tt> either the <tt>content</tt> passed in or a
|
172
|
+
# rendering result determined by the <tt>rendering</tt> keyword arguments, the content in the block,
|
173
|
+
# or the rendering of the content as a record. Examples:
|
174
|
+
#
|
175
|
+
# <%= turbo_stream.append_all ".clearances", "<div class='clearance_item'>Append this to .clearance_group</div>" %>
|
176
|
+
# <%= turbo_stream.append_all ".clearances", clearance %>
|
177
|
+
# <%= turbo_stream.append_all ".clearances", partial: "clearances/new_clearance", locals: { clearance: clearance } %>
|
178
|
+
# <%= turbo_stream.append_all ".clearances" do %>
|
179
|
+
# <div id='clearance_item'>Append this to .clearances</div>
|
180
|
+
# <% end %>
|
181
|
+
def append_all(targets, content = nil, **rendering, &block)
|
182
|
+
action_all :append, targets, content, **rendering, &block
|
183
|
+
end
|
184
|
+
|
109
185
|
# Prepend to the target in the dom identified with <tt>target</tt> either the <tt>content</tt> passed in or a
|
110
186
|
# rendering result determined by the <tt>rendering</tt> keyword arguments or the content in the block,
|
111
187
|
# or the rendering of the content as a record. Examples:
|
@@ -120,20 +196,34 @@ class Turbo::Streams::TagBuilder
|
|
120
196
|
action :prepend, target, content, **rendering, &block
|
121
197
|
end
|
122
198
|
|
123
|
-
#
|
199
|
+
# Prepend to the targets in the dom identified with <tt>targets</tt> either the <tt>content</tt> passed in or a
|
200
|
+
# rendering result determined by the <tt>rendering</tt> keyword arguments or the content in the block,
|
201
|
+
# or the rendering of the content as a record. Examples:
|
202
|
+
#
|
203
|
+
# <%= turbo_stream.prepend_all ".clearances", "<div class='clearance_item'>Prepend this to .clearances</div>" %>
|
204
|
+
# <%= turbo_stream.prepend_all ".clearances", clearance %>
|
205
|
+
# <%= turbo_stream.prepend_all ".clearances", partial: "clearances/new_clearance", locals: { clearance: clearance } %>
|
206
|
+
# <%= turbo_stream.prepend_all ".clearances" do %>
|
207
|
+
# <div class='clearance_item'>Prepend this to .clearances</div>
|
208
|
+
# <% end %>
|
209
|
+
def prepend_all(targets, content = nil, **rendering, &block)
|
210
|
+
action_all :prepend, targets, content, **rendering, &block
|
211
|
+
end
|
212
|
+
|
213
|
+
# Send an action of the type <tt>name</tt> to <tt>target</tt>. Options described in the concrete methods.
|
124
214
|
def action(name, target, content = nil, allow_inferred_rendering: true, **rendering, &block)
|
125
215
|
target_name = extract_target_name_from(target)
|
216
|
+
template = render_template(target, content, allow_inferred_rendering: allow_inferred_rendering, **rendering, &block)
|
126
217
|
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
end
|
218
|
+
turbo_stream_action_tag name, target: target_name, template: template
|
219
|
+
end
|
220
|
+
|
221
|
+
# Send an action of the type <tt>name</tt> to <tt>targets</tt>. Options described in the concrete methods.
|
222
|
+
def action_all(name, targets, content = nil, allow_inferred_rendering: true, **rendering, &block)
|
223
|
+
targets_name = extract_target_name_from(targets)
|
224
|
+
template = render_template(targets, content, allow_inferred_rendering: allow_inferred_rendering, **rendering, &block)
|
225
|
+
|
226
|
+
turbo_stream_action_tag name, targets: targets_name, template: template
|
137
227
|
end
|
138
228
|
|
139
229
|
private
|
@@ -145,6 +235,19 @@ class Turbo::Streams::TagBuilder
|
|
145
235
|
end
|
146
236
|
end
|
147
237
|
|
238
|
+
def render_template(target, content = nil, allow_inferred_rendering: true, **rendering, &block)
|
239
|
+
case
|
240
|
+
when content
|
241
|
+
allow_inferred_rendering ? (render_record(content) || content) : content
|
242
|
+
when block_given?
|
243
|
+
@view_context.capture(&block)
|
244
|
+
when rendering.any?
|
245
|
+
@view_context.render(formats: [ :html ], **rendering)
|
246
|
+
else
|
247
|
+
render_record(target) if allow_inferred_rendering
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
148
251
|
def render_record(possible_record)
|
149
252
|
if possible_record.respond_to?(:to_partial_path)
|
150
253
|
record = possible_record
|
@@ -1,31 +1,15 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
APP_JS_ROOT = Rails.root.join("app/assets/javascripts")
|
2
|
+
CABLE_CONFIG_PATH = Rails.root.join("config/cable.yml")
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
insert_into_file APPLICATION_LAYOUT_PATH.to_s, "\n <%= yield :head %>", before: /\s*<\/head>/
|
4
|
+
say "Import turbo-rails in existing app/assets/javascripts/application.js"
|
5
|
+
append_to_file APP_JS_ROOT.join("application.js"), %(import "@hotwired/turbo-rails"\n)
|
7
6
|
|
8
|
-
|
9
|
-
|
10
|
-
|
7
|
+
if CABLE_CONFIG_PATH.exist?
|
8
|
+
say "Enable redis in bundle"
|
9
|
+
uncomment_lines "Gemfile", %(gem 'redis')
|
11
10
|
|
12
|
-
|
13
|
-
|
14
|
-
insert_into_file IMPORTMAP_PATH, %( "turbo": "<%= asset_path "turbo" %>",\n), after: / "imports": {\s*\n/
|
15
|
-
end
|
16
|
-
else
|
17
|
-
say "Add Turbo include tags in application layout"
|
18
|
-
insert_into_file APPLICATION_LAYOUT_PATH.to_s, %(\n <%= javascript_include_tag "turbo", type: "module" %>), before: /\s*<\/head>/
|
19
|
-
end
|
11
|
+
say "Switch development cable to use redis"
|
12
|
+
gsub_file CABLE_CONFIG_PATH.to_s, /development:\n\s+adapter: async/, "development:\n adapter: redis\n url: redis://localhost:6379/1"
|
20
13
|
else
|
21
|
-
say
|
22
|
-
say %( Add <%= javascript_include_tag("turbo", type: "module-shim") %> and <%= yield :head %> within the <head> tag after Stimulus includes in your custom layout.)
|
14
|
+
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.'
|
23
15
|
end
|
24
|
-
|
25
|
-
say "Enable redis in bundle"
|
26
|
-
uncomment_lines "Gemfile", %(gem 'redis')
|
27
|
-
|
28
|
-
say "Switch development cable to use redis"
|
29
|
-
gsub_file "config/cable.yml", /development:\n\s+adapter: async/, "development:\n adapter: redis\n url: redis://localhost:6379/1"
|
30
|
-
|
31
|
-
say "Turbo successfully installed ⚡️", :green
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# Some Rails versions use commonJS(require) others use ESM(import).
|
2
2
|
TURBOLINKS_REGEX = /(import .* from "turbolinks".*\n|require\("turbolinks"\).*\n)/.freeze
|
3
3
|
ACTIVE_STORAGE_REGEX = /(import.*ActiveStorage|require.*@rails\/activestorage.*)/.freeze
|
4
|
+
CABLE_CONFIG_PATH = Rails.root.join("config/cable.yml")
|
4
5
|
|
5
6
|
abort "❌ Webpacker not found. Exiting." unless defined?(Webpacker::Engine)
|
6
7
|
|
@@ -15,10 +16,12 @@ run "#{RbConfig.ruby} bin/yarn remove turbolinks"
|
|
15
16
|
gsub_file "#{Webpacker.config.source_entry_path}/application.js", TURBOLINKS_REGEX, ''
|
16
17
|
gsub_file "#{Webpacker.config.source_entry_path}/application.js", /Turbolinks.start.*\n/, ''
|
17
18
|
|
18
|
-
|
19
|
-
|
19
|
+
if CABLE_CONFIG_PATH.exist?
|
20
|
+
say "Enable redis in bundle"
|
21
|
+
uncomment_lines "Gemfile", %(gem 'redis')
|
20
22
|
|
21
|
-
say "Switch development cable to use redis"
|
22
|
-
gsub_file
|
23
|
-
|
24
|
-
say "Turbo
|
23
|
+
say "Switch development cable to use redis"
|
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
|
data/lib/turbo/engine.rb
CHANGED
@@ -22,7 +22,13 @@ module Turbo
|
|
22
22
|
|
23
23
|
initializer "turbo.assets" do
|
24
24
|
if Rails.application.config.respond_to?(:assets)
|
25
|
-
Rails.application.config.assets.precompile += %w( turbo )
|
25
|
+
Rails.application.config.assets.precompile += %w( turbo.js )
|
26
|
+
end
|
27
|
+
end
|
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"
|
26
32
|
end
|
27
33
|
end
|
28
34
|
|
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.
|
4
|
+
version: 0.7.2
|
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-
|
13
|
+
date: 2021-08-14 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: rails
|