turbo-rails 2.0.5 → 2.0.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: 49c33a9c7eaf917754b0deb21c776b9db3a457e09453bfb4da400ce2ea676b6f
4
- data.tar.gz: a6823b945a543045681165fa5f429cc3a7fe0928b6ae274505b61a20dee90662
3
+ metadata.gz: 4553ee49b6504b50ebd79364400cd57bcba2a4fd544aff65f88a4c5a12ffdaac
4
+ data.tar.gz: 7500ba88dc381e1601b0778c31e68be1a16ce72dcc535bbd8b54ba4eebac5d0e
5
5
  SHA512:
6
- metadata.gz: 93ae8bb2c473aa705eb4b46ba0fe35fc61aa71513993755f2d33fbb6fcdec96881f19b9ae020e46d0dab98aaa8b771ba0dff26c2317370126404d7883d81a51e
7
- data.tar.gz: 303817c62ed4ce953084dae5e11025501eddab78398783d4266394b81fb4f5622486160abf30959633cea66a5b5a3aaf9c94cce82da2d0e5148cd98d60e32512
6
+ metadata.gz: 40376048e8a552fb9ac3570e6fce78f7d0741cdab4b8d3851305fb5541f50dd381a4d9616e7c4fb147a1d58d7b546ab6d07061ac79f3851b8163605084cabc62
7
+ data.tar.gz: 85414d553bc478bf46ba0294381b09031e58949edd93d47e8f02b721a0fcb556388a5b3b20741a3011f3547fb0281d9bdbcc7e518a97df27953474ce3d012add
data/README.md CHANGED
@@ -56,7 +56,7 @@ When the user clicks on the `Edit this todo` link, as a direct response to this
56
56
 
57
57
  ### A note on custom layouts
58
58
 
59
- 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).
59
+ 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).
60
60
  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:
61
61
 
62
62
  ```ruby
@@ -64,7 +64,7 @@ layout :custom_layout
64
64
 
65
65
  def custom_layout
66
66
  return "turbo_rails/frame" if turbo_frame_request?
67
-
67
+
68
68
  # ... your custom layout logic
69
69
  ```
70
70
 
@@ -74,14 +74,14 @@ If you are using a custom, but "static" layout,
74
74
  layout "some_static_layout"
75
75
  ```
76
76
 
77
- you **have** to change it to a layout method in order to conditionally return `false` for turbo frame requests:
77
+ you **have** to change it to a layout method in order to conditionally return `"turbo_rails/frame"` for turbo frame requests:
78
78
 
79
79
  ```ruby
80
80
  layout :custom_layout
81
81
 
82
82
  def custom_layout
83
83
  return "turbo_rails/frame" if turbo_frame_request?
84
-
84
+
85
85
  "some_static_layout"
