turbo-rails 0.5.10 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1abdc38a6d5a735bf6bbd2ff6fb8db49ea74d14e3452afa5c2bda93b23ffb073
4
- data.tar.gz: 0bf468488fd8232fec62b975fb9f0ef5449325cf57b932858e03af876d0548e6
3
+ metadata.gz: d791adac758f16b877d9d7c1ed0706e9bb684ba6238f94c3a1de0dcc424b85d4
4
+ data.tar.gz: 9a60a2a70b4b732687220f2cd209454b02a963a450af1ce07ff49708ad8bf0cd
5
5
  SHA512:
6
- metadata.gz: 79272181e9d15ab14434ba684ac33a2fa186df4fa15462d2ad96a77caaeb8caaf111beebd9bf70f67b7fee3eae0e35c5af5f6e86a4e2c970f07c7b1c58e983bf
7
- data.tar.gz: ed1f850a041f5d66ae7bafa8264ac3ee5679db81eb5f91869e4e8624ef0cfadb92f345e9f1de192ee917a568deb866e1231db248820ecdb0afcafc96f8296dc4
6
+ metadata.gz: 2c76c8475cb8509f86208a3553e6561d6dcaec91198d226aa2c23a6b9d45c0615db36cab291b77b82001330c46de12923c817250aa93d2ba9e28bc37b5fbd4b1
7
+ data.tar.gz: c78eb044de3328bb08edb26f552716df709913c640676ff1156ee878e956cc3d860f9a1f82fbe5889491182092563a83708dda4721db1747769b927fac1956a7
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Turbo
2
2
 
