@hotwired/turbo 7.2.0-beta.2 → 7.2.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,4 +11,8 @@ It's all done by sending HTML over the wire. And for those instances when that's
11
11
 
12
12
  Read more on [turbo.hotwired.dev](https://turbo.hotwired.dev).
13
13
 
14
+ ## Contributing
15
+
16
+ Please read [CONTRIBUTING.md](./CONTRIBUTING.md).
17
+
14
18
  © 2021 Basecamp, LLC.
@@ -1,5 +1,5 @@
1
1
  /*
2
- Turbo 7.2.0-beta.2
2
+ Turbo 7.2.0-rc.1
3
3
  Copyright © 2022 Basecamp, LLC
4
4
  */
5
5
  (function () {
@@ -404,6 +404,9 @@ function getAttribute(attributeName, ...elements) {
404
404
  }
405
405
  return null;
406
406
  }
407
+ function hasAttribute(attributeName, ...elements) {
408
+ return elements.some((element) => element && element.hasAttribute(attributeName));
409
+ }
407
410
  function markAsBusy(...elements) {
408
411
  for (const element of elements) {
409
412
  if (element.localName == "turbo-frame") {
@@ -445,6 +448,9 @@ function getVisitAction(...elements) {
445
448
  const action = getAttribute("data-turbo-action", ...elements);
446
449
  return isAction(action) ? action : null;
447
450
  }
451
+ function getBodyElementId() {
452
+ return getMetaContent("turbo-body");
453
+ }
448
454
  function getMetaElement(name) {
449
455
  return document.querySelector(`meta[name="${name}"]`);
450
456
  }
@@ -679,7 +685,7 @@ class FormSubmission {
679
685
  this.fetchRequest = new FetchRequest(this, this.method, this.location, this.body, this.formElement);
680
686
  this.mustRedirect = mustRedirect;
681
687
  }
682
- static confirmMethod(message, _element) {
688
+ static confirmMethod(message, _element, _submitter) {
683
689
  return Promise.resolve(confirm(message));
684
690
  }
685
691
  get method() {
@@ -717,17 +723,11 @@ class FormSubmission {
717
723
  return entries.concat(typeof value == "string" ? [[name, value]] : []);
718
724
  }, []);
719
725
  }
720
- get confirmationMessage() {
721
- var _a;
722
- return ((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("data-turbo-confirm")) || this.formElement.getAttribute("data-turbo-confirm");
723
- }
724
- get needsConfirmation() {
725
- return this.confirmationMessage !== null;
726
- }
727
726
  async start() {
728
727
  const { initialized, requesting } = FormSubmissionState;
729
- if (this.needsConfirmation) {
730
- const answer = await FormSubmission.confirmMethod(this.confirmationMessage, this.formElement);
728
+ const confirmationMessage = getAttribute("data-turbo-confirm", this.submitter, this.formElement);
729
+ if (typeof confirmationMessage === "string") {
730
+ const answer = await FormSubmission.confirmMethod(confirmationMessage, this.formElement, this.submitter);
731
731
  if (!answer) {
732
732
  return;
733
733
  }
@@ -809,7 +809,7 @@ class FormSubmission {
809
809
  return !request.isIdempotent && this.mustRedirect;
810
810
  }
811
811
  requestAcceptsTurboStreamResponse(request) {
812
- return !request.isIdempotent || this.formElement.hasAttribute("data-turbo-stream");
812
+ return !request.isIdempotent || hasAttribute("data-turbo-stream", this.submitter, this.formElement);
813
813
  }
814
814
  }
815
815
  function buildFormData(formElement, submitter) {
@@ -875,10 +875,10 @@ class Snapshot {
875
875
  return null;
876
876
  }
877
877
  get permanentElements() {
878
- return [...this.element.querySelectorAll("[id][data-turbo-permanent]")];
878
+ return queryPermanentElementsAll(this.element);
879
879
  }
880
880
  getPermanentElementById(id) {
881
- return this.element.querySelector(`#${id}[data-turbo-permanent]`);
881
+ return getPermanentElementById(this.element, id);
882
882
  }
883
883
  getPermanentElementMapForSnapshot(snapshot) {
884
884
  const permanentElementMap = {};
@@ -892,6 +892,12 @@ class Snapshot {
892
892
  return permanentElementMap;
893
893
  }
894
894
  }
895
+ function getPermanentElementById(node, id) {
896
+ return node.querySelector(`#${id}[data-turbo-permanent]`);
897
+ }
898
+ function queryPermanentElementsAll(node) {
899
+ return node.querySelectorAll("[id][data-turbo-permanent]");
900
+ }
895
901
 
896
902
  class FormSubmitObserver {
897
903
  constructor(delegate, eventTarget) {
@@ -1133,6 +1139,9 @@ class FormLinkClickObserver {
1133
1139
  const turboFrame = link.getAttribute("data-turbo-frame");
1134
1140
  if (turboFrame)
1135
1141
  form.setAttribute("data-turbo-frame", turboFrame);
1142
+ const turboAction = link.getAttribute("data-turbo-action");
1143
+ if (turboAction)
1144
+ form.setAttribute("data-turbo-action", turboAction);
1136
1145
  const turboConfirm = link.getAttribute("data-turbo-confirm");
1137
1146
  if (turboConfirm)
1138
1147
  form.setAttribute("data-turbo-confirm", turboConfirm);
@@ -1141,8 +1150,8 @@ class FormLinkClickObserver {
1141
1150
  form.setAttribute("data-turbo-stream", "");
1142
1151
  this.delegate.submittedFormLinkToLocation(link, location, form);
1143
1152
  document.body.appendChild(form);
1144
- form.requestSubmit();
1145
- form.remove();
1153
+ form.addEventListener("turbo:submit-end", () => form.remove(), { once: true });
1154
+ requestAnimationFrame(() => form.requestSubmit());
1146
1155
  }
1147
1156
  }
1148
1157
 
@@ -1513,19 +1522,19 @@ function elementIsTracked(element) {
1513
1522
  return element.getAttribute("data-turbo-track") == "reload";
1514
1523
  }
1515
1524
  function elementIsScript(element) {
1516
- const tagName = element.tagName.toLowerCase();
1525
+ const tagName = element.localName;
1517
1526
  return tagName == "script";
1518
1527
  }
1519
1528
  function elementIsNoscript(element) {
1520
- const tagName = element.tagName.toLowerCase();
1529
+ const tagName = element.localName;
1521
1530
  return tagName == "noscript";
1522
1531
  }
1523
1532
  function elementIsStylesheet(element) {
1524
- const tagName = element.tagName.toLowerCase();
1533
+ const tagName = element.localName;
1525
1534
  return tagName == "style" || (tagName == "link" && element.getAttribute("rel") == "stylesheet");
1526
1535
  }
1527
1536
  function elementIsMetaElementWithName(element, name) {
1528
- const tagName = element.tagName.toLowerCase();
1537
+ const tagName = element.localName;
1529
1538
  return tagName == "meta" && element.getAttribute("name") == name;
1530
1539
  }
1531
1540
  function elementWithoutNonce(element) {
@@ -1550,7 +1559,20 @@ class PageSnapshot extends Snapshot {
1550
1559
  return new this(body, new HeadSnapshot(head));
1551
1560
  }
1552
1561
  clone() {
1553
- return new PageSnapshot(this.element.cloneNode(true), this.headSnapshot);
1562
+ const clonedElement = this.element.cloneNode(true);
1563
+ const selectElements = this.element.querySelectorAll("select");
1564
+ const clonedSelectElements = clonedElement.querySelectorAll("select");
1565
+ for (const [index, source] of selectElements.entries()) {
1566
+ const clone = clonedSelectElements[index];
1567
+ for (const option of clone.selectedOptions)
1568
+ option.selected = false;
1569
+ for (const option of source.selectedOptions)
1570
+ clone.options[option.index].selected = true;
1571
+ }
1572
+ for (const clonedPasswordInput of clonedElement.querySelectorAll('input[type="password"]')) {
1573
+ clonedPasswordInput.value = "";
1574
+ }
1575
+ return new PageSnapshot(clonedElement, this.headSnapshot);
1554
1576
  }
1555
1577
  get headElement() {
1556
1578
  return this.headSnapshot.element;
@@ -1600,6 +1622,7 @@ const defaultOptions = {
1600
1622
  updateHistory: true,
1601
1623
  shouldCacheSnapshot: true,
1602
1624
  acceptsStreamResponse: false,
1625
+ initiator: document.documentElement,
1603
1626
  };
1604
1627
  var SystemStatusCode;
1605
1628
  (function (SystemStatusCode) {
@@ -1609,7 +1632,6 @@ var SystemStatusCode;
1609
1632
  })(SystemStatusCode || (SystemStatusCode = {}));
1610
1633
  class Visit {
1611
1634
  constructor(delegate, location, restorationIdentifier, options = {}) {
1612
- this.identifier = uuid();
1613
1635
  this.timingMetrics = {};
1614
1636
  this.followedRedirect = false;
1615
1637
  this.historyChanged = false;
@@ -1622,7 +1644,7 @@ class Visit {
1622
1644
  this.location = location;
1623
1645
  this.restorationIdentifier = restorationIdentifier || uuid();
1624
1646
  this.promise = new Promise((resolve, reject) => (this.resolvingFunctions = { resolve, reject }));
1625
- const { action, historyChanged, referrer, snapshotHTML, response, visitCachedSnapshot, willRender, updateHistory, shouldCacheSnapshot, acceptsStreamResponse, } = Object.assign(Object.assign({}, defaultOptions), options);
1647
+ const { action, historyChanged, referrer, snapshotHTML, response, visitCachedSnapshot, willRender, updateHistory, shouldCacheSnapshot, acceptsStreamResponse, initiator, } = Object.assign(Object.assign({}, defaultOptions), options);
1626
1648
  this.action = action;
1627
1649
  this.historyChanged = historyChanged;
1628
1650
  this.referrer = referrer;
@@ -1635,6 +1657,7 @@ class Visit {
1635
1657
  this.scrolled = !willRender;
1636
1658
  this.shouldCacheSnapshot = shouldCacheSnapshot;
1637
1659
  this.acceptsStreamResponse = acceptsStreamResponse;
1660
+ this.initiator = initiator;
1638
1661
  }
1639
1662
  get adapter() {
1640
1663
  return this.delegate.adapter;
@@ -1702,7 +1725,7 @@ class Visit {
1702
1725
  this.simulateRequest();
1703
1726
  }
1704
1727
  else if (this.shouldIssueRequest() && !this.request) {
1705
- this.request = new FetchRequest(this, FetchMethod.get, this.location);
1728
+ this.request = new FetchRequest(this, FetchMethod.get, this.location, undefined, this.initiator);
1706
1729
  this.request.perform();
1707
1730
  }
1708
1731
  }
@@ -1798,7 +1821,6 @@ class Visit {
1798
1821
  if (this.redirectedToLocation && !this.followedRedirect && ((_a = this.response) === null || _a === void 0 ? void 0 : _a.redirected)) {
1799
1822
  this.adapter.visitProposedToLocation(this.redirectedToLocation, {
1800
1823
  action: "replace",
1801
- willRender: false,
1802
1824
  response: this.response,
1803
1825
  });
1804
1826
  this.followedRedirect = true;
@@ -2208,7 +2230,7 @@ class Navigator {
2208
2230
  this.delegate = delegate;
2209
2231
  }
2210
2232
  proposeVisit(location, options = {}) {
2211
- if (this.delegate.allowsVisitingLocationWithAction(location, options.action)) {
2233
+ if (this.delegate.allowsVisitingLocation(location, options)) {
2212
2234
  if (locationIsVisitable(location, this.view.snapshot.rootLocation)) {
2213
2235
  return this.delegate.visitProposedToLocation(location, options);
2214
2236
  }
@@ -2416,6 +2438,30 @@ class ScrollObserver {
2416
2438
  }
2417
2439
  }
2418
2440
 
2441
+ class StreamMessageRenderer {
2442
+ render({ fragment }) {
2443
+ Bardo.preservingPermanentElements(this, getPermanentElementMapForFragment(fragment), () => document.documentElement.appendChild(fragment));
2444
+ }
2445
+ enteringBardo(currentPermanentElement, newPermanentElement) {
2446
+ newPermanentElement.replaceWith(currentPermanentElement.cloneNode(true));
2447
+ }
2448
+ leavingBardo() { }
2449
+ }
2450
+ function getPermanentElementMapForFragment(fragment) {
2451
+ const permanentElementsInDocument = queryPermanentElementsAll(document.documentElement);
2452
+ const permanentElementMap = {};
2453
+ for (const permanentElementInDocument of permanentElementsInDocument) {
2454
+ const { id } = permanentElementInDocument;
2455
+ for (const streamElement of fragment.querySelectorAll("turbo-stream")) {
2456
+ const elementInStream = getPermanentElementById(streamElement.templateElement.content, id);
2457
+ if (elementInStream) {
2458
+ permanentElementMap[id] = [permanentElementInDocument, elementInStream];
2459
+ }
2460
+ }
2461
+ }
2462
+ return permanentElementMap;
2463
+ }
2464
+
2419
2465
  class StreamObserver {
2420
2466
  constructor(delegate) {
2421
2467
  this.sources = new Set();
@@ -2516,16 +2562,19 @@ class ErrorRenderer extends Renderer {
2516
2562
  }
2517
2563
 
2518
2564
  class PageRenderer extends Renderer {
2519
- static renderElement(currentElement, newElement) {
2565
+ static async renderElement(currentElement, newElement) {
2566
+ await nextEventLoopTick();
2520
2567
  if (document.body && newElement instanceof HTMLBodyElement) {
2521
- document.body.replaceWith(newElement);
2568
+ const currentBody = PageRenderer.getBodyElement(currentElement);
2569
+ const newBody = PageRenderer.getBodyElement(newElement);
2570
+ currentBody.replaceWith(newBody);
2522
2571
  }
2523
2572
  else {
2524
2573
  document.documentElement.appendChild(newElement);
2525
2574
  }
2526
2575
  }
2527
2576
  get shouldRender() {
2528
- return this.newSnapshot.isVisitable && this.trackedElementsAreIdentical;
2577
+ return this.newSnapshot.isVisitable && this.trackedElementsAreIdentical && this.bodyElementMatches;
2529
2578
  }
2530
2579
  get reloadReason() {
2531
2580
  if (!this.newSnapshot.isVisitable) {
@@ -2538,6 +2587,11 @@ class PageRenderer extends Renderer {
2538
2587
  reason: "tracked_element_mismatch",
2539
2588
  };
2540
2589
  }
2590
+ if (!this.bodyElementMatches) {
2591
+ return {
2592
+ reason: "body_element_mismatch",
2593
+ };
2594
+ }
2541
2595
  }
2542
2596
  async prepareToRender() {
2543
2597
  await this.mergeHead();
@@ -2578,6 +2632,16 @@ class PageRenderer extends Renderer {
2578
2632
  get trackedElementsAreIdentical() {
2579
2633
  return this.currentHeadSnapshot.trackedElementSignature == this.newHeadSnapshot.trackedElementSignature;
2580
2634
  }
2635
+ get bodyElementMatches() {
2636
+ return PageRenderer.getBodyElement(this.newElement) !== null;
2637
+ }
2638
+ static get bodySelector() {
2639
+ const bodyId = getBodyElementId();
2640
+ return bodyId ? `#${bodyId}` : "body";
2641
+ }
2642
+ static getBodyElement(element) {
2643
+ return element.querySelector(this.bodySelector) || element;
2644
+ }
2581
2645
  async copyNewHeadStylesheetElements() {
2582
2646
  const loadingElements = [];
2583
2647
  for (const element of this.newHeadStylesheetElements) {
@@ -2776,6 +2840,7 @@ class Session {
2776
2840
  this.streamObserver = new StreamObserver(this);
2777
2841
  this.formLinkClickObserver = new FormLinkClickObserver(this, document.documentElement);
2778
2842
  this.frameRedirector = new FrameRedirector(this, document.documentElement);
2843
+ this.streamMessageRenderer = new StreamMessageRenderer();
2779
2844
  this.drive = true;
2780
2845
  this.enabled = true;
2781
2846
  this.progressBarDelay = 500;
@@ -2819,7 +2884,7 @@ class Session {
2819
2884
  this.adapter = adapter;
2820
2885
  }
2821
2886
  visit(location, options = {}) {
2822
- const frameElement = document.getElementById(options.frame || "");
2887
+ const frameElement = options.frame ? document.getElementById(options.frame) : null;
2823
2888
  if (frameElement instanceof FrameElement) {
2824
2889
  frameElement.src = location.toString();
2825
2890
  return frameElement.loaded;
@@ -2835,7 +2900,7 @@ class Session {
2835
2900
  this.streamObserver.disconnectStreamSource(source);
2836
2901
  }
2837
2902
  renderStreamMessage(message) {
2838
- document.documentElement.appendChild(StreamMessage.wrap(message).fragment);
2903
+ this.streamMessageRenderer.render(StreamMessage.wrap(message));
2839
2904
  }
2840
2905
  clearCache() {
2841
2906
  this.view.clearSnapshotCache();
@@ -2880,22 +2945,27 @@ class Session {
2880
2945
  followedLinkToLocation(link, location) {
2881
2946
  const action = this.getActionForLink(link);
2882
2947
  const acceptsStreamResponse = link.hasAttribute("data-turbo-stream");
2883
- this.visit(location.href, { action, acceptsStreamResponse });
2948
+ this.visit(location.href, { action, acceptsStreamResponse, initiator: link });
2884
2949
  }
2885
- allowsVisitingLocationWithAction(location, action) {
2886
- return this.locationWithActionIsSamePage(location, action) || this.applicationAllowsVisitingLocation(location);
2950
+ allowsVisitingLocation(location, options = {}) {
2951
+ return (this.locationWithActionIsSamePage(location, options.action) ||
2952
+ this.applicationAllowsVisitingLocation(location, options));
2887
2953
  }
2888
2954
  visitProposedToLocation(location, options) {
2889
2955
  extendURLWithDeprecatedProperties(location);
2890
2956
  return this.adapter.visitProposedToLocation(location, options);
2891
2957
  }
2892
2958
  visitStarted(visit) {
2959
+ if (!visit.acceptsStreamResponse) {
2960
+ markAsBusy(document.documentElement);
2961
+ }
2893
2962
  extendURLWithDeprecatedProperties(visit.location);
2894
2963
  if (!visit.silent) {
2895
- this.notifyApplicationAfterVisitingLocation(visit.location, visit.action);
2964
+ this.notifyApplicationAfterVisitingLocation(visit.location, visit.action, visit.initiator);
2896
2965
  }
2897
2966
  }
2898
2967
  visitCompleted(visit) {
2968
+ clearBusyState(document.documentElement);
2899
2969
  this.notifyApplicationAfterPageLoad(visit.getTimingMetrics());
2900
2970
  }
2901
2971
  locationWithActionIsSamePage(location, action) {
@@ -2955,16 +3025,12 @@ class Session {
2955
3025
  frameRendered(fetchResponse, frame) {
2956
3026
  this.notifyApplicationAfterFrameRender(fetchResponse, frame);
2957
3027
  }
2958
- frameMissing(frame, fetchResponse) {
2959
- console.warn(`Completing full-page visit as matching frame for #${frame.id} was missing from the response`);
2960
- return this.visit(fetchResponse.location);
2961
- }
2962
3028
  applicationAllowsFollowingLinkToLocation(link, location, ev) {
2963
3029
  const event = this.notifyApplicationAfterClickingLinkToLocation(link, location, ev);
2964
3030
  return !event.defaultPrevented;
2965
3031
  }
2966
- applicationAllowsVisitingLocation(location) {
2967
- const event = this.notifyApplicationBeforeVisitingLocation(location);
3032
+ applicationAllowsVisitingLocation(location, options = {}) {
3033
+ const event = this.notifyApplicationBeforeVisitingLocation(location, options.initiator);
2968
3034
  return !event.defaultPrevented;
2969
3035
  }
2970
3036
  notifyApplicationAfterClickingLinkToLocation(link, location, event) {
@@ -2974,15 +3040,18 @@ class Session {
2974
3040
  cancelable: true,
2975
3041
  });
2976
3042
  }
2977
- notifyApplicationBeforeVisitingLocation(location) {
3043
+ notifyApplicationBeforeVisitingLocation(location, element) {
2978
3044
  return dispatch("turbo:before-visit", {
3045
+ target: element,
2979
3046
  detail: { url: location.href },
2980
3047
  cancelable: true,
2981
3048
  });
2982
3049
  }
2983
- notifyApplicationAfterVisitingLocation(location, action) {
2984
- markAsBusy(document.documentElement);
2985
- return dispatch("turbo:visit", { detail: { url: location.href, action } });
3050
+ notifyApplicationAfterVisitingLocation(location, action, element) {
3051
+ return dispatch("turbo:visit", {
3052
+ target: element,
3053
+ detail: { url: location.href, action },
3054
+ });
2986
3055
  }
2987
3056
  notifyApplicationBeforeCachingSnapshot() {
2988
3057
  return dispatch("turbo:before-cache");
@@ -2997,7 +3066,6 @@ class Session {
2997
3066
  return dispatch("turbo:render");
2998
3067
  }
2999
3068
  notifyApplicationAfterPageLoad(timing = {}) {
3000
- clearBusyState(document.documentElement);
3001
3069
  return dispatch("turbo:load", {
3002
3070
  detail: { url: this.location.href, timing },
3003
3071
  });
@@ -3279,8 +3347,9 @@ class FrameController {
3279
3347
  session.frameLoaded(this.element);
3280
3348
  this.fetchResponseLoaded(fetchResponse);
3281
3349
  }
3282
- else if (this.sessionWillHandleMissingFrame(fetchResponse)) {
3283
- await session.frameMissing(this.element, fetchResponse);
3350
+ else if (this.willHandleFrameMissingFromResponse(fetchResponse)) {
3351
+ console.warn(`A matching frame for #${this.element.id} was missing from the response, transforming into full-page Visit.`);
3352
+ this.visitResponse(fetchResponse.response);
3284
3353
  }
3285
3354
  }
3286
3355
  }
@@ -3338,8 +3407,9 @@ class FrameController {
3338
3407
  await this.loadResponse(response);
3339
3408
  this.resolveVisitPromise();
3340
3409
  }
3341
- requestFailedWithResponse(request, response) {
3410
+ async requestFailedWithResponse(request, response) {
3342
3411
  console.error(response);
3412
+ await this.loadResponse(response);
3343
3413
  this.resolveVisitPromise();
3344
3414
  }
3345
3415
  requestErrored(request, error) {
@@ -3441,15 +3511,30 @@ class FrameController {
3441
3511
  session.history.update(method, expandURL(this.frame.src || ""), this.restorationIdentifier);
3442
3512
  }
3443
3513
  }
3444
- sessionWillHandleMissingFrame(fetchResponse) {
3514
+ willHandleFrameMissingFromResponse(fetchResponse) {
3445
3515
  this.element.setAttribute("complete", "");
3516
+ const response = fetchResponse.response;
3517
+ const visit = async (url, options = {}) => {
3518
+ if (url instanceof Response) {
3519
+ this.visitResponse(url);
3520
+ }
3521
+ else {
3522
+ session.visit(url, options);
3523
+ }
3524
+ };
3446
3525
  const event = dispatch("turbo:frame-missing", {
3447
3526
  target: this.element,
3448
- detail: { fetchResponse },
3527
+ detail: { response, visit },
3449
3528
  cancelable: true,
3450
3529
  });
3451
3530
  return !event.defaultPrevented;
3452
3531
  }
3532
+ async visitResponse(response) {
3533
+ const wrapped = new FetchResponse(response);
3534
+ const responseHTML = await wrapped.responseHTML;
3535
+ const { location, redirected, statusCode } = wrapped;
3536
+ return session.visit(location, { response: { redirected, statusCode, responseHTML } });
3537
+ }
3453
3538
  findFrameElement(element, submitter) {
3454
3539
  var _a;
3455
3540
  const id = getAttribute("data-turbo-frame", submitter, element) || this.element.getAttribute("target");
@@ -3585,6 +3670,9 @@ function activateElement(element, currentURL) {
3585
3670
  }
3586
3671
 
3587
3672
  class StreamElement extends HTMLElement {
3673
+ static async renderElement(newElement) {
3674
+ await newElement.performAction();
3675
+ }
3588
3676
  async connectedCallback() {
3589
3677
  try {
3590
3678
  await this.render();
@@ -3599,9 +3687,10 @@ class StreamElement extends HTMLElement {
3599
3687
  async render() {
3600
3688
  var _a;
3601
3689
  return ((_a = this.renderPromise) !== null && _a !== void 0 ? _a : (this.renderPromise = (async () => {
3602
- if (this.dispatchEvent(this.beforeRenderEvent)) {
3690
+ const event = this.beforeRenderEvent;
3691
+ if (this.dispatchEvent(event)) {
3603
3692
  await nextAnimationFrame();
3604
- this.performAction();
3693
+ await event.detail.render(this);
3605
3694
  }
3606
3695
  })()));
3607
3696
  }
@@ -3645,7 +3734,12 @@ class StreamElement extends HTMLElement {
3645
3734
  return this.templateElement.content.cloneNode(true);
3646
3735
  }
3647
3736
  get templateElement() {
3648
- if (this.firstElementChild instanceof HTMLTemplateElement) {
3737
+ if (this.firstElementChild === null) {
3738
+ const template = this.ownerDocument.createElement("template");
3739
+ this.appendChild(template);
3740
+ return template;
3741
+ }
3742
+ else if (this.firstElementChild instanceof HTMLTemplateElement) {
3649
3743
  return this.firstElementChild;
3650
3744
  }
3651
3745
  this.raise("first child element must be a <template> element");
@@ -3670,7 +3764,7 @@ class StreamElement extends HTMLElement {
3670
3764
  return new CustomEvent("turbo:before-stream-render", {
3671
3765
  bubbles: true,
3672
3766
  cancelable: true,
3673
- detail: { newStream: this },
3767
+ detail: { newStream: this, render: StreamElement.renderElement },
3674
3768
  });
3675
3769
  }
3676
3770
  get targetElementsById() {
@@ -3752,4 +3846,4 @@ if (customElements.get("turbo-stream-source") === undefined) {
3752
3846
  window.Turbo = Turbo;
3753
3847
  start();
3754
3848
 
3755
- export { FrameRenderer, PageRenderer, PageSnapshot, StreamActions, cache, clearCache, connectStreamSource, disconnectStreamSource, navigator$1 as navigator, registerAdapter, renderStreamMessage, session, setConfirmMethod, setFormMode, setProgressBarDelay, start, visit };
3849
+ export { FrameElement, FrameLoadingStyle, FrameRenderer, PageRenderer, PageSnapshot, StreamActions, StreamElement, StreamSourceElement, cache, clearCache, connectStreamSource, disconnectStreamSource, navigator$1 as navigator, registerAdapter, renderStreamMessage, session, setConfirmMethod, setFormMode, setProgressBarDelay, start, visit };