turbo-rails 1.3.3 → 1.5.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: ecb315ac4b0462119b892a555ba5942ac1de84d8bdc565e31d0e166ac244a6c1
4
- data.tar.gz: a2e2033e73b26803e9b87962ac451b825257e1f052cb86cef013e0b8eb16bbcd
3
+ metadata.gz: 216ef0fb1d4b07f9231e52702506e6888495d7c15f07a0778eee156692c84d74
4
+ data.tar.gz: 964ea9a3f3111be08955e9c707a591941dfe7b5f943566716bcd26fbd3d9b19b
5
5
  SHA512:
6
- metadata.gz: d830701cc6b05ceb930ed1d873998849d870f7b25701ccc3d5cede6f5659eb107e175a46465963a23bf1239fd3aeb798f105155047324644446305e1fea34d71
7
- data.tar.gz: 0bb9370f532eb74a8230320c1aed5ac1d4e5f434873d017454ffe1c005daa124f413ffe1ee11df1cb49f325f85fcd417b9db6bd6909b234d87032d56dcb99284
6
+ metadata.gz: 2f42f0e27dcb80e83b3bae71416b1c9c0cdd82ef462dfa96c57bc3fa2d2c88b548785d9e7b10945575ab53538cfc0cf99c49c3c7f5d6f4fdd0608f251ae38bad
7
+ data.tar.gz: 21bd467eed15705e4c46f3da62ae84d8466adc85dcd770abc837c1fbcb517ccca2d7a004f9c856bd2af1f6aed9f83df51825929de463428699a35a58c4b89846
data/README.md CHANGED
@@ -4,7 +4,7 @@
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](https://github.com/hotwired/turbo-android) or [iOS](https://github.com/hotwired/turbo-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
 
7
- Turbo is a language-agnostic framework written in TypeScript, but this gem builds on top of those basics to make the integration with Rails as smooth as possible. You can deliver turbo updates via model callbacks over Action Cable, respond to controller actions with native navigation or standard redirects, and render turbo frames with helpers and layout-free responses.
7
+ Turbo is a language-agnostic framework written in JavaScript, but this gem builds on top of those basics to make the integration with Rails as smooth as possible. You can deliver turbo updates via model callbacks over Action Cable, respond to controller actions with native navigation or standard redirects, and render turbo frames with helpers and layout-free responses.
8
8
 
9
9
 
10
10
  ## Navigate with Turbo Drive
@@ -18,7 +18,7 @@ Whereas Turbolinks previously just dealt with links, Turbo can now also process
18
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
19
 
20
20
  ```js
21
- import { Turbo } from "@hotwired/turbo-rails"
21
+ import "@hotwired/turbo-rails"
22
22
  Turbo.session.drive = false
23
23
  ```
24
24
 
@@ -55,6 +55,37 @@ When the user will click on the `Edit this todo` link, as direct response to thi
55
55
 
56
56
  [See documentation](https://turbo.hotwired.dev/handbook/frames).
57
57
 
58
+ ### A note on custom layouts
59
+
60
+ In order to render turbo frame requests without the application layout, Turbo registers a custom [layout method](https://api.rubyonrails.org/classes/ActionView/Layouts/ClassMethods.html#method-i-layout).
61
+ If your application uses custom layout resolution, you have to make sure to return `"turbo_rails/frame"` (or `false` for TurboRails < 1.4.0) for turbo frame requests:
62
+
63
+ ```ruby
64
+ layout :custom_layout
65
+
66
+ def custom_layout
67
+ return "turbo_rails/frame" if turbo_frame_request?
68
+
69
+ # ... your custom layout logic
70
+ ```
71
+
72
+ If you are using a custom, but "static" layout,
73
+
74
+ ```ruby
75
+ layout "some_static_layout"
76
+ ```
77
+
78
+ you **have** to change it to a layout method in order to conditionally return `false` for turbo frame requests:
79
+
80
+ ```ruby
81
+ layout :custom_layout
82
+
83
+ def custom_layout
84
+ return "turbo_rails/frame" if turbo_frame_request?
85
+
86
+ "some_static_layout"
87
+ ```
88
+
58
89
  ## Come Alive with Turbo Streams
59
90
 
60
91
  Partial page updates that are **delivered asynchronously over a web socket connection** is the hallmark of modern, reactive web applications. With Turbo Streams, you can get all of that modern goodness using the existing server-side HTML you're already rendering to deliver the first page load. With a set of simple CRUD container tags, you can send HTML fragments over the web socket (or in response to direct interactions), and see the page change in response to new data. Again, **no need to construct an entirely separate API**, **no need to wrangle JSON**, **no need to reimplement the HTML construction in JavaScript**. Take the HTML you're already making, wrap it in an update tag, and, voila, your page comes alive.
@@ -96,11 +127,25 @@ import "@hotwired/turbo-rails"
96
127
 
97
128
  You can watch [the video introduction to Hotwire](https://hotwired.dev/#screencast), which focuses extensively on demonstrating 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).
98
129
 
130
+ ### RubyDoc Documentation
131
+
132
+ For the API documentation covering this gem's classes and packages, [visit the
133
+ RubyDoc page][].
134
+
135
+ [visit the RubyDoc page](https://rubydoc.info/github/hotwired/turbo-rails/main)
99
136
 
100
137
  ## Compatibility with Rails UJS
101
138
 
102
139
  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).
103
140
 
141
+ ## Testing
142
+
143
+
144
+ The [`Turbo::TestAssertions`](./lib/turbo/test_assertions.rb) concern provides Turbo Stream test helpers that assert the presence or absence of `<turbo-stream>` elements in a rendered fragment of HTML. `Turbo::TestAssertions` are automatically included in [`ActiveSupport::TestCase`](https://edgeapi.rubyonrails.org/classes/ActiveSupport/TestCase.html) and depend on the presence of [`rails-dom-testing`](https://github.com/rails/rails-dom-testing/) assertions.
145
+
146
+ The [`Turbo::TestAssertions::IntegrationTestAssertions`](./lib/turbo/test_assertions/integration_test_assertions.rb) are built on top of `Turbo::TestAssertions`, and add support for passing a `status:` keyword. They are automatically included in [`ActionDispatch::IntegrationTest`](https://edgeguides.rubyonrails.org/testing.html#integration-testing).
147
+
148
+ The [`Turbo::Broadcastable::TestHelper`](./lib/turbo/broadcastable/test_helper.rb) concern provides Action Cable-aware test helpers that assert that `<turbo-stream>` elements were or were not broadcast over Action Cable. They are not automatically included. To use them in your tests, make sure to `include Turbo::Broadcastable::TestHelper`.
104
149
 
105
150
  ## Development
106
151
 
data/Rakefile CHANGED
@@ -9,7 +9,20 @@ load "rails/tasks/statistics.rake"
9
9
  Rake::TestTask.new do |test|
10
10
  test.libs << "test"
11
11
  test.test_files = FileList["test/**/*_test.rb"]
12
- test.warning = false
13
12
  end
14
13
 
15
- task default: :test
14
+ task :test_prereq do
15
+ puts "Installing Ruby dependencies"
16
+ `bundle install`
17
+
18
+ puts "Installing JavaScript dependencies"
19
+ `yarn install`
20
+
21
+ puts "Building JavaScript"
22
+ `yarn build`
23
+
24
+ puts "Preparing test database"
25
+ `cd test/dummy; ./bin/rails db:test:prepare; cd ../..`
26
+ end
27
+
28
+ task default: [:test_prereq, :test]
@@ -56,13 +56,11 @@ function clickCaptured(event) {
56
56
 
57
57
  (function() {
58
58
  if ("submitter" in Event.prototype) return;
59
- let prototype;
59
+ let prototype = window.Event.prototype;
60
60
  if ("SubmitEvent" in window && /Apple Computer/.test(navigator.vendor)) {
61
61
  prototype = window.SubmitEvent.prototype;
62
62
  } else if ("SubmitEvent" in window) {
63
63
  return;
64
- } else {
65
- prototype = window.Event.prototype;
66
64
  }
67
65
  addEventListener("click", clickCaptured, true);
68
66
  Object.defineProperty(prototype, "submitter", {
@@ -82,14 +80,14 @@ var FrameLoadingStyle;
82
80
  })(FrameLoadingStyle || (FrameLoadingStyle = {}));
83
81
 
84
82
  class FrameElement extends HTMLElement {
83
+ static get observedAttributes() {
84
+ return [ "disabled", "complete", "loading", "src" ];
85
+ }
85
86
  constructor() {
86
87
  super();
87
88
  this.loaded = Promise.resolve();
88
89
  this.delegate = new FrameElement.delegateConstructor(this);
89
90
  }
90
- static get observedAttributes() {
91
- return [ "disabled", "complete", "loading", "src" ];
92
- }
93
91
  connectedCallback() {
94
92
  this.delegate.connect();
95
93
  }
@@ -560,7 +558,7 @@ class FetchRequest {
560
558
  credentials: "same-origin",
561
559
  headers: this.headers,
562
560
  redirect: "follow",
563
- body: this.isIdempotent ? null : this.body,
561
+ body: this.isSafe ? null : this.body,
564
562
  signal: this.abortSignal,
565
563
  referrer: (_a = this.delegate.referrer) === null || _a === void 0 ? void 0 : _a.href
566
564
  };
@@ -570,8 +568,8 @@ class FetchRequest {
570
568
  Accept: "text/html, application/xhtml+xml"
571
569
  };
572
570
  }
573
- get isIdempotent() {
574
- return this.method == FetchMethod.get;
571
+ get isSafe() {
572
+ return this.method === FetchMethod.get;
575
573
  }
576
574
  get abortSignal() {
577
575
  return this.abortController.signal;
@@ -633,9 +631,6 @@ class AppearanceObserver {
633
631
  }
634
632
 
635
633
  class StreamMessage {
636
- constructor(fragment) {
637
- this.fragment = importStreamElements(fragment);
638
- }
639
634
  static wrap(message) {
640
635
  if (typeof message == "string") {
641
636
  return new this(createDocumentFragment(message));
@@ -643,6 +638,9 @@ class StreamMessage {
643
638
  return message;
644
639
  }
645
640
  }
641
+ constructor(fragment) {
642
+ this.fragment = importStreamElements(fragment);
643
+ }
646
644
  }
647
645
 
648
646
  StreamMessage.contentType = "text/vnd.turbo-stream.html";
@@ -691,6 +689,9 @@ function formEnctypeFromString(encoding) {
691
689
  }
692
690
 
693
691
  class FormSubmission {
692
+ static confirmMethod(message, _element, _submitter) {
693
+ return Promise.resolve(confirm(message));
694
+ }
694
695
  constructor(delegate, formElement, submitter, mustRedirect = false) {
695
696
  this.state = FormSubmissionState.initialized;
696
697
  this.delegate = delegate;
@@ -704,9 +705,6 @@ class FormSubmission {
704
705
  this.fetchRequest = new FetchRequest(this, this.method, this.location, this.body, this.formElement);
705
706
  this.mustRedirect = mustRedirect;
706
707
  }
707
- static confirmMethod(message, _element, _submitter) {
708
- return Promise.resolve(confirm(message));
709
- }
710
708
  get method() {
711
709
  var _a;
712
710
  const method = ((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formmethod")) || this.formElement.getAttribute("method") || "";
@@ -732,8 +730,8 @@ class FormSubmission {
732
730
  var _a;
733
731
  return formEnctypeFromString(((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formenctype")) || this.formElement.enctype);
734
732
  }
735
- get isIdempotent() {
736
- return this.fetchRequest.isIdempotent;
733
+ get isSafe() {
734
+ return this.fetchRequest.isSafe;
737
735
  }
738
736
  get stringFormData() {
739
737
  return [ ...this.formData ].reduce(((entries, [name, value]) => entries.concat(typeof value == "string" ? [ [ name, value ] ] : [])), []);
@@ -761,7 +759,7 @@ class FormSubmission {
761
759
  }
762
760
  }
763
761
  prepareRequest(request) {
764
- if (!request.isIdempotent) {
762
+ if (!request.isSafe) {
765
763
  const token = getCookieValue(getMetaContent("csrf-param")) || getMetaContent("csrf-token");
766
764
  if (token) {
767
765
  request.headers["X-CSRF-Token"] = token;
@@ -775,6 +773,7 @@ class FormSubmission {
775
773
  var _a;
776
774
  this.state = FormSubmissionState.waiting;
777
775
  (_a = this.submitter) === null || _a === void 0 ? void 0 : _a.setAttribute("disabled", "");
776
+ this.setSubmitsWith();
778
777
  dispatch("turbo:submit-start", {
779
778
  target: this.formElement,
780
779
  detail: {
@@ -822,6 +821,7 @@ class FormSubmission {
822
821
  var _a;
823
822
  this.state = FormSubmissionState.stopped;
824
823
  (_a = this.submitter) === null || _a === void 0 ? void 0 : _a.removeAttribute("disabled");
824
+ this.resetSubmitterText();
825
825
  dispatch("turbo:submit-end", {
826
826
  target: this.formElement,
827
827
  detail: Object.assign({
@@ -830,11 +830,35 @@ class FormSubmission {
830
830
  });
831
831
  this.delegate.formSubmissionFinished(this);
832
832
  }
833
+ setSubmitsWith() {
834
+ if (!this.submitter || !this.submitsWith) return;
835
+ if (this.submitter.matches("button")) {
836
+ this.originalSubmitText = this.submitter.innerHTML;
837
+ this.submitter.innerHTML = this.submitsWith;
838
+ } else if (this.submitter.matches("input")) {
839
+ const input = this.submitter;
840
+ this.originalSubmitText = input.value;
841
+ input.value = this.submitsWith;
842
+ }
843
+ }
844
+ resetSubmitterText() {
845
+ if (!this.submitter || !this.originalSubmitText) return;
846
+ if (this.submitter.matches("button")) {
847
+ this.submitter.innerHTML = this.originalSubmitText;
848
+ } else if (this.submitter.matches("input")) {
849
+ const input = this.submitter;
850
+ input.value = this.originalSubmitText;
851
+ }
852
+ }
833
853
  requestMustRedirect(request) {
834
- return !request.isIdempotent && this.mustRedirect;
854
+ return !request.isSafe && this.mustRedirect;
835
855
  }
836
856
  requestAcceptsTurboStreamResponse(request) {
837
- return !request.isIdempotent || hasAttribute("data-turbo-stream", this.submitter, this.formElement);
857
+ return !request.isSafe || hasAttribute("data-turbo-stream", this.submitter, this.formElement);
858
+ }
859
+ get submitsWith() {
860
+ var _a;
861
+ return (_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("data-turbo-submits-with");
838
862
  }
839
863
  }
840
864
 
@@ -1076,8 +1100,8 @@ class View {
1076
1100
  }
1077
1101
 
1078
1102
  class FrameView extends View {
1079
- invalidate() {
1080
- this.element.innerHTML = "";
1103
+ missing() {
1104
+ this.element.innerHTML = `<strong class="turbo-frame-error">Content missing</strong>`;
1081
1105
  }
1082
1106
  get snapshot() {
1083
1107
  return new Snapshot(this.element);
@@ -1232,16 +1256,16 @@ class FormLinkClickObserver {
1232
1256
  }
1233
1257
 
1234
1258
  class Bardo {
1235
- constructor(delegate, permanentElementMap) {
1236
- this.delegate = delegate;
1237
- this.permanentElementMap = permanentElementMap;
1238
- }
1239
1259
  static async preservingPermanentElements(delegate, permanentElementMap, callback) {
1240
1260
  const bardo = new this(delegate, permanentElementMap);
1241
1261
  bardo.enter();
1242
1262
  await callback();
1243
1263
  bardo.leave();
1244
1264
  }
1265
+ constructor(delegate, permanentElementMap) {
1266
+ this.delegate = delegate;
1267
+ this.permanentElementMap = permanentElementMap;
1268
+ }
1245
1269
  enter() {
1246
1270
  for (const id in this.permanentElementMap) {
1247
1271
  const [currentPermanentElement, newPermanentElement] = this.permanentElementMap[id];
@@ -1352,10 +1376,6 @@ function elementIsFocusable(element) {
1352
1376
  }
1353
1377
 
1354
1378
  class FrameRenderer extends Renderer {
1355
- constructor(delegate, currentSnapshot, newSnapshot, renderElement, isPreview, willRender = true) {
1356
- super(currentSnapshot, newSnapshot, renderElement, isPreview, willRender);
1357
- this.delegate = delegate;
1358
- }
1359
1379
  static renderElement(currentElement, newElement) {
1360
1380
  var _a;
1361
1381
  const destinationRange = document.createRange();
@@ -1368,6 +1388,10 @@ class FrameRenderer extends Renderer {
1368
1388
  currentElement.appendChild(sourceRange.extractContents());
1369
1389
  }
1370
1390
  }
1391
+ constructor(delegate, currentSnapshot, newSnapshot, renderElement, isPreview, willRender = true) {
1392
+ super(currentSnapshot, newSnapshot, renderElement, isPreview, willRender);
1393
+ this.delegate = delegate;
1394
+ }
1371
1395
  get shouldRender() {
1372
1396
  return true;
1373
1397
  }
@@ -1429,18 +1453,6 @@ function readScrollBehavior(value, defaultValue) {
1429
1453
  }
1430
1454
 
1431
1455
  class ProgressBar {
1432
- constructor() {
1433
- this.hiding = false;
1434
- this.value = 0;
1435
- this.visible = false;
1436
- this.trickle = () => {
1437
- this.setValue(this.value + Math.random() / 100);
1438
- };
1439
- this.stylesheetElement = this.createStylesheetElement();
1440
- this.progressElement = this.createProgressElement();
1441
- this.installStylesheetElement();
1442
- this.setValue(0);
1443
- }
1444
1456
  static get defaultCSS() {
1445
1457
  return unindent`
1446
1458
  .turbo-progress-bar {
@@ -1458,6 +1470,18 @@ class ProgressBar {
1458
1470
  }
1459
1471
  `;
1460
1472
  }
1473
+ constructor() {
1474
+ this.hiding = false;
1475
+ this.value = 0;
1476
+ this.visible = false;
1477
+ this.trickle = () => {
1478
+ this.setValue(this.value + Math.random() / 100);
1479
+ };
1480
+ this.stylesheetElement = this.createStylesheetElement();
1481
+ this.progressElement = this.createProgressElement();
1482
+ this.installStylesheetElement();
1483
+ this.setValue(0);
1484
+ }
1461
1485
  show() {
1462
1486
  if (!this.visible) {
1463
1487
  this.visible = true;
@@ -1626,10 +1650,6 @@ function elementWithoutNonce(element) {
1626
1650
  }
1627
1651
 
1628
1652
  class PageSnapshot extends Snapshot {
1629
- constructor(element, headSnapshot) {
1630
- super(element);
1631
- this.headSnapshot = headSnapshot;
1632
- }
1633
1653
  static fromHTMLString(html = "") {
1634
1654
  return this.fromDocument(parseHTMLDocument(html));
1635
1655
  }
@@ -1639,6 +1659,10 @@ class PageSnapshot extends Snapshot {
1639
1659
  static fromDocument({head: head, body: body}) {
1640
1660
  return new this(body, new HeadSnapshot(head));
1641
1661
  }
1662
+ constructor(element, headSnapshot) {
1663
+ super(element);
1664
+ this.headSnapshot = headSnapshot;
1665
+ }
1642
1666
  clone() {
1643
1667
  const clonedElement = this.element.cloneNode(true);
1644
1668
  const selectElements = this.element.querySelectorAll("select");
@@ -2143,10 +2167,11 @@ class BrowserAdapter {
2143
2167
 
2144
2168
  class CacheObserver {
2145
2169
  constructor() {
2170
+ this.selector = "[data-turbo-temporary]";
2171
+ this.deprecatedSelector = "[data-turbo-cache=false]";
2146
2172
  this.started = false;
2147
- this.removeStaleElements = _event => {
2148
- const staleElements = [ ...document.querySelectorAll('[data-turbo-cache="false"]') ];
2149
- for (const element of staleElements) {
2173
+ this.removeTemporaryElements = _event => {
2174
+ for (const element of this.temporaryElements) {
2150
2175
  element.remove();
2151
2176
  }
2152
2177
  };
@@ -2154,15 +2179,25 @@ class CacheObserver {
2154
2179
  start() {
2155
2180
  if (!this.started) {
2156
2181
  this.started = true;
2157
- addEventListener("turbo:before-cache", this.removeStaleElements, false);
2182
+ addEventListener("turbo:before-cache", this.removeTemporaryElements, false);
2158
2183
  }
2159
2184
  }
2160
2185
  stop() {
2161
2186
  if (this.started) {
2162
2187
  this.started = false;
2163
- removeEventListener("turbo:before-cache", this.removeStaleElements, false);
2188
+ removeEventListener("turbo:before-cache", this.removeTemporaryElements, false);
2164
2189
  }
2165
2190
  }
2191
+ get temporaryElements() {
2192
+ return [ ...document.querySelectorAll(this.selector), ...this.temporaryElementsWithDeprecation ];
2193
+ }
2194
+ get temporaryElementsWithDeprecation() {
2195
+ const elements = document.querySelectorAll(this.deprecatedSelector);
2196
+ if (elements.length) {
2197
+ console.warn(`The ${this.deprecatedSelector} selector is deprecated and will be removed in a future version. Use ${this.selector} instead.`);
2198
+ }
2199
+ return [ ...elements ];
2200
+ }
2166
2201
  }
2167
2202
 
2168
2203
  class FrameRedirector {
@@ -2361,7 +2396,7 @@ class Navigator {
2361
2396
  if (formSubmission == this.formSubmission) {
2362
2397
  const responseHTML = await fetchResponse.responseHTML;
2363
2398
  if (responseHTML) {
2364
- const shouldCacheSnapshot = formSubmission.method == FetchMethod.get;
2399
+ const shouldCacheSnapshot = formSubmission.isSafe;
2365
2400
  if (!shouldCacheSnapshot) {
2366
2401
  this.view.clearSnapshotCache();
2367
2402
  }
@@ -3362,6 +3397,8 @@ var Turbo = Object.freeze({
3362
3397
  StreamActions: StreamActions
3363
3398
  });
3364
3399
 
3400
+ class TurboFrameMissingError extends Error {}
3401
+
3365
3402
  class FrameController {
3366
3403
  constructor(element) {
3367
3404
  this.fetchResponseLoaded = _fetchResponse => {};
@@ -3458,26 +3495,14 @@ class FrameController {
3458
3495
  try {
3459
3496
  const html = await fetchResponse.responseHTML;
3460
3497
  if (html) {
3461
- const {body: body} = parseHTMLDocument(html);
3462
- const newFrameElement = await this.extractForeignFrameElement(body);
3463
- if (newFrameElement) {
3464
- const snapshot = new Snapshot(newFrameElement);
3465
- const renderer = new FrameRenderer(this, this.view.snapshot, snapshot, FrameRenderer.renderElement, false, false);
3466
- if (this.view.renderPromise) await this.view.renderPromise;
3467
- this.changeHistory();
3468
- await this.view.render(renderer);
3469
- this.complete = true;
3470
- session.frameRendered(fetchResponse, this.element);
3471
- session.frameLoaded(this.element);
3472
- this.fetchResponseLoaded(fetchResponse);
3473
- } else if (this.willHandleFrameMissingFromResponse(fetchResponse)) {
3474
- console.warn(`A matching frame for #${this.element.id} was missing from the response, transforming into full-page Visit.`);
3475
- this.visitResponse(fetchResponse.response);
3498
+ const document = parseHTMLDocument(html);
3499
+ const pageSnapshot = PageSnapshot.fromDocument(document);
3500
+ if (pageSnapshot.isVisitable) {
3501
+ await this.loadFrameResponse(fetchResponse, document);
3502
+ } else {
3503
+ await this.handleUnvisitableFrameResponse(fetchResponse);
3476
3504
  }
3477
3505
  }
3478
- } catch (error) {
3479
- console.error(error);
3480
- this.view.invalidate();
3481
3506
  } finally {
3482
3507
  this.fetchResponseLoaded = () => {};
3483
3508
  }
@@ -3529,7 +3554,6 @@ class FrameController {
3529
3554
  this.resolveVisitPromise();
3530
3555
  }
3531
3556
  async requestFailedWithResponse(request, response) {
3532
- console.error(response);
3533
3557
  await this.loadResponse(response);
3534
3558
  this.resolveVisitPromise();
3535
3559
  }
@@ -3547,9 +3571,13 @@ class FrameController {
3547
3571
  const frame = this.findFrameElement(formSubmission.formElement, formSubmission.submitter);
3548
3572
  frame.delegate.proposeVisitIfNavigatedWithAction(frame, formSubmission.formElement, formSubmission.submitter);
3549
3573
  frame.delegate.loadResponse(response);
3574
+ if (!formSubmission.isSafe) {
3575
+ session.clearCache();
3576
+ }
3550
3577
  }
3551
3578
  formSubmissionFailedWithResponse(formSubmission, fetchResponse) {
3552
3579
  this.element.delegate.loadResponse(fetchResponse);
3580
+ session.clearCache();
3553
3581
  }
3554
3582
  formSubmissionErrored(formSubmission, error) {
3555
3583
  console.error(error);
@@ -3579,6 +3607,22 @@ class FrameController {
3579
3607
  willRenderFrame(currentElement, _newElement) {
3580
3608
  this.previousFrameElement = currentElement.cloneNode(true);
3581
3609
  }
3610
+ async loadFrameResponse(fetchResponse, document) {
3611
+ const newFrameElement = await this.extractForeignFrameElement(document.body);
3612
+ if (newFrameElement) {
3613
+ const snapshot = new Snapshot(newFrameElement);
3614
+ const renderer = new FrameRenderer(this, this.view.snapshot, snapshot, FrameRenderer.renderElement, false, false);
3615
+ if (this.view.renderPromise) await this.view.renderPromise;
3616
+ this.changeHistory();
3617
+ await this.view.render(renderer);
3618
+ this.complete = true;
3619
+ session.frameRendered(fetchResponse, this.element);
3620
+ session.frameLoaded(this.element);
3621
+ this.fetchResponseLoaded(fetchResponse);
3622
+ } else if (this.willHandleFrameMissingFromResponse(fetchResponse)) {
3623
+ this.handleFrameMissingFromResponse(fetchResponse);
3624
+ }
3625
+ }
3582
3626
  async visit(url) {
3583
3627
  var _a;
3584
3628
  const request = new FetchRequest(this, FetchMethod.get, url, new URLSearchParams, this.element);
@@ -3634,6 +3678,10 @@ class FrameController {
3634
3678
  session.history.update(method, expandURL(this.element.src || ""), this.restorationIdentifier);
3635
3679
  }
3636
3680
  }
3681
+ async handleUnvisitableFrameResponse(fetchResponse) {
3682
+ console.warn(`The response (${fetchResponse.statusCode}) from <turbo-frame id="${this.element.id}"> is performing a full page visit due to turbo-visit-control.`);
3683
+ await this.visitResponse(fetchResponse.response);
3684
+ }
3637
3685
  willHandleFrameMissingFromResponse(fetchResponse) {
3638
3686
  this.element.setAttribute("complete", "");
3639
3687
  const response = fetchResponse.response;
@@ -3654,6 +3702,14 @@ class FrameController {
3654
3702
  });
3655
3703
  return !event.defaultPrevented;
3656
3704
  }
3705
+ handleFrameMissingFromResponse(fetchResponse) {
3706
+ this.view.missing();
3707
+ this.throwFrameMissingError(fetchResponse);
3708
+ }
3709
+ throwFrameMissingError(fetchResponse) {
3710
+ const message = `The response (${fetchResponse.statusCode}) did not contain the expected <turbo-frame id="${this.element.id}"> and will be ignored. To perform a full page visit instead, set turbo-visit-control to reload.`;
3711
+ throw new TurboFrameMissingError(message);
3712
+ }
3657
3713
  async visitResponse(response) {
3658
3714
  const wrapped = new FetchResponse(response);
3659
3715
  const responseHTML = await wrapped.responseHTML;
@@ -4048,7 +4104,9 @@ class TurboCableStreamSourceElement extends HTMLElement {
4048
4104
  async connectedCallback() {
4049
4105
  connectStreamSource(this);
4050
4106
  this.subscription = await subscribeTo(this.channel, {
4051
- received: this.dispatchMessageEvent.bind(this)
4107
+ received: this.dispatchMessageEvent.bind(this),
4108
+ connected: this.subscriptionConnected.bind(this),
4109
+ disconnected: this.subscriptionDisconnected.bind(this)
4052
4110
  });
4053
4111
  }
4054
4112
  disconnectedCallback() {
@@ -4061,6 +4119,12 @@ class TurboCableStreamSourceElement extends HTMLElement {
4061
4119
  });
4062
4120
  return this.dispatchEvent(event);
4063
4121
  }
4122
+ subscriptionConnected() {
4123
+ this.setAttribute("connected", "");
4124
+ }
4125
+ subscriptionDisconnected() {
4126
+ this.removeAttribute("connected");
4127
+ }
4064
4128
  get channel() {
4065
4129
  const channel = this.getAttribute("channel");
4066
4130
  const signed_stream_name = this.getAttribute("signed-stream-name");
@@ -4113,7 +4177,9 @@ function determineFetchMethod(submitter, body, form) {
4113
4177
 
4114
4178
  function determineFormMethod(submitter) {
4115
4179
  if (submitter instanceof HTMLButtonElement || submitter instanceof HTMLInputElement) {
4116
- if (submitter.hasAttribute("formmethod")) {
4180
+ if (submitter.name === "_method") {
4181
+ return submitter.value;
4182
+ } else if (submitter.hasAttribute("formmethod")) {
4117
4183
  return submitter.formMethod;
4118
4184
  } else {
4119
4185
  return null;