3
- [Turbo](https://turbo.hotwire.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).
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 either the cable consumer or the Turbo instance, you can import [`Turbo`](https://turbo.hotwire.dev/reference/drive) and/or [`cable`](https://github.com/hotwired/turbo-rails/blob/main/app/javascript/turbo/cable.js) (`import { Turbo, 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)).
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
- If you're using a [native adapter](https://turbo.hotwire.dev/handbook/native), you'll need to assign `window.Turbo`, even if it's not used for anything else:
45
+ The `Turbo` instance is automatically assigned to `window.Turbo` upon import:
46
46
 
47
47
  ```js
48
- import { Turbo } from "@hotwired/turbo-rails"
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://hotwire.dev/#screencast), which focuses extensively on demonstration Turbo in a Rails demo. Then you should familiarize yourself with [Turbo handbook](https://turbo.hotwire.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).
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
- Rails UJS includes helpers for sending links and forms over XMLHttpRequest, so you can respond with Ajax. Turbo supersedes this functionality, so you should ensure that you're either running Rails 6.1 with the defaults that turn this off for forms, or that you add `config.action_view.form_with_generates_remote_forms = false` to your `config/application.rb`.
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
- const anchorLength = url.hash.length;
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
- dispatch("turbo:before-fetch-request", {
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.delegate.viewWillRenderSnapshot(snapshot, isPreview);
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.convertLinkWithMethodClickToFormSubmission(event.target) || this.delegate.linkClickIntercepted(event.target, event.detail.url);
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
- await this.view.renderPage(snapshot, isPreview);
1431
- this.adapter.visitRendered(this);
1432
- if (!isPreview) {
1433
- this.complete();
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
- if (getAnchor(this.location)) {
1503
- this.view.scrollToAnchor(getAnchor(this.location));
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
- return this.action == "restore" ? !this.hasCachedSnapshot() : true;
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 [ ...this.newElement.querySelectorAll("script") ];
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.proposeVisit(location, {
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
- viewWillRenderSnapshot({element: element}, isPreview) {
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
- return this.shouldInterceptNavigation(element);
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
- viewWillRenderSnapshot(snapshot, isPreview) {}
2815
+ allowsImmediateRender(snapshot, resume) {
2816
+ return true;
2817
+ }
2681
2818
  viewRenderedSnapshot(snapshot, isPreview) {}
2682
2819
  viewInvalidated() {}
2683
2820
  async visit(url) {
@@ -2749,13 +2886,14 @@ class FrameController {
2749
2886
  set sourceURL(sourceURL) {
2750
2887
  this.settingSourceURL = true;
2751
2888
  this.element.src = sourceURL !== null && sourceURL !== void 0 ? sourceURL : null;
2889
+ this.currentURL = this.element.src;
2752
2890
  this.settingSourceURL = false;
2753
2891
  }
2754
2892
  get loadingStyle() {
2755
2893
  return this.element.loading;
2756
2894
  }
2757
2895
  get isLoading() {
2758
- return this.formSubmission !== undefined || this.resolveVisitPromise !== undefined;
2896
+ return this.formSubmission !== undefined || this.resolveVisitPromise() !== undefined;
2759
2897
  }
2760
2898
  get isActive() {
2761
2899
  return this.element.isActive && this.connected;
@@ -2789,36 +2927,36 @@ function activateElement(element, currentURL) {
2789
2927
 
2790
2928
  const StreamActions = {
2791
2929
  after() {
2792
- var _a, _b;
2793
- (_b = (_a = this.targetElement) === null || _a === void 0 ? void 0 : _a.parentElement) === null || _b === void 0 ? void 0 : _b.insertBefore(this.templateContent, this.targetElement.nextSibling);
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
+ }));
2794
2934
  },
2795
2935
  append() {
2796
- var _a;
2797
2936
  this.removeDuplicateTargetChildren();
2798
- (_a = this.targetElement) === null || _a === void 0 ? void 0 : _a.append(this.templateContent);
2937
+ this.targetElements.forEach((e => e.append(this.templateContent)));
2799
2938
  },
2800
2939
  before() {
2801
- var _a, _b;
2802
- (_b = (_a = this.targetElement) === null || _a === void 0 ? void 0 : _a.parentElement) === null || _b === void 0 ? void 0 : _b.insertBefore(this.templateContent, this.targetElement);
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
+ }));
2803
2944
  },
2804
2945
  prepend() {
2805
- var _a;
2806
2946
  this.removeDuplicateTargetChildren();
2807
- (_a = this.targetElement) === null || _a === void 0 ? void 0 : _a.prepend(this.templateContent);
2947
+ this.targetElements.forEach((e => e.prepend(this.templateContent)));
2808
2948
  },
2809
2949
  remove() {
2810
- var _a;
2811
- (_a = this.targetElement) === null || _a === void 0 ? void 0 : _a.remove();
2950
+ this.targetElements.forEach((e => e.remove()));
2812
2951
  },
2813
2952
  replace() {
2814
- var _a;
2815
- (_a = this.targetElement) === null || _a === void 0 ? void 0 : _a.replaceWith(this.templateContent);
2953
+ this.targetElements.forEach((e => e.replaceWith(this.templateContent)));
2816
2954
  },
2817
2955
  update() {
2818
- if (this.targetElement) {
2819
- this.targetElement.innerHTML = "";
2820
- this.targetElement.append(this.templateContent);
2821
- }
2956
+ this.targetElements.forEach((e => {
2957
+ e.innerHTML = "";
2958
+ e.append(this.templateContent);
2959
+ }));
2822
2960
  }
2823
2961
  };
2824
2962
 
@@ -2847,19 +2985,13 @@ class StreamElement extends HTMLElement {
2847
2985
  } catch (_a) {}
2848
2986
  }
2849
2987
  removeDuplicateTargetChildren() {
2850
- this.duplicateChildren.forEach((({targetChild: targetChild}) => {
2851
- targetChild.remove();
2852
- }));
2988
+ this.duplicateChildren.forEach((c => c.remove()));
2853
2989
  }
2854
2990
  get duplicateChildren() {
2855
2991
  var _a;
2856
- return [ ...(_a = this.templateContent) === null || _a === void 0 ? void 0 : _a.children ].map((templateChild => {
2857
- let targetChild = [ ...this.targetElement.children ].filter((c => c.id === templateChild.id))[0];
2858
- return {
2859
- targetChild: targetChild,
2860
- templateChild: templateChild
2861
- };
2862
- })).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)));
2863
2995
  }
2864
2996
  get performAction() {
2865
2997
  if (this.action) {
@@ -2871,15 +3003,17 @@ class StreamElement extends HTMLElement {
2871
3003
  }
2872
3004
  this.raise("action attribute is missing");
2873
3005
  }
2874
- get targetElement() {
2875
- var _a;
3006
+ get targetElements() {
2876
3007
  if (this.target) {
2877
- return (_a = this.ownerDocument) === null || _a === void 0 ? void 0 : _a.getElementById(this.target);
3008
+ return this.targetElementsById;
3009
+ } else if (this.targets) {
3010
+ return this.targetElementsByQuery;
3011
+ } else {
3012
+ this.raise("target or targets attribute is missing");
2878
3013
  }
2879
- this.raise("target attribute is missing");
2880
3014
  }
2881
3015
  get templateContent() {
2882
- return this.templateElement.content;
3016
+ return this.templateElement.content.cloneNode(true);
2883
3017
  }
2884
3018
  get templateElement() {
2885
3019
  if (this.firstElementChild instanceof HTMLTemplateElement) {
@@ -2893,6 +3027,9 @@ class StreamElement extends HTMLElement {
2893
3027
  get target() {
2894
3028
  return this.getAttribute("target");
2895
3029
  }
3030
+ get targets() {
3031
+ return this.getAttribute("targets");
3032
+ }
2896
3033
  raise(message) {
2897
3034
  throw new Error(`${this.description}: ${message}`);
2898
3035
  }
@@ -2906,6 +3043,24 @@ class StreamElement extends HTMLElement {
2906
3043
  cancelable: true
2907
3044
  });
2908
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
+ }
2909
3064
  }
2910
3065
 
2911
3066
  FrameElement.delegateConstructor = FrameController;
@@ -2925,7 +3080,7 @@ customElements.define("turbo-stream", StreamElement);
2925
3080
 
2926
3081
  Load your application’s JavaScript bundle inside the <head> element instead. <script> elements in <body> are evaluated with each page change.
2927
3082
 
2928
- For more information, see: https://turbo.hotwire.dev/handbook/building#working-with-script-elements
3083
+ For more information, see: https://turbo.hotwired.dev/handbook/building#working-with-script-elements
2929
3084
 
2930
3085
  ——
2931
3086
  Suppress this warning by adding a "data-turbo-suppress-warning" attribute to: %s
@@ -2973,6 +3128,8 @@ function setProgressBarDelay(delay) {
2973
3128
  var Turbo = Object.freeze({
2974
3129
  __proto__: null,
2975
3130
  navigator: navigator,
3131
+ PageRenderer: PageRenderer,
3132
+ PageSnapshot: PageSnapshot,
2976
3133
  start: start,
2977
3134
  registerAdapter: registerAdapter,
2978
3135
  visit: visit,
@@ -2989,6 +3146,8 @@ start();
2989
3146
 
2990
3147
  var turbo_es2017Esm = Object.freeze({
2991
3148
  __proto__: null,
3149
+ PageRenderer: PageRenderer,
3150
+ PageSnapshot: PageSnapshot,
2992
3151
  clearCache: clearCache,
2993
3152
  connectStreamSource: connectStreamSource,
2994
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
- def turbo_stream_action_tag(action, target:, template: nil)
10
- target = convert_to_turbo_stream_dom_id(target)
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
- %(<turbo-stream action="#{action}" target="#{target}">#{template}</turbo-stream>).html_safe
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: self
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
- o[:object] ||= self
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
- # Send an action of the type <tt>name</tt>. Options described in the concrete methods.
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
- case
128
- when content
129
- turbo_stream_action_tag name, target: target_name, template: (render_record(content) if allow_inferred_rendering) || content
130
- when block_given?
131
- turbo_stream_action_tag name, target: target_name, template: @view_context.capture(&block)
132
- when rendering.any?
133
- turbo_stream_action_tag name, target: target_name, template: @view_context.render(formats: [ :html ], **rendering)
134
- else
135
- turbo_stream_action_tag name, target: target_name, template: (render_record(target) if allow_inferred_rendering)
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
- APPLICATION_LAYOUT_PATH = Rails.root.join("app/views/layouts/application.html.erb")
2
- IMPORTMAP_PATH = Rails.root.join("app/assets/javascripts/importmap.json.erb")
1
+ APP_JS_ROOT = Rails.root.join("app/assets/javascripts")
2
+ CABLE_CONFIG_PATH = Rails.root.join("config/cable.yml")
3
3
 
4
- if APPLICATION_LAYOUT_PATH.exist?
5
- say "Yield head in application layout for cache helper"
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
- if APPLICATION_LAYOUT_PATH.read =~ /stimulus/
9
- say "Add Turbo include tags in application layout"
10
- insert_into_file APPLICATION_LAYOUT_PATH.to_s, %(\n <%= javascript_include_tag "turbo", type: "module-shim" %>), after: /<%= stimulus_include_tags %>/
7
+ if CABLE_CONFIG_PATH.exist?
8
+ say "Enable redis in bundle"
9
+ uncomment_lines "Gemfile", %(gem 'redis')
11
10
 
12
- if IMPORTMAP_PATH.exist?
13
- say "Add Turbo to importmap"
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 "Default application.html.erb is missing!", :red
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
- say "Enable redis in bundle"
19
- uncomment_lines "Gemfile", %(gem 'redis')
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 "config/cable.yml", /development:\n\s+adapter: async/, "development:\n adapter: redis\n url: redis://localhost:6379/1"
23
-
24
- say "Turbo successfully installed ⚡️", :green
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
@@ -26,6 +26,14 @@ module Turbo
26
26
  end
27
27
  end
28
28
 
29
+ initializer "turbo.importmap" do
30
+ if Rails.application.config.respond_to?(:importmap)
31
+ Rails.application.config.importmap.paths.tap do |paths|
32
+ paths.asset "@hotwired/turbo-rails", path: "turbo"
33
+ end
34
+ end
35
+ end
36
+
29
37
  initializer "turbo.helpers", before: :load_config_initializers do
30
38
  ActiveSupport.on_load(:action_controller_base) do
31
39
  include Turbo::Streams::TurboStreamsTagBuilder, Turbo::Frames::FrameRequest, Turbo::Native::Navigation
data/lib/turbo/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Turbo
2
- VERSION = "0.5.10"
2
+ VERSION = "0.7.0"
3
3
  end
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.5.10
4
+ version: 0.7.0
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-06-12 00:00:00.000000000 Z
13
+ date: 2021-08-10 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: rails