phoenix_live_view 1.1.31 → 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) {
@@ -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
@@ -8,10 +8,7 @@ export const logError = (msg, obj) => console.error && console.error(msg, obj);
8
8
  // target over the existing socket. A full URL to a different origin (or a
9
9
  // non-http(s) scheme, which resolves to an opaque "null" origin) is a
10
10
  // programming error, so we fail loudly instead of attempting a broken join.
11
- export const ensureSameOrigin = (
12
- href,
13
- kind,
14
- ) => {
11
+ export const ensureSameOrigin = (href, kind) => {
15
12
  let url;
16
13
  try {
17
14
  url = new URL(href, window.location.href);
@@ -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;
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phoenix_live_view",
3
- "version": "1.1.31",
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",
@@ -650,7 +650,9 @@ var DOM = {
650
650
  if (this.once(el, "bind-debounce")) {
651
651
  el.addEventListener("blur", () => {
652
652
  clearTimeout(this.private(el, THROTTLED));
653
- this.triggerCycle(el, DEBOUNCE_TRIGGER);
653
+ if (asyncFilter()) {
654
+ this.triggerCycle(el, DEBOUNCE_TRIGGER);
655
+ }
654
656
  });
655
657
  }
656
658
  }
@@ -2756,7 +2758,7 @@ var DOMPatch = class {
2756
2758
  transitionPendingRemoves() {
2757
2759
  const { pendingRemoves, liveSocket } = this;
2758
2760
  if (pendingRemoves.length > 0) {
2759
- liveSocket.transitionRemoves(pendingRemoves, () => {
2761
+ liveSocket.transitionRemoves(pendingRemoves, this.view, () => {
2760
2762
  pendingRemoves.forEach((el) => {
2761
2763
  const child = dom_default.firstPhxChild(el);
2762
2764
  if (child) {
@@ -4448,6 +4450,7 @@ var View = class _View {
4448
4450
  if (container) {
4449
4451
  const [tag, attrs] = container;
4450
4452
  this.el = dom_default.replaceRootContainer(this.el, tag, attrs);
4453
+ dom_default.putPrivate(this.el, "view", this);
4451
4454
  }
4452
4455
  this.childJoins = 0;
4453
4456
  this.joinPending = true;
@@ -4530,6 +4533,7 @@ var View = class _View {
4530
4533
  }
4531
4534
  attachTrueDocEl() {
4532
4535
  this.el = dom_default.byId(this.id);
4536
+ dom_default.putPrivate(this.el, "view", this);
4533
4537
  this.el.setAttribute(PHX_ROOT_ID, this.root.id);
4534
4538
  }
4535
4539
  // this is invoked for dead and live views, so we must filter by
@@ -4713,6 +4717,7 @@ var View = class _View {
4713
4717
  rootEl.setAttribute(PHX_SESSION, this.getSession());
4714
4718
  rootEl.setAttribute(PHX_STATIC, this.getStatic());
4715
4719
  rootEl.setAttribute(PHX_PARENT_ID, this.parent ? this.parent.id : null);
4720
+ dom_default.putPrivate(rootEl, "view", this);
4716
4721
  const formsToRecover = (
4717
4722
  // we go over all forms in the new DOM; because this is only the HTML for the current
4718
4723
  // view, we can be sure that all forms are owned by this view:
@@ -5993,7 +5998,7 @@ var LiveSocket = class {
5993
5998
  }
5994
5999
  // public
5995
6000
  version() {
5996
- return "1.1.31";
6001
+ return "1.1.32";
5997
6002
  }
5998
6003
  isProfileEnabled() {
5999
6004
  return this.sessionStorage.getItem(PHX_LV_PROFILE) === "true";
@@ -6272,11 +6277,12 @@ var LiveSocket = class {
6272
6277
  `[${this.binding("remove")}]`
6273
6278
  ).filter((el) => !dom_default.isChildOfAny(el, stickies));
6274
6279
  const newMainEl = dom_default.cloneNode(this.outgoingMainEl, "");
6275
- this.main.showLoader(this.loaderTimeout);
6276
- this.main.destroy();
6280
+ const oldMainView = this.main;
6281
+ oldMainView.showLoader(this.loaderTimeout);
6282
+ oldMainView.destroy();
6277
6283
  this.main = this.newRootView(newMainEl, flash, liveReferer);
6278
6284
  this.main.setRedirect(href);
6279
- this.transitionRemoves(removeEls);
6285
+ this.transitionRemoves(removeEls, oldMainView);
6280
6286
  this.main.join((joinCount, onDone) => {
6281
6287
  if (joinCount === 1 && this.commitPendingLink(linkRef)) {
6282
6288
  this.requestDOMUpdate(() => {
@@ -6290,7 +6296,7 @@ var LiveSocket = class {
6290
6296
  }
6291
6297
  });
6292
6298
  }
6293
- transitionRemoves(elements, callback) {
6299
+ transitionRemoves(elements, view, callback) {
6294
6300
  const removeAttr = this.binding("remove");
6295
6301
  const silenceEvents = (e) => {
6296
6302
  e.preventDefault();
@@ -6300,7 +6306,8 @@ var LiveSocket = class {
6300
6306
  for (const event of this.boundEventNames) {
6301
6307
  el.addEventListener(event, silenceEvents, true);
6302
6308
  }
6303
- 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);
6304
6311
  });
6305
6312
  this.requestDOMUpdate(() => {
6306
6313
  elements.forEach((el) => {
@@ -6323,7 +6330,7 @@ var LiveSocket = class {
6323
6330
  let view;
6324
6331
  const viewEl = dom_default.closestViewEl(childEl);
6325
6332
  if (viewEl) {
6326
- view = this.getViewByEl(viewEl);
6333
+ view = dom_default.private(viewEl, "view");
6327
6334
  } else {
6328
6335
  if (!childEl.isConnected) {
6329
6336
  return null;