86
86
  ```
87
87
 
@@ -109,9 +109,8 @@ This gem is automatically configured for applications made with Rails 7+ (unless
109
109
  1. Add the `turbo-rails` gem to your Gemfile: `gem 'turbo-rails'`
110
110
  2. Run `./bin/bundle install`
111
111
  3. Run `./bin/rails turbo:install`
112
- 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.
113
112
 
114
- Running `turbo:install` will install through NPM if Node.js is used 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.
113
+ Running `turbo:install` will install through NPM or Bun if a JavaScript runtime is used 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.
115
114
 
116
115
  If you're using node and need to use the cable consumer, you can import [`cable`](https://github.com/hotwired/turbo-rails/blob/main/app/javascript/turbo/cable.js) (`import { cable } from "@hotwired/turbo-rails"`), but ensure that your application actually *uses* the members it `import`s when using this style (see [turbo-rails#48](https://github.com/hotwired/turbo-rails/issues/48)).
117
116
 
@@ -125,6 +124,8 @@ import "@hotwired/turbo-rails"
125
124
 
126
125
  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).
127
126
 
127
+ Note that in development, the default Action Cable adapter is the single-process `async` adapter. This means that turbo updates are only broadcast within that same process. So you can't start `bin/rails console` and trigger Turbo broadcasts and expect them to show up in a browser connected to a server running in a separate `bin/dev` or `bin/rails server` process. Instead, you should use the web-console when needing to manaually trigger Turbo broadcasts inside the same process. Add "console" to any action or "<%= console %>" in any view to make the web console appear.
128
+
128
129
  ### RubyDoc Documentation
129
130
 
130
131
  For the API documentation covering this gem's classes and packages, [visit the RubyDoc page](https://rubydoc.info/github/hotwired/turbo-rails/main).
@@ -152,10 +153,56 @@ The [`Turbo::TestAssertions::IntegrationTestAssertions`](./lib/turbo/test_assert
152
153
 
153
154
  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. `Turbo::Broadcastable::TestHelper` is automatically included in [`ActiveSupport::TestCase`](https://edgeapi.rubyonrails.org/classes/ActiveSupport/TestCase.html).
154
155
 
156
+ ### Rendering Outside of a Request
157
+
158
+ Turbo utilizes [ActionController::Renderer][] to render templates and partials
159
+ outside the context of the request-response cycle. If you need to render a
160
+ Turbo-aware template, partial, or component, use [ActionController::Renderer][]:
161
+
162
+ ```ruby
163
+ ApplicationController.renderer.render template: "posts/show", assigns: { post: Post.first } # => "<html>…"
164
+ PostsController.renderer.render :show, assigns: { post: Post.first } # => "<html>…"
165
+ ```
166
+
167
+ As a shortcut, you can also call render directly on the controller class itself:
168
+
169
+ ```ruby
170
+ ApplicationController.render template: "posts/show", assigns: { post: Post.first } # => "<html>…"
171
+ PostsController.render :show, assigns: { post: Post.first } # => "<html>…"
172
+ ```
173
+
174
+ [ActionController::Renderer]: https://api.rubyonrails.org/classes/ActionController/Renderer.html
175
+
155
176
  ## Development
156
177
 
157
178
  Run the tests with `./bin/test`.
158
179
 
180
+ ### Using local Turbo version
181
+
182
+ Often you might want to test changes made locally to [Turbo lib](https://github.com/hotwired/turbo) itself. To package your local development version of Turbo you can use [yarn link](https://classic.yarnpkg.com/lang/en/docs/cli/link/) feature:
183
+
184
+ ```sh
185
+ cd <local-turbo-dir>
186
+ yarn link
187
+
188
+ cd <local-turbo-rails-dir>
189
+ yarn link @hotwired/turbo
190
+
191
+ # Build the JS distribution files...
192
+ yarn build
193
+ # ...and commit the changes
194
+ ```
195
+
196
+ Now you can reference your version of turbo-rails in your Rails projects packaged with your local version of Turbo.
197
+
198
+ ## Contributing
199
+
200
+ Having a way to reproduce your issue will help people confirm, investigate, and ultimately fix your issue. You can do this by providing an executable test case. To make this process easier, we have prepared an [executable bug report Rails application](./bug_report_template.rb) for you to use as a starting point.
201
+
202
+ This template includes the boilerplate code to set up a System Test case. Copy the content of the template into a `.rb` file and make the necessary changes to demonstrate the issue. You can execute it by running `ruby the_file.rb` in your terminal. If all goes well, you should see your test case failing.
203
+
204
+ You can then share your executable test case as a gist or paste the content into the issue description.
205
+
159
206
  ## License
160
207
 
161
208
  Turbo is released under the [MIT License](https://opensource.org/licenses/MIT).
@@ -1,5 +1,5 @@
1
1
  /*!
2
- Turbo 8.0.4
2
+ Turbo 8.0.5
3
3
  Copyright © 2024 37signals LLC
4
4
  */
5
5
  (function(prototype) {
@@ -483,13 +483,17 @@ async function around(callback, reader) {
483
483
  return [ before, after ];
484
484
  }
485
485
 
486
- function doesNotTargetIFrame(anchor) {
487
- if (anchor.hasAttribute("target")) {
488
- for (const element of document.getElementsByName(anchor.target)) {
486
+ function doesNotTargetIFrame(name) {
487
+ if (name === "_blank") {
488
+ return false;
489
+ } else if (name) {
490
+ for (const element of document.getElementsByName(name)) {
489
491
  if (element instanceof HTMLIFrameElement) return false;
490
492
  }
493
+ return true;
494
+ } else {
495
+ return true;
491
496
  }
492
- return true;
493
497
  }
494
498
 
495
499
  function findLinkFromClickTarget(target) {
@@ -596,7 +600,7 @@ class FetchRequest {
596
600
  this.fetchOptions = {
597
601
  credentials: "same-origin",
598
602
  redirect: "follow",
599
- method: method,
603
+ method: method.toUpperCase(),
600
604
  headers: {
601
605
  ...this.defaultHeaders
602
606
  },
@@ -616,7 +620,7 @@ class FetchRequest {
616
620
  const [url, body] = buildResourceAndBody(this.url, fetchMethod, fetchBody, this.enctype);
617
621
  this.url = url;
618
622
  this.fetchOptions.body = body;
619
- this.fetchOptions.method = fetchMethod;
623
+ this.fetchOptions.method = fetchMethod.toUpperCase();
620
624
  }
621
625
  get headers() {
622
626
  return this.fetchOptions.headers;
@@ -1166,15 +1170,8 @@ function submissionDoesNotDismissDialog(form, submitter) {
1166
1170
  }
1167
1171
 
1168
1172
  function submissionDoesNotTargetIFrame(form, submitter) {
1169
- if (submitter?.hasAttribute("formtarget") || form.hasAttribute("target")) {
1170
- const target = submitter?.getAttribute("formtarget") || form.target;
1171
- for (const element of document.getElementsByName(target)) {
1172
- if (element instanceof HTMLIFrameElement) return false;
1173
- }
1174
- return true;
1175
- } else {
1176
- return true;
1177
- }
1173
+ const target = submitter?.getAttribute("formtarget") || form.getAttribute("target");
1174
+ return doesNotTargetIFrame(target);
1178
1175
  }
1179
1176
 
1180
1177
  class View {
@@ -1307,14 +1304,14 @@ class LinkInterceptor {
1307
1304
  document.removeEventListener("turbo:before-visit", this.willVisit);
1308
1305
  }
1309
1306
  clickBubbled=event => {
1310
- if (this.respondsToEventTarget(event.target)) {
1307
+ if (this.clickEventIsSignificant(event)) {
1311
1308
  this.clickEvent = event;
1312
1309
  } else {
1313
1310
  delete this.clickEvent;
1314
1311
  }
1315
1312
  };
1316
1313
  linkClicked=event => {
1317
- if (this.clickEvent && this.respondsToEventTarget(event.target) && event.target instanceof Element) {
1314
+ if (this.clickEvent && this.clickEventIsSignificant(event)) {
1318
1315
  if (this.delegate.shouldInterceptLinkClick(event.target, event.detail.url, event.detail.originalEvent)) {
1319
1316
  this.clickEvent.preventDefault();
1320
1317
  event.preventDefault();
@@ -1326,9 +1323,10 @@ class LinkInterceptor {
1326
1323
  willVisit=_event => {
1327
1324
  delete this.clickEvent;
1328
1325
  };
1329
- respondsToEventTarget(target) {
1330
- const element = target instanceof Element ? target : target instanceof Node ? target.parentElement : null;
1331
- return element && element.closest("turbo-frame, html") == this.element;
1326
+ clickEventIsSignificant(event) {
1327
+ const target = event.composed ? event.target?.parentElement : event.target;
1328
+ const element = findLinkFromClickTarget(target) || target;
1329
+ return element instanceof Element && element.closest("turbo-frame, html") == this.element;
1332
1330
  }
1333
1331
  }
1334
1332
 
@@ -1358,7 +1356,7 @@ class LinkClickObserver {
1358
1356
  if (event instanceof MouseEvent && this.clickEventIsSignificant(event)) {
1359
1357
  const target = event.composedPath && event.composedPath()[0] || event.target;
1360
1358
  const link = findLinkFromClickTarget(target);
1361
- if (link && doesNotTargetIFrame(link)) {
1359
+ if (link && doesNotTargetIFrame(link.target)) {
1362
1360
  const location = getLocationForLink(link);
1363
1361
  if (this.delegate.willFollowLinkToLocation(link, location, event)) {
1364
1362
  event.preventDefault();
@@ -1496,6 +1494,9 @@ class Renderer {
1496
1494
  get shouldRender() {
1497
1495
  return true;
1498
1496
  }
1497
+ get shouldAutofocus() {
1498
+ return true;
1499
+ }
1499
1500
  get reloadReason() {
1500
1501
  return;
1501
1502
  }
@@ -1513,9 +1514,11 @@ class Renderer {
1513
1514
  await Bardo.preservingPermanentElements(this, this.permanentElementMap, callback);
1514
1515
  }
1515
1516
  focusFirstAutofocusableElement() {
1516
- const element = this.connectedSnapshot.firstAutofocusableElement;
1517
- if (element) {
1518
- element.focus();
1517
+ if (this.shouldAutofocus) {
1518
+ const element = this.connectedSnapshot.firstAutofocusableElement;
1519
+ if (element) {
1520
+ element.focus();
1521
+ }
1519
1522
  }
1520
1523
  }
1521
1524
  enteringBardo(currentPermanentElement) {
@@ -2629,7 +2632,7 @@ class LinkPrefetchObserver {
2629
2632
  this.#prefetchedLink = null;
2630
2633
  };
2631
2634
  #tryToUsePrefetchedRequest=event => {
2632
- if (event.target.tagName !== "FORM" && event.detail.fetchOptions.method === "get") {
2635
+ if (event.target.tagName !== "FORM" && event.detail.fetchOptions.method === "GET") {
2633
2636
  const cached = prefetchCache.get(event.detail.url.toString());
2634
2637
  if (cached) {
2635
2638
  event.detail.fetchRequest = cached;
@@ -2798,6 +2801,7 @@ class Navigator {
2798
2801
  }
2799
2802
  visitCompleted(visit) {
2800
2803
  this.delegate.visitCompleted(visit);
2804
+ delete this.currentVisit;
2801
2805
  }
2802
2806
  locationWithActionIsSamePage(location, action) {
2803
2807
  const anchor = getAnchor(location);
@@ -3628,6 +3632,80 @@ var Idiomorph = function() {
3628
3632
  };
3629
3633
  }();
3630
3634
 
3635
+ function morphElements(currentElement, newElement, {callbacks: callbacks, ...options} = {}) {
3636
+ Idiomorph.morph(currentElement, newElement, {
3637
+ ...options,
3638
+ callbacks: new DefaultIdiomorphCallbacks(callbacks)
3639
+ });
3640
+ }
3641
+
3642
+ function morphChildren(currentElement, newElement) {
3643
+ morphElements(currentElement, newElement.children, {
3644
+ morphStyle: "innerHTML"
3645
+ });
3646
+ }
3647
+
3648
+ class DefaultIdiomorphCallbacks {
3649
+ #beforeNodeMorphed;
3650
+ constructor({beforeNodeMorphed: beforeNodeMorphed} = {}) {
3651
+ this.#beforeNodeMorphed = beforeNodeMorphed || (() => true);
3652
+ }
3653
+ beforeNodeAdded=node => !(node.id && node.hasAttribute("data-turbo-permanent") && document.getElementById(node.id));
3654
+ beforeNodeMorphed=(currentElement, newElement) => {
3655
+ if (currentElement instanceof Element) {
3656
+ if (!currentElement.hasAttribute("data-turbo-permanent") && this.#beforeNodeMorphed(currentElement, newElement)) {
3657
+ const event = dispatch("turbo:before-morph-element", {
3658
+ cancelable: true,
3659
+ target: currentElement,
3660
+ detail: {
3661
+ currentElement: currentElement,
3662
+ newElement: newElement
3663
+ }
3664
+ });
3665
+ return !event.defaultPrevented;
3666
+ } else {
3667
+ return false;
3668
+ }
3669
+ }
3670
+ };
3671
+ beforeAttributeUpdated=(attributeName, target, mutationType) => {
3672
+ const event = dispatch("turbo:before-morph-attribute", {
3673
+ cancelable: true,
3674
+ target: target,
3675
+ detail: {
3676
+ attributeName: attributeName,
3677
+ mutationType: mutationType
3678
+ }
3679
+ });
3680
+ return !event.defaultPrevented;
3681
+ };
3682
+ beforeNodeRemoved=node => this.beforeNodeMorphed(node);
3683
+ afterNodeMorphed=(currentElement, newElement) => {
3684
+ if (currentElement instanceof Element) {
3685
+ dispatch("turbo:morph-element", {
3686
+ target: currentElement,
3687
+ detail: {
3688
+ currentElement: currentElement,
3689
+ newElement: newElement
3690
+ }
3691
+ });
3692
+ }
3693
+ };
3694
+ }
3695
+
3696
+ class MorphingFrameRenderer extends FrameRenderer {
3697
+ static renderElement(currentElement, newElement) {
3698
+ dispatch("turbo:before-frame-morph", {
3699
+ target: currentElement,
3700
+ detail: {
3701
+ currentElement: currentElement,
3702
+ newElement: newElement
3703
+ }
3704
+ });
3705
+ morphChildren(currentElement, newElement);
3706
+ }
3707
+ }
3708
+
3631
3709
  class PageRenderer extends Renderer {
3632
3710
  static renderElement(currentElement, newElement) {
3633
3711
  if (document.body && newElement instanceof HTMLBodyElement) {
@@ -3796,108 +3874,47 @@ class PageRenderer extends Renderer {
3796
3874
  }
3797
3875
  }
3798
3876
 
3799
- class MorphRenderer extends PageRenderer {
3800
- async render() {
3801
- if (this.willRender) await this.#morphBody();
3802
- }
3803
- get renderMethod() {
3804
- return "morph";
3805
- }
3806
- async #morphBody() {
3807
- this.#morphElements(this.currentElement, this.newElement);
3808
- this.#reloadRemoteFrames();
3809
- dispatch("turbo:morph", {
3810
- detail: {
3811
- currentElement: this.currentElement,
3812
- newElement: this.newElement
3813
- }
3814
- });
3815
- }
3816
- #morphElements(currentElement, newElement, morphStyle = "outerHTML") {
3817
- this.isMorphingTurboFrame = this.#isFrameReloadedWithMorph(currentElement);
3818
- Idiomorph.morph(currentElement, newElement, {
3819
- morphStyle: morphStyle,
3877
+ class MorphingPageRenderer extends PageRenderer {
3878
+ static renderElement(currentElement, newElement) {
3879
+ morphElements(currentElement, newElement, {
3820
3880
  callbacks: {
3821
- beforeNodeAdded: this.#shouldAddElement,
3822
- beforeNodeMorphed: this.#shouldMorphElement,
3823
- beforeAttributeUpdated: this.#shouldUpdateAttribute,
3824
- beforeNodeRemoved: this.#shouldRemoveElement,
3825
- afterNodeMorphed: this.#didMorphElement
3881
+ beforeNodeMorphed: element => !canRefreshFrame(element)
3826
3882
  }
3827
3883
  });
3828
- }
3829
- #shouldAddElement=node => !(node.id && node.hasAttribute("data-turbo-permanent") && document.getElementById(node.id));
3830
- #shouldMorphElement=(oldNode, newNode) => {
3831
- if (oldNode instanceof HTMLElement) {
3832
- if (!oldNode.hasAttribute("data-turbo-permanent") && (this.isMorphingTurboFrame || !this.#isFrameReloadedWithMorph(oldNode))) {
3833
- const event = dispatch("turbo:before-morph-element", {
3834
- cancelable: true,
3835
- target: oldNode,
3836
- detail: {
3837
- newElement: newNode
3838
- }
3839
- });
3840
- return !event.defaultPrevented;
3841
- } else {
3842
- return false;
3843
- }
3884
+ for (const frame of currentElement.querySelectorAll("turbo-frame")) {
3885
+ if (canRefreshFrame(frame)) refreshFrame(frame);
3844
3886
  }
3845
- };
3846
- #shouldUpdateAttribute=(attributeName, target, mutationType) => {
3847
- const event = dispatch("turbo:before-morph-attribute", {
3848
- cancelable: true,
3849
- target: target,
3850
- detail: {
3851
- attributeName: attributeName,
3852
- mutationType: mutationType
3853
- }
3854
- });
3855
- return !event.defaultPrevented;
3856
- };
3857
- #didMorphElement=(oldNode, newNode) => {
3858
- if (newNode instanceof HTMLElement) {
3859
- dispatch("turbo:morph-element", {
3860
- target: oldNode,
3861
- detail: {
3862
- newElement: newNode
3863
- }
3864
- });
3865
- }
3866
- };
3867
- #shouldRemoveElement=node => this.#shouldMorphElement(node);
3868
- #reloadRemoteFrames() {
3869
- this.#remoteFrames().forEach((frame => {
3870
- if (this.#isFrameReloadedWithMorph(frame)) {
3871
- this.#renderFrameWithMorph(frame);
3872
- frame.reload();
3873
- }
3874
- }));
3875
- }
3876
- #renderFrameWithMorph(frame) {
3877
- frame.addEventListener("turbo:before-frame-render", (event => {
3878
- event.detail.render = this.#morphFrameUpdate;
3879
- }), {
3880
- once: true
3881
- });
3882
- }
3883
- #morphFrameUpdate=(currentElement, newElement) => {
3884
- dispatch("turbo:before-frame-morph", {
3885
- target: currentElement,
3887
+ dispatch("turbo:morph", {
3886
3888
  detail: {
3887
3889
  currentElement: currentElement,
3888
3890
  newElement: newElement
3889
3891
  }
3890
3892
  });
3891
- this.#morphElements(currentElement, newElement.children, "innerHTML");
3892
- };
3893
- #isFrameReloadedWithMorph(element) {
3894
- return element.src && element.refresh === "morph";
3895
3893
  }
3896
- #remoteFrames() {
3897
- return Array.from(document.querySelectorAll("turbo-frame[src]")).filter((frame => !frame.closest("[data-turbo-permanent]")));
3894
+ async preservingPermanentElements(callback) {
3895
+ return await callback();
3896
+ }
3897
+ get renderMethod() {
3898
+ return "morph";
3899
+ }
3900
+ get shouldAutofocus() {
3901
+ return false;
3898
3902
  }
3899
3903
  }
3900
3904
 
3905
+ function canRefreshFrame(frame) {
3906
+ return frame instanceof FrameElement && frame.src && frame.refresh === "morph" && !frame.closest("[data-turbo-permanent]");
3907
+ }
3908
+
3909
+ function refreshFrame(frame) {
3910
+ frame.addEventListener("turbo:before-frame-render", (({detail: detail}) => {
3911
+ detail.render = MorphingFrameRenderer.renderElement;
3912
+ }), {
3913
+ once: true
3914
+ });
3915
+ frame.reload();
3916
+ }
3917
+
3901
3918
  class SnapshotCache {
3902
3919
  keys=[];
3903
3920
  snapshots={};
@@ -3951,8 +3968,8 @@ class PageView extends View {
3951
3968
  }
3952
3969
  renderPage(snapshot, isPreview = false, willRender = true, visit) {
3953
3970
  const shouldMorphPage = this.isPageRefresh(visit) && this.snapshot.shouldMorphPage;
3954
- const rendererClass = shouldMorphPage ? MorphRenderer : PageRenderer;
3955
- const renderer = new rendererClass(this.snapshot, snapshot, PageRenderer.renderElement, isPreview, willRender);
3971
+ const rendererClass = shouldMorphPage ? MorphingPageRenderer : PageRenderer;
3972
+ const renderer = new rendererClass(this.snapshot, snapshot, rendererClass.renderElement, isPreview, willRender);
3956
3973
  if (!renderer.shouldRender) {
3957
3974
  this.forceReloaded = true;
3958
3975
  } else {
@@ -4143,7 +4160,7 @@ class Session {
4143
4160
  }
4144
4161
  refresh(url, requestId) {
4145
4162
  const isRecentRequest = requestId && this.recentRequests.has(requestId);
4146
- if (!isRecentRequest) {
4163
+ if (!isRecentRequest && !this.navigator.currentVisit) {
4147
4164
  this.visit(url, {
4148
4165
  action: "replace",
4149
4166
  shouldCacheSnapshot: false
@@ -4969,12 +4986,24 @@ const StreamActions = {
4969
4986
  this.targetElements.forEach((e => e.remove()));
4970
4987
  },
4971
4988
  replace() {
4972
- this.targetElements.forEach((e => e.replaceWith(this.templateContent)));
4989
+ const method = this.getAttribute("method");
4990
+ this.targetElements.forEach((targetElement => {
4991
+ if (method === "morph") {
4992
+ morphElements(targetElement, this.templateContent);
4993
+ } else {
4994
+ targetElement.replaceWith(this.templateContent);
4995
+ }
4996
+ }));
4973
4997
  },
4974
4998
  update() {
4999
+ const method = this.getAttribute("method");
4975
5000
  this.targetElements.forEach((targetElement => {
4976
- targetElement.innerHTML = "";
4977
- targetElement.append(this.templateContent);
5001
+ if (method === "morph") {
5002
+ morphChildren(targetElement, this.templateContent);
5003
+ } else {
5004
+ targetElement.innerHTML = "";
5005
+ targetElement.append(this.templateContent);
5006
+ }
4978
5007
  }));
4979
5008
  },
4980
5009
  refresh() {