phoenix_live_view 1.1.30 → 1.1.32

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.
@@ -370,7 +370,9 @@ const DOM = {
370
370
  // we also clear the throttle timeout to prevent the callback
371
371
  // from being called again after the timeout fires
372
372
  clearTimeout(this.private(el, THROTTLED));
373
- this.triggerCycle(el, DEBOUNCE_TRIGGER);
373
+ if (asyncFilter()) {
374
+ this.triggerCycle(el, DEBOUNCE_TRIGGER);
375
+ }
374
376
  });
375
377
  }
376
378
  }
@@ -711,7 +711,7 @@ export default class DOMPatch {
711
711
  transitionPendingRemoves() {
712
712
  const { pendingRemoves, liveSocket } = this;
713
713
  if (pendingRemoves.length > 0) {
714
- liveSocket.transitionRemoves(pendingRemoves, () => {
714
+ liveSocket.transitionRemoves(pendingRemoves, this.view, () => {
715
715
  pendingRemoves.forEach((el) => {
716
716
  const child = DOM.firstPhxChild(el);
717
717
  if (child) {
@@ -30,7 +30,7 @@ export default class EntryUploader {
30
30
  this.uploadChannel
31
31
  .join()
32
32
  .receive("ok", (_data) => this.readNextChunk())
33
- .receive("error", (reason) => this.error(reason));
33
+ .receive("error", ({ reason }) => this.error(reason));
34
34
  }
35
35
 
36
36
  isDone() {
@@ -1,5 +1,6 @@
1
1
  import JS from "./js";
2
2
  import LiveSocket from "./live_socket";
3
+ import { ensureSameOrigin } from "./utils";
3
4
 
4
5
  type Transition = string | string[];
5
6
 
@@ -346,6 +347,7 @@ export default (
346
347
  });
347
348
  },
348
349
  navigate(href, opts = {}) {
350
+ ensureSameOrigin(href, "navigate");
349
351
  const customEvent = new CustomEvent("phx:exec");
350
352
  liveSocket.historyRedirect(
351
353
  customEvent,
@@ -356,6 +358,7 @@ export default (
356
358
  );
357
359
  },
358
360
  patch(href, opts = {}) {
361
+ ensureSameOrigin(href, "patch");
359
362
  const customEvent = new CustomEvent("phx:exec");
360
363
  liveSocket.pushHistoryPatch(
361
364
  customEvent,
@@ -472,12 +472,15 @@ export default class LiveSocket {
472
472
  ).filter((el) => !DOM.isChildOfAny(el, stickies));
473
473
 
474
474
  const newMainEl = DOM.cloneNode(this.outgoingMainEl, "");
475
- this.main.showLoader(this.loaderTimeout);
476
- this.main.destroy();
475
+ const oldMainView = this.main;
476
+ oldMainView.showLoader(this.loaderTimeout);
477
+ oldMainView.destroy();
477
478
 
478
479
  this.main = this.newRootView(newMainEl, flash, liveReferer);
479
480
  this.main.setRedirect(href);
480
- this.transitionRemoves(removeEls);
481
+ // the old view is destroyed at this point; pass it explicitly so the
482
+ // phx-remove commands execute in the context of the outgoing view
483
+ this.transitionRemoves(removeEls, oldMainView);
481
484
  this.main.join((joinCount, onDone) => {
482
485
  if (joinCount === 1 && this.commitPendingLink(linkRef)) {
483
486
  this.requestDOMUpdate(() => {
@@ -493,7 +496,7 @@ export default class LiveSocket {
493
496
  });
494
497
  }
495
498
 
496
- transitionRemoves(elements, callback) {
499
+ transitionRemoves(elements, view, callback) {
497
500
  const removeAttr = this.binding("remove");
498
501
  const silenceEvents = (e) => {
499
502
  e.preventDefault();
@@ -505,7 +508,8 @@ export default class LiveSocket {
505
508
  for (const event of this.boundEventNames) {
506
509
  el.addEventListener(event, silenceEvents, true);
507
510
  }
508
- this.execJS(el, el.getAttribute(removeAttr), "remove");
511
+ const e = new CustomEvent("phx:exec", { detail: { sourceElement: el } });
512
+ JS.exec(e, "remove", el.getAttribute(removeAttr), view, el);
509
513
  });
510
514
  // remove the silenced listeners when transitions are done incase the element is re-used
511
515
  // and call caller's callback as soon as we are done with transitions
@@ -533,9 +537,12 @@ export default class LiveSocket {
533
537
  let view;
534
538
  const viewEl = DOM.closestViewEl(childEl);
535
539
  if (viewEl) {
536
- // it can happen that we find a view that is already destroyed;
537
- // in that case we DO NOT want to fallback to the main element
538
- view = this.getViewByEl(viewEl);
540
+ // resolve the view by element identity instead of id; during live
541
+ // navigation the new view is registered under the same id while the
542
+ // old DOM is still attached, and events from the old DOM must not be
543
+ // routed to the new view. A destroyed view removes its element binding,
544
+ // in which case we DO NOT want to fallback to the main element
545
+ view = DOM.private(viewEl, "view");
539
546
  } else {
540
547
  if (!childEl.isConnected) {
541
548
  // if the element is not part of the DOM any more
@@ -4,6 +4,27 @@ import EntryUploader from "./entry_uploader";
4
4
 
5
5
  export const logError = (msg, obj) => console.error && console.error(msg, obj);
6
6
 
7
+ // Live navigation can only stay within the current origin, as it joins the
8
+ // target over the existing socket. A full URL to a different origin (or a
9
+ // non-http(s) scheme, which resolves to an opaque "null" origin) is a
10
+ // programming error, so we fail loudly instead of attempting a broken join.
11
+ export const ensureSameOrigin = (href, kind) => {
12
+ let url;
13
+ try {
14
+ url = new URL(href, window.location.href);
15
+ } catch {
16
+ throw new Error(
17
+ `expected ${kind} destination to be a valid URL, got: ${href}`,
18
+ );
19
+ }
20
+ if (url.origin !== window.location.origin) {
21
+ throw new Error(
22
+ `cannot ${kind} to "${href}" because its origin does not match the ` +
23
+ `current origin "${window.location.origin}". Use window.location directly for cross-origin navigation.`,
24
+ );
25
+ }
26
+ };
27
+
7
28
  export const isCid = (cid) => {
8
29
  const type = typeof cid;
9
30
  return type === "number" || (type === "string" && /^(0|[1-9]\d*)$/.test(cid));
@@ -443,6 +443,7 @@ export default class View {
443
443
  if (container) {
444
444
  const [tag, attrs] = container;
445
445
  this.el = DOM.replaceRootContainer(this.el, tag, attrs);
446
+ DOM.putPrivate(this.el, "view", this);
446
447
  }
447
448
  this.childJoins = 0;
448
449
  this.joinPending = true;
@@ -548,6 +549,7 @@ export default class View {
548
549
 
549
550
  attachTrueDocEl() {
550
551
  this.el = DOM.byId(this.id);
552
+ DOM.putPrivate(this.el, "view", this);
551
553
  this.el.setAttribute(PHX_ROOT_ID, this.root.id);
552
554
  }
553
555
 
@@ -770,6 +772,7 @@ export default class View {
770
772
  });
771
773
 
772
774
  // because we work with a template element, we must manually copy the attributes
775
+ // and bind the template root to this view,
773
776
  // otherwise the owner / target helpers don't work properly
774
777
  const rootEl = template.content.firstElementChild;
775
778
  rootEl.id = this.id;
@@ -777,6 +780,7 @@ export default class View {
777
780
  rootEl.setAttribute(PHX_SESSION, this.getSession());
778
781
  rootEl.setAttribute(PHX_STATIC, this.getStatic());
779
782
  rootEl.setAttribute(PHX_PARENT_ID, this.parent ? this.parent.id : null);
783
+ DOM.putPrivate(rootEl, "view", this);
780
784
 
781
785
  // we go over all form elements in the new HTML for the LV
782
786
  // and look for old forms in the `formsForRecovery` object;
@@ -73,38 +73,45 @@ export interface HookInterface<E extends HTMLElement = HTMLElement> {
73
73
  js(): HookJSCommands;
74
74
 
75
75
  /**
76
- * Pushes an event to the server.
76
+ * Pushes an event to the server and invokes a callback with the server's reply.
77
77
  *
78
- * @param event - The event name.
79
- * @param [payload] - The payload to send to the server. Defaults to an empty object.
80
- * @param [onReply] - A callback to handle the server's reply.
78
+ * **Note:** this version silently ignores push errors.
79
+ * Use the {@link pushEvent | promise-returning version} to handle errors.
81
80
  *
82
- * When onReply is not provided, the method returns a Promise that
83
- * When onReply is provided, the method returns void.
81
+ * @param event - The event name.
82
+ * @param payload - The payload to send to the server. Defaults to an empty object.
83
+ * @param onReply - A callback to handle the server's reply.
84
84
  */
85
85
  pushEvent(event: string, payload: any, onReply: OnReply): void;
86
+ /**
87
+ * Pushes an event to the server and returns a Promise that resolves with the server's reply.
88
+ *
89
+ * The promise will be rejected in case of errors
90
+ * such as a disconnected state, timeout, or the server rejecting the event.
91
+ *
92
+ * @param event - The event name.
93
+ * @param [payload] - The payload to send to the server. Defaults to an empty object.
94
+ * @returns A promise that fulfills or rejects with the server's reply.
95
+ */
86
96
  pushEvent(event: string, payload?: any): Promise<any>;
87
97
 
88
98
  /**
89
- * Pushed a targeted event to the server.
99
+ * Pushes a targeted event to the server and invokes a callback with the server's reply.
90
100
  *
91
101
  * It sends the event to the LiveComponent or LiveView the `selectorOrTarget` is defined in,
92
102
  * where its value can be either a query selector, an actual DOM element, or a CID (component id)
93
103
  * returned by the `@myself` assign.
94
104
  *
95
- * If the query selector returns more than one element it will send the event to all of them,
96
- * even if all the elements are in the same LiveComponent or LiveView. Because of this,
97
- * if no callback is passed, a promise is returned that matches the return value of
98
- * [`Promise.allSettled()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled#return_value).
99
- * Individual fulfilled values are of the format `{ reply, ref }`, where `reply` is the server's reply.
105
+ * If the selector matches multiple elements, the event is sent to all of them,
106
+ * even if they belong to the same LiveComponent or LiveView.
107
+ *
108
+ * **Note:** this version silently ignores push errors.
109
+ * Use the {@link pushEventTo | promise-returning version} to handle errors.
100
110
  *
101
111
  * @param selectorOrTarget - The selector, element, or CID to target.
102
112
  * @param event - The event name.
103
- * @param [payload] - The payload to send to the server. Defaults to an empty object.
104
- * @param [onReply] - A callback to handle the server's reply.
105
- *
106
- * When onReply is not provided, the method returns a Promise.
107
- * When onReply is provided, the method returns void.
113
+ * @param payload - The payload to send to the server. Defaults to an empty object.
114
+ * @param onReply - A callback to handle the server's reply.
108
115
  */
109
116
  pushEventTo(
110
117
  selectorOrTarget: PhxTarget,
@@ -112,6 +119,27 @@ export interface HookInterface<E extends HTMLElement = HTMLElement> {
112
119
  payload: object,
113
120
  onReply: OnReply,
114
121
  ): void;
122
+ /**
123
+ * Pushes a targeted event to the server and returns a Promise that resolves with the server's reply.
124
+ *
125
+ * It sends the event to the LiveComponent or LiveView the `selectorOrTarget` is defined in,
126
+ * where its value can be either a query selector, an actual DOM element, or a CID (component id)
127
+ * returned by the `@myself` assign.
128
+ *
129
+ * If the selector matches multiple elements, the event is sent to all of them,
130
+ * even if they belong to the same LiveComponent or LiveView.
131
+ * Because of this, it returns a single promise that matches the return value of
132
+ * [`Promise.allSettled()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled#return_value).
133
+ * Individual fulfilled values are of the format `{ reply, ref }`, where `reply` is the server's reply.
134
+ *
135
+ * The individual promises will be rejected in case of errors
136
+ * such as a disconnected state, timeout, or the server rejecting the event.
137
+ *
138
+ * @param selectorOrTarget - The selector, element, or CID to target.
139
+ * @param event - The event name.
140
+ * @param [payload] - The payload to send to the server. Defaults to an empty object.
141
+ * @returns A promise that resolves when the event has been handled by all targets.
142
+ */
115
143
  pushEventTo(
116
144
  selectorOrTarget: PhxTarget,
117
145
  event: string,
@@ -89,7 +89,7 @@ export default class LiveSocket {
89
89
  joinRootViews(): boolean;
90
90
  redirect(to: any, flash: any, reloadToken: any): void;
91
91
  replaceMain(href: any, flash: any, callback?: any, linkRef?: number): void;
92
- transitionRemoves(elements: any, callback: any): void;
92
+ transitionRemoves(elements: any, view: any, callback: any): void;
93
93
  isPhxView(el: any): boolean;
94
94
  newRootView(el: any, flash: any, liveReferer: any): View;
95
95
  owner(childEl: any, callback: any): any;
@@ -1,6 +1,7 @@
1
1
  export function detectDuplicateIds(): void;
2
2
  export function detectInvalidStreamInserts(inserts: any): void;
3
3
  export function logError(msg: any, obj: any): void;
4
+ export function ensureSameOrigin(href: any, kind: any): void;
4
5
  export function isCid(cid: any): boolean;
5
6
  export function debug(view: any, kind: any, msg: any, obj: any): void;
6
7
  export function closure(val: any): any;
@@ -59,39 +59,67 @@ export interface HookInterface<E extends HTMLElement = HTMLElement> {
59
59
  */
60
60
  js(): HookJSCommands;
61
61
  /**
62
- * Pushes an event to the server.
62
+ * Pushes an event to the server and invokes a callback with the server's reply.
63
63
  *
64
- * @param event - The event name.
65
- * @param [payload] - The payload to send to the server. Defaults to an empty object.
66
- * @param [onReply] - A callback to handle the server's reply.
64
+ * **Note:** this version silently ignores push errors.
65
+ * Use the {@link pushEvent | promise-returning version} to handle errors.
67
66
  *
68
- * When onReply is not provided, the method returns a Promise that
69
- * When onReply is provided, the method returns void.
67
+ * @param event - The event name.
68
+ * @param payload - The payload to send to the server. Defaults to an empty object.
69
+ * @param onReply - A callback to handle the server's reply.
70
70
  */
71
71
  pushEvent(event: string, payload: any, onReply: OnReply): void;
72
+ /**
73
+ * Pushes an event to the server and returns a Promise that resolves with the server's reply.
74
+ *
75
+ * The promise will be rejected in case of errors
76
+ * such as a disconnected state, timeout, or the server rejecting the event.
77
+ *
78
+ * @param event - The event name.
79
+ * @param [payload] - The payload to send to the server. Defaults to an empty object.
80
+ * @returns A promise that fulfills or rejects with the server's reply.
81
+ */
72
82
  pushEvent(event: string, payload?: any): Promise<any>;
73
83
  /**
74
- * Pushed a targeted event to the server.
84
+ * Pushes a targeted event to the server and invokes a callback with the server's reply.
85
+ *
86
+ * It sends the event to the LiveComponent or LiveView the `selectorOrTarget` is defined in,
87
+ * where its value can be either a query selector, an actual DOM element, or a CID (component id)
88
+ * returned by the `@myself` assign.
89
+ *
90
+ * If the selector matches multiple elements, the event is sent to all of them,
91
+ * even if they belong to the same LiveComponent or LiveView.
92
+ *
93
+ * **Note:** this version silently ignores push errors.
94
+ * Use the {@link pushEventTo | promise-returning version} to handle errors.
95
+ *
96
+ * @param selectorOrTarget - The selector, element, or CID to target.
97
+ * @param event - The event name.
98
+ * @param payload - The payload to send to the server. Defaults to an empty object.
99
+ * @param onReply - A callback to handle the server's reply.
100
+ */
101
+ pushEventTo(selectorOrTarget: PhxTarget, event: string, payload: object, onReply: OnReply): void;
102
+ /**
103
+ * Pushes a targeted event to the server and returns a Promise that resolves with the server's reply.
75
104
  *
76
105
  * It sends the event to the LiveComponent or LiveView the `selectorOrTarget` is defined in,
77
106
  * where its value can be either a query selector, an actual DOM element, or a CID (component id)
78
107
  * returned by the `@myself` assign.
79
108
  *
80
- * If the query selector returns more than one element it will send the event to all of them,
81
- * even if all the elements are in the same LiveComponent or LiveView. Because of this,
82
- * if no callback is passed, a promise is returned that matches the return value of
109
+ * If the selector matches multiple elements, the event is sent to all of them,
110
+ * even if they belong to the same LiveComponent or LiveView.
111
+ * Because of this, it returns a single promise that matches the return value of
83
112
  * [`Promise.allSettled()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled#return_value).
84
113
  * Individual fulfilled values are of the format `{ reply, ref }`, where `reply` is the server's reply.
85
114
  *
115
+ * The individual promises will be rejected in case of errors
116
+ * such as a disconnected state, timeout, or the server rejecting the event.
117
+ *
86
118
  * @param selectorOrTarget - The selector, element, or CID to target.
87
119
  * @param event - The event name.
88
120
  * @param [payload] - The payload to send to the server. Defaults to an empty object.
89
- * @param [onReply] - A callback to handle the server's reply.
90
- *
91
- * When onReply is not provided, the method returns a Promise.
92
- * When onReply is provided, the method returns void.
121
+ * @returns A promise that resolves when the event has been handled by all targets.
93
122
  */
94
- pushEventTo(selectorOrTarget: PhxTarget, event: string, payload: object, onReply: OnReply): void;
95
123
  pushEventTo(selectorOrTarget: PhxTarget, event: string, payload?: object): Promise<PromiseSettledResult<{
96
124
  reply: any;
97
125
  ref: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phoenix_live_view",
3
- "version": "1.1.30",
3
+ "version": "1.1.32",
4
4
  "description": "The Phoenix LiveView JavaScript client.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -37,7 +37,7 @@
37
37
  "@babel/preset-env": "7.27.2",
38
38
  "@babel/preset-typescript": "^7.27.1",
39
39
  "@eslint/js": "^9.29.0",
40
- "@playwright/test": "^1.56.1",
40
+ "@playwright/test": "^1.60.0",
41
41
  "@types/jest": "^30.0.0",
42
42
  "@types/phoenix": "^1.6.6",
43
43
  "css.escape": "^1.5.1",
@@ -172,7 +172,7 @@ var EntryUploader = class {
172
172
  }
173
173
  upload() {
174
174
  this.uploadChannel.onError((reason) => this.error(reason));
175
- this.uploadChannel.join().receive("ok", (_data) => this.readNextChunk()).receive("error", (reason) => this.error(reason));
175
+ this.uploadChannel.join().receive("ok", (_data) => this.readNextChunk()).receive("error", ({ reason }) => this.error(reason));
176
176
  }
177
177
  isDone() {
178
178
  return this.offset >= this.entry.file.size;
@@ -215,6 +215,21 @@ var EntryUploader = class {
215
215
 
216
216
  // js/phoenix_live_view/utils.js
217
217
  var logError = (msg, obj) => console.error && console.error(msg, obj);
218
+ var ensureSameOrigin = (href, kind) => {
219
+ let url;
220
+ try {
221
+ url = new URL(href, window.location.href);
222
+ } catch {
223
+ throw new Error(
224
+ `expected ${kind} destination to be a valid URL, got: ${href}`
225
+ );
226
+ }
227
+ if (url.origin !== window.location.origin) {
228
+ throw new Error(
229
+ `cannot ${kind} to "${href}" because its origin does not match the current origin "${window.location.origin}". Use window.location directly for cross-origin navigation.`
230
+ );
231
+ }
232
+ };
218
233
  var isCid = (cid) => {
219
234
  const type = typeof cid;
220
235
  return type === "number" || type === "string" && /^(0|[1-9]\d*)$/.test(cid);
@@ -635,7 +650,9 @@ var DOM = {
635
650
  if (this.once(el, "bind-debounce")) {
636
651
  el.addEventListener("blur", () => {
637
652
  clearTimeout(this.private(el, THROTTLED));
638
- this.triggerCycle(el, DEBOUNCE_TRIGGER);
653
+ if (asyncFilter()) {
654
+ this.triggerCycle(el, DEBOUNCE_TRIGGER);
655
+ }
639
656
  });
640
657
  }
641
658
  }
@@ -2741,7 +2758,7 @@ var DOMPatch = class {
2741
2758
  transitionPendingRemoves() {
2742
2759
  const { pendingRemoves, liveSocket } = this;
2743
2760
  if (pendingRemoves.length > 0) {
2744
- liveSocket.transitionRemoves(pendingRemoves, () => {
2761
+ liveSocket.transitionRemoves(pendingRemoves, this.view, () => {
2745
2762
  pendingRemoves.forEach((el) => {
2746
2763
  const child = dom_default.firstPhxChild(el);
2747
2764
  if (child) {
@@ -3882,6 +3899,7 @@ var js_commands_default = (liveSocket, eventType) => {
3882
3899
  });
3883
3900
  },
3884
3901
  navigate(href, opts = {}) {
3902
+ ensureSameOrigin(href, "navigate");
3885
3903
  const customEvent = new CustomEvent("phx:exec");
3886
3904
  liveSocket.historyRedirect(
3887
3905
  customEvent,
@@ -3892,6 +3910,7 @@ var js_commands_default = (liveSocket, eventType) => {
3892
3910
  );
3893
3911
  },
3894
3912
  patch(href, opts = {}) {
3913
+ ensureSameOrigin(href, "patch");
3895
3914
  const customEvent = new CustomEvent("phx:exec");
3896
3915
  liveSocket.pushHistoryPatch(
3897
3916
  customEvent,
@@ -4431,6 +4450,7 @@ var View = class _View {
4431
4450
  if (container) {
4432
4451
  const [tag, attrs] = container;
4433
4452
  this.el = dom_default.replaceRootContainer(this.el, tag, attrs);
4453
+ dom_default.putPrivate(this.el, "view", this);
4434
4454
  }
4435
4455
  this.childJoins = 0;
4436
4456
  this.joinPending = true;
@@ -4513,6 +4533,7 @@ var View = class _View {
4513
4533
  }
4514
4534
  attachTrueDocEl() {
4515
4535
  this.el = dom_default.byId(this.id);
4536
+ dom_default.putPrivate(this.el, "view", this);
4516
4537
  this.el.setAttribute(PHX_ROOT_ID, this.root.id);
4517
4538
  }
4518
4539
  // this is invoked for dead and live views, so we must filter by
@@ -4696,6 +4717,7 @@ var View = class _View {
4696
4717
  rootEl.setAttribute(PHX_SESSION, this.getSession());
4697
4718
  rootEl.setAttribute(PHX_STATIC, this.getStatic());
4698
4719
  rootEl.setAttribute(PHX_PARENT_ID, this.parent ? this.parent.id : null);
4720
+ dom_default.putPrivate(rootEl, "view", this);
4699
4721
  const formsToRecover = (
4700
4722
  // we go over all forms in the new DOM; because this is only the HTML for the current
4701
4723
  // view, we can be sure that all forms are owned by this view:
@@ -5976,7 +5998,7 @@ var LiveSocket = class {
5976
5998
  }
5977
5999
  // public
5978
6000
  version() {
5979
- return "1.1.30";
6001
+ return "1.1.32";
5980
6002
  }
5981
6003
  isProfileEnabled() {
5982
6004
  return this.sessionStorage.getItem(PHX_LV_PROFILE) === "true";
@@ -6255,11 +6277,12 @@ var LiveSocket = class {
6255
6277
  `[${this.binding("remove")}]`
6256
6278
  ).filter((el) => !dom_default.isChildOfAny(el, stickies));
6257
6279
  const newMainEl = dom_default.cloneNode(this.outgoingMainEl, "");
6258
- this.main.showLoader(this.loaderTimeout);
6259
- this.main.destroy();
6280
+ const oldMainView = this.main;
6281
+ oldMainView.showLoader(this.loaderTimeout);
6282
+ oldMainView.destroy();
6260
6283
  this.main = this.newRootView(newMainEl, flash, liveReferer);
6261
6284
  this.main.setRedirect(href);
6262
- this.transitionRemoves(removeEls);
6285
+ this.transitionRemoves(removeEls, oldMainView);
6263
6286
  this.main.join((joinCount, onDone) => {
6264
6287
  if (joinCount === 1 && this.commitPendingLink(linkRef)) {
6265
6288
  this.requestDOMUpdate(() => {
@@ -6273,7 +6296,7 @@ var LiveSocket = class {
6273
6296
  }
6274
6297
  });
6275
6298
  }
6276
- transitionRemoves(elements, callback) {
6299
+ transitionRemoves(elements, view, callback) {
6277
6300
  const removeAttr = this.binding("remove");
6278
6301
  const silenceEvents = (e) => {
6279
6302
  e.preventDefault();
@@ -6283,7 +6306,8 @@ var LiveSocket = class {
6283
6306
  for (const event of this.boundEventNames) {
6284
6307
  el.addEventListener(event, silenceEvents, true);
6285
6308
  }
6286
- this.execJS(el, el.getAttribute(removeAttr), "remove");
6309
+ const e = new CustomEvent("phx:exec", { detail: { sourceElement: el } });
6310
+ js_default.exec(e, "remove", el.getAttribute(removeAttr), view, el);
6287
6311
  });
6288
6312
  this.requestDOMUpdate(() => {
6289
6313
  elements.forEach((el) => {
@@ -6306,7 +6330,7 @@ var LiveSocket = class {
6306
6330
  let view;
6307
6331
  const viewEl = dom_default.closestViewEl(childEl);
6308
6332
  if (viewEl) {
6309
- view = this.getViewByEl(viewEl);
6333
+ view = dom_default.private(viewEl, "view");
6310
6334
  } else {
6311
6335
  if (!childEl.isConnected) {
6312
6336
  return null;