turbo-rails 0.5.11 → 0.7.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +9 -12
- data/app/assets/javascripts/turbo.js +248 -90
- 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 +34 -3
- 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 +9 -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: d693019f1ad4faadbaad84b55386d17aa4874d33be169d7641fdd4ac47ac0eb0
|
4
|
+
data.tar.gz: 01562da91f25cad722e92ece91c21a5280cd91cc61a8359510573bfb5d205454
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5d8c769812b11bfafa30a960aa65f78fb5a9b6c9b7eaafad62a88fc305169be864baaec58d9012f56cd5ec1fbd402382154fdb60a75708e77d0a102e254d0976
|
7
|
+
data.tar.gz: 13020b72abb6e42edd6cbf16af8ac5d84340c68f40197f37cebbb9ffc76c8fc3d9c0aba396986947539de051ee5029f6945e382a028053290cf905e566af0298
|
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,34 +38,31 @@ 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
|
-
If you're using Webpack and need to use
|
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
|
|
45
|
-
|
45
|
+
The `Turbo` instance is automatically assigned to `window.Turbo` upon import:
|
46
46
|
|
47
47
|
```js
|
48
|
-
import
|
49
|
-
window.Turbo = Turbo
|
48
|
+
import "@hotwired/turbo-rails"
|
50
49
|
```
|
51
50
|
|
52
51
|
## Usage
|
53
52
|
|
54
|
-
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).
|
55
54
|
|
56
55
|
|
57
56
|
## Compatibility with Rails UJS
|
58
57
|
|
59
|
-
|
60
|
-
|
61
|
-
Note that the helpers that turn `link_to` into remote invocations will _not_ currently work with Turbo. Links that have been made remote will not stick within frames nor will they allow you to respond with turbo stream actions. The recommendation is to replace these links with styled `button_to`, so you'll flow through a regular form, and you'll be better off with a11y compliance.
|
62
|
-
|
63
|
-
You can still use the `data-confirm` and `data-disable-with`.
|
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).
|
64
59
|
|
65
60
|
|
66
61
|
## Development
|
67
62
|
|
68
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`
|
69
66
|
* To compile the JavaScript for the asset pipeline: `yarn build`
|
70
67
|
|
71
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;
|
@@ -943,6 +959,9 @@ class Renderer {
|
|
943
959
|
return element;
|
944
960
|
} else {
|
945
961
|
const createdScriptElement = document.createElement("script");
|
962
|
+
if (this.cspNonce) {
|
963
|
+
createdScriptElement.nonce = this.cspNonce;
|
964
|
+
}
|
946
965
|
createdScriptElement.textContent = element.textContent;
|
947
966
|
createdScriptElement.async = false;
|
948
967
|
copyElementAttributes(createdScriptElement, element);
|
@@ -970,6 +989,10 @@ class Renderer {
|
|
970
989
|
get permanentElementMap() {
|
971
990
|
return this.currentSnapshot.getPermanentElementMapForSnapshot(this.newSnapshot);
|
972
991
|
}
|
992
|
+
get cspNonce() {
|
993
|
+
var _a;
|
994
|
+
return (_a = document.head.querySelector('meta[name="csp-nonce"]')) === null || _a === void 0 ? void 0 : _a.getAttribute("content");
|
995
|
+
}
|
973
996
|
}
|
974
997
|
|
975
998
|
function copyElementAttributes(destinationElement, sourceElement) {
|
@@ -994,6 +1017,8 @@ class FrameRenderer extends Renderer {
|
|
994
1017
|
this.scrollFrameIntoView();
|
995
1018
|
await nextAnimationFrame();
|
996
1019
|
this.focusFirstAutofocusableElement();
|
1020
|
+
await nextAnimationFrame();
|
1021
|
+
this.activateScriptElements();
|
997
1022
|
}
|
998
1023
|
loadFrameElement() {
|
999
1024
|
var _a;
|
@@ -1020,6 +1045,15 @@ class FrameRenderer extends Renderer {
|
|
1020
1045
|
}
|
1021
1046
|
return false;
|
1022
1047
|
}
|
1048
|
+
activateScriptElements() {
|
1049
|
+
for (const inertScriptElement of this.newScriptElements) {
|
1050
|
+
const activatedScriptElement = this.createScriptElement(inertScriptElement);
|
1051
|
+
inertScriptElement.replaceWith(activatedScriptElement);
|
1052
|
+
}
|
1053
|
+
}
|
1054
|
+
get newScriptElements() {
|
1055
|
+
return this.currentElement.querySelectorAll("script");
|
1056
|
+
}
|
1023
1057
|
}
|
1024
1058
|
|
1025
1059
|
function readScrollLogicalPosition(value, defaultValue) {
|
@@ -1132,7 +1166,7 @@ ProgressBar.animationDuration = 300;
|
|
1132
1166
|
class HeadSnapshot extends Snapshot {
|
1133
1167
|
constructor() {
|
1134
1168
|
super(...arguments);
|
1135
|
-
this.detailsByOuterHTML = this.children.reduce(((result, element) => {
|
1169
|
+
this.detailsByOuterHTML = this.children.filter((element => !elementIsNoscript(element))).reduce(((result, element) => {
|
1136
1170
|
const {outerHTML: outerHTML} = element;
|
1137
1171
|
const details = outerHTML in result ? result[outerHTML] : {
|
1138
1172
|
type: elementType(element),
|
@@ -1199,6 +1233,11 @@ function elementIsScript(element) {
|
|
1199
1233
|
return tagName == "script";
|
1200
1234
|
}
|
1201
1235
|
|
1236
|
+
function elementIsNoscript(element) {
|
1237
|
+
const tagName = element.tagName.toLowerCase();
|
1238
|
+
return tagName == "noscript";
|
1239
|
+
}
|
1240
|
+
|
1202
1241
|
function elementIsStylesheet(element) {
|
1203
1242
|
const tagName = element.tagName.toLowerCase();
|
1204
1243
|
return tagName == "style" || tagName == "link" && element.getAttribute("rel") == "stylesheet";
|
@@ -1301,6 +1340,7 @@ class Visit {
|
|
1301
1340
|
this.referrer = referrer;
|
1302
1341
|
this.snapshotHTML = snapshotHTML;
|
1303
1342
|
this.response = response;
|
1343
|
+
this.isSamePage = this.delegate.locationWithActionIsSamePage(this.location, this.action);
|
1304
1344
|
}
|
1305
1345
|
get adapter() {
|
1306
1346
|
return this.delegate.adapter;
|
@@ -1393,6 +1433,7 @@ class Visit {
|
|
1393
1433
|
const {statusCode: statusCode, responseHTML: responseHTML} = this.response;
|
1394
1434
|
this.render((async () => {
|
1395
1435
|
this.cacheSnapshot();
|
1436
|
+
if (this.view.renderPromise) await this.view.renderPromise;
|
1396
1437
|
if (isSuccessful(statusCode) && responseHTML != null) {
|
1397
1438
|
await this.view.renderPage(PageSnapshot.fromHTMLString(responseHTML));
|
1398
1439
|
this.adapter.visitRendered(this);
|
@@ -1427,10 +1468,15 @@ class Visit {
|
|
1427
1468
|
const isPreview = this.shouldIssueRequest();
|
1428
1469
|
this.render((async () => {
|
1429
1470
|
this.cacheSnapshot();
|
1430
|
-
|
1431
|
-
|
1432
|
-
|
1433
|
-
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
|
+
}
|
1434
1480
|
}
|
1435
1481
|
}));
|
1436
1482
|
}
|
@@ -1442,6 +1488,14 @@ class Visit {
|
|
1442
1488
|
this.followedRedirect = true;
|
1443
1489
|
}
|
1444
1490
|
}
|
1491
|
+
goToSamePageAnchor() {
|
1492
|
+
if (this.isSamePage) {
|
1493
|
+
this.render((async () => {
|
1494
|
+
this.cacheSnapshot();
|
1495
|
+
this.adapter.visitRendered(this);
|
1496
|
+
}));
|
1497
|
+
}
|
1498
|
+
}
|
1445
1499
|
requestStarted() {
|
1446
1500
|
this.startRequest();
|
1447
1501
|
}
|
@@ -1484,10 +1538,13 @@ class Visit {
|
|
1484
1538
|
performScroll() {
|
1485
1539
|
if (!this.scrolled) {
|
1486
1540
|
if (this.action == "restore") {
|
1487
|
-
this.scrollToRestoredPosition() || this.scrollToTop();
|
1541
|
+
this.scrollToRestoredPosition() || this.scrollToAnchor() || this.scrollToTop();
|
1488
1542
|
} else {
|
1489
1543
|
this.scrollToAnchor() || this.scrollToTop();
|
1490
1544
|
}
|
1545
|
+
if (this.isSamePage) {
|
1546
|
+
this.delegate.visitScrolledToSamePageLocation(this.view.lastRenderedLocation, this.location);
|
1547
|
+
}
|
1491
1548
|
this.scrolled = true;
|
1492
1549
|
}
|
1493
1550
|
}
|
@@ -1499,8 +1556,9 @@ class Visit {
|
|
1499
1556
|
}
|
1500
1557
|
}
|
1501
1558
|
scrollToAnchor() {
|
1502
|
-
|
1503
|
-
|
1559
|
+
const anchor = getAnchor(this.location);
|
1560
|
+
if (anchor != null) {
|
1561
|
+
this.view.scrollToAnchor(anchor);
|
1504
1562
|
return true;
|
1505
1563
|
}
|
1506
1564
|
}
|
@@ -1530,7 +1588,13 @@ class Visit {
|
|
1530
1588
|
return typeof this.response == "object";
|
1531
1589
|
}
|
1532
1590
|
shouldIssueRequest() {
|
1533
|
-
|
1591
|
+
if (this.isSamePage) {
|
1592
|
+
return false;
|
1593
|
+
} else if (this.action == "restore") {
|
1594
|
+
return !this.hasCachedSnapshot();
|
1595
|
+
} else {
|
1596
|
+
return true;
|
1597
|
+
}
|
1534
1598
|
}
|
1535
1599
|
cacheSnapshot() {
|
1536
1600
|
if (!this.snapshotCached) {
|
@@ -1543,7 +1607,7 @@ class Visit {
|
|
1543
1607
|
await new Promise((resolve => {
|
1544
1608
|
this.frame = requestAnimationFrame((() => resolve()));
|
1545
1609
|
}));
|
1546
|
-
callback();
|
1610
|
+
await callback();
|
1547
1611
|
delete this.frame;
|
1548
1612
|
this.performScroll();
|
1549
1613
|
}
|
@@ -1573,6 +1637,7 @@ class BrowserAdapter {
|
|
1573
1637
|
visitStarted(visit) {
|
1574
1638
|
visit.issueRequest();
|
1575
1639
|
visit.changeHistory();
|
1640
|
+
visit.goToSamePageAnchor();
|
1576
1641
|
visit.loadCachedSnapshot();
|
1577
1642
|
}
|
1578
1643
|
visitRequestStarted(visit) {
|
@@ -1627,6 +1692,30 @@ class BrowserAdapter {
|
|
1627
1692
|
}
|
1628
1693
|
}
|
1629
1694
|
|
1695
|
+
class CacheObserver {
|
1696
|
+
constructor() {
|
1697
|
+
this.started = false;
|
1698
|
+
}
|
1699
|
+
start() {
|
1700
|
+
if (!this.started) {
|
1701
|
+
this.started = true;
|
1702
|
+
addEventListener("turbo:before-cache", this.removeStaleElements, false);
|
1703
|
+
}
|
1704
|
+
}
|
1705
|
+
stop() {
|
1706
|
+
if (this.started) {
|
1707
|
+
this.started = false;
|
1708
|
+
removeEventListener("turbo:before-cache", this.removeStaleElements, false);
|
1709
|
+
}
|
1710
|
+
}
|
1711
|
+
removeStaleElements() {
|
1712
|
+
const staleElements = [ ...document.querySelectorAll('[data-turbo-cache="false"]') ];
|
1713
|
+
for (const element of staleElements) {
|
1714
|
+
element.remove();
|
1715
|
+
}
|
1716
|
+
}
|
1717
|
+
}
|
1718
|
+
|
1630
1719
|
class FormSubmitObserver {
|
1631
1720
|
constructor(delegate) {
|
1632
1721
|
this.started = false;
|
@@ -1926,6 +2015,12 @@ class Navigator {
|
|
1926
2015
|
visitCompleted(visit) {
|
1927
2016
|
this.delegate.visitCompleted(visit);
|
1928
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
|
+
}
|
1929
2024
|
get location() {
|
1930
2025
|
return this.history.location;
|
1931
2026
|
}
|
@@ -2216,7 +2311,7 @@ class PageRenderer extends Renderer {
|
|
2216
2311
|
return this.newHeadSnapshot.provisionalElements;
|
2217
2312
|
}
|
2218
2313
|
get newBodyScriptElements() {
|
2219
|
-
return
|
2314
|
+
return this.newElement.querySelectorAll("script");
|
2220
2315
|
}
|
2221
2316
|
}
|
2222
2317
|
|
@@ -2276,7 +2371,7 @@ class PageView extends View {
|
|
2276
2371
|
}
|
2277
2372
|
renderError(snapshot) {
|
2278
2373
|
const renderer = new ErrorRenderer(this.snapshot, snapshot, false);
|
2279
|
-
this.render(renderer);
|
2374
|
+
return this.render(renderer);
|
2280
2375
|
}
|
2281
2376
|
clearSnapshotCache() {
|
2282
2377
|
this.snapshotCache.clear();
|
@@ -2307,6 +2402,7 @@ class Session {
|
|
2307
2402
|
this.view = new PageView(this, document.documentElement);
|
2308
2403
|
this.adapter = new BrowserAdapter(this);
|
2309
2404
|
this.pageObserver = new PageObserver(this);
|
2405
|
+
this.cacheObserver = new CacheObserver;
|
2310
2406
|
this.linkClickObserver = new LinkClickObserver(this);
|
2311
2407
|
this.formSubmitObserver = new FormSubmitObserver(this);
|
2312
2408
|
this.scrollObserver = new ScrollObserver(this);
|
@@ -2319,6 +2415,7 @@ class Session {
|
|
2319
2415
|
start() {
|
2320
2416
|
if (!this.started) {
|
2321
2417
|
this.pageObserver.start();
|
2418
|
+
this.cacheObserver.start();
|
2322
2419
|
this.linkClickObserver.start();
|
2323
2420
|
this.formSubmitObserver.start();
|
2324
2421
|
this.scrollObserver.start();
|
@@ -2335,6 +2432,7 @@ class Session {
|
|
2335
2432
|
stop() {
|
2336
2433
|
if (this.started) {
|
2337
2434
|
this.pageObserver.stop();
|
2435
|
+
this.cacheObserver.stop();
|
2338
2436
|
this.linkClickObserver.stop();
|
2339
2437
|
this.formSubmitObserver.stop();
|
2340
2438
|
this.scrollObserver.stop();
|
@@ -2371,9 +2469,9 @@ class Session {
|
|
2371
2469
|
get restorationIdentifier() {
|
2372
2470
|
return this.history.restorationIdentifier;
|
2373
2471
|
}
|
2374
|
-
historyPoppedToLocationWithRestorationIdentifier(location) {
|
2472
|
+
historyPoppedToLocationWithRestorationIdentifier(location, restorationIdentifier) {
|
2375
2473
|
if (this.enabled) {
|
2376
|
-
this.navigator.
|
2474
|
+
this.navigator.startVisit(location, restorationIdentifier, {
|
2377
2475
|
action: "restore",
|
2378
2476
|
historyChanged: true
|
2379
2477
|
});
|
@@ -2391,10 +2489,26 @@ class Session {
|
|
2391
2489
|
}
|
2392
2490
|
followedLinkToLocation(link, location) {
|
2393
2491
|
const action = this.getActionForLink(link);
|
2394
|
-
this.visit(location.href, {
|
2492
|
+
this.convertLinkWithMethodClickToFormSubmission(link) || this.visit(location.href, {
|
2395
2493
|
action: action
|
2396
2494
|
});
|
2397
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
|
+
}
|
2398
2512
|
allowsVisitingLocation(location) {
|
2399
2513
|
return this.applicationAllowsVisitingLocation(location);
|
2400
2514
|
}
|
@@ -2404,11 +2518,17 @@ class Session {
|
|
2404
2518
|
}
|
2405
2519
|
visitStarted(visit) {
|
2406
2520
|
extendURLWithDeprecatedProperties(visit.location);
|
2407
|
-
this.notifyApplicationAfterVisitingLocation(visit.location);
|
2521
|
+
this.notifyApplicationAfterVisitingLocation(visit.location, visit.action);
|
2408
2522
|
}
|
2409
2523
|
visitCompleted(visit) {
|
2410
2524
|
this.notifyApplicationAfterPageLoad(visit.getTimingMetrics());
|
2411
2525
|
}
|
2526
|
+
locationWithActionIsSamePage(location, action) {
|
2527
|
+
return this.navigator.locationWithActionIsSamePage(location, action);
|
2528
|
+
}
|
2529
|
+
visitScrolledToSamePageLocation(oldURL, newURL) {
|
2530
|
+
this.notifyApplicationAfterVisitingSamePageLocation(oldURL, newURL);
|
2531
|
+
}
|
2412
2532
|
willSubmitForm(form, submitter) {
|
2413
2533
|
return elementIsNavigable(form) && elementIsNavigable(submitter);
|
2414
2534
|
}
|
@@ -2431,8 +2551,9 @@ class Session {
|
|
2431
2551
|
viewWillCacheSnapshot() {
|
2432
2552
|
this.notifyApplicationBeforeCachingSnapshot();
|
2433
2553
|
}
|
2434
|
-
|
2435
|
-
this.notifyApplicationBeforeRender(element);
|
2554
|
+
allowsImmediateRender({element: element}, resume) {
|
2555
|
+
const event = this.notifyApplicationBeforeRender(element, resume);
|
2556
|
+
return !event.defaultPrevented;
|
2436
2557
|
}
|
2437
2558
|
viewRenderedSnapshot(snapshot, isPreview) {
|
2438
2559
|
this.view.lastRenderedLocation = this.history.location;
|
@@ -2466,21 +2587,24 @@ class Session {
|
|
2466
2587
|
cancelable: true
|
2467
2588
|
});
|
2468
2589
|
}
|
2469
|
-
notifyApplicationAfterVisitingLocation(location) {
|
2590
|
+
notifyApplicationAfterVisitingLocation(location, action) {
|
2470
2591
|
return dispatch("turbo:visit", {
|
2471
2592
|
detail: {
|
2472
|
-
url: location.href
|
2593
|
+
url: location.href,
|
2594
|
+
action: action
|
2473
2595
|
}
|
2474
2596
|
});
|
2475
2597
|
}
|
2476
2598
|
notifyApplicationBeforeCachingSnapshot() {
|
2477
2599
|
return dispatch("turbo:before-cache");
|
2478
2600
|
}
|
2479
|
-
notifyApplicationBeforeRender(newBody) {
|
2601
|
+
notifyApplicationBeforeRender(newBody, resume) {
|
2480
2602
|
return dispatch("turbo:before-render", {
|
2481
2603
|
detail: {
|
2482
|
-
newBody: newBody
|
2483
|
-
|
2604
|
+
newBody: newBody,
|
2605
|
+
resume: resume
|
2606
|
+
},
|
2607
|
+
cancelable: true
|
2484
2608
|
});
|
2485
2609
|
}
|
2486
2610
|
notifyApplicationAfterRender() {
|
@@ -2494,6 +2618,12 @@ class Session {
|
|
2494
2618
|
}
|
2495
2619
|
});
|
2496
2620
|
}
|
2621
|
+
notifyApplicationAfterVisitingSamePageLocation(oldURL, newURL) {
|
2622
|
+
dispatchEvent(new HashChangeEvent("hashchange", {
|
2623
|
+
oldURL: oldURL.toString(),
|
2624
|
+
newURL: newURL.toString()
|
2625
|
+
}));
|
2626
|
+
}
|
2497
2627
|
getActionForLink(link) {
|
2498
2628
|
const action = link.getAttribute("data-turbo-action");
|
2499
2629
|
return isAction(action) ? action : "advance";
|
@@ -2603,6 +2733,7 @@ class FrameController {
|
|
2603
2733
|
const {body: body} = parseHTMLDocument(html);
|
2604
2734
|
const snapshot = new Snapshot(await this.extractForeignFrameElement(body));
|
2605
2735
|
const renderer = new FrameRenderer(this.view.snapshot, snapshot, false);
|
2736
|
+
if (this.view.renderPromise) await this.view.renderPromise;
|
2606
2737
|
await this.view.render(renderer);
|
2607
2738
|
}
|
2608
2739
|
} catch (error) {
|
@@ -2614,7 +2745,11 @@ class FrameController {
|
|
2614
2745
|
this.loadSourceURL();
|
2615
2746
|
}
|
2616
2747
|
shouldInterceptLinkClick(element, url) {
|
2617
|
-
|
2748
|
+
if (element.hasAttribute("data-turbo-method")) {
|
2749
|
+
return false;
|
2750
|
+
} else {
|
2751
|
+
return this.shouldInterceptNavigation(element);
|
2752
|
+
}
|
2618
2753
|
}
|
2619
2754
|
linkClickIntercepted(element, url) {
|
2620
2755
|
this.navigateFrame(element, url);
|
@@ -2677,7 +2812,9 @@ class FrameController {
|
|
2677
2812
|
const frame = this.findFrameElement(formSubmission.formElement);
|
2678
2813
|
frame.removeAttribute("busy");
|
2679
2814
|
}
|
2680
|
-
|
2815
|
+
allowsImmediateRender(snapshot, resume) {
|
2816
|
+
return true;
|
2817
|
+
}
|
2681
2818
|
viewRenderedSnapshot(snapshot, isPreview) {}
|
2682
2819
|
viewInvalidated() {}
|
2683
2820
|
async visit(url) {
|
@@ -2790,36 +2927,36 @@ function activateElement(element, currentURL) {
|
|
2790
2927
|
|
2791
2928
|
const StreamActions = {
|
2792
2929
|
after() {
|
2793
|
-
|
2794
|
-
|
2930
|
+
this.targetElements.forEach((e => {
|
2931
|
+
var _a;
|
2932
|
+
return (_a = e.parentElement) === null || _a === void 0 ? void 0 : _a.insertBefore(this.templateContent, e.nextSibling);
|
2933
|
+
}));
|
2795
2934
|
},
|
2796
2935
|
append() {
|
2797
|
-
var _a;
|
2798
2936
|
this.removeDuplicateTargetChildren();
|
2799
|
-
|
2937
|
+
this.targetElements.forEach((e => e.append(this.templateContent)));
|
2800
2938
|
},
|
2801
2939
|
before() {
|
2802
|
-
|
2803
|
-
|
2940
|
+
this.targetElements.forEach((e => {
|
2941
|
+
var _a;
|
2942
|
+
return (_a = e.parentElement) === null || _a === void 0 ? void 0 : _a.insertBefore(this.templateContent, e);
|
2943
|
+
}));
|
2804
2944
|
},
|
2805
2945
|
prepend() {
|
2806
|
-
var _a;
|
2807
2946
|
this.removeDuplicateTargetChildren();
|
2808
|
-
|
2947
|
+
this.targetElements.forEach((e => e.prepend(this.templateContent)));
|
2809
2948
|
},
|
2810
2949
|
remove() {
|
2811
|
-
|
2812
|
-
(_a = this.targetElement) === null || _a === void 0 ? void 0 : _a.remove();
|
2950
|
+
this.targetElements.forEach((e => e.remove()));
|
2813
2951
|
},
|
2814
2952
|
replace() {
|
2815
|
-
|
2816
|
-
(_a = this.targetElement) === null || _a === void 0 ? void 0 : _a.replaceWith(this.templateContent);
|
2953
|
+
this.targetElements.forEach((e => e.replaceWith(this.templateContent)));
|
2817
2954
|
},
|
2818
2955
|
update() {
|
2819
|
-
|
2820
|
-
|
2821
|
-
|
2822
|
-
}
|
2956
|
+
this.targetElements.forEach((e => {
|
2957
|
+
e.innerHTML = "";
|
2958
|
+
e.append(this.templateContent);
|
2959
|
+
}));
|
2823
2960
|
}
|
2824
2961
|
};
|
2825
2962
|
|
@@ -2848,19 +2985,13 @@ class StreamElement extends HTMLElement {
|
|
2848
2985
|
} catch (_a) {}
|
2849
2986
|
}
|
2850
2987
|
removeDuplicateTargetChildren() {
|
2851
|
-
this.duplicateChildren.forEach((
|
2852
|
-
targetChild.remove();
|
2853
|
-
}));
|
2988
|
+
this.duplicateChildren.forEach((c => c.remove()));
|
2854
2989
|
}
|
2855
2990
|
get duplicateChildren() {
|
2856
2991
|
var _a;
|
2857
|
-
|
2858
|
-
|
2859
|
-
|
2860
|
-
targetChild: targetChild,
|
2861
|
-
templateChild: templateChild
|
2862
|
-
};
|
2863
|
-
})).filter((({targetChild: targetChild}) => targetChild));
|
2992
|
+
const existingChildren = this.targetElements.flatMap((e => [ ...e.children ])).filter((c => !!c.id));
|
2993
|
+
const newChildrenIds = [ ...(_a = this.templateContent) === null || _a === void 0 ? void 0 : _a.children ].filter((c => !!c.id)).map((c => c.id));
|
2994
|
+
return existingChildren.filter((c => newChildrenIds.includes(c.id)));
|
2864
2995
|
}
|
2865
2996
|
get performAction() {
|
2866
2997
|
if (this.action) {
|
@@ -2872,15 +3003,17 @@ class StreamElement extends HTMLElement {
|
|
2872
3003
|
}
|
2873
3004
|
this.raise("action attribute is missing");
|
2874
3005
|
}
|
2875
|
-
get
|
2876
|
-
var _a;
|
3006
|
+
get targetElements() {
|
2877
3007
|
if (this.target) {
|
2878
|
-
return
|
3008
|
+
return this.targetElementsById;
|
3009
|
+
} else if (this.targets) {
|
3010
|
+
return this.targetElementsByQuery;
|
3011
|
+
} else {
|
3012
|
+
this.raise("target or targets attribute is missing");
|
2879
3013
|
}
|
2880
|
-
this.raise("target attribute is missing");
|
2881
3014
|
}
|
2882
3015
|
get templateContent() {
|
2883
|
-
return this.templateElement.content;
|
3016
|
+
return this.templateElement.content.cloneNode(true);
|
2884
3017
|
}
|
2885
3018
|
get templateElement() {
|
2886
3019
|
if (this.firstElementChild instanceof HTMLTemplateElement) {
|
@@ -2894,6 +3027,9 @@ class StreamElement extends HTMLElement {
|
|
2894
3027
|
get target() {
|
2895
3028
|
return this.getAttribute("target");
|
2896
3029
|
}
|
3030
|
+
get targets() {
|
3031
|
+
return this.getAttribute("targets");
|
3032
|
+
}
|
2897
3033
|
raise(message) {
|
2898
3034
|
throw new Error(`${this.description}: ${message}`);
|
2899
3035
|
}
|
@@ -2907,6 +3043,24 @@ class StreamElement extends HTMLElement {
|
|
2907
3043
|
cancelable: true
|
2908
3044
|
});
|
2909
3045
|
}
|
3046
|
+
get targetElementsById() {
|
3047
|
+
var _a;
|
3048
|
+
const element = (_a = this.ownerDocument) === null || _a === void 0 ? void 0 : _a.getElementById(this.target);
|
3049
|
+
if (element !== null) {
|
3050
|
+
return [ element ];
|
3051
|
+
} else {
|
3052
|
+
return [];
|
3053
|
+
}
|
3054
|
+
}
|
3055
|
+
get targetElementsByQuery() {
|
3056
|
+
var _a;
|
3057
|
+
const elements = (_a = this.ownerDocument) === null || _a === void 0 ? void 0 : _a.querySelectorAll(this.targets);
|
3058
|
+
if (elements.length !== 0) {
|
3059
|
+
return Array.prototype.slice.call(elements);
|
3060
|
+
} else {
|
3061
|
+
return [];
|
3062
|
+
}
|
3063
|
+
}
|
2910
3064
|
}
|
2911
3065
|
|
2912
3066
|
FrameElement.delegateConstructor = FrameController;
|
@@ -2926,7 +3080,7 @@ customElements.define("turbo-stream", StreamElement);
|
|
2926
3080
|
|
2927
3081
|
Load your application’s JavaScript bundle inside the <head> element instead. <script> elements in <body> are evaluated with each page change.
|
2928
3082
|
|
2929
|
-
For more information, see: https://turbo.
|
3083
|
+
For more information, see: https://turbo.hotwired.dev/handbook/building#working-with-script-elements
|
2930
3084
|
|
2931
3085
|
——
|
2932
3086
|
Suppress this warning by adding a "data-turbo-suppress-warning" attribute to: %s
|
@@ -2974,6 +3128,8 @@ function setProgressBarDelay(delay) {
|
|
2974
3128
|
var Turbo = Object.freeze({
|
2975
3129
|
__proto__: null,
|
2976
3130
|
navigator: navigator,
|
3131
|
+
PageRenderer: PageRenderer,
|
3132
|
+
PageSnapshot: PageSnapshot,
|
2977
3133
|
start: start,
|
2978
3134
|
registerAdapter: registerAdapter,
|
2979
3135
|
visit: visit,
|
@@ -2990,6 +3146,8 @@ start();
|
|
2990
3146
|
|
2991
3147
|
var turbo_es2017Esm = Object.freeze({
|
2992
3148
|
__proto__: null,
|
3149
|
+
PageRenderer: PageRenderer,
|
3150
|
+
PageSnapshot: PageSnapshot,
|
2993
3151
|
clearCache: clearCache,
|
2994
3152
|
connectStreamSource: connectStreamSource,
|
2995
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)
|
@@ -265,7 +294,9 @@ module Turbo::Broadcastable
|
|
265
294
|
|
266
295
|
def broadcast_rendering_with_defaults(options)
|
267
296
|
options.tap do |o|
|
268
|
-
|
297
|
+
# Add the current instance into the locals with the element name (which is the un-namespaced name)
|
298
|
+
# as the key. This parallels how the ActionView::ObjectRenderer would create a local variable.
|
299
|
+
o[:locals] = (o[:locals] || {}).reverse_merge!(model_name.element.to_sym => self)
|
269
300
|
o[:partial] ||= to_partial_path
|
270
301
|
end
|
271
302
|
end
|
@@ -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,15 @@ 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.paths.tap do |paths|
|
32
|
+
paths.asset "@hotwired/turbo-rails", path: "turbo.js"
|
33
|
+
end
|
26
34
|
end
|
27
35
|
end
|
28
36
|
|
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.1
|
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-13 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: rails
|