phoenix_live_view 1.1.28 → 1.2.0-rc.0

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.
Files changed (34) hide show
  1. package/assets/js/phoenix_live_view/constants.js +1 -0
  2. package/assets/js/phoenix_live_view/hooks.js +9 -3
  3. package/assets/js/phoenix_live_view/index.ts +6 -2
  4. package/assets/js/phoenix_live_view/js.js +3 -2
  5. package/assets/js/phoenix_live_view/js_commands.ts +16 -7
  6. package/assets/js/phoenix_live_view/live_socket.js +1 -1
  7. package/assets/js/phoenix_live_view/view.js +110 -89
  8. package/assets/js/phoenix_live_view/view_hook.ts +2 -2
  9. package/package.json +2 -2
  10. package/priv/static/phoenix_live_view.cjs.js +90 -73
  11. package/priv/static/phoenix_live_view.cjs.js.map +3 -3
  12. package/priv/static/phoenix_live_view.esm.js +90 -73
  13. package/priv/static/phoenix_live_view.esm.js.map +3 -3
  14. package/priv/static/phoenix_live_view.js +90 -73
  15. package/priv/static/phoenix_live_view.min.js +6 -6
  16. package/assets/js/types/aria.d.ts +0 -9
  17. package/assets/js/types/browser.d.ts +0 -15
  18. package/assets/js/types/constants.d.ts +0 -97
  19. package/assets/js/types/dom.d.ts +0 -63
  20. package/assets/js/types/dom_patch.d.ts +0 -50
  21. package/assets/js/types/dom_post_morph_restorer.d.ts +0 -8
  22. package/assets/js/types/element_ref.d.ts +0 -15
  23. package/assets/js/types/entry_uploader.d.ts +0 -16
  24. package/assets/js/types/hooks.d.ts +0 -9
  25. package/assets/js/types/index.d.ts +0 -313
  26. package/assets/js/types/js.d.ts +0 -99
  27. package/assets/js/types/js_commands.d.ts +0 -217
  28. package/assets/js/types/live_socket.d.ts +0 -149
  29. package/assets/js/types/live_uploader.d.ts +0 -29
  30. package/assets/js/types/rendered.d.ts +0 -49
  31. package/assets/js/types/upload_entry.d.ts +0 -42
  32. package/assets/js/types/utils.d.ts +0 -14
  33. package/assets/js/types/view.d.ts +0 -151
  34. package/assets/js/types/view_hook.d.ts +0 -233
@@ -85,6 +85,7 @@ export const PHX_LV_PID = "data-phx-pid";
85
85
  export const PHX_KEY = "key";
86
86
  export const PHX_PRIVATE = "phxPrivate";
87
87
  export const PHX_AUTO_RECOVER = "auto-recover";
88
+ export const PHX_NO_UNUSED_FIELD = "no-unused-field";
88
89
  export const PHX_LV_DEBUG = "phx:live-socket:debug";
89
90
  export const PHX_LV_PROFILE = "phx:live-socket:profiling";
90
91
  export const PHX_LV_LATENCY_SIM = "phx:live-socket:latency-sim";
@@ -131,7 +131,6 @@ const isAtViewportTop = (el, scrollContainer) => {
131
131
  const rect = el.getBoundingClientRect();
132
132
  return (
133
133
  Math.ceil(rect.top) >= top(scrollContainer) &&
134
- Math.ceil(rect.left) >= 0 &&
135
134
  Math.floor(rect.top) <= bottom(scrollContainer)
136
135
  );
137
136
  };
@@ -140,7 +139,6 @@ const isAtViewportBottom = (el, scrollContainer) => {
140
139
  const rect = el.getBoundingClientRect();
141
140
  return (
142
141
  Math.ceil(rect.bottom) >= top(scrollContainer) &&
143
- Math.ceil(rect.left) >= 0 &&
144
142
  Math.floor(rect.bottom) <= bottom(scrollContainer)
145
143
  );
146
144
  };
@@ -149,7 +147,6 @@ const isWithinViewport = (el, scrollContainer) => {
149
147
  const rect = el.getBoundingClientRect();
150
148
  return (
151
149
  Math.ceil(rect.top) >= top(scrollContainer) &&
152
- Math.ceil(rect.left) >= 0 &&
153
150
  Math.floor(rect.top) <= bottom(scrollContainer)
154
151
  );
155
152
  };
