turbo-rails 0.7.4 → 0.7.8

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: 2608402a944433e82b78b6aed5ac64e54579566e23a5a3b1a6837a695c7031ff
4
- data.tar.gz: b342da8a4a13bc4b9fbbec5ee1a06afff0db881e3ab4103adba4c47188dd351a
3
+ metadata.gz: b462e206a285eef400a2bab8a76a9eb74a9eba0d198acc1629b30095a87fa196
4
+ data.tar.gz: 1201d9ad8acb6179db7418850fe4b4afd22089b006d7d88ae23f3c87b918cbd5
5
5
  SHA512:
6
- metadata.gz: 9183cb91a6ce9b2bc0b401ec35b7a3ff835b30d2732c96c6ab8db6ef23a3276e80e08ade5648ceb118c00a8cb5b2b07bb87e6ac8451224bf72885d12fcde2204
7
- data.tar.gz: 0665d67d6cb734a670c12412588e23359a63204a72d92fce01a940e19b34e3e8a4217acf42e88b61d60b0700b6bc36ed2fba3e533140ea69503a7a047597fea0
6
+ metadata.gz: 347aa59be1a2cec371863c180680d01942d1d2f0ada0ec6bdf73f79a9492b5af33bdcb4e58b0de14f9c155cee558583e9821813ef10a9d0c11b30b6fb3cc7638
7
+ data.tar.gz: 140dfbfe9c4091351fddce82c3e717563803d3979689ac546b5a41e1215fc668793c4895466be6d37967fb274d674c643075d37945267098c4551723901d1344
data/README.md CHANGED
@@ -15,6 +15,15 @@ During rendering, Turbo replaces the current `<body>` element outright and merge
15
15
 
16
16
  Whereas Turbolinks previously just dealt with links, Turbo can now also process form submissions and responses. This means the entire flow in the web application is wrapped into Turbo, making all the parts fast. No more need for `data-remote=true`.
17
17
 
18
+ Turbo Drive can be disabled on a per-element basis by annotating the element or any of its ancestors with `data-turbo="false"`. If you want Turbo Drive to be disabled by default, then you can adjust your import like this:
19
+
20
+ ```js
21
+ import { Turbo } from "@hotwired/turbo-rails"
22
+ Turbo.session.drive = false
23
+ ```
24
+
25
+ Then you can use `data-turbo="true"` to enable Drive on a per-element basis.
26
+
18
27
 
19
28
  ## Turbo Frames
20
29
 
@@ -37,6 +46,7 @@ The JavaScript for Turbo can either be run through the asset pipeline, which is
37
46
  1. Add the `turbo-rails` gem to your Gemfile: `gem 'turbo-rails'`
38
47
  2. Run `./bin/bundle install`
39
48
  3. Run `./bin/rails turbo:install`
49
+ 4. Run `./bin/rails turbo:install:redis` to change the development Action Cable adapter from Async (the default one) to Redis. The Async adapter does not support Turbo Stream broadcasting.
40
50
 
41
51
  Running `turbo:install` will install through NPM if Webpacker is installed in the application. Otherwise the asset pipeline version is used. To use the asset pipeline version, you must have `importmap-rails` installed first and listed higher in the Gemfile.
42
52
 
@@ -48,6 +58,7 @@ The `Turbo` instance is automatically assigned to `window.Turbo` upon import:
48
58
  import "@hotwired/turbo-rails"
