phoenix_live_view 1.2.0 → 1.2.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.
@@ -411,7 +411,9 @@ const DOM = {
411
411
  // we also clear the throttle timeout to prevent the callback
412
412
  // from being called again after the timeout fires
413
413
  clearTimeout(this.private(el, THROTTLED));
414
- this.triggerCycle(el, DEBOUNCE_TRIGGER);
414
+ if (asyncFilter()) {
415
+ this.triggerCycle(el, DEBOUNCE_TRIGGER);
416
+ }
415
417
  });
416
418
  }
417
419
  }
@@ -792,7 +792,7 @@ export default class DOMPatch {
792
792
  private transitionPendingRemoves() {
793
793
  const { pendingRemoves, liveSocket } = this;
794
794
  if (pendingRemoves.length > 0) {
795
- liveSocket.transitionRemoves(pendingRemoves, () => {
795
+ liveSocket.transitionRemoves(pendingRemoves, this.view, () => {
796
796
  pendingRemoves.forEach((el) => {
797
797
  const child = DOM.firstPhxChild(el);
798
798
  if (child) {
@@ -823,12 +823,15 @@ export default class LiveSocket {
823
823
  ).filter((el) => !DOM.isChildOfAny(el, stickies));
824
824
 
825
825
  const newMainEl = DOM.cloneNode(this.outgoingMainEl, "");
826
- this.main.showLoader(this.loaderTimeout);
827
- this.main.destroy();
826
+ const oldMainView = this.main;
827
+ oldMainView.showLoader(this.loaderTimeout);
828
+ oldMainView.destroy();
828
829
 
829
830
  this.main = this.newRootView(newMainEl, flash, liveReferer);
830
831
  this.main.setRedirect(href);
831
- this.transitionRemoves(removeEls);
832
+ // the old view is destroyed at this point; pass it explicitly so the
833
+ // phx-remove commands execute in the context of the outgoing view
834
+ this.transitionRemoves(removeEls, oldMainView);
832
835
  this.main.join((joinCount, onDone) => {
833
836
  if (joinCount === 1 && this.commitPendingLink(linkRef)) {
834
837
  this.requestDOMUpdate(() => {
@@ -845,7 +848,7 @@ export default class LiveSocket {
845
848
  }
846
849
 
847
850
  /** @internal */
848
- transitionRemoves(elements, callback?) {
851
+ transitionRemoves(elements, view: View, callback?) {
849
852
  const removeAttr = this.binding("remove");
850
853
  const silenceEvents = (e) => {
851
854
  e.preventDefault();
@@ -857,7 +860,8 @@ export default class LiveSocket {
857
860
  for (const event of this.boundEventNames) {
858
861
  el.addEventListener(event, silenceEvents, true);
859
862
  }
860
- this.execJS(el, el.getAttribute(removeAttr), "remove");
863
+ const e = new CustomEvent("phx:exec", { detail: { sourceElement: el } });
864
+ JS.exec(e, "remove", el.getAttribute(removeAttr), view, el);
861
865
  });
862
866
  // remove the silenced listeners when transitions are done in case the element is re-used
863
867
  // and call caller's callback as soon as we are done with transitions
@@ -885,12 +889,15 @@ export default class LiveSocket {
885
889
 
886
890
  /** @internal */
887
891
  owner(childEl: Element, callback?: (view: View) => any) {
888
- let view: View;
892
+ let view: View | undefined;
889
893
  const viewEl = DOM.closestViewEl(childEl);
890
894
  if (viewEl) {
891
- // it can happen that we find a view that is already destroyed;
892
- // in that case we DO NOT want to fallback to the main element
893
- view = this.getViewByEl(viewEl);
895
+ // resolve the view by element identity instead of id; during live
896
+ // navigation the new view is registered under the same id while the
897
+ // old DOM is still attached, and events from the old DOM must not be
898
+ // routed to the new view. A destroyed view removes its element binding,
899
+ // in which case we DO NOT want to fallback to the main element
900
+ view = DOM.private(viewEl, "view");
894
901
  } else {
895
902
  if (!childEl.isConnected) {
896
903
  // if the element is not part of the DOM any more
@@ -408,6 +408,7 @@ export default class View {
408
408
  if (container) {
409
409
  const [tag, attrs] = container;
410
410
  this.el = DOM.replaceRootContainer(this.el, tag, attrs);
411
+ DOM.putPrivate(this.el, "view", this);
411
412
  }
412
413
  this.childJoins = 0;
413
414
  this.joinPending = true;
@@ -516,7 +517,10 @@ export default class View {
516
517
  if (!el) {
517
518
  throw new Error("unable to find root element for view");
518
519
  }
520
+ // child views are initially bound to an element inside the join HTML
521
+ // fragment (see onJoinComplete), so the binding must move to the real el
519
522
  this.el = el;
523
+ DOM.putPrivate(this.el, "view", this);
520
524
  this.el.setAttribute(PHX_ROOT_ID, this.root.id);
521
525
  }
522
526
 
@@ -746,6 +750,7 @@ export default class View {
746
750
  });
747
751
 
748
752
  // because we work with a template element, we must manually copy the attributes
753
+ // and bind the template root to this view,
749
754
  // otherwise the owner / target helpers don't work properly
750
755
  const rootEl = template.content.firstElementChild!;
751
756
  rootEl.id = this.id;
@@ -753,6 +758,7 @@ export default class View {
753
758
  rootEl.setAttribute(PHX_SESSION, this.getSession());
754
759
  rootEl.setAttribute(PHX_STATIC, this.getStatic() ?? "");
755
760
  this.parent && rootEl.setAttribute(PHX_PARENT_ID, this.parent.id);
761
+ DOM.putPrivate(rootEl, "view", this);
756
762
 
757
763
  // we go over all form elements in the new HTML for the LV
758
764
  // and look for old forms in the `formsForRecovery` object;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phoenix_live_view",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
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.59.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",
@@ -671,7 +671,9 @@ var DOM = {
671
671
  if (this.once(el, "bind-debounce")) {
672
672
  el.addEventListener("blur", () => {
673
673
  clearTimeout(this.private(el, THROTTLED));
674
- this.triggerCycle(el, DEBOUNCE_TRIGGER);
674
+ if (asyncFilter()) {
675
+ this.triggerCycle(el, DEBOUNCE_TRIGGER);
676
+ }
675
677
  });
676
678
  }
677
679
  }
@@ -2825,7 +2827,7 @@ var DOMPatch = class {
2825
2827
  transitionPendingRemoves() {
2826
2828
  const { pendingRemoves, liveSocket } = this;
2827
2829
  if (pendingRemoves.length > 0) {
2828
- liveSocket.transitionRemoves(pendingRemoves, () => {
2830
+ liveSocket.transitionRemoves(pendingRemoves, this.view, () => {
2829
2831
  pendingRemoves.forEach((el) => {
2830
2832
  const child = dom_default.firstPhxChild(el);
2831
2833
  if (child) {
@@ -4453,6 +4455,7 @@ var View = class _View {
4453
4455
  if (container) {
4454
4456
  const [tag, attrs] = container;
4455
4457
  this.el = dom_default.replaceRootContainer(this.el, tag, attrs);
4458
+ dom_default.putPrivate(this.el, "view", this);
4456
4459
  }
4457
4460
  this.childJoins = 0;
4458
4461
  this.joinPending = true;
@@ -4539,6 +4542,7 @@ var View = class _View {
4539
4542
  throw new Error("unable to find root element for view");
4540
4543
  }
4541
4544
  this.el = el;
4545
+ dom_default.putPrivate(this.el, "view", this);
4542
4546
  this.el.setAttribute(PHX_ROOT_ID, this.root.id);
4543
4547
  }
4544
4548
  // this is invoked for dead and live views, so we must filter by
@@ -4727,6 +4731,7 @@ var View = class _View {
4727
4731
  rootEl.setAttribute(PHX_SESSION, this.getSession());
4728
4732
  rootEl.setAttribute(PHX_STATIC, this.getStatic() ?? "");
4729
4733
  this.parent && rootEl.setAttribute(PHX_PARENT_ID, this.parent.id);
4734
+ dom_default.putPrivate(rootEl, "view", this);
4730
4735
  const formsToRecover = (
4731
4736
  // we go over all forms in the new DOM; because this is only the HTML for the current
4732
4737
  // view, we can be sure that all forms are owned by this view:
@@ -6089,7 +6094,7 @@ var LiveSocket = class {
6089
6094
  * Returns the version of the LiveView client.
6090
6095
  */
6091
6096
  version() {
6092
- return "1.2.0";
6097
+ return "1.2.1";
6093
6098
  }
6094
6099
  /**
6095
6100
  * Returns true if profiling is enabled. See {@link enableProfiling} and {@link disableProfiling}.
@@ -6442,11 +6447,12 @@ var LiveSocket = class {
6442
6447
  `[${this.binding("remove")}]`
6443
6448
  ).filter((el) => !dom_default.isChildOfAny(el, stickies));
6444
6449
  const newMainEl = dom_default.cloneNode(this.outgoingMainEl, "");
6445
- this.main.showLoader(this.loaderTimeout);
6446
- this.main.destroy();
6450
+ const oldMainView = this.main;
6451
+ oldMainView.showLoader(this.loaderTimeout);
6452
+ oldMainView.destroy();
6447
6453
  this.main = this.newRootView(newMainEl, flash, liveReferer);
6448
6454
  this.main.setRedirect(href);
6449
- this.transitionRemoves(removeEls);
6455
+ this.transitionRemoves(removeEls, oldMainView);
6450
6456
  this.main.join((joinCount, onDone) => {
6451
6457
  if (joinCount === 1 && this.commitPendingLink(linkRef)) {
6452
6458
  this.requestDOMUpdate(() => {
@@ -6461,7 +6467,7 @@ var LiveSocket = class {
6461
6467
  });
6462
6468
  }
6463
6469
  /** @internal */
6464
- transitionRemoves(elements, callback) {
6470
+ transitionRemoves(elements, view, callback) {
6465
6471
  const removeAttr = this.binding("remove");
6466
6472
  const silenceEvents = (e) => {
6467
6473
  e.preventDefault();
@@ -6471,7 +6477,8 @@ var LiveSocket = class {
6471
6477
  for (const event of this.boundEventNames) {
6472
6478
  el.addEventListener(event, silenceEvents, true);
6473
6479
  }
6474
- this.execJS(el, el.getAttribute(removeAttr), "remove");
6480
+ const e = new CustomEvent("phx:exec", { detail: { sourceElement: el } });
6481
+ js_default.exec(e, "remove", el.getAttribute(removeAttr), view, el);
6475
6482
  });
6476
6483
  this.requestDOMUpdate(() => {
6477
6484
  elements.forEach((el) => {
@@ -6497,7 +6504,7 @@ var LiveSocket = class {
6497
6504
  let view;
6498
6505
  const viewEl = dom_default.closestViewEl(childEl);
6499
6506
  if (viewEl) {
6500
- view = this.getViewByEl(viewEl);
6507
+ view = dom_default.private(viewEl, "view");
6501
6508
  } else {
6502
6509
  if (!childEl.isConnected) {
6503
6510
  return null;