phoenix_live_view 1.1.2 → 1.1.4
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.
- package/assets/js/phoenix_live_view/dom_patch.js +19 -2
- package/assets/js/phoenix_live_view/live_socket.js +3 -0
- package/assets/js/phoenix_live_view/view.js +45 -12
- package/assets/js/phoenix_live_view/view_hook.ts +2 -2
- package/assets/js/types/live_socket.d.ts +4 -1
- package/assets/js/types/view.d.ts +8 -2
- package/assets/js/types/view_hook.d.ts +2 -2
- package/package.json +3 -3
- package/priv/static/phoenix_live_view.cjs.js +29 -6
- package/priv/static/phoenix_live_view.cjs.js.map +2 -2
- package/priv/static/phoenix_live_view.esm.js +29 -6
- package/priv/static/phoenix_live_view.esm.js.map +2 -2
- package/priv/static/phoenix_live_view.js +29 -6
- package/priv/static/phoenix_live_view.min.js +6 -6
|
@@ -85,11 +85,28 @@ export default class DOMPatch {
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
perform(isJoinPatch) {
|
|
88
|
-
const { view, liveSocket, html, container
|
|
89
|
-
|
|
88
|
+
const { view, liveSocket, html, container } = this;
|
|
89
|
+
let targetContainer = this.targetContainer;
|
|
90
|
+
|
|
91
|
+
if (this.isCIDPatch() && !this.targetContainer) {
|
|
90
92
|
return;
|
|
91
93
|
}
|
|
92
94
|
|
|
95
|
+
if (this.isCIDPatch()) {
|
|
96
|
+
// https://github.com/phoenixframework/phoenix_live_view/pull/3942
|
|
97
|
+
// we need to ensure that no parent is locked
|
|
98
|
+
const closestLock = targetContainer.closest(`[${PHX_REF_LOCK}]`);
|
|
99
|
+
if (closestLock) {
|
|
100
|
+
const clonedTree = DOM.private(closestLock, PHX_REF_LOCK);
|
|
101
|
+
if (clonedTree) {
|
|
102
|
+
// if a parent is locked with a cloned tree, we need to patch the cloned tree instead
|
|
103
|
+
targetContainer = clonedTree.querySelector(
|
|
104
|
+
`[data-phx-component="${this.targetCID}"]`,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
93
110
|
const focused = liveSocket.getActiveElement();
|
|
94
111
|
const { selectionStart, selectionEnd } =
|
|
95
112
|
focused && DOM.hasSelectionRange(focused) ? focused : {};
|
|
@@ -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
|
-
//
|
|
2059
|
-
|
|
2060
|
-
//
|
|
2061
|
-
//
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
16
|
-
|
|
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:
|
|
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:
|
|
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.
|
|
3
|
+
"version": "1.1.4",
|
|
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
|
-
"
|
|
14
|
-
"
|
|
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
|
},
|
|
@@ -2241,10 +2241,22 @@ var DOMPatch = class {
|
|
|
2241
2241
|
);
|
|
2242
2242
|
}
|
|
2243
2243
|
perform(isJoinPatch) {
|
|
2244
|
-
const { view, liveSocket, html, container
|
|
2245
|
-
|
|
2244
|
+
const { view, liveSocket, html, container } = this;
|
|
2245
|
+
let targetContainer = this.targetContainer;
|
|
2246
|
+
if (this.isCIDPatch() && !this.targetContainer) {
|
|
2246
2247
|
return;
|
|
2247
2248
|
}
|
|
2249
|
+
if (this.isCIDPatch()) {
|
|
2250
|
+
const closestLock = targetContainer.closest(`[${PHX_REF_LOCK}]`);
|
|
2251
|
+
if (closestLock) {
|
|
2252
|
+
const clonedTree = dom_default.private(closestLock, PHX_REF_LOCK);
|
|
2253
|
+
if (clonedTree) {
|
|
2254
|
+
targetContainer = clonedTree.querySelector(
|
|
2255
|
+
`[data-phx-component="${this.targetCID}"]`
|
|
2256
|
+
);
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2248
2260
|
const focused = liveSocket.getActiveElement();
|
|
2249
2261
|
const { selectionStart, selectionEnd } = focused && dom_default.hasSelectionRange(focused) ? focused : {};
|
|
2250
2262
|
const phxUpdate = liveSocket.binding(PHX_UPDATE);
|
|
@@ -5616,9 +5628,20 @@ var View = class _View {
|
|
|
5616
5628
|
return dom_default.all(this.el, `form[${phxChange}]`).filter((form) => form.id).filter((form) => form.elements.length > 0).filter(
|
|
5617
5629
|
(form) => form.getAttribute(this.binding(PHX_AUTO_RECOVER)) !== "ignore"
|
|
5618
5630
|
).map((form) => {
|
|
5619
|
-
const clonedForm = form.cloneNode(
|
|
5620
|
-
|
|
5621
|
-
|
|
5631
|
+
const clonedForm = form.cloneNode(true);
|
|
5632
|
+
morphdom_esm_default(clonedForm, form, {
|
|
5633
|
+
onBeforeElUpdated: (fromEl, toEl) => {
|
|
5634
|
+
dom_default.copyPrivates(fromEl, toEl);
|
|
5635
|
+
return true;
|
|
5636
|
+
}
|
|
5637
|
+
});
|
|
5638
|
+
const externalElements = document.querySelectorAll(
|
|
5639
|
+
`[form="${form.id}"]`
|
|
5640
|
+
);
|
|
5641
|
+
Array.from(externalElements).forEach((el) => {
|
|
5642
|
+
if (form.contains(el)) {
|
|
5643
|
+
return;
|
|
5644
|
+
}
|
|
5622
5645
|
const clonedEl = el.cloneNode(true);
|
|
5623
5646
|
morphdom_esm_default(clonedEl, el);
|
|
5624
5647
|
dom_default.copyPrivates(clonedEl, el);
|
|
@@ -5759,7 +5782,7 @@ var LiveSocket = class {
|
|
|
5759
5782
|
}
|
|
5760
5783
|
// public
|
|
5761
5784
|
version() {
|
|
5762
|
-
return "1.1.
|
|
5785
|
+
return "1.1.4";
|
|
5763
5786
|
}
|
|
5764
5787
|
isProfileEnabled() {
|
|
5765
5788
|
return this.sessionStorage.getItem(PHX_LV_PROFILE) === "true";
|