turbo-rails 0.7.2 → 0.7.7

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e401e540c8c64780f9984ed0d5120511777c47ca3b5d4c023f980469e10085e8
4
- data.tar.gz: 44d76bd4452ec01a2580d58e828616f2b634d8cb29a4470f082dd1c4ece899b5
3
+ metadata.gz: 4f96104fa6d1282a5f3845a119e748b04a819078840d3750604ebebeac081871
4
+ data.tar.gz: 0fdbf552964ff5ada90d746a32ec5575181c010471f4dcd921c2b443bfc732af
5
5
  SHA512:
6
- metadata.gz: 5ff10bfdaecd3c50008a3f9ac63976be979b5a1208c0f2cf39b786889156bdaef33e863ad5e6cdb45e2cf2e61acb8672e42a177326d8b0155e7da7d0bd559006
7
- data.tar.gz: 77413a732f4014a257f724c117d47b07ebb3190dc70802348462a9bcd19c6a525e9d18a22c5342bab6f74b70f0f461b871e4c92a5650b5ec10ced95304c6e472
6
+ metadata.gz: 662af2c0c6187537aafc70747ed3f66215bc65bde320475d12d10aeb30d2aca17ed1a3fa1403033a73f51323969782e3e482d5c145a3d0382921045eff22f2cb
7
+ data.tar.gz: 23a86030ac88022ba35c3b9369e1cf73bc2803e3c6106abb22ac9b179bb170b78e732fbab873eb56698f7a4c262298f2ab962f89359bd2369ac71820205df513
data/README.md CHANGED
@@ -15,6 +15,15 @@ During rendering, Turbo replaces the current `<body>` element outright and merge
15
15
 
16
16
  Whereas Turbolinks previously just dealt with links, Turbo can now also process form submissions and responses. This means the entire flow in the web application is wrapped into Turbo, making all the parts fast. No more need for `data-remote=true`.
17
17
 
18
+ Turbo Drive can be disabled on a per-element basis by annotating the element or any of its ancestors with `data-turbo="false"`. If you want Turbo Drive to be disabled by default, then you can adjust your import like this:
19
+
20
+ ```js
21
+ import { Turbo } from "@hotwired/turbo-rails"
22
+ Turbo.session.drive = false
23
+ ```
24
+
25
+ Then you can use `data-turbo="true"` to enable Drive on a per-element basis.
26
+
18
27
 
19
28
  ## Turbo Frames
20
29
 
@@ -37,6 +46,7 @@ The JavaScript for Turbo can either be run through the asset pipeline, which is
37
46
  1. Add the `turbo-rails` gem to your Gemfile: `gem 'turbo-rails'`
38
47
  2. Run `./bin/bundle install`
39
48
  3. Run `./bin/rails turbo:install`
49
+ 4. Run `./bin/rails turbo:install:redis` to change the development Action Cable adapter from Async (the default one) to Redis. The Async adapter does not support Turbo Stream broadcasting.
40
50
 
41
51
  Running `turbo:install` will install through NPM if Webpacker is installed in the application. Otherwise the asset pipeline version is used. To use the asset pipeline version, you must have `importmap-rails` installed first and listed higher in the Gemfile.
42
52
 
@@ -48,6 +58,7 @@ The `Turbo` instance is automatically assigned to `window.Turbo` upon import:
48
58
  import "@hotwired/turbo-rails"