49
59
  ```
50
60
 
61
+
51
62
  ## Usage
52
63
 
53
64
  You can watch [the video introduction to Hotwire](https://hotwired.dev/#screencast), which focuses extensively on demonstration Turbo in a Rails demo. Then you should familiarize yourself with [Turbo handbook](https://turbo.hotwired.dev/handbook/introduction) to understand Drive, Frames, and Streams in-depth. Finally, dive into the code documentation by starting with [`Turbo::FramesHelper`](https://github.com/hotwired/turbo-rails/blob/main/app/helpers/turbo/frames_helper.rb), [`Turbo::StreamsHelper`](https://github.com/hotwired/turbo-rails/blob/main/app/helpers/turbo/streams_helper.rb), [`Turbo::Streams::TagBuilder`](https://github.com/hotwired/turbo-rails/blob/main/app/models/turbo/streams/tag_builder.rb), and [`Turbo::Broadcastable`](https://github.com/hotwired/turbo-rails/blob/main/app/models/concerns/turbo/broadcastable.rb).
@@ -361,8 +361,10 @@ class FetchRequest {
361
361
  const response = await fetch(this.url.href, fetchOptions);
362
362
  return await this.receive(response);
363
363
  } catch (error) {
364
- this.delegate.requestErrored(this, error);
365
- throw error;
364
+ if (error.name !== "AbortError") {
365
+ this.delegate.requestErrored(this, error);
366
+ throw error;
367
+ }
366
368
  } finally {
367
369
  this.delegate.requestFinished(this);
368
370
  }
@@ -385,13 +387,15 @@ class FetchRequest {
385
387
  return fetchResponse;
386
388
  }
387
389
  get fetchOptions() {
390
+ var _a;
388
391
  return {
389
392
  method: FetchMethod[this.method].toUpperCase(),
390
393
  credentials: "same-origin",
391
394
  headers: this.headers,
392
395
  redirect: "follow",
393
396
  body: this.body,
394
- signal: this.abortSignal
397
+ signal: this.abortSignal,
398
+ referrer: (_a = this.delegate.referrer) === null || _a === void 0 ? void 0 : _a.href
395
399
  };
396
400
  }
397
401
  get defaultHeaders() {
@@ -544,7 +548,8 @@ class FormSubmission {
544
548
  }
545
549
  get action() {
546
550
  var _a;
547
- return ((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formaction")) || this.formElement.action;
551
+ const formElementAction = typeof this.formElement.action === "string" ? this.formElement.action : null;
552
+ return ((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formaction")) || this.formElement.getAttribute("action") || formElementAction || "";
548
553
  }
549
554
  get location() {
550
555
  return expandURL(this.action);
@@ -691,11 +696,7 @@ class Snapshot {
691
696
  return this.getElementForAnchor(anchor) != null;
692
697
  }
693
698
  getElementForAnchor(anchor) {
694
- try {
695
- return this.element.querySelector(`[id='${anchor}'], a[name='${anchor}']`);
696
- } catch (_a) {
697
- return null;
698
- }
699
+ return anchor ? this.element.querySelector(`[id='${anchor}'], a[name='${anchor}']`) : null;
699
700
  }
700
701
  get isConnected() {
701
702
  return this.element.isConnected;
@@ -785,6 +786,12 @@ class View {
785
786
  scrollToPosition({x: x, y: y}) {
786
787
  this.scrollRoot.scrollTo(x, y);
787
788
  }
789
+ scrollToTop() {
790
+ this.scrollToPosition({
791
+ x: 0,
792
+ y: 0
793
+ });
794
+ }
788
795
  get scrollRoot() {
789
796
  return window;
790
797
  }
@@ -1354,6 +1361,9 @@ class Visit {
1354
1361
  get restorationData() {
1355
1362
  return this.history.getRestorationDataForIdentifier(this.restorationIdentifier);
1356
1363
  }
1364
+ get silent() {
1365
+ return this.isSamePage;
1366
+ }
1357
1367
  start() {
1358
1368
  if (this.state == VisitState.initialized) {
1359
1369
  this.recordTimingMetric(TimingMetric.visitStart);
@@ -1377,6 +1387,7 @@ class Visit {
1377
1387
  this.state = VisitState.completed;
1378
1388
  this.adapter.visitCompleted(this);
1379
1389
  this.delegate.visitCompleted(this);
1390
+ this.followRedirect();
1380
1391
  }
1381
1392
  }
1382
1393
  fail() {
@@ -1483,8 +1494,10 @@ class Visit {
1483
1494
  }
1484
1495
  followRedirect() {
1485
1496
  if (this.redirectedToLocation && !this.followedRedirect) {
1486
- this.location = this.redirectedToLocation;
1487
- this.history.replace(this.redirectedToLocation, this.restorationIdentifier);
1497
+ this.adapter.visitProposedToLocation(this.redirectedToLocation, {
1498
+ action: "replace",
1499
+ response: this.response
1500
+ });
1488
1501
  this.followedRedirect = true;
1489
1502
  }
1490
1503
  }
@@ -1538,9 +1551,9 @@ class Visit {
1538
1551
  performScroll() {
1539
1552
  if (!this.scrolled) {
1540
1553
  if (this.action == "restore") {
1541
- this.scrollToRestoredPosition() || this.scrollToAnchor() || this.scrollToTop();
1554
+ this.scrollToRestoredPosition() || this.scrollToAnchor() || this.view.scrollToTop();
1542
1555
  } else {
1543
- this.scrollToAnchor() || this.scrollToTop();
1556
+ this.scrollToAnchor() || this.view.scrollToTop();
1544
1557
  }
1545
1558
  if (this.isSamePage) {
1546
1559
  this.delegate.visitScrolledToSamePageLocation(this.view.lastRenderedLocation, this.location);
@@ -1562,12 +1575,6 @@ class Visit {
1562
1575
  return true;
1563
1576
  }
1564
1577
  }
1565
- scrollToTop() {
1566
- this.view.scrollToPosition({
1567
- x: 0,
1568
- y: 0
1569
- });
1570
- }
1571
1578
  recordTimingMetric(metric) {
1572
1579
  this.timingMetrics[metric] = (new Date).getTime();
1573
1580
  }
@@ -1643,7 +1650,7 @@ class BrowserAdapter {
1643
1650
  visitRequestStarted(visit) {
1644
1651
  this.progressBar.setValue(0);
1645
1652
  if (visit.hasCachedSnapshot() || visit.action != "restore") {
1646
- this.showProgressBarAfterDelay();
1653
+ this.showVisitProgressBarAfterDelay();
1647
1654
  } else {
1648
1655
  this.showProgressBar();
1649
1656
  }
@@ -1664,24 +1671,42 @@ class BrowserAdapter {
1664
1671
  }
1665
1672
  visitRequestFinished(visit) {
1666
1673
  this.progressBar.setValue(1);
1667
- this.hideProgressBar();
1668
- }
1669
- visitCompleted(visit) {
1670
- visit.followRedirect();
1674
+ this.hideVisitProgressBar();
1671
1675
  }
1676
+ visitCompleted(visit) {}
1672
1677
  pageInvalidated() {
1673
1678
  this.reload();
1674
1679
  }
1675
1680
  visitFailed(visit) {}
1676
1681
  visitRendered(visit) {}
1677
- showProgressBarAfterDelay() {
1678
- this.progressBarTimeout = window.setTimeout(this.showProgressBar, this.session.progressBarDelay);
1682
+ formSubmissionStarted(formSubmission) {
1683
+ this.progressBar.setValue(0);
1684
+ this.showFormProgressBarAfterDelay();
1679
1685
  }
1680
- hideProgressBar() {
1686
+ formSubmissionFinished(formSubmission) {
1687
+ this.progressBar.setValue(1);
1688
+ this.hideFormProgressBar();
1689
+ }
1690
+ showVisitProgressBarAfterDelay() {
1691
+ this.visitProgressBarTimeout = window.setTimeout(this.showProgressBar, this.session.progressBarDelay);
1692
+ }
1693
+ hideVisitProgressBar() {
1681
1694
  this.progressBar.hide();
1682
- if (this.progressBarTimeout != null) {
1683
- window.clearTimeout(this.progressBarTimeout);
1684
- delete this.progressBarTimeout;
1695
+ if (this.visitProgressBarTimeout != null) {
1696
+ window.clearTimeout(this.visitProgressBarTimeout);
1697
+ delete this.visitProgressBarTimeout;
1698
+ }
1699
+ }
1700
+ showFormProgressBarAfterDelay() {
1701
+ if (this.formProgressBarTimeout == null) {
1702
+ this.formProgressBarTimeout = window.setTimeout(this.showProgressBar, this.session.progressBarDelay);
1703
+ }
1704
+ }
1705
+ hideFormProgressBar() {
1706
+ this.progressBar.hide();
1707
+ if (this.formProgressBarTimeout != null) {
1708
+ window.clearTimeout(this.formProgressBarTimeout);
1709
+ delete this.formProgressBarTimeout;
1685
1710
  }
1686
1711
  }
1687
1712
  reload() {
@@ -1772,6 +1797,7 @@ class FrameRedirector {
1772
1797
  linkClickIntercepted(element, url) {
1773
1798
  const frame = this.findFrameElement(element);
1774
1799
  if (frame) {
1800
+ frame.setAttribute("reloadable", "");
1775
1801
  frame.src = url;
1776
1802
  }
1777
1803
  }
@@ -1781,6 +1807,7 @@ class FrameRedirector {
1781
1807
  formSubmissionIntercepted(element, submitter) {
1782
1808
  const frame = this.findFrameElement(element);
1783
1809
  if (frame) {
1810
+ frame.removeAttribute("reloadable");
1784
1811
  frame.delegate.formSubmissionIntercepted(element, submitter);
1785
1812
  }
1786
1813
  }
@@ -1891,7 +1918,8 @@ class LinkClickObserver {
1891
1918
  };
1892
1919
  this.clickBubbled = event => {
1893
1920
  if (this.clickEventIsSignificant(event)) {
1894
- const link = this.findLinkFromClickTarget(event.target);
1921
+ const target = event.composedPath && event.composedPath()[0] || event.target;
1922
+ const link = this.findLinkFromClickTarget(target);
1895
1923
  if (link) {
1896
1924
  const location = this.getLocationForLink(link);
1897
1925
  if (this.delegate.willFollowLinkToLocation(link, location)) {
@@ -1937,7 +1965,7 @@ class Navigator {
1937
1965
  this.delegate = delegate;
1938
1966
  }
1939
1967
  proposeVisit(location, options = {}) {
1940
- if (this.delegate.allowsVisitingLocation(location)) {
1968
+ if (this.delegate.allowsVisitingLocationWithAction(location, options.action)) {
1941
1969
  this.delegate.visitProposedToLocation(location, options);
1942
1970
  }
1943
1971
  }
@@ -1978,7 +2006,11 @@ class Navigator {
1978
2006
  get history() {
1979
2007
  return this.delegate.history;
1980
2008
  }
1981
- formSubmissionStarted(formSubmission) {}
2009
+ formSubmissionStarted(formSubmission) {
2010
+ if (typeof this.adapter.formSubmissionStarted === "function") {
2011
+ this.adapter.formSubmissionStarted(formSubmission);
2012
+ }
2013
+ }
1982
2014
  async formSubmissionSucceededWithResponse(formSubmission, fetchResponse) {
1983
2015
  if (formSubmission == this.formSubmission) {
1984
2016
  const responseHTML = await fetchResponse.responseHTML;
@@ -2001,14 +2033,23 @@ class Navigator {
2001
2033
  const responseHTML = await fetchResponse.responseHTML;
2002
2034
  if (responseHTML) {
2003
2035
  const snapshot = PageSnapshot.fromHTMLString(responseHTML);
2004
- await this.view.renderPage(snapshot);
2036
+ if (fetchResponse.serverError) {
2037
+ await this.view.renderError(snapshot);
2038
+ } else {
2039
+ await this.view.renderPage(snapshot);
2040
+ }
2041
+ this.view.scrollToTop();
2005
2042
  this.view.clearSnapshotCache();
2006
2043
  }
2007
2044
  }
2008
2045
  formSubmissionErrored(formSubmission, error) {
2009
2046
  console.error(error);
2010
2047
  }
2011
- formSubmissionFinished(formSubmission) {}
2048
+ formSubmissionFinished(formSubmission) {
2049
+ if (typeof this.adapter.formSubmissionFinished === "function") {
2050
+ this.adapter.formSubmissionFinished(formSubmission);
2051
+ }
2052
+ }
2012
2053
  visitStarted(visit) {
2013
2054
  this.delegate.visitStarted(visit);
2014
2055
  }
@@ -2016,7 +2057,10 @@ class Navigator {
2016
2057
  this.delegate.visitCompleted(visit);
2017
2058
  }
2018
2059
  locationWithActionIsSamePage(location, action) {
2019
- return getRequestURL(location) === getRequestURL(this.view.lastRenderedLocation) && (getAnchor(location) != null || action == "restore");
2060
+ const anchor = getAnchor(location);
2061
+ const currentAnchor = getAnchor(this.view.lastRenderedLocation);
2062
+ const isRestorationToTop = action === "restore" && typeof anchor === "undefined";
2063
+ return action !== "replace" && getRequestURL(location) === getRequestURL(this.view.lastRenderedLocation) && (isRestorationToTop || anchor != null && anchor !== currentAnchor);
2020
2064
  }
2021
2065
  visitScrolledToSamePageLocation(oldURL, newURL) {
2022
2066
  this.delegate.visitScrolledToSamePageLocation(oldURL, newURL);
@@ -2408,6 +2452,7 @@ class Session {
2408
2452
  this.scrollObserver = new ScrollObserver(this);
2409
2453
  this.streamObserver = new StreamObserver(this);
2410
2454
  this.frameRedirector = new FrameRedirector(document.documentElement);
2455
+ this.drive = true;
2411
2456
  this.enabled = true;
2412
2457
  this.progressBarDelay = 500;
2413
2458
  this.started = false;
@@ -2485,7 +2530,7 @@ class Session {
2485
2530
  });
2486
2531
  }
2487
2532
  willFollowLinkToLocation(link, location) {
2488
- return elementIsNavigable(link) && this.locationIsVisitable(location) && this.applicationAllowsFollowingLinkToLocation(link, location);
2533
+ return this.elementDriveEnabled(link) && this.locationIsVisitable(location) && this.applicationAllowsFollowingLinkToLocation(link, location);
2489
2534
  }
2490
2535
  followedLinkToLocation(link, location) {
2491
2536
  const action = this.getActionForLink(link);
@@ -2494,13 +2539,12 @@ class Session {
2494
2539
  });
2495
2540
  }
2496
2541
  convertLinkWithMethodClickToFormSubmission(link) {
2497
- var _a;
2498
2542
  const linkMethod = link.getAttribute("data-turbo-method");
2499
2543
  if (linkMethod) {
2500
2544
  const form = document.createElement("form");
2501
2545
  form.method = linkMethod;
2502
2546
  form.action = link.getAttribute("href") || "undefined";
2503
- (_a = link.parentNode) === null || _a === void 0 ? void 0 : _a.insertBefore(form, link);
2547
+ document.body.appendChild(form);
2504
2548
  return dispatch("submit", {
2505
2549
  cancelable: true,
2506
2550
  target: form
@@ -2509,8 +2553,8 @@ class Session {
2509
2553
  return false;
2510
2554
  }
2511
2555
  }
2512
- allowsVisitingLocation(location) {
2513
- return this.applicationAllowsVisitingLocation(location);
2556
+ allowsVisitingLocationWithAction(location, action) {
2557
+ return this.locationWithActionIsSamePage(location, action) || this.applicationAllowsVisitingLocation(location);
2514
2558
  }
2515
2559
  visitProposedToLocation(location, options) {
2516
2560
  extendURLWithDeprecatedProperties(location);
@@ -2518,7 +2562,9 @@ class Session {
2518
2562
  }
2519
2563
  visitStarted(visit) {
2520
2564
  extendURLWithDeprecatedProperties(visit.location);
2521
- this.notifyApplicationAfterVisitingLocation(visit.location, visit.action);
2565
+ if (!visit.silent) {
2566
+ this.notifyApplicationAfterVisitingLocation(visit.location, visit.action);
2567
+ }
2522
2568
  }
2523
2569
  visitCompleted(visit) {
2524
2570
  this.notifyApplicationAfterPageLoad(visit.getTimingMetrics());
@@ -2530,7 +2576,7 @@ class Session {
2530
2576
  this.notifyApplicationAfterVisitingSamePageLocation(oldURL, newURL);
2531
2577
  }
2532
2578
  willSubmitForm(form, submitter) {
2533
- return elementIsNavigable(form) && elementIsNavigable(submitter);
2579
+ return this.elementDriveEnabled(form) && this.elementDriveEnabled(submitter);
2534
2580
  }
2535
2581
  formSubmitted(form, submitter) {
2536
2582
  this.navigator.submitForm(form, submitter);
@@ -2549,7 +2595,10 @@ class Session {
2549
2595
  this.renderStreamMessage(message);
2550
2596
  }
2551
2597
  viewWillCacheSnapshot() {
2552
- this.notifyApplicationBeforeCachingSnapshot();
2598
+ var _a;
2599
+ if (!((_a = this.navigator.currentVisit) === null || _a === void 0 ? void 0 : _a.silent)) {
2600
+ this.notifyApplicationBeforeCachingSnapshot();
2601
+ }
2553
2602
  }
2554
2603
  allowsImmediateRender({element: element}, resume) {
2555
2604
  const event = this.notifyApplicationBeforeRender(element, resume);
@@ -2562,6 +2611,12 @@ class Session {
2562
2611
  viewInvalidated() {
2563
2612
  this.adapter.pageInvalidated();
2564
2613
  }
2614
+ frameLoaded(frame) {
2615
+ this.notifyApplicationAfterFrameLoad(frame);
2616
+ }
2617
+ frameRendered(fetchResponse, frame) {
2618
+ this.notifyApplicationAfterFrameRender(fetchResponse, frame);
2619
+ }
2565
2620
  applicationAllowsFollowingLinkToLocation(link, location) {
2566
2621
  const event = this.notifyApplicationAfterClickingLinkToLocation(link, location);
2567
2622
  return !event.defaultPrevented;
@@ -2624,6 +2679,36 @@ class Session {
2624
2679
  newURL: newURL.toString()
2625
2680
  }));
2626
2681
  }
2682
+ notifyApplicationAfterFrameLoad(frame) {
2683
+ return dispatch("turbo:frame-load", {
2684
+ target: frame
2685
+ });
2686
+ }
2687
+ notifyApplicationAfterFrameRender(fetchResponse, frame) {
2688
+ return dispatch("turbo:frame-render", {
2689
+ detail: {
2690
+ fetchResponse: fetchResponse
2691
+ },
2692
+ target: frame,
2693
+ cancelable: true
2694
+ });
2695
+ }
2696
+ elementDriveEnabled(element) {
2697
+ const container = element === null || element === void 0 ? void 0 : element.closest("[data-turbo]");
2698
+ if (this.drive) {
2699
+ if (container) {
2700
+ return container.getAttribute("data-turbo") != "false";
2701
+ } else {
2702
+ return true;
2703
+ }
2704
+ } else {
2705
+ if (container) {
2706
+ return container.getAttribute("data-turbo") == "true";
2707
+ } else {
2708
+ return false;
2709
+ }
2710
+ }
2711
+ }
2627
2712
  getActionForLink(link) {
2628
2713
  const action = link.getAttribute("data-turbo-action");
2629
2714
  return isAction(action) ? action : "advance";
@@ -2636,15 +2721,6 @@ class Session {
2636
2721
  }
2637
2722
  }
2638
2723
 
2639
- function elementIsNavigable(element) {
2640
- const container = element === null || element === void 0 ? void 0 : element.closest("[data-turbo]");
2641
- if (container) {
2642
- return container.getAttribute("data-turbo") != "false";
2643
- } else {
2644
- return true;
2645
- }
2646
- }
2647
-
2648
2724
  function extendURLWithDeprecatedProperties(url) {
2649
2725
  Object.defineProperties(url, deprecatedLocationPropertyDescriptors);
2650
2726
  }
@@ -2657,6 +2733,58 @@ const deprecatedLocationPropertyDescriptors = {
2657
2733
  }
2658
2734
  };
2659
2735
 
2736
+ const session = new Session;
2737
+
2738
+ const {navigator: navigator} = session;
2739
+
2740
+ function start() {
2741
+ session.start();
2742
+ }
2743
+
2744
+ function registerAdapter(adapter) {
2745
+ session.registerAdapter(adapter);
2746
+ }
2747
+
2748
+ function visit(location, options) {
2749
+ session.visit(location, options);
2750
+ }
2751
+
2752
+ function connectStreamSource(source) {
2753
+ session.connectStreamSource(source);
2754
+ }
2755
+
2756
+ function disconnectStreamSource(source) {
2757
+ session.disconnectStreamSource(source);
2758
+ }
2759
+
2760
+ function renderStreamMessage(message) {
2761
+ session.renderStreamMessage(message);
2762
+ }
2763
+
2764
+ function clearCache() {
2765
+ session.clearCache();
2766
+ }
2767
+
2768
+ function setProgressBarDelay(delay) {
2769
+ session.setProgressBarDelay(delay);
2770
+ }
2771
+
2772
+ var Turbo = Object.freeze({
2773
+ __proto__: null,
2774
+ navigator: navigator,
2775
+ session: session,
2776
+ PageRenderer: PageRenderer,
2777
+ PageSnapshot: PageSnapshot,
2778
+ start: start,
2779
+ registerAdapter: registerAdapter,
2780
+ visit: visit,
2781
+ connectStreamSource: connectStreamSource,
2782
+ disconnectStreamSource: disconnectStreamSource,
2783
+ renderStreamMessage: renderStreamMessage,
2784
+ clearCache: clearCache,
2785
+ setProgressBarDelay: setProgressBarDelay
2786
+ });
2787
+
2660
2788
  class FrameController {
2661
2789
  constructor(element) {
2662
2790
  this.resolveVisitPromise = () => {};
@@ -2672,6 +2800,7 @@ class FrameController {
2672
2800
  connect() {
2673
2801
  if (!this.connected) {
2674
2802
  this.connected = true;
2803
+ this.reloadable = false;
2675
2804
  if (this.loadingStyle == FrameLoadingStyle.lazy) {
2676
2805
  this.appearanceObserver.start();
2677
2806
  }
@@ -2707,7 +2836,7 @@ class FrameController {
2707
2836
  }
2708
2837
  }
2709
2838
  async loadSourceURL() {
2710
- if (!this.settingSourceURL && this.enabled && this.isActive && this.sourceURL != this.currentURL) {
2839
+ if (!this.settingSourceURL && this.enabled && this.isActive && (this.reloadable || this.sourceURL != this.currentURL)) {
2711
2840
  const previousURL = this.currentURL;
2712
2841
  this.currentURL = this.sourceURL;
2713
2842
  if (this.sourceURL) {
@@ -2716,6 +2845,7 @@ class FrameController {
2716
2845
  this.appearanceObserver.stop();
2717
2846
  await this.element.loaded;
2718
2847
  this.hasBeenLoaded = true;
2848
+ session.frameLoaded(this.element);
2719
2849
  } catch (error) {
2720
2850
  this.currentURL = previousURL;
2721
2851
  throw error;
@@ -2735,6 +2865,7 @@ class FrameController {
2735
2865
  const renderer = new FrameRenderer(this.view.snapshot, snapshot, false);
2736
2866
  if (this.view.renderPromise) await this.view.renderPromise;
2737
2867
  await this.view.render(renderer);
2868
+ session.frameRendered(fetchResponse, this.element);
2738
2869
  }
2739
2870
  } catch (error) {
2740
2871
  console.error(error);
@@ -2752,6 +2883,7 @@ class FrameController {
2752
2883
  }
2753
2884
  }
2754
2885
  linkClickIntercepted(element, url) {
2886
+ this.reloadable = true;
2755
2887
  this.navigateFrame(element, url);
2756
2888
  }
2757
2889
  shouldInterceptFormSubmission(element, submitter) {
@@ -2761,6 +2893,7 @@ class FrameController {
2761
2893
  if (this.formSubmission) {
2762
2894
  this.formSubmission.stop();
2763
2895
  }
2896
+ this.reloadable = false;
2764
2897
  this.formSubmission = new FormSubmission(this, element, submitter);
2765
2898
  if (this.formSubmission.fetchRequest.isIdempotent) {
2766
2899
  this.navigateFrame(element, this.formSubmission.fetchRequest.url.href);
@@ -2864,10 +2997,10 @@ class FrameController {
2864
2997
  return !frameElement.disabled;
2865
2998
  }
2866
2999
  }
2867
- if (!elementIsNavigable(element)) {
3000
+ if (!session.elementDriveEnabled(element)) {
2868
3001
  return false;
2869
3002
  }
2870
- if (submitter && !elementIsNavigable(submitter)) {
3003
+ if (submitter && !session.elementDriveEnabled(submitter)) {
2871
3004
  return false;
2872
3005
  }
2873
3006
  return true;
@@ -2883,6 +3016,18 @@ class FrameController {
2883
3016
  return this.element.src;
2884
3017
  }
2885
3018
  }
3019
+ get reloadable() {
3020
+ const frame = this.findFrameElement(this.element);
3021
+ return frame.hasAttribute("reloadable");
3022
+ }
3023
+ set reloadable(value) {
3024
+ const frame = this.findFrameElement(this.element);
3025
+ if (value) {
3026
+ frame.setAttribute("reloadable", "");
3027
+ } else {
3028
+ frame.removeAttribute("reloadable");
3029
+ }
3030
+ }
2886
3031
  set sourceURL(sourceURL) {
2887
3032
  this.settingSourceURL = true;
2888
3033
  this.element.src = sourceURL !== null && sourceURL !== void 0 ? sourceURL : null;
@@ -3089,57 +3234,6 @@ customElements.define("turbo-stream", StreamElement);
3089
3234
  }
3090
3235
  })();
3091
3236
 
3092
- const session = new Session;
3093
-
3094
- const {navigator: navigator} = session;
3095
-
3096
- function start() {
3097
- session.start();
3098
- }
3099
-
3100
- function registerAdapter(adapter) {
3101
- session.registerAdapter(adapter);
3102
- }
3103
-
3104
- function visit(location, options) {
3105
- session.visit(location, options);
3106
- }
3107
-
3108
- function connectStreamSource(source) {
3109
- session.connectStreamSource(source);
3110
- }
3111
-
3112
- function disconnectStreamSource(source) {
3113
- session.disconnectStreamSource(source);
3114
- }
3115
-
3116
- function renderStreamMessage(message) {
3117
- session.renderStreamMessage(message);
3118
- }
3119
-
3120
- function clearCache() {
3121
- session.clearCache();
3122
- }
3123
-
3124
- function setProgressBarDelay(delay) {
3125
- session.setProgressBarDelay(delay);
3126
- }
3127
-
3128
- var Turbo = Object.freeze({
3129
- __proto__: null,
3130
- navigator: navigator,
3131
- PageRenderer: PageRenderer,
3132
- PageSnapshot: PageSnapshot,
3133
- start: start,
3134
- registerAdapter: registerAdapter,
3135
- visit: visit,
3136
- connectStreamSource: connectStreamSource,
3137
- disconnectStreamSource: disconnectStreamSource,
3138
- renderStreamMessage: renderStreamMessage,
3139
- clearCache: clearCache,
3140
- setProgressBarDelay: setProgressBarDelay
3141
- });
3142
-
3143
3237
  window.Turbo = Turbo;
3144
3238
 
3145
3239
  start();
@@ -3154,6 +3248,7 @@ var turbo_es2017Esm = Object.freeze({
3154
3248
  navigator: navigator,
3155
3249
  registerAdapter: registerAdapter,
3156
3250
  renderStreamMessage: renderStreamMessage,
3251
+ session: session,
3157
3252
  setProgressBarDelay: setProgressBarDelay,
3158
3253
  start: start,
3159
3254
  visit: visit
@@ -5,71 +5,69 @@
5
5
  module Turbo::Streams::Broadcasts
6
6
  include Turbo::Streams::ActionHelper
7
7
 
8
- def broadcast_remove_to(*streamables, target:)
9
- broadcast_action_to *streamables, action: :remove, target: target
8
+ def broadcast_remove_to(*streamables, **opts)
9
+ broadcast_action_to *streamables, action: :remove, **opts
10
10
  end
11
11
 
12
- def broadcast_replace_to(*streamables, target:, **rendering)
13
- broadcast_action_to *streamables, action: :replace, target: target, **rendering
12
+ def broadcast_replace_to(*streamables, **opts)
13
+ broadcast_action_to *streamables, action: :replace, **opts
14
14
  end
15
15
 
16
- def broadcast_update_to(*streamables, target:, **rendering)
17
- broadcast_action_to *streamables, action: :update, target: target, **rendering
16
+ def broadcast_update_to(*streamables, **opts)
17
+ broadcast_action_to *streamables, action: :update, **opts
18
18
  end
19
19
 
20
- def broadcast_before_to(*streamables, target:, **rendering)
21
- broadcast_action_to *streamables, action: :before, target: target, **rendering
20
+ def broadcast_before_to(*streamables, **opts)
21
+ broadcast_action_to *streamables, action: :before, **opts
22
22
  end
23
23
 
24
- def broadcast_after_to(*streamables, target:, **rendering)
25
- broadcast_action_to *streamables, action: :after, target: target, **rendering
24
+ def broadcast_after_to(*streamables, **opts)
25
+ broadcast_action_to *streamables, action: :after, **opts
26
26
  end
27
27
 
28
- def broadcast_append_to(*streamables, target:, **rendering)
29
- broadcast_action_to *streamables, action: :append, target: target, **rendering
28
+ def broadcast_append_to(*streamables, **opts)
29
+ broadcast_action_to *streamables, action: :append, **opts
30
30
  end
31
31
 
32
- def broadcast_prepend_to(*streamables, target:, **rendering)
33
- broadcast_action_to *streamables, action: :prepend, target: target, **rendering
32
+ def broadcast_prepend_to(*streamables, **opts)
33
+ broadcast_action_to *streamables, action: :prepend, **opts
34
34
  end
35
35
 
36
- def broadcast_action_to(*streamables, action:, target:, **rendering)
37
- broadcast_stream_to *streamables, content: turbo_stream_action_tag(action, target: target, template:
36
+ def broadcast_action_to(*streamables, action:, target: nil, targets: nil, **rendering)
37
+ broadcast_stream_to *streamables, content: turbo_stream_action_tag(action, target: target, targets: targets, template:
38
38
  rendering.delete(:content) || (rendering.any? ? render_format(:html, **rendering) : nil)
39
39
  )
40
40
  end
41
41
 
42
-
43
- def broadcast_replace_later_to(*streamables, target:, **rendering)
44
- broadcast_action_later_to *streamables, action: :replace, target: target, **rendering
42
+ def broadcast_replace_later_to(*streamables, **opts)
43
+ broadcast_action_later_to *streamables, action: :replace, **opts
45
44
  end
46
45
 
47
- def broadcast_update_later_to(*streamables, target:, **rendering)
48
- broadcast_action_later_to *streamables, action: :update, target: target, **rendering
46
+ def broadcast_update_later_to(*streamables, **opts)
47
+ broadcast_action_later_to *streamables, action: :update, **opts
49
48
  end
50
49
 
51
- def broadcast_before_later_to(*streamables, target:, **rendering)
52
- broadcast_action_later_to *streamables, action: :before, target: target, **rendering
50
+ def broadcast_before_later_to(*streamables, **opts)
51
+ broadcast_action_later_to *streamables, action: :before, **opts
53
52
  end
54
53
 
55
- def broadcast_after_later_to(*streamables, target:, **rendering)
56
- broadcast_action_later_to *streamables, action: :after, target: target, **rendering
54
+ def broadcast_after_later_to(*streamables, **opts)
55
+ broadcast_action_later_to *streamables, action: :after, **opts
57
56
  end
58
57
 
59
- def broadcast_append_later_to(*streamables, target:, **rendering)
60
- broadcast_action_later_to *streamables, action: :append, target: target, **rendering
58
+ def broadcast_append_later_to(*streamables, **opts)
59
+ broadcast_action_later_to *streamables, action: :append, **opts
61
60
  end
62
61
 
63
- def broadcast_prepend_later_to(*streamables, target:, **rendering)
64
- broadcast_action_later_to *streamables, action: :prepend, target: target, **rendering
62
+ def broadcast_prepend_later_to(*streamables, **opts)
63
+ broadcast_action_later_to *streamables, action: :prepend, **opts
65
64
  end
66
65
 
67
- def broadcast_action_later_to(*streamables, action:, target:, **rendering)
66
+ def broadcast_action_later_to(*streamables, action:, target: nil, targets: nil, **rendering)
68
67
  Turbo::Streams::ActionBroadcastJob.perform_later \
69
- stream_name_from(streamables), action: action, target: target, **rendering
68
+ stream_name_from(streamables), action: action, target: target, targets: targets, **rendering
70
69
  end
71
70
 
72
-
73
71
  def broadcast_render_to(*streamables, **rendering)
74
72
  broadcast_stream_to *streamables, content: render_format(:turbo_stream, **rendering)
75
73
  end
@@ -12,10 +12,9 @@ module Turbo::Streams::ActionHelper
12
12
  def turbo_stream_action_tag(action, target: nil, targets: nil, template: nil)
13
13
  template = action.to_sym == :remove ? "" : "<template>#{template}</template>"
14
14
 
15
- if target
16
- target = convert_to_turbo_stream_dom_id(target)
15
+ if target = convert_to_turbo_stream_dom_id(target)
17
16
  %(<turbo-stream action="#{action}" target="#{target}">#{template}</turbo-stream>).html_safe
18
- elsif targets
17
+ elsif targets = convert_to_turbo_stream_dom_id(targets)
19
18
  %(<turbo-stream action="#{action}" targets="#{targets}">#{template}</turbo-stream>).html_safe
20
19
  else
21
20
  raise ArgumentError, "target or targets must be supplied"
@@ -212,29 +212,19 @@ class Turbo::Streams::TagBuilder
212
212
 
213
213
  # Send an action of the type <tt>name</tt> to <tt>target</tt>. Options described in the concrete methods.
214
214
  def action(name, target, content = nil, allow_inferred_rendering: true, **rendering, &block)
215
- target_name = extract_target_name_from(target)
216
215
  template = render_template(target, content, allow_inferred_rendering: allow_inferred_rendering, **rendering, &block)
217
216
 
218
- turbo_stream_action_tag name, target: target_name, template: template
217
+ turbo_stream_action_tag name, target: target, template: template
219
218
  end
220
219
 
221
220
  # Send an action of the type <tt>name</tt> to <tt>targets</tt>. Options described in the concrete methods.
222
221
  def action_all(name, targets, content = nil, allow_inferred_rendering: true, **rendering, &block)
223
- targets_name = extract_target_name_from(targets)
224
222
  template = render_template(targets, content, allow_inferred_rendering: allow_inferred_rendering, **rendering, &block)
225
223
 
226
- turbo_stream_action_tag name, targets: targets_name, template: template
224
+ turbo_stream_action_tag name, targets: targets, template: template
227
225
  end
228
226
 
229
227
  private
230
- def extract_target_name_from(target)
231
- if target.respond_to?(:to_key)
232
- ActionView::RecordIdentifier.dom_id(target)
233
- else
234
- target
235
- end
236
- end
237
-
238
228
  def render_template(target, content = nil, allow_inferred_rendering: true, **rendering, &block)
239
229
  case
240
230
  when content
@@ -0,0 +1,9 @@
1
+ if (cable_config_path = Rails.root.join("config/cable.yml")).exist?
2
+ say "Enable redis in bundle"
3
+ uncomment_lines "Gemfile", %(gem 'redis')
4
+
5
+ say "Switch development cable to use redis"
6
+ gsub_file cable_config_path.to_s, /development:\n\s+adapter: async/, "development:\n adapter: redis\n url: redis://localhost:6379/1"
7
+ else
8
+ say 'ActionCable config file (config/cable.yml) is missing. Uncomment "gem \'redis\'" in your Gemfile and create config/cable.yml to use the Turbo Streams broadcast feature.'
9
+ end
@@ -1,34 +1,23 @@
1
- APP_JS_PATH = Rails.root.join("app/javascript/application.js")
2
- CABLE_CONFIG_PATH = Rails.root.join("config/cable.yml")
3
- IMPORTMAP_PATH = Rails.root.join("config/importmap.rb")
4
-
5
- if APP_JS_PATH.exist?
1
+ if (app_js_path = Rails.root.join("app/javascript/application.js")).exist?
6
2
  say "Import turbo-rails in existing app/javascript/application.js"
7
- append_to_file APP_JS_PATH, %(import "@hotwired/turbo-rails"\n)
3
+ append_to_file app_js_path, %(import "@hotwired/turbo-rails"\n)
8
4
  else
9
5
  say <<~INSTRUCTIONS, :red
10
- You must import @hotwire/turbo-rails in your application.js.
6
+ You must import @hotwired/turbo-rails in your application.js.
11
7
  INSTRUCTIONS
12
8
  end
13
9
 
14
- if IMPORTMAP_PATH.exist?
10
+ if (importmap_path = Rails.root.join("config/importmap.rb")).exist?
15
11
  say "Pin @hotwired/turbo-rails in config/importmap.rb"
16
12
  insert_into_file \
17
- IMPORTMAP_PATH.to_s,
13
+ importmap_path.to_s,
18
14
  %( pin "@hotwired/turbo-rails", to: "turbo.js"\n\n),
19
15
  after: "Rails.application.config.importmap.draw do\n"
20
16
  else
21
17
  say <<~INSTRUCTIONS, :red
22
- You must add @hotwire/turbo-rails to your importmap to reference them via ESM.
18
+ You must add @hotwired/turbo-rails to your importmap to reference them via ESM.
19
+ Example: pin "@hotwired/turbo-rails", to: "turbo.js"
23
20
  INSTRUCTIONS
24
21
  end
25
22
 
26
- if CABLE_CONFIG_PATH.exist?
27
- say "Enable redis in bundle"
28
- uncomment_lines "Gemfile", %(gem 'redis')
29
-
30
- say "Switch development cable to use redis"
31
- gsub_file CABLE_CONFIG_PATH.to_s, /development:\n\s+adapter: async/, "development:\n adapter: redis\n url: redis://localhost:6379/1"
32
- else
33
- 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.'
34
- end
23
+ say "Run turbo:install:redis to switch on Redis and use it in development for turbo streams", :red
@@ -4,14 +4,4 @@ append_to_file "#{Webpacker.config.source_entry_path}/application.js", %(\nimpor
4
4
  say "Install Turbo"
5
5
  run "yarn add @hotwired/turbo-rails"
6
6
 
7
- CABLE_CONFIG_PATH = Rails.root.join("config/cable.yml")
8
-
9
- if CABLE_CONFIG_PATH.exist?
10
- say "Enable redis in bundle"
11
- uncomment_lines "Gemfile", %(gem 'redis')
12
-
13
- say "Switch development cable to use redis"
14
- gsub_file CABLE_CONFIG_PATH.to_s, /development:\n\s+adapter: async/, "development:\n adapter: redis\n url: redis://localhost:6379/1"
15
- else
16
- 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.'
17
- end
7
+ say "Run turbo:install:redis to switch on Redis and use it in development for turbo streams"
@@ -20,5 +20,10 @@ namespace :turbo do
20
20
  task :webpacker do
21
21
  run_turbo_install_template "turbo_with_webpacker"
22
22
  end
23
+
24
+ desc "Switch on Redis and use it in development"
25
+ task :redis do
26
+ run_turbo_install_template "turbo_needs_redis"
27
+ end
23
28
  end
24
29
  end
data/lib/turbo/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Turbo
2
- VERSION = "0.7.4"
2
+ VERSION = "0.7.8"
3
3
  end
metadata CHANGED
@@ -1,16 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: turbo-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.4
4
+ version: 0.7.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sam Stephenson
8
8
  - Javan Mahkmali
9
9
  - David Heinemeier Hansson
10
- autorequire:
10
+ autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2021-08-20 00:00:00.000000000 Z
13
+ date: 2021-08-30 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: rails
@@ -26,7 +26,7 @@ dependencies:
26
26
  - - ">="
27
27
  - !ruby/object:Gem::Version
28
28
  version: 6.0.0
29
- description:
29
+ description:
30
30
  email: david@loudthinking.com
31
31
  executables: []
32
32
  extensions: []
@@ -56,6 +56,7 @@ files:
56
56
  - app/models/concerns/turbo/broadcastable.rb
57
57
  - app/models/turbo/streams/tag_builder.rb
58
58
  - config/routes.rb
59
+ - lib/install/turbo_needs_redis.rb
59
60
  - lib/install/turbo_with_asset_pipeline.rb
60
61
  - lib/install/turbo_with_webpacker.rb
61
62
  - lib/tasks/turbo_tasks.rake
@@ -67,7 +68,7 @@ homepage: https://github.com/hotwired/turbo-rails
67
68
  licenses:
68
69
  - MIT
69
70
  metadata: {}
70
- post_install_message:
71
+ post_install_message:
71
72
  rdoc_options: []
72
73
  require_paths:
73
74
  - lib
@@ -83,7 +84,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
83
84
  version: '0'
84
85
  requirements: []
85
86
  rubygems_version: 3.1.4
86
- signing_key:
87
+ signing_key:
87
88
  specification_version: 4
88
89
  summary: The speed of a single-page web application without having to write any JavaScript.
89
90
  test_files: []