@@ -264,6 +261,15 @@ Hooks.InfiniteScroll = {
264
261
  }
265
262
  },
266
263
 
264
+ updated() {
265
+ // Check if the scroll container still exists
266
+ // https://github.com/phoenixframework/phoenix_live_view/issues/4169.
267
+ if (!this.scrollContainer.isConnected) {
268
+ this.destroyed();
269
+ this.mounted();
270
+ }
271
+ },
272
+
267
273
  destroyed() {
268
274
  if (this.scrollContainer) {
269
275
  this.scrollContainer.removeEventListener("scroll", this.onScroll);
@@ -12,7 +12,7 @@ import { ViewHook } from "./view_hook";
12
12
  import View from "./view";
13
13
  import { logError } from "./utils";
14
14
 
15
- import type { LiveSocketJSCommands } from "./js_commands";
15
+ import type { EncodedJS, LiveSocketJSCommands } from "./js_commands";
16
16
  import type { Hook, HooksOptions } from "./view_hook";
17
17
  import type { Socket as PhoenixSocket } from "phoenix";
18
18
 
@@ -266,7 +266,11 @@ export interface LiveSocketInstanceInterface {
266
266
  *
267
267
  * See [`Phoenix.LiveView.JS`](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.JS.html) for more information.
268
268
  */
269
- execJS(el: HTMLElement, encodedJS: string, eventType?: string | null): void;
269
+ execJS(
270
+ el: HTMLElement,
271
+ encodedJS: EncodedJS,
272
+ eventType?: string | null,
273
+ ): void;
270
274
  /**
271
275
  * Returns an object with methods to manipulate the DOM and execute JavaScript.
272
276
  * The applied changes integrate with server DOM patching.
@@ -11,8 +11,9 @@ const JS = {
11
11
  null,
12
12
  { callback: defaults && defaults.callback },
13
13
  ];
14
- const commands =
15
- phxEvent.charAt(0) === "["
14
+ const commands = Array.isArray(phxEvent)
15
+ ? phxEvent
16
+ : typeof phxEvent === "string" && phxEvent.startsWith("[")
16
17
  ? JSON.parse(phxEvent)
17
18
  : [[defaultKind, defaultArgs]];
18
19
 
@@ -1,6 +1,15 @@
1
1
  import JS from "./js";
2
2
  import LiveSocket from "./live_socket";
3
3
 
4
+ /**
5
+ * An encoded JS command. Use functions in the `Phoenix.LiveView.JS` module on
6
+ * the server to create and compose JS commands.
7
+ *
8
+ * The underlying primitive type is considered opaque, and may change in future
9
+ * versions.
10
+ */
11
+ export type EncodedJS = string | Array<any>;
12
+
4
13
  type Transition = string | string[];
5
14
 
6
15
  // Base options for commands involving transitions and timing
@@ -76,13 +85,13 @@ type NavigationOpts = {
76
85
  */
77
86
  interface AllJSCommands {
78
87
  /**
79
- * Executes encoded JavaScript in the context of the element.
88
+ * Executes an encoded JS command in the context of an element.
80
89
  * This version is for general use via liveSocket.js().
81
90
  *
82
- * @param el - The element in whose context to execute the JavaScript.
83
- * @param encodedJS - The encoded JavaScript string to execute.
91
+ * @param el - The element in whose context to execute the JS command.
92
+ * @param encodedJS - The encoded JS command with operations to execute.
84
93
  */
85
- exec(el: HTMLElement, encodedJS: string): void;
94
+ exec(el: HTMLElement, encodedJS: EncodedJS): void;
86
95
 
87
96
  /**
88
97
  * Shows an element.
@@ -382,9 +391,9 @@ export type LiveSocketJSCommands = AllJSCommands;
382
391
  */
383
392
  export interface HookJSCommands extends Omit<AllJSCommands, "exec"> {
384
393
  /**
385
- * Executes encoded JavaScript in the context of the hook's element.
394
+ * Executes a JS command in the context of the hook's element.
386
395
  *
387
- * @param {string} encodedJS - The encoded JavaScript string to execute.
396
+ * @param encodedJS - The encoded JS command with operations to execute.
388
397
  */
389
- exec(encodedJS: string): void;
398
+ exec(encodedJS: EncodedJS): void;
390
399
  }
@@ -224,7 +224,7 @@ export default class LiveSocket {
224
224
 
225
225
  /**
226
226
  * @param {HTMLElement} el
227
- * @param {string} encodedJS
227
+ * @param {import("./js_commands").EncodedJS} encodedJS
228
228
  * @param {string | null} [eventType]
229
229
  */
230
230
  execJS(el, encodedJS, eventType = null) {
@@ -38,6 +38,7 @@ import {
38
38
  PHX_VIEWPORT_BOTTOM,
39
39
  MAX_CHILD_JOIN_ATTEMPTS,
40
40
  PHX_LV_PID,
41
+ PHX_NO_UNUSED_FIELD,
41
42
  PHX_PORTAL,
42
43
  PHX_TELEPORTED_REF,
43
44
  PHX_TELEPORTED_SRC,
@@ -77,90 +78,6 @@ export const prependFormDataKey = (key, prefix) => {
77
78
  return baseKey;
78
79
  };
79
80
 
80
- const serializeForm = (form, opts, onlyNames = []) => {
81
- const { submitter } = opts;
82
-
83
- // We must inject the submitter in the order that it exists in the DOM
84
- // relative to other inputs. For example, for checkbox groups, the order must be maintained.
85
- let injectedElement;
86
- if (submitter && submitter.name) {
87
- const input = document.createElement("input");
88
- input.type = "hidden";
89
- // set the form attribute if the submitter has one;
90
- // this can happen if the element is outside the actual form element
91
- const formId = submitter.getAttribute("form");
92
- if (formId) {
93
- input.setAttribute("form", formId);
94
- }
95
- input.name = submitter.name;
96
- input.value = submitter.value;
97
- submitter.parentElement.insertBefore(input, submitter);
98
- injectedElement = input;
99
- }
100
-
101
- const formData = new FormData(form);
102
- const toRemove = [];
103
-
104
- formData.forEach((val, key, _index) => {
105
- if (val instanceof File) {
106
- toRemove.push(key);
107
- }
108
- });
109
-
110
- // Cleanup after building fileData
111
- toRemove.forEach((key) => formData.delete(key));
112
-
113
- const params = new URLSearchParams();
114
-
115
- const { inputsUnused, onlyHiddenInputs } = Array.from(form.elements).reduce(
116
- (acc, input) => {
117
- const { inputsUnused, onlyHiddenInputs } = acc;
118
- const key = input.name;
119
- if (!key) {
120
- return acc;
121
- }
122
-
123
- if (inputsUnused[key] === undefined) {
124
- inputsUnused[key] = true;
125
- }
126
- if (onlyHiddenInputs[key] === undefined) {
127
- onlyHiddenInputs[key] = true;
128
- }
129
-
130
- const isUsed =
131
- DOM.private(input, PHX_HAS_FOCUSED) ||
132
- DOM.private(input, PHX_HAS_SUBMITTED);
133
- const isHidden = input.type === "hidden";
134
- inputsUnused[key] = inputsUnused[key] && !isUsed;
135
- onlyHiddenInputs[key] = onlyHiddenInputs[key] && isHidden;
136
-
137
- return acc;
138
- },
139
- { inputsUnused: {}, onlyHiddenInputs: {} },
140
- );
141
-
142
- for (const [key, val] of formData.entries()) {
143
- if (onlyNames.length === 0 || onlyNames.indexOf(key) >= 0) {
144
- const isUnused = inputsUnused[key];
145
- const hidden = onlyHiddenInputs[key];
146
- if (isUnused && !(submitter && submitter.name == key) && !hidden) {
147
- params.append(prependFormDataKey(key, "_unused_"), "");
148
- }
149
- if (typeof val === "string") {
150
- params.append(key, val);
151
- }
152
- }
153
- }
154
-
155
- // remove the injected element again
156
- // (it would be removed by the next dom patch anyway, but this is cleaner)
157
- if (submitter && injectedElement) {
158
- submitter.parentElement.removeChild(injectedElement);
159
- }
160
-
161
- return params.toString();
162
- };
163
-
164
81
  export default class View {
165
82
  static closestView(el) {
166
83
  const liveViewEl = el.closest(PHX_VIEW_SELECTOR);
@@ -689,7 +606,8 @@ export default class View {
689
606
 
690
607
  patch.after("updated", (el) => {
691
608
  if (updatedHookIds.has(el.id)) {
692
- this.getHook(el).__updated();
609
+ const hook = this.getHook(el);
610
+ hook && hook.__updated();
693
611
  }
694
612
  });
695
613
 
@@ -1665,6 +1583,107 @@ export default class View {
1665
1583
  return meta;
1666
1584
  }
1667
1585
 
1586
+ serializeForm(form, opts, onlyNames = []) {
1587
+ const { submitter } = opts;
1588
+
1589
+ // We must inject the submitter in the order that it exists in the DOM
1590
+ // relative to other inputs. For example, for checkbox groups, the order must be maintained.
1591
+ let injectedElement;
1592
+ if (submitter && submitter.name) {
1593
+ const input = document.createElement("input");
1594
+ input.type = "hidden";
1595
+ // set the form attribute if the submitter has one;
1596
+ // this can happen if the element is outside the actual form element
1597
+ const formId = submitter.getAttribute("form");
1598
+ if (formId) {
1599
+ input.setAttribute("form", formId);
1600
+ }
1601
+ input.name = submitter.name;
1602
+ input.value = submitter.value;
1603
+ submitter.parentElement.insertBefore(input, submitter);
1604
+ injectedElement = input;
1605
+ }
1606
+
1607
+ const formData = new FormData(form);
1608
+ const toRemove = [];
1609
+
1610
+ formData.forEach((val, key, _index) => {
1611
+ if (val instanceof File) {
1612
+ toRemove.push(key);
1613
+ }
1614
+ });
1615
+
1616
+ // Cleanup after building fileData
1617
+ toRemove.forEach((key) => formData.delete(key));
1618
+
1619
+ const params = new URLSearchParams();
1620
+
1621
+ const { inputsUnused, onlyHiddenInputs } = Array.from(form.elements).reduce(
1622
+ (acc, input) => {
1623
+ const { inputsUnused, onlyHiddenInputs } = acc;
1624
+ const key = input.name;
1625
+ if (!key) {
1626
+ return acc;
1627
+ }
1628
+
1629
+ if (inputsUnused[key] === undefined) {
1630
+ inputsUnused[key] = true;
1631
+ }
1632
+ if (onlyHiddenInputs[key] === undefined) {
1633
+ onlyHiddenInputs[key] = true;
1634
+ }
1635
+
1636
+ const inputSkipUnusedField = input.hasAttribute(
1637
+ this.binding(PHX_NO_UNUSED_FIELD),
1638
+ );
1639
+
1640
+ const isUsed =
1641
+ DOM.private(input, PHX_HAS_FOCUSED) ||
1642
+ DOM.private(input, PHX_HAS_SUBMITTED) ||
1643
+ inputSkipUnusedField;
1644
+
1645
+ const isHidden = input.type === "hidden";
1646
+ inputsUnused[key] = inputsUnused[key] && !isUsed;
1647
+ onlyHiddenInputs[key] = onlyHiddenInputs[key] && isHidden;
1648
+
1649
+ return acc;
1650
+ },
1651
+ { inputsUnused: {}, onlyHiddenInputs: {} },
1652
+ );
1653
+
1654
+ const formSkipUnusedFields = form.hasAttribute(
1655
+ this.binding(PHX_NO_UNUSED_FIELD),
1656
+ );
1657
+
1658
+ for (const [key, val] of formData.entries()) {
1659
+ if (onlyNames.length === 0 || onlyNames.indexOf(key) >= 0) {
1660
+ const isUnused = inputsUnused[key];
1661
+ const hidden = onlyHiddenInputs[key];
1662
+ const skipUnusedCheck = formSkipUnusedFields;
1663
+
1664
+ if (
1665
+ !skipUnusedCheck &&
1666
+ isUnused &&
1667
+ !(submitter && submitter.name == key) &&
1668
+ !hidden
1669
+ ) {
1670
+ params.append(prependFormDataKey(key, "_unused_"), "");
1671
+ }
1672
+ if (typeof val === "string") {
1673
+ params.append(key, val);
1674
+ }
1675
+ }
1676
+ }
1677
+
1678
+ // remove the injected element again
1679
+ // (it would be removed by the next dom patch anyway, but this is cleaner)
1680
+ if (submitter && injectedElement) {
1681
+ submitter.parentElement.removeChild(injectedElement);
1682
+ }
1683
+
1684
+ return params.toString();
1685
+ }
1686
+
1668
1687
  pushEvent(type, el, targetCtx, phxEvent, meta, opts = {}, onReply) {
1669
1688
  this.pushWithReply(
1670
1689
  (maybePayload) =>
@@ -1726,9 +1745,11 @@ export default class View {
1726
1745
  serializeOpts.submitter = inputEl;
1727
1746
  }
1728
1747
  if (inputEl.getAttribute(this.binding("change"))) {
1729
- formData = serializeForm(inputEl.form, serializeOpts, [inputEl.name]);
1748
+ formData = this.serializeForm(inputEl.form, serializeOpts, [
1749
+ inputEl.name,
1750
+ ]);
1730
1751
  } else {
1731
- formData = serializeForm(inputEl.form, serializeOpts);
1752
+ formData = this.serializeForm(inputEl.form, serializeOpts);
1732
1753
  }
1733
1754
  if (
1734
1755
  DOM.isUploadInput(inputEl) &&
@@ -1905,7 +1926,7 @@ export default class View {
1905
1926
  return this.undoRefs(ref, phxEvent);
1906
1927
  }
1907
1928
  const meta = this.extractMeta(formEl, {}, opts.value);
1908
- const formData = serializeForm(formEl, { submitter });
1929
+ const formData = this.serializeForm(formEl, { submitter });
1909
1930
  this.pushWithReply(proxyRefGen, "event", {
1910
1931
  type: "form",
1911
1932
  event: phxEvent,
@@ -1923,7 +1944,7 @@ export default class View {
1923
1944
  )
1924
1945
  ) {
1925
1946
  const meta = this.extractMeta(formEl, {}, opts.value);
1926
- const formData = serializeForm(formEl, { submitter });
1947
+ const formData = this.serializeForm(formEl, { submitter });
1927
1948
  this.pushWithReply(refGenerator, "event", {
1928
1949
  type: "form",
1929
1950
  event: phxEvent,
@@ -1,4 +1,4 @@
1
- import jsCommands, { HookJSCommands } from "./js_commands";
1
+ import jsCommands, { EncodedJS, HookJSCommands } from "./js_commands";
2
2
  import DOM from "./dom";
3
3
  import LiveSocket from "./live_socket";
4
4
  import View from "./view";
@@ -395,7 +395,7 @@ export class ViewHook<E extends HTMLElement = HTMLElement>
395
395
  js(): HookJSCommands {
396
396
  return {
397
397
  ...jsCommands(this.__view().liveSocket, "hook"),
398
- exec: (encodedJS: string) => {
398
+ exec: (encodedJS: EncodedJS) => {
399
399
  this.__view().liveSocket.execJS(this.el, encodedJS, "hook");
400
400
  },
401
401
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phoenix_live_view",
3
- "version": "1.1.28",
3
+ "version": "1.2.0-rc.0",
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.59.1",
41
41
  "@types/jest": "^30.0.0",
42
42
  "@types/phoenix": "^1.6.6",
43
43
  "css.escape": "^1.5.1",
@@ -114,6 +114,7 @@ var PHX_LV_PID = "data-phx-pid";
114
114
  var PHX_KEY = "key";
115
115
  var PHX_PRIVATE = "phxPrivate";
116
116
  var PHX_AUTO_RECOVER = "auto-recover";
117
+ var PHX_NO_UNUSED_FIELD = "no-unused-field";
117
118
  var PHX_LV_DEBUG = "phx:live-socket:debug";
118
119
  var PHX_LV_PROFILE = "phx:live-socket:profiling";
119
120
  var PHX_LV_LATENCY_SIM = "phx:live-socket:latency-sim";
@@ -1359,15 +1360,15 @@ var top = (scrollContainer) => {
1359
1360
  };
1360
1361
  var isAtViewportTop = (el, scrollContainer) => {
1361
1362
  const rect = el.getBoundingClientRect();
1362
- return Math.ceil(rect.top) >= top(scrollContainer) && Math.ceil(rect.left) >= 0 && Math.floor(rect.top) <= bottom(scrollContainer);
1363
+ return Math.ceil(rect.top) >= top(scrollContainer) && Math.floor(rect.top) <= bottom(scrollContainer);
1363
1364
  };
1364
1365
  var isAtViewportBottom = (el, scrollContainer) => {
1365
1366
  const rect = el.getBoundingClientRect();
1366
- return Math.ceil(rect.bottom) >= top(scrollContainer) && Math.ceil(rect.left) >= 0 && Math.floor(rect.bottom) <= bottom(scrollContainer);
1367
+ return Math.ceil(rect.bottom) >= top(scrollContainer) && Math.floor(rect.bottom) <= bottom(scrollContainer);
1367
1368
  };
1368
1369
  var isWithinViewport = (el, scrollContainer) => {
1369
1370
  const rect = el.getBoundingClientRect();
1370
- return Math.ceil(rect.top) >= top(scrollContainer) && Math.ceil(rect.left) >= 0 && Math.floor(rect.top) <= bottom(scrollContainer);
1371
+ return Math.ceil(rect.top) >= top(scrollContainer) && Math.floor(rect.top) <= bottom(scrollContainer);
1371
1372
  };
1372
1373
  Hooks.InfiniteScroll = {
1373
1374
  mounted() {
@@ -1458,6 +1459,12 @@ Hooks.InfiniteScroll = {
1458
1459
  window.addEventListener("scroll", this.onScroll);
1459
1460
  }
1460
1461
  },
1462
+ updated() {
1463
+ if (!this.scrollContainer.isConnected) {
1464
+ this.destroyed();
1465
+ this.mounted();
1466
+ }
1467
+ },
1461
1468
  destroyed() {
1462
1469
  if (this.scrollContainer) {
1463
1470
  this.scrollContainer.removeEventListener("scroll", this.onScroll);
@@ -3264,7 +3271,7 @@ var JS = {
3264
3271
  null,
3265
3272
  { callback: defaults && defaults.callback }
3266
3273
  ];
3267
- const commands = phxEvent.charAt(0) === "[" ? JSON.parse(phxEvent) : [[defaultKind, defaultArgs]];
3274
+ const commands = Array.isArray(phxEvent) ? phxEvent : typeof phxEvent === "string" && phxEvent.startsWith("[") ? JSON.parse(phxEvent) : [[defaultKind, defaultArgs]];
3268
3275
  commands.forEach(([kind, args]) => {
3269
3276
  if (kind === defaultKind) {
3270
3277
  args = { ...defaultArgs, ...args };
@@ -4099,68 +4106,6 @@ var prependFormDataKey = (key, prefix) => {
4099
4106
  }
4100
4107
  return baseKey;
4101
4108
  };
4102
- var serializeForm = (form, opts, onlyNames = []) => {
4103
- const { submitter } = opts;
4104
- let injectedElement;
4105
- if (submitter && submitter.name) {
4106
- const input = document.createElement("input");
4107
- input.type = "hidden";
4108
- const formId = submitter.getAttribute("form");
4109
- if (formId) {
4110
- input.setAttribute("form", formId);
4111
- }
4112
- input.name = submitter.name;
4113
- input.value = submitter.value;
4114
- submitter.parentElement.insertBefore(input, submitter);
4115
- injectedElement = input;
4116
- }
4117
- const formData = new FormData(form);
4118
- const toRemove = [];
4119
- formData.forEach((val, key, _index) => {
4120
- if (val instanceof File) {
4121
- toRemove.push(key);
4122
- }
4123
- });
4124
- toRemove.forEach((key) => formData.delete(key));
4125
- const params = new URLSearchParams();
4126
- const { inputsUnused, onlyHiddenInputs } = Array.from(form.elements).reduce(
4127
- (acc, input) => {
4128
- const { inputsUnused: inputsUnused2, onlyHiddenInputs: onlyHiddenInputs2 } = acc;
4129
- const key = input.name;
4130
- if (!key) {
4131
- return acc;
4132
- }
4133
- if (inputsUnused2[key] === void 0) {
4134
- inputsUnused2[key] = true;
4135
- }
4136
- if (onlyHiddenInputs2[key] === void 0) {
4137
- onlyHiddenInputs2[key] = true;
4138
- }
4139
- const isUsed = dom_default.private(input, PHX_HAS_FOCUSED) || dom_default.private(input, PHX_HAS_SUBMITTED);
4140
- const isHidden = input.type === "hidden";
4141
- inputsUnused2[key] = inputsUnused2[key] && !isUsed;
4142
- onlyHiddenInputs2[key] = onlyHiddenInputs2[key] && isHidden;
4143
- return acc;
4144
- },
4145
- { inputsUnused: {}, onlyHiddenInputs: {} }
4146
- );
4147
- for (const [key, val] of formData.entries()) {
4148
- if (onlyNames.length === 0 || onlyNames.indexOf(key) >= 0) {
4149
- const isUnused = inputsUnused[key];
4150
- const hidden = onlyHiddenInputs[key];
4151
- if (isUnused && !(submitter && submitter.name == key) && !hidden) {
4152
- params.append(prependFormDataKey(key, "_unused_"), "");
4153
- }
4154
- if (typeof val === "string") {
4155
- params.append(key, val);
4156
- }
4157
- }
4158
- }
4159
- if (submitter && injectedElement) {
4160
- submitter.parentElement.removeChild(injectedElement);
4161
- }
4162
- return params.toString();
4163
- };
4164
4109
  var View = class _View {
4165
4110
  static closestView(el) {
4166
4111
  const liveViewEl = el.closest(PHX_VIEW_SELECTOR);
@@ -4593,7 +4538,8 @@ var View = class _View {
4593
4538
  });
4594
4539
  patch.after("updated", (el) => {
4595
4540
  if (updatedHookIds.has(el.id)) {
4596
- this.getHook(el).__updated();
4541
+ const hook = this.getHook(el);
4542
+ hook && hook.__updated();
4597
4543
  }
4598
4544
  });
4599
4545
  patch.after("discarded", (el) => {
@@ -5388,6 +5334,75 @@ var View = class _View {
5388
5334
  }
5389
5335
  return meta;
5390
5336
  }
5337
+ serializeForm(form, opts, onlyNames = []) {
5338
+ const { submitter } = opts;
5339
+ let injectedElement;
5340
+ if (submitter && submitter.name) {
5341
+ const input = document.createElement("input");
5342
+ input.type = "hidden";
5343
+ const formId = submitter.getAttribute("form");
5344
+ if (formId) {
5345
+ input.setAttribute("form", formId);
5346
+ }
5347
+ input.name = submitter.name;
5348
+ input.value = submitter.value;
5349
+ submitter.parentElement.insertBefore(input, submitter);
5350
+ injectedElement = input;
5351
+ }
5352
+ const formData = new FormData(form);
5353
+ const toRemove = [];
5354
+ formData.forEach((val, key, _index) => {
5355
+ if (val instanceof File) {
5356
+ toRemove.push(key);
5357
+ }
5358
+ });
5359
+ toRemove.forEach((key) => formData.delete(key));
5360
+ const params = new URLSearchParams();
5361
+ const { inputsUnused, onlyHiddenInputs } = Array.from(form.elements).reduce(
5362
+ (acc, input) => {
5363
+ const { inputsUnused: inputsUnused2, onlyHiddenInputs: onlyHiddenInputs2 } = acc;
5364
+ const key = input.name;
5365
+ if (!key) {
5366
+ return acc;
5367
+ }
5368
+ if (inputsUnused2[key] === void 0) {
5369
+ inputsUnused2[key] = true;
5370
+ }
5371
+ if (onlyHiddenInputs2[key] === void 0) {
5372
+ onlyHiddenInputs2[key] = true;
5373
+ }
5374
+ const inputSkipUnusedField = input.hasAttribute(
5375
+ this.binding(PHX_NO_UNUSED_FIELD)
5376
+ );
5377
+ const isUsed = dom_default.private(input, PHX_HAS_FOCUSED) || dom_default.private(input, PHX_HAS_SUBMITTED) || inputSkipUnusedField;
5378
+ const isHidden = input.type === "hidden";
5379
+ inputsUnused2[key] = inputsUnused2[key] && !isUsed;
5380
+ onlyHiddenInputs2[key] = onlyHiddenInputs2[key] && isHidden;
5381
+ return acc;
5382
+ },
5383
+ { inputsUnused: {}, onlyHiddenInputs: {} }
5384
+ );
5385
+ const formSkipUnusedFields = form.hasAttribute(
5386
+ this.binding(PHX_NO_UNUSED_FIELD)
5387
+ );
5388
+ for (const [key, val] of formData.entries()) {
5389
+ if (onlyNames.length === 0 || onlyNames.indexOf(key) >= 0) {
5390
+ const isUnused = inputsUnused[key];
5391
+ const hidden = onlyHiddenInputs[key];
5392
+ const skipUnusedCheck = formSkipUnusedFields;
5393
+ if (!skipUnusedCheck && isUnused && !(submitter && submitter.name == key) && !hidden) {
5394
+ params.append(prependFormDataKey(key, "_unused_"), "");
5395
+ }
5396
+ if (typeof val === "string") {
5397
+ params.append(key, val);
5398
+ }
5399
+ }
5400
+ }
5401
+ if (submitter && injectedElement) {
5402
+ submitter.parentElement.removeChild(injectedElement);
5403
+ }
5404
+ return params.toString();
5405
+ }
5391
5406
  pushEvent(type, el, targetCtx, phxEvent, meta, opts = {}, onReply) {
5392
5407
  this.pushWithReply(
5393
5408
  (maybePayload) => this.putRef([{ el, loading: true, lock: true }], phxEvent, type, {
@@ -5439,9 +5454,11 @@ var View = class _View {
5439
5454
  serializeOpts.submitter = inputEl;
5440
5455
  }
5441
5456
  if (inputEl.getAttribute(this.binding("change"))) {
5442
- formData = serializeForm(inputEl.form, serializeOpts, [inputEl.name]);
5457
+ formData = this.serializeForm(inputEl.form, serializeOpts, [
5458
+ inputEl.name
5459
+ ]);
5443
5460
  } else {
5444
- formData = serializeForm(inputEl.form, serializeOpts);
5461
+ formData = this.serializeForm(inputEl.form, serializeOpts);
5445
5462
  }
5446
5463
  if (dom_default.isUploadInput(inputEl) && inputEl.files && inputEl.files.length > 0) {
5447
5464
  LiveUploader.trackFiles(inputEl, Array.from(inputEl.files));
@@ -5582,7 +5599,7 @@ var View = class _View {
5582
5599
  return this.undoRefs(ref, phxEvent);
5583
5600
  }
5584
5601
  const meta = this.extractMeta(formEl, {}, opts.value);
5585
- const formData = serializeForm(formEl, { submitter });
5602
+ const formData = this.serializeForm(formEl, { submitter });
5586
5603
  this.pushWithReply(proxyRefGen, "event", {
5587
5604
  type: "form",
5588
5605
  event: phxEvent,
@@ -5593,7 +5610,7 @@ var View = class _View {
5593
5610
  });
5594
5611
  } else if (!(formEl.hasAttribute(PHX_REF_SRC) && formEl.classList.contains("phx-submit-loading"))) {
5595
5612
  const meta = this.extractMeta(formEl, {}, opts.value);
5596
- const formData = serializeForm(formEl, { submitter });
5613
+ const formData = this.serializeForm(formEl, { submitter });
5597
5614
  this.pushWithReply(refGenerator, "event", {
5598
5615
  type: "form",
5599
5616
  event: phxEvent,
@@ -5935,7 +5952,7 @@ var LiveSocket = class {
5935
5952
  }
5936
5953
  // public
5937
5954
  version() {
5938
- return "1.1.28";
5955
+ return "1.2.0-rc.0";
5939
5956
  }
5940
5957
  isProfileEnabled() {
5941
5958
  return this.sessionStorage.getItem(PHX_LV_PROFILE) === "true";
@@ -6012,7 +6029,7 @@ var LiveSocket = class {
6012
6029
  }
6013
6030
  /**
6014
6031
  * @param {HTMLElement} el
6015
- * @param {string} encodedJS
6032
+ * @param {import("./js_commands").EncodedJS} encodedJS
6016
6033
  * @param {string | null} [eventType]
6017
6034
  */
6018
6035
  execJS(el, encodedJS, eventType = null) {