49
59
  ```
50
60
 
61
+
51
62
  ## Usage
52
63
 
53
64
  You can watch [the video introduction to Hotwire](https://hotwired.dev/#screencast), which focuses extensively on demonstration Turbo in a Rails demo. Then you should familiarize yourself with [Turbo handbook](https://turbo.hotwired.dev/handbook/introduction) to understand Drive, Frames, and Streams in-depth. Finally, dive into the code documentation by starting with [`Turbo::FramesHelper`](https://github.com/hotwired/turbo-rails/blob/main/app/helpers/turbo/frames_helper.rb), [`Turbo::StreamsHelper`](https://github.com/hotwired/turbo-rails/blob/main/app/helpers/turbo/streams_helper.rb), [`Turbo::Streams::TagBuilder`](https://github.com/hotwired/turbo-rails/blob/main/app/models/turbo/streams/tag_builder.rb), and [`Turbo::Broadcastable`](https://github.com/hotwired/turbo-rails/blob/main/app/models/concerns/turbo/broadcastable.rb).
@@ -361,8 +361,10 @@ class FetchRequest {
361
361
  const response = await fetch(this.url.href, fetchOptions);
362
362
  return await this.receive(response);
363
363
  } catch (error) {
364
- 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
  }
@@ -1666,14 +1673,20 @@ class BrowserAdapter {
1666
1673
  this.progressBar.setValue(1);
1667
1674
  this.hideProgressBar();
1668
1675
  }
1669
- visitCompleted(visit) {
1670
- visit.followRedirect();
1671
- }
1676
+ visitCompleted(visit) {}
1672
1677
  pageInvalidated() {
1673
1678
  this.reload();
1674
1679
  }
1675
1680
  visitFailed(visit) {}
1676
1681
  visitRendered(visit) {}
1682
+ formSubmissionStarted(formSubmission) {
1683
+ this.progressBar.setValue(0);
1684
+ this.showProgressBarAfterDelay();
1685
+ }
1686
+ formSubmissionFinished(formSubmission) {
1687
+ this.progressBar.setValue(1);
1688
+ this.hideProgressBar();
1689
+ }
1677
1690
  showProgressBarAfterDelay() {
1678
1691
  this.progressBarTimeout = window.setTimeout(this.showProgressBar, this.session.progressBarDelay);
1679
1692
  }
@@ -1891,7 +1904,8 @@ class LinkClickObserver {
1891
1904
  };
1892
1905
  this.clickBubbled = event => {
1893
1906
  if (this.clickEventIsSignificant(event)) {
1894
- const link = this.findLinkFromClickTarget(event.target);
1907
+ const target = event.composedPath && event.composedPath()[0] || event.target;
1908
+ const link = this.findLinkFromClickTarget(target);
1895
1909
  if (link) {
1896
1910
  const location = this.getLocationForLink(link);
1897
1911
  if (this.delegate.willFollowLinkToLocation(link, location)) {
@@ -1937,7 +1951,7 @@ class Navigator {
1937
1951
  this.delegate = delegate;
1938
1952
  }
1939
1953
  proposeVisit(location, options = {}) {
1940
- if (this.delegate.allowsVisitingLocation(location)) {
1954
+ if (this.delegate.allowsVisitingLocationWithAction(location, options.action)) {
1941
1955
  this.delegate.visitProposedToLocation(location, options);
1942
1956
  }
1943
1957
  }
@@ -1978,7 +1992,11 @@ class Navigator {
1978
1992
  get history() {
1979
1993
  return this.delegate.history;
1980
1994
  }
1981
- formSubmissionStarted(formSubmission) {}
1995
+ formSubmissionStarted(formSubmission) {
1996
+ if (typeof this.adapter.formSubmissionStarted === "function") {
1997
+ this.adapter.formSubmissionStarted(formSubmission);
1998
+ }
1999
+ }
1982
2000
  async formSubmissionSucceededWithResponse(formSubmission, fetchResponse) {
1983
2001
  if (formSubmission == this.formSubmission) {
1984
2002
  const responseHTML = await fetchResponse.responseHTML;
@@ -2001,14 +2019,23 @@ class Navigator {
2001
2019
  const responseHTML = await fetchResponse.responseHTML;
2002
2020
  if (responseHTML) {
2003
2021
  const snapshot = PageSnapshot.fromHTMLString(responseHTML);
2004
- await this.view.renderPage(snapshot);
2022
+ if (fetchResponse.serverError) {
2023
+ await this.view.renderError(snapshot);
2024
+ } else {
2025
+ await this.view.renderPage(snapshot);
2026
+ }
2027
+ this.view.scrollToTop();
2005
2028
  this.view.clearSnapshotCache();
2006
2029
  }
2007
2030
  }
2008
2031
  formSubmissionErrored(formSubmission, error) {
2009
2032
  console.error(error);
2010
2033
  }
2011
- formSubmissionFinished(formSubmission) {}
2034
+ formSubmissionFinished(formSubmission) {
2035
+ if (typeof this.adapter.formSubmissionFinished === "function") {
2036
+ this.adapter.formSubmissionFinished(formSubmission);
2037
+ }
2038
+ }
2012
2039
  visitStarted(visit) {
2013
2040
  this.delegate.visitStarted(visit);
2014
2041
  }
@@ -2016,7 +2043,10 @@ class Navigator {
2016
2043
  this.delegate.visitCompleted(visit);
2017
2044
  }
2018
2045
  locationWithActionIsSamePage(location, action) {
2019
- return getRequestURL(location) === getRequestURL(this.view.lastRenderedLocation) && (getAnchor(location) != null || action == "restore");
2046
+ const anchor = getAnchor(location);
2047
+ const currentAnchor = getAnchor(this.view.lastRenderedLocation);
2048
+ const isRestorationToTop = action === "restore" && typeof anchor === "undefined";
2049
+ return action !== "replace" && getRequestURL(location) === getRequestURL(this.view.lastRenderedLocation) && (isRestorationToTop || anchor != null && anchor !== currentAnchor);
2020
2050
  }
2021
2051
  visitScrolledToSamePageLocation(oldURL, newURL) {
2022
2052
  this.delegate.visitScrolledToSamePageLocation(oldURL, newURL);
@@ -2408,6 +2438,7 @@ class Session {
2408
2438
  this.scrollObserver = new ScrollObserver(this);
2409
2439
  this.streamObserver = new StreamObserver(this);
2410
2440
  this.frameRedirector = new FrameRedirector(document.documentElement);
2441
+ this.drive = true;
2411
2442
  this.enabled = true;
2412
2443
  this.progressBarDelay = 500;
2413
2444
  this.started = false;
@@ -2485,7 +2516,7 @@ class Session {
2485
2516
  });
2486
2517
  }
2487
2518
  willFollowLinkToLocation(link, location) {
2488
- return elementIsNavigable(link) && this.locationIsVisitable(location) && this.applicationAllowsFollowingLinkToLocation(link, location);
2519
+ return this.elementDriveEnabled(link) && this.locationIsVisitable(location) && this.applicationAllowsFollowingLinkToLocation(link, location);
2489
2520
  }
2490
2521
  followedLinkToLocation(link, location) {
2491
2522
  const action = this.getActionForLink(link);
@@ -2494,13 +2525,12 @@ class Session {
2494
2525
  });
2495
2526
  }
2496
2527
  convertLinkWithMethodClickToFormSubmission(link) {
2497
- var _a;
2498
2528
  const linkMethod = link.getAttribute("data-turbo-method");
2499
2529
  if (linkMethod) {
2500
2530
  const form = document.createElement("form");
2501
2531
  form.method = linkMethod;
2502
2532
  form.action = link.getAttribute("href") || "undefined";
2503
- (_a = link.parentNode) === null || _a === void 0 ? void 0 : _a.insertBefore(form, link);
2533
+ document.body.appendChild(form);
2504
2534
  return dispatch("submit", {
2505
2535
  cancelable: true,
2506
2536
  target: form
@@ -2509,8 +2539,8 @@ class Session {
2509
2539
  return false;
2510
2540
  }
2511
2541
  }
2512
- allowsVisitingLocation(location) {
2513
- return this.applicationAllowsVisitingLocation(location);
2542
+ allowsVisitingLocationWithAction(location, action) {
2543
+ return this.locationWithActionIsSamePage(location, action) || this.applicationAllowsVisitingLocation(location);
2514
2544
  }
2515
2545
  visitProposedToLocation(location, options) {
2516
2546
  extendURLWithDeprecatedProperties(location);
@@ -2518,7 +2548,9 @@ class Session {
2518
2548
  }
2519
2549
  visitStarted(visit) {
2520
2550
  extendURLWithDeprecatedProperties(visit.location);
2521
- this.notifyApplicationAfterVisitingLocation(visit.location, visit.action);
2551
+ if (!visit.silent) {
2552
+ this.notifyApplicationAfterVisitingLocation(visit.location, visit.action);
2553
+ }
2522
2554
  }
2523
2555
  visitCompleted(visit) {
2524
2556
  this.notifyApplicationAfterPageLoad(visit.getTimingMetrics());
@@ -2530,7 +2562,7 @@ class Session {
2530
2562
  this.notifyApplicationAfterVisitingSamePageLocation(oldURL, newURL);
2531
2563
  }
2532
2564
  willSubmitForm(form, submitter) {
2533
- return elementIsNavigable(form) && elementIsNavigable(submitter);
2565
+ return this.elementDriveEnabled(form) && this.elementDriveEnabled(submitter);
2534
2566
  }
2535
2567
  formSubmitted(form, submitter) {
2536
2568
  this.navigator.submitForm(form, submitter);
@@ -2549,7 +2581,10 @@ class Session {
2549
2581
  this.renderStreamMessage(message);
2550
2582
  }
2551
2583
  viewWillCacheSnapshot() {
2552
- this.notifyApplicationBeforeCachingSnapshot();
2584
+ var _a;
2585
+ if (!((_a = this.navigator.currentVisit) === null || _a === void 0 ? void 0 : _a.silent)) {
2586
+ this.notifyApplicationBeforeCachingSnapshot();
2587
+ }
2553
2588
  }
2554
2589
  allowsImmediateRender({element: element}, resume) {
2555
2590
  const event = this.notifyApplicationBeforeRender(element, resume);
@@ -2562,6 +2597,12 @@ class Session {
2562
2597
  viewInvalidated() {
2563
2598
  this.adapter.pageInvalidated();
2564
2599
  }
2600
+ frameLoaded(frame) {
2601
+ this.notifyApplicationAfterFrameLoad(frame);
2602
+ }
2603
+ frameRendered(fetchResponse, frame) {
2604
+ this.notifyApplicationAfterFrameRender(fetchResponse, frame);
2605
+ }
2565
2606
  applicationAllowsFollowingLinkToLocation(link, location) {
2566
2607
  const event = this.notifyApplicationAfterClickingLinkToLocation(link, location);
2567
2608
  return !event.defaultPrevented;
@@ -2624,6 +2665,36 @@ class Session {
2624
2665
  newURL: newURL.toString()
2625
2666
  }));
2626
2667
  }
2668
+ notifyApplicationAfterFrameLoad(frame) {
2669
+ return dispatch("turbo:frame-load", {
2670
+ target: frame
2671
+ });
2672
+ }
2673
+ notifyApplicationAfterFrameRender(fetchResponse, frame) {
2674
+ return dispatch("turbo:frame-render", {
2675
+ detail: {
2676
+ fetchResponse: fetchResponse
2677
+ },
2678
+ target: frame,
2679
+ cancelable: true
2680
+ });
2681
+ }
2682
+ elementDriveEnabled(element) {
2683
+ const container = element === null || element === void 0 ? void 0 : element.closest("[data-turbo]");
2684
+ if (this.drive) {
2685
+ if (container) {
2686
+ return container.getAttribute("data-turbo") != "false";
2687
+ } else {
2688
+ return true;
2689
+ }
2690
+ } else {
2691
+ if (container) {
2692
+ return container.getAttribute("data-turbo") == "true";
2693
+ } else {
2694
+ return false;
2695
+ }
2696
+ }
2697
+ }
2627
2698
  getActionForLink(link) {
2628
2699
  const action = link.getAttribute("data-turbo-action");
2629
2700
  return isAction(action) ? action : "advance";
@@ -2636,15 +2707,6 @@ class Session {
2636
2707
  }
2637
2708
  }
2638
2709
 
2639
- function elementIsNavigable(element) {
2640
- const container = element === null || element === void 0 ? void 0 : element.closest("[data-turbo]");
2641
- if (container) {
2642
- return container.getAttribute("data-turbo") != "false";
2643
- } else {
2644
- return true;
2645
- }
2646
- }
2647
-
2648
2710
  function extendURLWithDeprecatedProperties(url) {
2649
2711
  Object.defineProperties(url, deprecatedLocationPropertyDescriptors);
2650
2712
  }
@@ -2657,6 +2719,58 @@ const deprecatedLocationPropertyDescriptors = {
2657
2719
  }
2658
2720
  };
2659
2721
 
2722
+ const session = new Session;
2723
+
2724
+ const {navigator: navigator} = session;
2725
+
2726
+ function start() {
2727
+ session.start();
2728
+ }
2729
+
2730
+ function registerAdapter(adapter) {
2731
+ session.registerAdapter(adapter);
2732
+ }
2733
+
2734
+ function visit(location, options) {
2735
+ session.visit(location, options);
2736
+ }
2737
+
2738
+ function connectStreamSource(source) {
2739
+ session.connectStreamSource(source);
2740
+ }
2741
+
2742
+ function disconnectStreamSource(source) {
2743
+ session.disconnectStreamSource(source);
2744
+ }
2745
+
2746
+ function renderStreamMessage(message) {
2747
+ session.renderStreamMessage(message);
2748
+ }
2749
+
2750
+ function clearCache() {
2751
+ session.clearCache();
2752
+ }
2753
+
2754
+ function setProgressBarDelay(delay) {
2755
+ session.setProgressBarDelay(delay);
2756
+ }
2757
+
2758
+ var Turbo = Object.freeze({
2759
+ __proto__: null,
2760
+ navigator: navigator,
2761
+ session: session,
2762
+ PageRenderer: PageRenderer,
2763
+ PageSnapshot: PageSnapshot,
2764
+ start: start,
2765
+ registerAdapter: registerAdapter,
2766
+ visit: visit,
2767
+ connectStreamSource: connectStreamSource,
2768
+ disconnectStreamSource: disconnectStreamSource,
2769
+ renderStreamMessage: renderStreamMessage,
2770
+ clearCache: clearCache,
2771
+ setProgressBarDelay: setProgressBarDelay
2772
+ });
2773
+
2660
2774
  class FrameController {
2661
2775
  constructor(element) {
2662
2776
  this.resolveVisitPromise = () => {};
@@ -2672,6 +2786,7 @@ class FrameController {
2672
2786
  connect() {
2673
2787
  if (!this.connected) {
2674
2788
  this.connected = true;
2789
+ this.reloadable = false;
2675
2790
  if (this.loadingStyle == FrameLoadingStyle.lazy) {
2676
2791
  this.appearanceObserver.start();
2677
2792
  }
@@ -2707,7 +2822,7 @@ class FrameController {
2707
2822
  }
2708
2823
  }
2709
2824
  async loadSourceURL() {
2710
- if (!this.settingSourceURL && this.enabled && this.isActive && this.sourceURL != this.currentURL) {
2825
+ if (!this.settingSourceURL && this.enabled && this.isActive && (this.reloadable || this.sourceURL != this.currentURL)) {
2711
2826
  const previousURL = this.currentURL;
2712
2827
  this.currentURL = this.sourceURL;
2713
2828
  if (this.sourceURL) {
@@ -2716,6 +2831,7 @@ class FrameController {
2716
2831
  this.appearanceObserver.stop();
2717
2832
  await this.element.loaded;
2718
2833
  this.hasBeenLoaded = true;
2834
+ session.frameLoaded(this.element);
2719
2835
  } catch (error) {
2720
2836
  this.currentURL = previousURL;
2721
2837
  throw error;
@@ -2735,6 +2851,7 @@ class FrameController {
2735
2851
  const renderer = new FrameRenderer(this.view.snapshot, snapshot, false);
2736
2852
  if (this.view.renderPromise) await this.view.renderPromise;
2737
2853
  await this.view.render(renderer);
2854
+ session.frameRendered(fetchResponse, this.element);
2738
2855
  }
2739
2856
  } catch (error) {
2740
2857
  console.error(error);
@@ -2752,6 +2869,7 @@ class FrameController {
2752
2869
  }
2753
2870
  }
2754
2871
  linkClickIntercepted(element, url) {
2872
+ this.reloadable = true;
2755
2873
  this.navigateFrame(element, url);
2756
2874
  }
2757
2875
  shouldInterceptFormSubmission(element, submitter) {
@@ -2761,6 +2879,7 @@ class FrameController {
2761
2879
  if (this.formSubmission) {
2762
2880
  this.formSubmission.stop();
2763
2881
  }
2882
+ this.reloadable = false;
2764
2883
  this.formSubmission = new FormSubmission(this, element, submitter);
2765
2884
  if (this.formSubmission.fetchRequest.isIdempotent) {
2766
2885
  this.navigateFrame(element, this.formSubmission.fetchRequest.url.href);
@@ -2864,10 +2983,10 @@ class FrameController {
2864
2983
  return !frameElement.disabled;
2865
2984
  }
2866
2985
  }
2867
- if (!elementIsNavigable(element)) {
2986
+ if (!session.elementDriveEnabled(element)) {
2868
2987
  return false;
2869
2988
  }
2870
- if (submitter && !elementIsNavigable(submitter)) {
2989
+ if (submitter && !session.elementDriveEnabled(submitter)) {
2871
2990
  return false;
2872
2991
  }
2873
2992
  return true;
@@ -2883,6 +3002,18 @@ class FrameController {
2883
3002
  return this.element.src;
2884
3003
  }
2885
3004
  }
3005
+ get reloadable() {
3006
+ const frame = this.findFrameElement(this.element);
3007
+ return frame.hasAttribute("reloadable");
3008
+ }
3009
+ set reloadable(value) {
3010
+ const frame = this.findFrameElement(this.element);
3011
+ if (value) {
3012
+ frame.setAttribute("reloadable", "");
3013
+ } else {
3014
+ frame.removeAttribute("reloadable");
3015
+ }
3016
+ }
2886
3017
  set sourceURL(sourceURL) {
2887
3018
  this.settingSourceURL = true;
2888
3019
  this.element.src = sourceURL !== null && sourceURL !== void 0 ? sourceURL : null;
@@ -3089,57 +3220,6 @@ customElements.define("turbo-stream", StreamElement);
3089
3220
  }
3090
3221
  })();
3091
3222
 
3092
- const session = new Session;
3093
-
3094
- const {navigator: navigator} = session;
3095
-
3096
- function start() {
3097
- session.start();
3098
- }
3099
-
3100
- function registerAdapter(adapter) {
3101
- session.registerAdapter(adapter);
3102
- }
3103
-
3104
- function visit(location, options) {
3105
- session.visit(location, options);
3106
- }
3107
-
3108
- function connectStreamSource(source) {
3109
- session.connectStreamSource(source);
3110
- }
3111
-
3112
- function disconnectStreamSource(source) {
3113
- session.disconnectStreamSource(source);
3114
- }
3115
-
3116
- function renderStreamMessage(message) {
3117
- session.renderStreamMessage(message);
3118
- }
3119
-
3120
- function clearCache() {
3121
- session.clearCache();
3122
- }
3123
-
3124
- function setProgressBarDelay(delay) {
3125
- session.setProgressBarDelay(delay);
3126
- }
3127
-
3128
- var Turbo = Object.freeze({
3129
- __proto__: null,
3130
- navigator: navigator,
3131
- PageRenderer: PageRenderer,
3132
- PageSnapshot: PageSnapshot,
3133
- start: start,
3134
- registerAdapter: registerAdapter,
3135
- visit: visit,
3136
- connectStreamSource: connectStreamSource,
3137
- disconnectStreamSource: disconnectStreamSource,
3138
- renderStreamMessage: renderStreamMessage,
3139
- clearCache: clearCache,
3140
- setProgressBarDelay: setProgressBarDelay
3141
- });
3142
-
3143
3223
  window.Turbo = Turbo;
3144
3224
 
3145
3225
  start();
@@ -3154,6 +3234,7 @@ var turbo_es2017Esm = Object.freeze({
3154
3234
  navigator: navigator,
3155
3235
  registerAdapter: registerAdapter,
3156
3236
  renderStreamMessage: renderStreamMessage,
3237
+ session: session,
3157
3238
  setProgressBarDelay: setProgressBarDelay,
3158
3239
  start: start,
3159
3240
  visit: visit
@@ -5,71 +5,69 @@
5
5
  module Turbo::Streams::Broadcasts
6
6
  include Turbo::Streams::ActionHelper
7
7
 
8
- def broadcast_remove_to(*streamables, 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,15 +1,23 @@
1
- APP_JS_ROOT = Rails.root.join("app/assets/javascripts")
2
- CABLE_CONFIG_PATH = Rails.root.join("config/cable.yml")
3
-
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)
6
-
7
- if CABLE_CONFIG_PATH.exist?
8
- say "Enable redis in bundle"
9
- uncomment_lines "Gemfile", %(gem 'redis')
1
+ if (app_js_path = Rails.root.join("app/javascript/application.js")).exist?
2
+ say "Import turbo-rails in existing app/javascript/application.js"
3
+ append_to_file app_js_path, %(import "@hotwired/turbo-rails"\n)
4
+ else
5
+ say <<~INSTRUCTIONS, :red
6
+ You must import @hotwired/turbo-rails in your application.js.
7
+ INSTRUCTIONS
8
+ end
10
9
 
11
- 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"
10
+ if (importmap_path = Rails.root.join("config/importmap.rb")).exist?
11
+ say "Pin @hotwired/turbo-rails in config/importmap.rb"
12
+ insert_into_file \
13
+ importmap_path.to_s,
14
+ %( pin "@hotwired/turbo-rails", to: "turbo.js"\n\n),
15
+ after: "Rails.application.config.importmap.draw do\n"
13
16
  else
14
- say '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
+ say <<~INSTRUCTIONS, :red
18
+ You must add @hotwired/turbo-rails to your importmap to reference them via ESM.
19
+ Example: pin "@hotwired/turbo-rails", to: "turbo.js"
20
+ INSTRUCTIONS
15
21
  end
22
+
23
+ say "Run turbo:install:redis to switch on Redis and use it in development for turbo streams", :red
@@ -1,27 +1,7 @@
1
- # Some Rails versions use commonJS(require) others use ESM(import).
2
- TURBOLINKS_REGEX = /(import .* from "turbolinks".*\n|require\("turbolinks"\).*\n)/.freeze
3
- ACTIVE_STORAGE_REGEX = /(import.*ActiveStorage|require.*@rails\/activestorage.*)/.freeze
4
- CABLE_CONFIG_PATH = Rails.root.join("config/cable.yml")
5
-
6
- abort "❌ Webpacker not found. Exiting." unless defined?(Webpacker::Engine)
1
+ say "Appending Turbo setup code to #{Webpacker.config.source_entry_path}/application.js"
2
+ append_to_file "#{Webpacker.config.source_entry_path}/application.js", %(\nimport "@hotwired/turbo-rails"\n)
7
3
 
8
4
  say "Install Turbo"
9
5
  run "yarn add @hotwired/turbo-rails"
10
- insert_into_file "#{Webpacker.config.source_entry_path}/application.js", "import \"@hotwired/turbo-rails\"\n", before: ACTIVE_STORAGE_REGEX
11
-
12
- say "Remove Turbolinks"
13
- run "#{RbConfig.ruby} bin/bundle remove turbolinks"
14
- run "#{RbConfig.ruby} bin/bundle", capture: true
15
- run "#{RbConfig.ruby} bin/yarn remove turbolinks"
16
- gsub_file "#{Webpacker.config.source_entry_path}/application.js", TURBOLINKS_REGEX, ''
17
- gsub_file "#{Webpacker.config.source_entry_path}/application.js", /Turbolinks.start.*\n/, ''
18
-
19
- if CABLE_CONFIG_PATH.exist?
20
- say "Enable redis in bundle"
21
- uncomment_lines "Gemfile", %(gem 'redis')
22
6
 
23
- 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
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/engine.rb CHANGED
@@ -26,12 +26,6 @@ module Turbo
26
26
  end
27
27
  end
28
28
 
29
- initializer "turbo.importmap" do
30
- if Rails.application.config.respond_to?(:importmap)
31
- Rails.application.config.importmap.pin "@hotwired/turbo-rails", to: "turbo.js"
32
- end
33
- end
34
-
35
29
  initializer "turbo.helpers", before: :load_config_initializers do
36
30
  ActiveSupport.on_load(:action_controller_base) do
37
31
  include Turbo::Streams::TurboStreamsTagBuilder, Turbo::Frames::FrameRequest, Turbo::Native::Navigation
data/lib/turbo/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Turbo
2
- VERSION = "0.7.2"
2
+ VERSION = "0.7.7"
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.7.2
4
+ version: 0.7.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sam Stephenson
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2021-08-14 00:00:00.000000000 Z
13
+ date: 2021-08-26 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: rails
@@ -56,6 +56,7 @@ files:
56
56
  - app/models/concerns/turbo/broadcastable.rb
57
57
  - app/models/turbo/streams/tag_builder.rb
58
58
  - config/routes.rb
59
+ - lib/install/turbo_needs_redis.rb
59
60
  - lib/install/turbo_with_asset_pipeline.rb
60
61
  - lib/install/turbo_with_webpacker.rb
61
62
  - lib/tasks/turbo_tasks.rake