phoenix_live_view 1.1.2 → 1.1.3

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.
@@ -82,6 +82,9 @@ export default class LiveSocket {
82
82
  this.uploaders = opts.uploaders || {};
83
83
  this.loaderTimeout = opts.loaderTimeout || LOADER_TIMEOUT;
84
84
  this.disconnectedTimeout = opts.disconnectedTimeout || DISCONNECTED_TIMEOUT;
85
+ /**
86
+ * @type {ReturnType<typeof setTimeout> | null}
87
+ */
85
88
  this.reloadWithJitterTimer = null;
86
89
  this.maxReloads = opts.maxReloads || MAX_RELOADS;
87
90
  this.reloadJitterMin = opts.reloadJitterMin || RELOAD_JITTER_MIN;
@@ -192,7 +192,13 @@ export default class View {
192
192
  this.ref = 0;
193
193
  this.lastAckRef = null;
194
194
  this.childJoins = 0;
195
+ /**
196
+ * @type {ReturnType<typeof setTimeout> | null}
197
+ */
195
198
  this.loaderTimer = null;
199
+ /**
200
+ * @type {ReturnType<typeof setTimeout> | null}
201
+ */
196
202
  this.disconnectedTimer = null;
197
203
  this.pendingDiffs = [];
198
204
  this.pendingForms = new Set();
@@ -2041,6 +2047,19 @@ export default class View {
2041
2047
  }
2042
2048
 
2043
2049
  getFormsForRecovery() {
2050
+ // Form recovery is complex in LiveView:
2051
+ // We want to support nested LiveViews and also provide a good user experience.
2052
+ // Therefore, when the channel rejoins, we copy all forms that are eligible for
2053
+ // recovery to be able to access them later.
2054
+ // Why do we need to copy them? Because when the main LiveView joins, any forms
2055
+ // in nested LiveViews would be lost.
2056
+ //
2057
+ // We should rework this in the future to serialize the form payload here
2058
+ // instead of cloning the DOM nodes, but making this work correctly is tedious,
2059
+ // as sending the correct form payload relies on JS.push to extract values
2060
+ // from JS commands (phx-change={JS.push("event", value: ..., target: ...)}),
2061
+ // as well as view.pushInput, which expects DOM elements.
2062
+
2044
2063
  if (this.joinCount === 0) {
2045
2064
  return {};
2046
2065
  }
@@ -2055,19 +2074,33 @@ export default class View {
2055
2074
  form.getAttribute(this.binding(PHX_AUTO_RECOVER)) !== "ignore",
2056
2075
  )
2057
2076
  .map((form) => {
2058
- // we perform a shallow clone and manually copy all elements
2059
- const clonedForm = form.cloneNode(false);
2060
- // we need to copy the private data as it contains
2061
- // the information about touched fields
2062
- DOM.copyPrivates(clonedForm, form);
2063
- Array.from(form.elements).forEach((el) => {
2064
- // we need to clone all child nodes as well,
2065
- // because those could also be selects
2077
+ // We need to clone the whole form, as relying on form.elements can lead to
2078
+ // situations where we have
2079
+ //
2080
+ // <form><fieldset disabled><input name="foo" value="bar"></fieldset></form>
2081
+ //
2082
+ // and form.elements returns both the fieldset and the input separately.
2083
+ // Because the fieldset is disabled, the input should NOT be sent though.
2084
+ // We can only reliably serialize the form by cloning it fully.
2085
+ const clonedForm = form.cloneNode(true);
2086
+ // we call morphdom to copy any special state
2087
+ // like the selected option of a <select> element;
2088
+ // any also copy over privates (which contain information about touched fields)
2089
+ morphdom(clonedForm, form, {
2090
+ onBeforeElUpdated: (fromEl, toEl) => {
2091
+ DOM.copyPrivates(fromEl, toEl);
2092
+ return true;
2093
+ },
2094
+ });
2095
+ // next up, we also need to clone any elements with form="id" parameter
2096
+ const externalElements = document.querySelectorAll(
2097
+ `[form="${form.id}"]`,
2098
+ );
2099
+ Array.from(externalElements).forEach((el) => {
2100
+ if (form.contains(el)) {
2101
+ return;
2102
+ }
2066
2103
  const clonedEl = el.cloneNode(true);
2067
- // we call morphdom to copy any special state
2068
- // like the selected option of a <select> element;
2069
- // this should be plenty fast as we call it on a small subset of the DOM,
2070
- // single inputs or a select with children
2071
2104
  morphdom(clonedEl, el);
2072
2105
  DOM.copyPrivates(clonedEl, el);
2073
2106
  clonedForm.appendChild(clonedEl);
@@ -154,7 +154,7 @@ export interface HookInterface {
154
154
  uploadTo(selectorOrTarget: PhxTarget, name: any, files: any): any;
155
155
 
156
156
  // allow unknown methods, as people can define them in their hooks
157
- [key: string]: any;
157
+ [key: PropertyKey]: any;
158
158
  }
159
159
 
160
160
  // based on https://github.com/DefinitelyTyped/DefinitelyTyped/blob/fac1aa75acdddbf4f1a95e98ee2297b54ce4b4c9/types/phoenix_live_view/hooks.d.ts#L26
@@ -204,7 +204,7 @@ export interface Hook<T = object> {
204
204
  reconnected?: (this: T & HookInterface) => void;
205
205
 
206
206
  // Allow custom methods with any signature and custom properties
207
- [key: string]: ((this: T & HookInterface, ...args: any[]) => any) | any;
207
+ [key: PropertyKey]: any;
208
208
  }
209
209
 
210
210
  /**
@@ -23,7 +23,10 @@ export default class LiveSocket {
23
23
  uploaders: any;
24
24
  loaderTimeout: any;
25
25
  disconnectedTimeout: any;
26
- reloadWithJitterTimer: NodeJS.Timeout;
26
+ /**
27
+ * @type {ReturnType<typeof setTimeout> | null}
28
+ */
29
+ reloadWithJitterTimer: ReturnType<typeof setTimeout> | null;
27
30
  maxReloads: any;
28
31
  reloadJitterMin: any;
29
32
  reloadJitterMax: any;
@@ -12,8 +12,14 @@ export default class View {
12
12
  ref: number;
13
13
  lastAckRef: any;
14
14
  childJoins: number;
15
- loaderTimer: NodeJS.Timeout;
16
- disconnectedTimer: NodeJS.Timeout;
15
+ /**
16
+ * @type {ReturnType<typeof setTimeout> | null}
17
+ */
18
+ loaderTimer: ReturnType<typeof setTimeout> | null;
19
+ /**
20
+ * @type {ReturnType<typeof setTimeout> | null}
21
+ */
22
+ disconnectedTimer: ReturnType<typeof setTimeout> | null;
17
23
  pendingDiffs: any[];
18
24
  pendingForms: Set<any>;
19
25
  redirect: boolean;
@@ -128,7 +128,7 @@ export interface HookInterface {
128
128
  * @param files - The files to upload.
129
129
  */
130
130
  uploadTo(selectorOrTarget: PhxTarget, name: any, files: any): any;
131
- [key: string]: any;
131
+ [key: PropertyKey]: any;
132
132
  }
133
133
  export interface Hook<T = object> {
134
134
  /**
@@ -168,7 +168,7 @@ export interface Hook<T = object> {
168
168
  * Called when the element's parent LiveView has reconnected to the server.
169
169
  */
170
170
  reconnected?: (this: T & HookInterface) => void;
171
- [key: string]: ((this: T & HookInterface, ...args: any[]) => any) | any;
171
+ [key: PropertyKey]: any;
172
172
  }
173
173
  /**
174
174
  * Base class for LiveView hooks. Users extend this class to define their hooks.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phoenix_live_view",
3
- "version": "1.1.2",
3
+ "version": "1.1.3",
4
4
  "description": "The Phoenix LiveView JavaScript client.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -10,8 +10,8 @@
10
10
  "jsdelivr": "./priv/static/phoenix_live_view.min.js",
11
11
  "exports": {
12
12
  "import": {
13
- "default": "./priv/static/phoenix_live_view.esm.js",
14
- "types": "./assets/js/types/index.d.ts"
13
+ "types": "./assets/js/types/index.d.ts",
14
+ "default": "./priv/static/phoenix_live_view.esm.js"
15
15
  },
16
16
  "require": "./priv/static/phoenix_live_view.cjs.js"
17
17
  },
@@ -5616,9 +5616,20 @@ var View = class _View {
5616
5616
  return dom_default.all(this.el, `form[${phxChange}]`).filter((form) => form.id).filter((form) => form.elements.length > 0).filter(
5617
5617
  (form) => form.getAttribute(this.binding(PHX_AUTO_RECOVER)) !== "ignore"
5618
5618
  ).map((form) => {
5619
- const clonedForm = form.cloneNode(false);
5620
- dom_default.copyPrivates(clonedForm, form);
5621
- Array.from(form.elements).forEach((el) => {
5619
+ const clonedForm = form.cloneNode(true);
5620
+ morphdom_esm_default(clonedForm, form, {
5621
+ onBeforeElUpdated: (fromEl, toEl) => {
5622
+ dom_default.copyPrivates(fromEl, toEl);
5623
+ return true;
5624
+ }
5625
+ });
5626
+ const externalElements = document.querySelectorAll(
5627
+ `[form="${form.id}"]`
5628
+ );
5629
+ Array.from(externalElements).forEach((el) => {
5630
+ if (form.contains(el)) {
5631
+ return;
5632
+ }
5622
5633
  const clonedEl = el.cloneNode(true);
5623
5634
  morphdom_esm_default(clonedEl, el);
5624
5635
  dom_default.copyPrivates(clonedEl, el);
@@ -5759,7 +5770,7 @@ var LiveSocket = class {
5759
5770
  }
5760
5771
  // public
5761
5772
  version() {
5762
- return "1.1.2";
5773
+ return "1.1.3";
5763
5774
  }
5764
5775
  isProfileEnabled() {
5765
5776
  return this.sessionStorage.getItem(PHX_LV_PROFILE) === "true";