phoenix_live_view 1.0.2 → 1.0.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/README.md +2 -10
- package/assets/js/phoenix_live_view/dom.js +6 -1
- package/assets/js/phoenix_live_view/dom_patch.js +9 -37
- package/assets/js/phoenix_live_view/element_ref.js +9 -0
- package/assets/js/phoenix_live_view/hooks.js +26 -5
- package/assets/js/phoenix_live_view/js.js +9 -13
- package/assets/js/phoenix_live_view/live_socket.js +22 -29
- package/assets/js/phoenix_live_view/utils.js +11 -0
- package/assets/js/phoenix_live_view/view.js +32 -17
- package/package.json +24 -6
- package/priv/static/phoenix_live_view.cjs.js +109 -105
- package/priv/static/phoenix_live_view.cjs.js.map +3 -3
- package/priv/static/phoenix_live_view.esm.js +109 -105
- package/priv/static/phoenix_live_view.esm.js.map +3 -3
- package/priv/static/phoenix_live_view.js +109 -105
- package/priv/static/phoenix_live_view.min.js +6 -6
- package/assets/package.json +0 -29
package/README.md
CHANGED
|
@@ -193,19 +193,11 @@ $ npm run test
|
|
|
193
193
|
|
|
194
194
|
Running the JavaScript unit tests:
|
|
195
195
|
|
|
196
|
-
```bash
|
|
197
|
-
$ cd assets
|
|
198
|
-
$ npm install
|
|
199
|
-
$ npm run test
|
|
200
|
-
# to automatically run tests for files that have been changed
|
|
201
|
-
$ npm run test.watch
|
|
202
|
-
```
|
|
203
|
-
|
|
204
|
-
or simply:
|
|
205
|
-
|
|
206
196
|
```bash
|
|
207
197
|
$ npm run setup
|
|
208
198
|
$ npm run js:test
|
|
199
|
+
# to automatically run tests for files that have been changed
|
|
200
|
+
$ npm run js:test.watch
|
|
209
201
|
```
|
|
210
202
|
|
|
211
203
|
Running the JavaScript end-to-end tests:
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
PHX_PARENT_ID,
|
|
11
11
|
PHX_PRIVATE,
|
|
12
12
|
PHX_REF_SRC,
|
|
13
|
+
PHX_REF_LOCK,
|
|
13
14
|
PHX_PENDING_ATTRS,
|
|
14
15
|
PHX_ROOT_ID,
|
|
15
16
|
PHX_SESSION,
|
|
@@ -148,7 +149,7 @@ let DOM = {
|
|
|
148
149
|
cids.forEach(cid => {
|
|
149
150
|
this.filterWithinSameLiveView(this.all(node, `[${PHX_COMPONENT}="${cid}"]`), node).forEach(parent => {
|
|
150
151
|
parentCids.add(cid)
|
|
151
|
-
this.all(parent, `[${PHX_COMPONENT}]`)
|
|
152
|
+
this.filterWithinSameLiveView(this.all(parent, `[${PHX_COMPONENT}]`), parent)
|
|
152
153
|
.map(el => parseInt(el.getAttribute(PHX_COMPONENT)))
|
|
153
154
|
.forEach(childCID => childrenCids.add(childCID))
|
|
154
155
|
})
|
|
@@ -545,6 +546,10 @@ let DOM = {
|
|
|
545
546
|
if(!ops){ return }
|
|
546
547
|
|
|
547
548
|
ops.forEach(([name, op, _stashed]) => this.putSticky(el, name, op))
|
|
549
|
+
},
|
|
550
|
+
|
|
551
|
+
isLocked(el){
|
|
552
|
+
return el.hasAttribute && el.hasAttribute(PHX_REF_LOCK)
|
|
548
553
|
}
|
|
549
554
|
}
|
|
550
555
|
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
|
|
19
19
|
import {
|
|
20
20
|
detectDuplicateIds,
|
|
21
|
+
detectInvalidStreamInserts,
|
|
21
22
|
isCid
|
|
22
23
|
} from "./utils"
|
|
23
24
|
|
|
@@ -26,40 +27,7 @@ import DOMPostMorphRestorer from "./dom_post_morph_restorer"
|
|
|
26
27
|
import morphdom from "morphdom"
|
|
27
28
|
|
|
28
29
|
export default class DOMPatch {
|
|
29
|
-
|
|
30
|
-
let focused = liveSocket.getActiveElement()
|
|
31
|
-
let {selectionStart, selectionEnd} = focused && DOM.hasSelectionRange(focused) ? focused : {}
|
|
32
|
-
let phxUpdate = liveSocket.binding(PHX_UPDATE)
|
|
33
|
-
let externalFormTriggered = null
|
|
34
|
-
|
|
35
|
-
morphdom(container, clonedTree, {
|
|
36
|
-
childrenOnly: false,
|
|
37
|
-
onBeforeElUpdated: (fromEl, toEl) => {
|
|
38
|
-
DOM.syncPendingAttrs(fromEl, toEl)
|
|
39
|
-
// we cannot morph locked children
|
|
40
|
-
if(!container.isSameNode(fromEl) && fromEl.hasAttribute(PHX_REF_LOCK)){ return false }
|
|
41
|
-
if(DOM.isIgnored(fromEl, phxUpdate)){ return false }
|
|
42
|
-
if(focused && focused.isSameNode(fromEl) && DOM.isFormInput(fromEl)){
|
|
43
|
-
DOM.mergeFocusedInput(fromEl, toEl)
|
|
44
|
-
return false
|
|
45
|
-
}
|
|
46
|
-
if(DOM.isNowTriggerFormExternal(toEl, liveSocket.binding(PHX_TRIGGER_ACTION))){
|
|
47
|
-
externalFormTriggered = toEl
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
if(externalFormTriggered){
|
|
53
|
-
liveSocket.unload()
|
|
54
|
-
// use prototype's submit in case there's a form control with name or id of "submit"
|
|
55
|
-
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/submit
|
|
56
|
-
Object.getPrototypeOf(externalFormTriggered).submit.call(externalFormTriggered)
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
liveSocket.silenceEvents(() => DOM.restoreFocus(focused, selectionStart, selectionEnd))
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
constructor(view, container, id, html, streams, targetCID){
|
|
30
|
+
constructor(view, container, id, html, streams, targetCID, opts={}){
|
|
63
31
|
this.view = view
|
|
64
32
|
this.liveSocket = view.liveSocket
|
|
65
33
|
this.container = container
|
|
@@ -79,6 +47,8 @@ export default class DOMPatch {
|
|
|
79
47
|
afteradded: [], afterupdated: [], afterdiscarded: [], afterphxChildAdded: [],
|
|
80
48
|
aftertransitionsDiscarded: []
|
|
81
49
|
}
|
|
50
|
+
this.withChildren = opts.withChildren || opts.undoRef || false
|
|
51
|
+
this.undoRef = opts.undoRef
|
|
82
52
|
}
|
|
83
53
|
|
|
84
54
|
before(kind, callback){ this.callbacks[`before${kind}`].push(callback) }
|
|
@@ -115,7 +85,7 @@ export default class DOMPatch {
|
|
|
115
85
|
|
|
116
86
|
let externalFormTriggered = null
|
|
117
87
|
|
|
118
|
-
function morph(targetContainer, source, withChildren=
|
|
88
|
+
function morph(targetContainer, source, withChildren=this.withChildren){
|
|
119
89
|
let morphCallbacks = {
|
|
120
90
|
// normally, we are running with childrenOnly, as the patch HTML for a LV
|
|
121
91
|
// does not include the LV attrs (data-phx-session, etc.)
|
|
@@ -247,7 +217,8 @@ export default class DOMPatch {
|
|
|
247
217
|
// apply any changes that happened while the element was locked.
|
|
248
218
|
let isFocusedFormEl = focused && fromEl.isSameNode(focused) && DOM.isFormInput(fromEl)
|
|
249
219
|
let focusedSelectChanged = isFocusedFormEl && this.isChangedSelect(fromEl, toEl)
|
|
250
|
-
if
|
|
220
|
+
// only perform the clone step if this is not a patch that unlocks
|
|
221
|
+
if(fromEl.hasAttribute(PHX_REF_SRC) && fromEl.getAttribute(PHX_REF_LOCK) != this.undoRef){
|
|
251
222
|
if(DOM.isUploadInput(fromEl)){
|
|
252
223
|
DOM.mergeAttrs(fromEl, toEl, {isIgnored: true})
|
|
253
224
|
this.trackBefore("updated", fromEl, toEl)
|
|
@@ -341,6 +312,7 @@ export default class DOMPatch {
|
|
|
341
312
|
|
|
342
313
|
if(liveSocket.isDebugEnabled()){
|
|
343
314
|
detectDuplicateIds()
|
|
315
|
+
detectInvalidStreamInserts(this.streamInserts)
|
|
344
316
|
// warn if there are any inputs named "id"
|
|
345
317
|
Array.from(document.querySelectorAll("input[name=id]")).forEach(node => {
|
|
346
318
|
if(node.form){
|
|
@@ -461,7 +433,7 @@ export default class DOMPatch {
|
|
|
461
433
|
transitionPendingRemoves(){
|
|
462
434
|
let {pendingRemoves, liveSocket} = this
|
|
463
435
|
if(pendingRemoves.length > 0){
|
|
464
|
-
liveSocket.transitionRemoves(pendingRemoves,
|
|
436
|
+
liveSocket.transitionRemoves(pendingRemoves, () => {
|
|
465
437
|
pendingRemoves.forEach(el => {
|
|
466
438
|
let child = DOM.firstPhxChild(el)
|
|
467
439
|
if(child){ liveSocket.destroyViewByEl(child) }
|
|
@@ -11,6 +11,15 @@ import {
|
|
|
11
11
|
import DOM from "./dom"
|
|
12
12
|
|
|
13
13
|
export default class ElementRef {
|
|
14
|
+
static onUnlock(el, callback){
|
|
15
|
+
if(!DOM.isLocked(el) && !el.closest(`[${PHX_REF_LOCK}]`)){ return callback() }
|
|
16
|
+
const closestLock = el.closest(`[${PHX_REF_LOCK}]`)
|
|
17
|
+
const ref = closestLock.closest(`[${PHX_REF_LOCK}]`).getAttribute(PHX_REF_LOCK)
|
|
18
|
+
closestLock.addEventListener(`phx:undo-lock:${ref}`, () => {
|
|
19
|
+
callback()
|
|
20
|
+
}, {once: true})
|
|
21
|
+
}
|
|
22
|
+
|
|
14
23
|
constructor(el){
|
|
15
24
|
this.el = el
|
|
16
25
|
this.loadingRef = el.hasAttribute(PHX_REF_LOADING) ? parseInt(el.getAttribute(PHX_REF_LOADING), 10) : null
|
|
@@ -47,11 +47,32 @@ let Hooks = {
|
|
|
47
47
|
mounted(){
|
|
48
48
|
this.focusStart = this.el.firstElementChild
|
|
49
49
|
this.focusEnd = this.el.lastElementChild
|
|
50
|
-
this.focusStart.addEventListener("focus", () =>
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
50
|
+
this.focusStart.addEventListener("focus", (e) => {
|
|
51
|
+
if(!e.relatedTarget || !this.el.contains(e.relatedTarget)){
|
|
52
|
+
// Handle focus entering from outside (e.g. Tab when body is focused)
|
|
53
|
+
// https://github.com/phoenixframework/phoenix_live_view/issues/3636
|
|
54
|
+
const nextFocus = e.target.nextElementSibling
|
|
55
|
+
ARIA.attemptFocus(nextFocus) || ARIA.focusFirst(nextFocus)
|
|
56
|
+
} else {
|
|
57
|
+
ARIA.focusLast(this.el)
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
this.focusEnd.addEventListener("focus", (e) => {
|
|
61
|
+
if(!e.relatedTarget || !this.el.contains(e.relatedTarget)){
|
|
62
|
+
// Handle focus entering from outside (e.g. Shift+Tab when body is focused)
|
|
63
|
+
// https://github.com/phoenixframework/phoenix_live_view/issues/3636
|
|
64
|
+
const nextFocus = e.target.previousElementSibling
|
|
65
|
+
ARIA.attemptFocus(nextFocus) || ARIA.focusLast(nextFocus)
|
|
66
|
+
} else {
|
|
67
|
+
ARIA.focusFirst(this.el)
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
// only try to change the focus if it is not already inside
|
|
71
|
+
if(!this.el.contains(document.activeElement)){
|
|
72
|
+
this.el.addEventListener("phx:show-end", () => this.el.focus())
|
|
73
|
+
if(window.getComputedStyle(this.el).display !== "none"){
|
|
74
|
+
ARIA.focusFirst(this.el)
|
|
75
|
+
}
|
|
55
76
|
}
|
|
56
77
|
}
|
|
57
78
|
}
|
|
@@ -97,11 +97,11 @@ let JS = {
|
|
|
97
97
|
},
|
|
98
98
|
|
|
99
99
|
exec_focus(e, eventType, phxEvent, view, sourceEl, el){
|
|
100
|
-
|
|
100
|
+
ARIA.attemptFocus(el)
|
|
101
101
|
},
|
|
102
102
|
|
|
103
103
|
exec_focus_first(e, eventType, phxEvent, view, sourceEl, el){
|
|
104
|
-
|
|
104
|
+
ARIA.focusFirstInteractive(el) || ARIA.focusFirst(el)
|
|
105
105
|
},
|
|
106
106
|
|
|
107
107
|
exec_push_focus(e, eventType, phxEvent, view, sourceEl, el){
|
|
@@ -219,18 +219,14 @@ let JS = {
|
|
|
219
219
|
}
|
|
220
220
|
} else {
|
|
221
221
|
if(this.isVisible(el)){
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
el.dispatchEvent(new Event("phx:hide-end"))
|
|
226
|
-
})
|
|
222
|
+
el.dispatchEvent(new Event("phx:hide-start"))
|
|
223
|
+
DOM.putSticky(el, "toggle", currentEl => currentEl.style.display = "none")
|
|
224
|
+
el.dispatchEvent(new Event("phx:hide-end"))
|
|
227
225
|
} else {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
el.dispatchEvent(new Event("phx:show-end"))
|
|
233
|
-
})
|
|
226
|
+
el.dispatchEvent(new Event("phx:show-start"))
|
|
227
|
+
let stickyDisplay = display || this.defaultDisplay(el)
|
|
228
|
+
DOM.putSticky(el, "toggle", currentEl => currentEl.style.display = stickyDisplay)
|
|
229
|
+
el.dispatchEvent(new Event("phx:show-end"))
|
|
234
230
|
}
|
|
235
231
|
}
|
|
236
232
|
},
|
|
@@ -375,7 +375,9 @@ export default class LiveSocket {
|
|
|
375
375
|
DOM.all(document, `${PHX_VIEW_SELECTOR}:not([${PHX_PARENT_ID}])`, rootEl => {
|
|
376
376
|
if(!this.getRootById(rootEl.id)){
|
|
377
377
|
let view = this.newRootView(rootEl)
|
|
378
|
-
|
|
378
|
+
// stickies cannot be mounted at the router and therefore should not
|
|
379
|
+
// get a href set on them
|
|
380
|
+
if(!DOM.isPhxSticky(rootEl)){ view.setHref(this.getHref()) }
|
|
379
381
|
view.join()
|
|
380
382
|
if(rootEl.hasAttribute(PHX_MAIN)){ this.main = view }
|
|
381
383
|
}
|
|
@@ -391,22 +393,26 @@ export default class LiveSocket {
|
|
|
391
393
|
}
|
|
392
394
|
|
|
393
395
|
replaceMain(href, flash, callback = null, linkRef = this.setPendingLink(href)){
|
|
394
|
-
|
|
396
|
+
const liveReferer = this.currentLocation.href
|
|
395
397
|
this.outgoingMainEl = this.outgoingMainEl || this.main.el
|
|
396
|
-
|
|
397
|
-
|
|
398
|
+
|
|
399
|
+
const stickies = DOM.findPhxSticky(document) || []
|
|
400
|
+
const removeEls = DOM.all(this.outgoingMainEl, `[${this.binding("remove")}]`)
|
|
401
|
+
.filter(el => !DOM.isChildOfAny(el, stickies))
|
|
402
|
+
|
|
403
|
+
const newMainEl = DOM.cloneNode(this.outgoingMainEl, "")
|
|
398
404
|
this.main.showLoader(this.loaderTimeout)
|
|
399
405
|
this.main.destroy()
|
|
400
406
|
|
|
401
407
|
this.main = this.newRootView(newMainEl, flash, liveReferer)
|
|
402
408
|
this.main.setRedirect(href)
|
|
403
|
-
this.transitionRemoves(removeEls
|
|
409
|
+
this.transitionRemoves(removeEls)
|
|
404
410
|
this.main.join((joinCount, onDone) => {
|
|
405
411
|
if(joinCount === 1 && this.commitPendingLink(linkRef)){
|
|
406
412
|
this.requestDOMUpdate(() => {
|
|
407
413
|
// remove phx-remove els right before we replace the main element
|
|
408
414
|
removeEls.forEach(el => el.remove())
|
|
409
|
-
|
|
415
|
+
stickies.forEach(el => newMainEl.appendChild(el))
|
|
410
416
|
this.outgoingMainEl.replaceWith(newMainEl)
|
|
411
417
|
this.outgoingMainEl = null
|
|
412
418
|
callback && callback(linkRef)
|
|
@@ -416,12 +422,8 @@ export default class LiveSocket {
|
|
|
416
422
|
})
|
|
417
423
|
}
|
|
418
424
|
|
|
419
|
-
transitionRemoves(elements,
|
|
425
|
+
transitionRemoves(elements, callback){
|
|
420
426
|
let removeAttr = this.binding("remove")
|
|
421
|
-
if(skipSticky){
|
|
422
|
-
const stickies = DOM.findPhxSticky(document) || []
|
|
423
|
-
elements = elements.filter(el => !DOM.isChildOfAny(el, stickies))
|
|
424
|
-
}
|
|
425
427
|
let silenceEvents = (e) => {
|
|
426
428
|
e.preventDefault()
|
|
427
429
|
e.stopImmediatePropagation()
|
|
@@ -693,7 +695,7 @@ export default class LiveSocket {
|
|
|
693
695
|
})
|
|
694
696
|
window.addEventListener("popstate", event => {
|
|
695
697
|
if(!this.registerNewLocation(window.location)){ return }
|
|
696
|
-
let {type, backType, id,
|
|
698
|
+
let {type, backType, id, scroll, position} = event.state || {}
|
|
697
699
|
let href = window.location.href
|
|
698
700
|
|
|
699
701
|
// Compare positions to determine direction
|
|
@@ -707,15 +709,11 @@ export default class LiveSocket {
|
|
|
707
709
|
|
|
708
710
|
DOM.dispatchEvent(window, "phx:navigate", {detail: {href, patch: type === "patch", pop: true, direction: isForward ? "forward" : "backward"}})
|
|
709
711
|
this.requestDOMUpdate(() => {
|
|
712
|
+
const callback = () => { this.maybeScroll(scroll) }
|
|
710
713
|
if(this.main.isConnected() && (type === "patch" && id === this.main.id)){
|
|
711
|
-
this.main.pushLinkPatch(event, href, null,
|
|
712
|
-
this.maybeScroll(scroll)
|
|
713
|
-
})
|
|
714
|
+
this.main.pushLinkPatch(event, href, null, callback)
|
|
714
715
|
} else {
|
|
715
|
-
this.replaceMain(href, null,
|
|
716
|
-
if(root){ this.replaceRootHistory() }
|
|
717
|
-
this.maybeScroll(scroll)
|
|
718
|
-
})
|
|
716
|
+
this.replaceMain(href, null, callback)
|
|
719
717
|
}
|
|
720
718
|
})
|
|
721
719
|
}, false)
|
|
@@ -802,7 +800,8 @@ export default class LiveSocket {
|
|
|
802
800
|
}
|
|
803
801
|
|
|
804
802
|
historyRedirect(e, href, linkState, flash, targetEl){
|
|
805
|
-
|
|
803
|
+
const clickLoading = targetEl && e.isTrusted && e.type !== "popstate"
|
|
804
|
+
if(clickLoading){ targetEl.classList.add("phx-click-loading") }
|
|
806
805
|
if(!this.isConnected() || !this.main.isMain()){ return Browser.redirect(href, flash) }
|
|
807
806
|
|
|
808
807
|
// convert to full href if only path prefix
|
|
@@ -831,20 +830,14 @@ export default class LiveSocket {
|
|
|
831
830
|
DOM.dispatchEvent(window, "phx:navigate", {detail: {href, patch: false, pop: false, direction: "forward"}})
|
|
832
831
|
this.registerNewLocation(window.location)
|
|
833
832
|
}
|
|
833
|
+
// explicitly undo click-loading class
|
|
834
|
+
// (in case it originated in a sticky live view, otherwise it would be removed anyway)
|
|
835
|
+
if(clickLoading){ targetEl.classList.remove("phx-click-loading") }
|
|
834
836
|
done()
|
|
835
837
|
})
|
|
836
838
|
})
|
|
837
839
|
}
|
|
838
840
|
|
|
839
|
-
replaceRootHistory(){
|
|
840
|
-
Browser.pushState("replace", {
|
|
841
|
-
root: true,
|
|
842
|
-
type: "patch",
|
|
843
|
-
id: this.main.id,
|
|
844
|
-
position: this.currentHistoryPosition // Preserve current position
|
|
845
|
-
})
|
|
846
|
-
}
|
|
847
|
-
|
|
848
841
|
registerNewLocation(newLocation){
|
|
849
842
|
let {pathname, search} = this.currentLocation
|
|
850
843
|
if(pathname + search === newLocation.pathname + newLocation.search){
|
|
@@ -23,6 +23,17 @@ export function detectDuplicateIds(){
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
export function detectInvalidStreamInserts(inserts){
|
|
27
|
+
const errors = new Set()
|
|
28
|
+
Object.keys(inserts).forEach((id) => {
|
|
29
|
+
const streamEl = document.getElementById(id)
|
|
30
|
+
if(streamEl && streamEl.parentElement && streamEl.parentElement.getAttribute("phx-update") !== "stream"){
|
|
31
|
+
errors.add(`The stream container with id "${streamEl.parentElement.id}" is missing the phx-update="stream" attribute. Ensure it is set for streams to work properly.`)
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
errors.forEach(error => console.error(error))
|
|
35
|
+
}
|
|
36
|
+
|
|
26
37
|
export let debug = (view, kind, msg, obj) => {
|
|
27
38
|
if(view.liveSocket.isDebugEnabled()){
|
|
28
39
|
console.log(`${view.id} ${kind}: ${msg} - `, obj)
|
|
@@ -318,8 +318,12 @@ export default class View {
|
|
|
318
318
|
this.formsForRecovery = this.getFormsForRecovery()
|
|
319
319
|
}
|
|
320
320
|
if(this.isMain() && window.history.state === null){
|
|
321
|
-
// set initial history entry if this is the first page load
|
|
322
|
-
|
|
321
|
+
// set initial history entry if this is the first page load (no history)
|
|
322
|
+
Browser.pushState("replace", {
|
|
323
|
+
type: "patch",
|
|
324
|
+
id: this.id,
|
|
325
|
+
position: this.liveSocket.currentHistoryPosition
|
|
326
|
+
})
|
|
323
327
|
}
|
|
324
328
|
|
|
325
329
|
if(liveview_version !== this.liveSocket.version()){
|
|
@@ -697,6 +701,9 @@ export default class View {
|
|
|
697
701
|
addHook(el){
|
|
698
702
|
let hookElId = ViewHook.elementID(el)
|
|
699
703
|
|
|
704
|
+
// only ever try to add hooks to elements owned by this view
|
|
705
|
+
if(el.getAttribute && !this.ownsElement(el)){ return }
|
|
706
|
+
|
|
700
707
|
if(hookElId && !this.viewHooks[hookElId]){
|
|
701
708
|
// hook created, but not attached (createHook for web component)
|
|
702
709
|
let hook = DOM.getCustomElHook(el) || logError(`no hook found for custom element: ${el.id}`)
|
|
@@ -710,7 +717,6 @@ export default class View {
|
|
|
710
717
|
} else {
|
|
711
718
|
// new hook found with phx-hook attribute
|
|
712
719
|
let hookName = el.getAttribute(`data-phx-${PHX_HOOK}`) || el.getAttribute(this.binding(PHX_HOOK))
|
|
713
|
-
if(hookName && !this.ownsElement(el)){ return }
|
|
714
720
|
let callbacks = this.liveSocket.getHookCallbacks(hookName)
|
|
715
721
|
|
|
716
722
|
if(callbacks){
|
|
@@ -725,9 +731,12 @@ export default class View {
|
|
|
725
731
|
}
|
|
726
732
|
|
|
727
733
|
destroyHook(hook){
|
|
734
|
+
// __destroyed clears the elementID from the hook, therefore
|
|
735
|
+
// we need to get it before calling __destroyed
|
|
736
|
+
const hookId = ViewHook.elementID(hook.el)
|
|
728
737
|
hook.__destroyed()
|
|
729
738
|
hook.__cleanup__()
|
|
730
|
-
delete this.viewHooks[
|
|
739
|
+
delete this.viewHooks[hookId]
|
|
731
740
|
}
|
|
732
741
|
|
|
733
742
|
applyPendingUpdates(){
|
|
@@ -971,11 +980,12 @@ export default class View {
|
|
|
971
980
|
let elRef = new ElementRef(el)
|
|
972
981
|
|
|
973
982
|
elRef.maybeUndo(ref, phxEvent, clonedTree => {
|
|
974
|
-
|
|
975
|
-
|
|
983
|
+
// we need to perform a full patch on unlocked elements
|
|
984
|
+
// to perform all the necessary logic (like calling updated for hooks, etc.)
|
|
985
|
+
let patch = new DOMPatch(this, el, this.id, clonedTree, [], null, {undoRef: ref})
|
|
986
|
+
const phxChildrenAdded = this.performPatch(patch, true)
|
|
976
987
|
DOM.all(el, `[${PHX_REF_SRC}="${this.refSrc()}"]`, child => this.undoElRef(child, ref, phxEvent))
|
|
977
|
-
this.
|
|
978
|
-
if(hook){ hook.__updated() }
|
|
988
|
+
if(phxChildrenAdded){ this.joinNewChildren() }
|
|
979
989
|
})
|
|
980
990
|
}
|
|
981
991
|
|
|
@@ -1182,15 +1192,20 @@ export default class View {
|
|
|
1182
1192
|
}
|
|
1183
1193
|
this.pushWithReply(refGenerator, "event", event).then(({resp}) => {
|
|
1184
1194
|
if(DOM.isUploadInput(inputEl) && DOM.isAutoUpload(inputEl)){
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
this.undoRefs(ref, phxEvent)
|
|
1192
|
-
|
|
1193
|
-
|
|
1195
|
+
// the element could be inside a locked parent for other unrelated changes;
|
|
1196
|
+
// we can only start uploads when the tree is unlocked and the
|
|
1197
|
+
// necessary data attributes are set in the real DOM
|
|
1198
|
+
ElementRef.onUnlock(inputEl, () => {
|
|
1199
|
+
if(LiveUploader.filesAwaitingPreflight(inputEl).length > 0){
|
|
1200
|
+
let [ref, _els] = refGenerator()
|
|
1201
|
+
this.undoRefs(ref, phxEvent, [inputEl.form])
|
|
1202
|
+
this.uploadFiles(inputEl.form, phxEvent, targetCtx, ref, cid, (_uploads) => {
|
|
1203
|
+
callback && callback(resp)
|
|
1204
|
+
this.triggerAwaitingSubmit(inputEl.form, phxEvent)
|
|
1205
|
+
this.undoRefs(ref, phxEvent)
|
|
1206
|
+
})
|
|
1207
|
+
}
|
|
1208
|
+
})
|
|
1194
1209
|
} else {
|
|
1195
1210
|
callback && callback(resp)
|
|
1196
1211
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "phoenix_live_view",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "The Phoenix LiveView JavaScript client.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"module": "./priv/static/phoenix_live_view.esm.js",
|
|
@@ -23,17 +23,35 @@
|
|
|
23
23
|
"priv/static/*",
|
|
24
24
|
"assets/js/phoenix_live_view/*"
|
|
25
25
|
],
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"morphdom": "2.7.4"
|
|
28
|
+
},
|
|
26
29
|
"devDependencies": {
|
|
27
|
-
"@
|
|
28
|
-
"@
|
|
30
|
+
"@babel/cli": "7.26.4",
|
|
31
|
+
"@babel/core": "7.26.0",
|
|
32
|
+
"@babel/preset-env": "7.26.0",
|
|
33
|
+
"@eslint/js": "^9.18.0",
|
|
34
|
+
"@playwright/test": "^1.49.1",
|
|
35
|
+
"@stylistic/eslint-plugin-js": "^2.12.1",
|
|
36
|
+
"css.escape": "^1.5.1",
|
|
37
|
+
"eslint": "9.18.0",
|
|
38
|
+
"eslint-plugin-jest": "28.10.0",
|
|
29
39
|
"eslint-plugin-playwright": "^2.1.0",
|
|
30
|
-
"
|
|
40
|
+
"globals": "^15.14.0",
|
|
41
|
+
"jest": "^29.7.0",
|
|
42
|
+
"jest-environment-jsdom": "^29.7.0",
|
|
43
|
+
"jest-monocart-coverage": "^1.1.1",
|
|
44
|
+
"monocart-reporter": "^2.9.13",
|
|
45
|
+
"phoenix": "1.7.18"
|
|
31
46
|
},
|
|
32
47
|
"scripts": {
|
|
33
|
-
"setup": "mix deps.get && npm install
|
|
48
|
+
"setup": "mix deps.get && npm install",
|
|
34
49
|
"e2e:server": "MIX_ENV=e2e mix test --cover --export-coverage e2e test/e2e/test_helper.exs",
|
|
35
50
|
"e2e:test": "mix assets.build && cd test/e2e && npx playwright install && npx playwright test",
|
|
36
|
-
"js:test": "
|
|
51
|
+
"js:test": "jest",
|
|
52
|
+
"js:test.coverage": "jest --coverage",
|
|
53
|
+
"js:test.watch": "jest --watch",
|
|
54
|
+
"js:lint": "eslint --fix && cd assets && eslint --fix",
|
|
37
55
|
"test": "npm run js:test && npm run e2e:test",
|
|
38
56
|
"cover:merge": "node test/e2e/merge-coverage.mjs",
|
|
39
57
|
"cover": "npm run test && npm run cover:merge",
|