phoenix_live_view 0.17.6 → 0.17.9
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/CHANGELOG.md +38 -7
- package/README.md +6 -1
- package/assets/js/phoenix_live_view/dom.js +5 -3
- package/assets/js/phoenix_live_view/js.js +13 -10
- package/assets/js/phoenix_live_view/live_socket.js +34 -23
- package/assets/js/phoenix_live_view/rendered.js +1 -1
- package/assets/js/phoenix_live_view/view.js +30 -11
- package/assets/js/phoenix_live_view/view_hook.js +1 -1
- package/package.json +1 -1
- package/priv/static/phoenix_live_view.cjs.js +80 -51
- package/priv/static/phoenix_live_view.cjs.js.map +2 -2
- package/priv/static/phoenix_live_view.esm.js +80 -51
- package/priv/static/phoenix_live_view.esm.js.map +2 -2
- package/priv/static/phoenix_live_view.js +80 -51
- package/priv/static/phoenix_live_view.min.js +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.17.9 (2022-04-07)
|
|
4
|
+
|
|
5
|
+
### Bug fixes
|
|
6
|
+
- Fix sticky LiveViews failing to be patched during live navigation
|
|
7
|
+
- Do not raise on dynamic `phx-update` value
|
|
8
|
+
|
|
9
|
+
## 0.17.8 (2022-04-06)
|
|
10
|
+
|
|
11
|
+
### Enhancements
|
|
12
|
+
- Add HEEx formatter
|
|
13
|
+
- Support `phx-change` on individual inputs
|
|
14
|
+
- Dispatch `MouseEvent` on client
|
|
15
|
+
- Add `:bubbles` option to `JS.dispatch` to control event bubbling
|
|
16
|
+
- Expose underlying `liveSocket` instance on hooks
|
|
17
|
+
- Enable client debug by default on localhost
|
|
18
|
+
|
|
19
|
+
### Bug fixes
|
|
20
|
+
- Fix hook and sticky LiveView issues caused by back-to-back live redirects from mount
|
|
21
|
+
- Fix hook destroyed callback failing to be invoked for children of phx-remove in some cases
|
|
22
|
+
- Do not failsafe reload the page on push timeout if disconnected
|
|
23
|
+
- Do not bubble navigation click events to regular phx-click's
|
|
24
|
+
|
|
25
|
+
## 0.17.7 (2022-02-07)
|
|
26
|
+
|
|
27
|
+
### Enhancements
|
|
28
|
+
- Optimize nested for comprehension diffs
|
|
29
|
+
|
|
30
|
+
### Bug fixes
|
|
31
|
+
- Fix error when `live_redirect` links are clicked when not connected in certain cases
|
|
32
|
+
|
|
3
33
|
## 0.17.6 (2022-01-18)
|
|
4
34
|
|
|
5
35
|
### Enhancements
|
|
@@ -21,6 +51,7 @@
|
|
|
21
51
|
### Deprecations
|
|
22
52
|
- Deprecate `Phoenix.LiveView.get_connect_info/1` in favor of `get_connect_info/2`
|
|
23
53
|
- Deprecate `Phoenix.LiveViewTest.put_connect_info/2` in favor of calling the relevant functions in `Plug.Conn`
|
|
54
|
+
- Deprecate returning "raw" values from upload callbacks on `Phoenix.LiveView.consume_uploaded_entry/3` and `Phoenix.LiveView.consume_uploaded_entries/3`. The callback must return either `{:ok, value}` or `{:postpone, value}`. Returning any other value will emit a warning.
|
|
24
55
|
|
|
25
56
|
## 0.17.5 (2021-11-02)
|
|
26
57
|
|
|
@@ -191,7 +222,7 @@ Some functionality that was previously deprecated has been removed:
|
|
|
191
222
|
|
|
192
223
|
## 0.16.0 (2021-08-10)
|
|
193
224
|
|
|
194
|
-
|
|
225
|
+
### Security Considerations Upgrading from 0.15
|
|
195
226
|
|
|
196
227
|
LiveView v0.16 optimizes live redirects by supporting navigation purely
|
|
197
228
|
over the existing WebSocket connection. This is accomplished by the new
|
|
@@ -606,7 +637,7 @@ as LiveView introduces a macro with that name and it is special cased by the und
|
|
|
606
637
|
- No longer send event metadata by default. Metadata is now opt-in and user defined at the `LiveSocket` level.
|
|
607
638
|
To maintain backwards compatibility with pre-0.13 behaviour, you can provide the following metadata option:
|
|
608
639
|
|
|
609
|
-
```
|
|
640
|
+
```
|
|
610
641
|
let liveSocket = new LiveSocket("/live", Socket, {
|
|
611
642
|
params: {_csrf_token: csrfToken},
|
|
612
643
|
metadata: {
|
|
@@ -705,7 +736,7 @@ The new implementation will check there is a button at `#term .buttons a`, with
|
|
|
705
736
|
- `Phoenix.LiveViewTest.assert_remove/3` has been removed. If the LiveView crashes, it will cause the test to crash too
|
|
706
737
|
- Passing a path with DOM IDs to `render_*` test functions is deprecated. Furthermore, they now require a `phx-target="<%= @id %>"` on the given DOM ID:
|
|
707
738
|
|
|
708
|
-
```
|
|
739
|
+
```heex
|
|
709
740
|
<div id="component-id" phx-target="component-id">
|
|
710
741
|
...
|
|
711
742
|
</div>
|
|
@@ -922,14 +953,14 @@ The steps are:
|
|
|
922
953
|
|
|
923
954
|
4) You should define the CSRF meta tag inside <head> in your layout, before `app.js` is included:
|
|
924
955
|
|
|
925
|
-
```
|
|
956
|
+
```heex
|
|
926
957
|
<%= csrf_meta_tag() %>
|
|
927
958
|
<script type="text/javascript" src="<%= Routes.static_path(@conn, "/js/app.js") %>"></script>
|
|
928
959
|
```
|
|
929
960
|
|
|
930
961
|
5) Then in your app.js:
|
|
931
962
|
|
|
932
|
-
```
|
|
963
|
+
```
|
|
933
964
|
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content");
|
|
934
965
|
let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}});
|
|
935
966
|
```
|
|
@@ -1010,7 +1041,7 @@ Also note that **the session from now on will have string keys**. LiveView will
|
|
|
1010
1041
|
- All `phx-update` containers now require a unique ID
|
|
1011
1042
|
- `LiveSocket` JavaScript constructor now requires explicit dependency injection of Phoenix Socket constructor. For example:
|
|
1012
1043
|
|
|
1013
|
-
```
|
|
1044
|
+
```
|
|
1014
1045
|
import {Socket} from "phoenix"
|
|
1015
1046
|
import LiveSocket from "phoenix_live_view"
|
|
1016
1047
|
|
|
@@ -1026,7 +1057,7 @@ let liveSocket = new LiveSocket("/live", Socket, {...})
|
|
|
1026
1057
|
- Fix params failing to update on re-mounts after live_redirect
|
|
1027
1058
|
- Fix blur event metadata being sent with type of `"focus"`
|
|
1028
1059
|
|
|
1029
|
-
## 0.1.2
|
|
1060
|
+
## 0.1.2 (2019-08-28)
|
|
1030
1061
|
|
|
1031
1062
|
### Backwards incompatible changes
|
|
1032
1063
|
- `phx-value` has no effect, use `phx-value-*` instead
|
package/README.md
CHANGED
|
@@ -5,6 +5,11 @@
|
|
|
5
5
|
Phoenix LiveView enables rich, real-time user experiences
|
|
6
6
|
with server-rendered HTML.
|
|
7
7
|
|
|
8
|
+
Visit the [https://livebeats.fly.dev](https://livebeats.fly.dev/) demo to see the kinds of applications
|
|
9
|
+
you can build, or see a sneak peak below:
|
|
10
|
+
|
|
11
|
+
https://user-images.githubusercontent.com/576796/162234098-31b580fe-e424-47e6-b01d-cd2cfcf823a9.mp4
|
|
12
|
+
|
|
8
13
|
After you [install Elixir](https://elixir-lang.org/install.html)
|
|
9
14
|
in your machine, you can create your first LiveView app in two
|
|
10
15
|
steps:
|
|
@@ -125,7 +130,7 @@ $ npm install --save --prefix assets mdn-polyfills url-search-params-polyfill fo
|
|
|
125
130
|
|
|
126
131
|
Note: The `shim-keyboard-event-key` polyfill is also required for [MS Edge 12-18](https://caniuse.com/#feat=keyboardevent-key).
|
|
127
132
|
|
|
128
|
-
```
|
|
133
|
+
```
|
|
129
134
|
// assets/js/app.js
|
|
130
135
|
import "mdn-polyfills/Object.assign"
|
|
131
136
|
import "mdn-polyfills/CustomEvent"
|
|
@@ -250,8 +250,10 @@ let DOM = {
|
|
|
250
250
|
return this.isPhxChild(el) ? el : this.all(el, `[${PHX_PARENT_ID}]`)[0]
|
|
251
251
|
},
|
|
252
252
|
|
|
253
|
-
dispatchEvent(target,
|
|
254
|
-
let
|
|
253
|
+
dispatchEvent(target, name, opts = {}){
|
|
254
|
+
let bubbles = opts.bubbles === undefined ? true : !!opts.bubbles
|
|
255
|
+
let eventOpts = {bubbles: bubbles, cancelable: true, detail: opts.detail || {}}
|
|
256
|
+
let event = name === "click" ? new MouseEvent("click", eventOpts) : new CustomEvent(name, eventOpts)
|
|
255
257
|
target.dispatchEvent(event)
|
|
256
258
|
},
|
|
257
259
|
|
|
@@ -287,7 +289,7 @@ let DOM = {
|
|
|
287
289
|
|
|
288
290
|
mergeFocusedInput(target, source){
|
|
289
291
|
// skip selects because FF will reset highlighted index for any setAttribute
|
|
290
|
-
if(!(target instanceof HTMLSelectElement)){ DOM.mergeAttrs(target, source, {
|
|
292
|
+
if(!(target instanceof HTMLSelectElement)){ DOM.mergeAttrs(target, source, {exclude: ["value"]}) }
|
|
291
293
|
if(source.readOnly){
|
|
292
294
|
target.setAttribute("readonly", true)
|
|
293
295
|
} else {
|
|
@@ -24,18 +24,23 @@ let JS = {
|
|
|
24
24
|
|
|
25
25
|
// commands
|
|
26
26
|
|
|
27
|
-
exec_dispatch(eventType, phxEvent, view, sourceEl, el, {to, event, detail}){
|
|
28
|
-
|
|
27
|
+
exec_dispatch(eventType, phxEvent, view, sourceEl, el, {to, event, detail, bubbles}){
|
|
28
|
+
detail = detail || {}
|
|
29
|
+
detail.dispatcher = sourceEl
|
|
30
|
+
DOM.dispatchEvent(el, event, {detail, bubbles})
|
|
29
31
|
},
|
|
30
32
|
|
|
31
33
|
exec_push(eventType, phxEvent, view, sourceEl, el, args){
|
|
32
|
-
|
|
34
|
+
if(!view.isConnected()){ return }
|
|
35
|
+
|
|
36
|
+
let {event, data, target, page_loading, loading, value, dispatcher} = args
|
|
33
37
|
let pushOpts = {loading, value, target, page_loading: !!page_loading}
|
|
34
|
-
let targetSrc = eventType === "change" ?
|
|
38
|
+
let targetSrc = eventType === "change" && dispatcher ? dispatcher : sourceEl
|
|
35
39
|
let phxTarget = target || targetSrc.getAttribute(view.binding("target")) || targetSrc
|
|
36
40
|
view.withinTargets(phxTarget, (targetView, targetCtx) => {
|
|
37
41
|
if(eventType === "change"){
|
|
38
42
|
let {newCid, _target, callback} = args
|
|
43
|
+
_target = _target || (sourceEl instanceof HTMLInputElement ? sourceEl.name : undefined)
|
|
39
44
|
if(_target){ pushOpts._target = _target }
|
|
40
45
|
targetView.pushInput(sourceEl, targetCtx, newCid, event || phxEvent, pushOpts, callback)
|
|
41
46
|
} else if(eventType === "submit"){
|
|
@@ -170,10 +175,10 @@ let JS = {
|
|
|
170
175
|
|
|
171
176
|
setOrRemoveAttrs(el, sets, removes){
|
|
172
177
|
let [prevSets, prevRemoves] = DOM.getSticky(el, "attrs", [[], []])
|
|
173
|
-
|
|
174
|
-
let
|
|
175
|
-
let newSets = prevSets.filter(([attr, _val]) =>
|
|
176
|
-
let newRemoves = prevRemoves.filter(attr => !
|
|
178
|
+
|
|
179
|
+
let alteredAttrs = sets.map(([attr, _val]) => attr).concat(removes);
|
|
180
|
+
let newSets = prevSets.filter(([attr, _val]) => !alteredAttrs.includes(attr)).concat(sets);
|
|
181
|
+
let newRemoves = prevRemoves.filter((attr) => !alteredAttrs.includes(attr)).concat(removes);
|
|
177
182
|
|
|
178
183
|
DOM.putSticky(el, "attrs", currentEl => {
|
|
179
184
|
newRemoves.forEach(attr => currentEl.removeAttribute(attr))
|
|
@@ -182,8 +187,6 @@ let JS = {
|
|
|
182
187
|
})
|
|
183
188
|
},
|
|
184
189
|
|
|
185
|
-
hasSet(sets, nameSearch){ return sets.find(([name, val]) => name === nameSearch) },
|
|
186
|
-
|
|
187
190
|
hasAllClasses(el, classes){ return classes.every(name => el.classList.contains(name)) },
|
|
188
191
|
|
|
189
192
|
isToggledOut(el, outClasses){
|
|
@@ -123,7 +123,7 @@ export default class LiveSocket {
|
|
|
123
123
|
a phoenix Socket must be provided as the second argument to the LiveSocket constructor. For example:
|
|
124
124
|
|
|
125
125
|
import {Socket} from "phoenix"
|
|
126
|
-
import LiveSocket from "phoenix_live_view"
|
|
126
|
+
import {LiveSocket} from "phoenix_live_view"
|
|
127
127
|
let liveSocket = new LiveSocket("/live", Socket, {...})
|
|
128
128
|
`)
|
|
129
129
|
}
|
|
@@ -138,8 +138,9 @@ export default class LiveSocket {
|
|
|
138
138
|
this.prevActive = null
|
|
139
139
|
this.silenced = false
|
|
140
140
|
this.main = null
|
|
141
|
+
this.outgoingMainEl = null
|
|
142
|
+
this.clickStartedAtTarget = null
|
|
141
143
|
this.linkRef = 1
|
|
142
|
-
this.clickRef = 1
|
|
143
144
|
this.roots = {}
|
|
144
145
|
this.href = window.location.href
|
|
145
146
|
this.pendingLink = null
|
|
@@ -173,11 +174,13 @@ export default class LiveSocket {
|
|
|
173
174
|
|
|
174
175
|
isDebugEnabled(){ return this.sessionStorage.getItem(PHX_LV_DEBUG) === "true" }
|
|
175
176
|
|
|
177
|
+
isDebugDisabled(){ return this.sessionStorage.getItem(PHX_LV_DEBUG) === "false" }
|
|
178
|
+
|
|
176
179
|
enableDebug(){ this.sessionStorage.setItem(PHX_LV_DEBUG, "true") }
|
|
177
180
|
|
|
178
181
|
enableProfiling(){ this.sessionStorage.setItem(PHX_LV_PROFILE, "true") }
|
|
179
182
|
|
|
180
|
-
disableDebug(){ this.sessionStorage.
|
|
183
|
+
disableDebug(){ this.sessionStorage.setItem(PHX_LV_DEBUG, "false") }
|
|
181
184
|
|
|
182
185
|
disableProfiling(){ this.sessionStorage.removeItem(PHX_LV_PROFILE) }
|
|
183
186
|
|
|
@@ -197,6 +200,8 @@ export default class LiveSocket {
|
|
|
197
200
|
getSocket(){ return this.socket }
|
|
198
201
|
|
|
199
202
|
connect(){
|
|
203
|
+
// enable debug by default if on localhost and not explicitly disabled
|
|
204
|
+
if(window.location.hostname === "localhost" && !this.isDebugDisabled()){ this.enableDebug() }
|
|
200
205
|
let doConnect = () => {
|
|
201
206
|
if(this.joinRootViews()){
|
|
202
207
|
this.bindTopLevelEvents()
|
|
@@ -262,7 +267,7 @@ export default class LiveSocket {
|
|
|
262
267
|
let latency = this.getLatencySim()
|
|
263
268
|
let oldJoinCount = view.joinCount
|
|
264
269
|
if(!latency){
|
|
265
|
-
if(opts.timeout){
|
|
270
|
+
if(this.isConnected() && opts.timeout){
|
|
266
271
|
return push().receive("timeout", () => {
|
|
267
272
|
if(view.joinCount === oldJoinCount && !view.isDestroyed()){
|
|
268
273
|
this.reloadWithJitter(view, () => {
|
|
@@ -342,8 +347,8 @@ export default class LiveSocket {
|
|
|
342
347
|
}
|
|
343
348
|
|
|
344
349
|
replaceMain(href, flash, callback = null, linkRef = this.setPendingLink(href)){
|
|
345
|
-
|
|
346
|
-
let newMainEl = DOM.cloneNode(
|
|
350
|
+
this.outgoingMainEl = this.outgoingMainEl || this.main.el
|
|
351
|
+
let newMainEl = DOM.cloneNode(this.outgoingMainEl, "")
|
|
347
352
|
this.main.showLoader(this.loaderTimeout)
|
|
348
353
|
this.main.destroy()
|
|
349
354
|
|
|
@@ -354,7 +359,8 @@ export default class LiveSocket {
|
|
|
354
359
|
if(joinCount === 1 && this.commitPendingLink(linkRef)){
|
|
355
360
|
this.requestDOMUpdate(() => {
|
|
356
361
|
DOM.findPhxSticky(document).forEach(el => newMainEl.appendChild(el))
|
|
357
|
-
|
|
362
|
+
this.outgoingMainEl.replaceWith(newMainEl)
|
|
363
|
+
this.outgoingMainEl = null
|
|
358
364
|
callback && callback()
|
|
359
365
|
onDone()
|
|
360
366
|
})
|
|
@@ -457,7 +463,7 @@ export default class LiveSocket {
|
|
|
457
463
|
this.boundTopLevelEvents = true
|
|
458
464
|
// enter failsafe reload if server has gone away intentionally, such as "disconnect" broadcast
|
|
459
465
|
this.socket.onClose(event => {
|
|
460
|
-
if(event.code === 1000 && this.main){
|
|
466
|
+
if(event && event.code === 1000 && this.main){
|
|
461
467
|
this.reloadWithJitter(this.main)
|
|
462
468
|
}
|
|
463
469
|
})
|
|
@@ -569,6 +575,7 @@ export default class LiveSocket {
|
|
|
569
575
|
}
|
|
570
576
|
|
|
571
577
|
bindClicks(){
|
|
578
|
+
window.addEventListener("mousedown", e => this.clickStartedAtTarget = e.target)
|
|
572
579
|
this.bindClick("click", "click", false)
|
|
573
580
|
this.bindClick("mousedown", "capture-click", true)
|
|
574
581
|
}
|
|
@@ -576,15 +583,14 @@ export default class LiveSocket {
|
|
|
576
583
|
bindClick(eventName, bindingName, capture){
|
|
577
584
|
let click = this.binding(bindingName)
|
|
578
585
|
window.addEventListener(eventName, e => {
|
|
579
|
-
if(!this.isConnected()){ return }
|
|
580
|
-
this.clickRef++
|
|
581
|
-
let clickRefWas = this.clickRef
|
|
582
586
|
let target = null
|
|
583
587
|
if(capture){
|
|
584
588
|
target = e.target.matches(`[${click}]`) ? e.target : e.target.querySelector(`[${click}]`)
|
|
585
589
|
} else {
|
|
586
|
-
|
|
587
|
-
|
|
590
|
+
let clickStartedAtTarget = this.clickStartedAtTarget || e.target
|
|
591
|
+
target = closestPhxBinding(clickStartedAtTarget, click)
|
|
592
|
+
this.dispatchClickAway(e, clickStartedAtTarget)
|
|
593
|
+
this.clickStartedAtTarget = null
|
|
588
594
|
}
|
|
589
595
|
let phxEvent = target && target.getAttribute(click)
|
|
590
596
|
if(!phxEvent){ return }
|
|
@@ -598,15 +604,13 @@ export default class LiveSocket {
|
|
|
598
604
|
}, capture)
|
|
599
605
|
}
|
|
600
606
|
|
|
601
|
-
dispatchClickAway(e,
|
|
607
|
+
dispatchClickAway(e, clickStartedAt){
|
|
602
608
|
let phxClickAway = this.binding("click-away")
|
|
603
|
-
let phxClick = this.binding("click")
|
|
604
609
|
DOM.all(document, `[${phxClickAway}]`, el => {
|
|
605
|
-
if(!(el.isSameNode(
|
|
610
|
+
if(!(el.isSameNode(clickStartedAt) || el.contains(clickStartedAt))){
|
|
606
611
|
this.withinOwners(e.target, view => {
|
|
607
612
|
let phxEvent = el.getAttribute(phxClickAway)
|
|
608
613
|
if(JS.isVisible(el)){
|
|
609
|
-
let target = e.target.closest(`[${phxClick}]`) || e.target
|
|
610
614
|
JS.exec("click", phxEvent, view, el, ["push", {data: this.eventMeta("click", e, e.target)}])
|
|
611
615
|
}
|
|
612
616
|
})
|
|
@@ -649,9 +653,11 @@ export default class LiveSocket {
|
|
|
649
653
|
let type = target && target.getAttribute(PHX_LIVE_LINK)
|
|
650
654
|
let wantsNewTab = e.metaKey || e.ctrlKey || e.button === 1
|
|
651
655
|
if(!type || !this.isConnected() || !this.main || wantsNewTab){ return }
|
|
656
|
+
|
|
652
657
|
let href = target.href
|
|
653
658
|
let linkState = target.getAttribute(PHX_LINK_STATE)
|
|
654
659
|
e.preventDefault()
|
|
660
|
+
e.stopImmediatePropagation() // do not bubble click to regular phx-click bindings
|
|
655
661
|
if(this.pendingLink === href){ return }
|
|
656
662
|
|
|
657
663
|
this.requestDOMUpdate(() => {
|
|
@@ -667,7 +673,7 @@ export default class LiveSocket {
|
|
|
667
673
|
}
|
|
668
674
|
|
|
669
675
|
dispatchEvent(event, payload = {}){
|
|
670
|
-
DOM.dispatchEvent(window, `phx:${event}`, payload)
|
|
676
|
+
DOM.dispatchEvent(window, `phx:${event}`, {detail: payload})
|
|
671
677
|
}
|
|
672
678
|
|
|
673
679
|
dispatchEvents(events){
|
|
@@ -675,8 +681,8 @@ export default class LiveSocket {
|
|
|
675
681
|
}
|
|
676
682
|
|
|
677
683
|
withPageLoading(info, callback){
|
|
678
|
-
DOM.dispatchEvent(window, "phx:page-loading-start", info)
|
|
679
|
-
let done = () => DOM.dispatchEvent(window, "phx:page-loading-stop", info)
|
|
684
|
+
DOM.dispatchEvent(window, "phx:page-loading-start", {detail: info})
|
|
685
|
+
let done = () => DOM.dispatchEvent(window, "phx:page-loading-stop", {detail: info})
|
|
680
686
|
return callback ? callback(done) : done
|
|
681
687
|
}
|
|
682
688
|
|
|
@@ -735,10 +741,15 @@ export default class LiveSocket {
|
|
|
735
741
|
|
|
736
742
|
for(let type of ["change", "input"]){
|
|
737
743
|
this.on(type, e => {
|
|
744
|
+
let phxChange = this.binding("change")
|
|
738
745
|
let input = e.target
|
|
739
|
-
let
|
|
746
|
+
let inputEvent = input.getAttribute(phxChange)
|
|
747
|
+
let formEvent = input.form && input.form.getAttribute(phxChange)
|
|
748
|
+
let phxEvent = inputEvent || formEvent
|
|
740
749
|
if(!phxEvent){ return }
|
|
741
750
|
if(input.type === "number" && input.validity && input.validity.badInput){ return }
|
|
751
|
+
|
|
752
|
+
let dispatcher = inputEvent ? input : input.form
|
|
742
753
|
let currentIterations = iterations
|
|
743
754
|
iterations++
|
|
744
755
|
let {at: at, type: lastType} = DOM.private(input, "prev-iteration") || {}
|
|
@@ -748,12 +759,12 @@ export default class LiveSocket {
|
|
|
748
759
|
DOM.putPrivate(input, "prev-iteration", {at: currentIterations, type: type})
|
|
749
760
|
|
|
750
761
|
this.debounce(input, e, () => {
|
|
751
|
-
this.withinOwners(
|
|
762
|
+
this.withinOwners(dispatcher, view => {
|
|
752
763
|
DOM.putPrivate(input, PHX_HAS_FOCUSED, true)
|
|
753
764
|
if(!DOM.isTextualInput(input)){
|
|
754
765
|
this.setActiveElement(input)
|
|
755
766
|
}
|
|
756
|
-
JS.exec("change", phxEvent, view, input, ["push", {_target: e.target.name}])
|
|
767
|
+
JS.exec("change", phxEvent, view, input, ["push", {_target: e.target.name, dispatcher: dispatcher}])
|
|
757
768
|
})
|
|
758
769
|
})
|
|
759
770
|
}, false)
|
|
@@ -167,7 +167,7 @@ export default class Rendered {
|
|
|
167
167
|
comprehensionToBuffer(rendered, templates, output){
|
|
168
168
|
let {[DYNAMICS]: dynamics, [STATIC]: statics} = rendered
|
|
169
169
|
statics = this.templateStatic(statics, templates)
|
|
170
|
-
let compTemplates = rendered[TEMPLATES]
|
|
170
|
+
let compTemplates = templates || rendered[TEMPLATES]
|
|
171
171
|
|
|
172
172
|
for(let d = 0; d < dynamics.length; d++){
|
|
173
173
|
let dynamic = dynamics[d]
|
|
@@ -50,7 +50,7 @@ import Rendered from "./rendered"
|
|
|
50
50
|
import ViewHook from "./view_hook"
|
|
51
51
|
import JS from "./js"
|
|
52
52
|
|
|
53
|
-
let serializeForm = (form, meta =
|
|
53
|
+
let serializeForm = (form, meta, onlyNames = []) => {
|
|
54
54
|
let formData = new FormData(form)
|
|
55
55
|
let toRemove = []
|
|
56
56
|
|
|
@@ -62,7 +62,11 @@ let serializeForm = (form, meta = {}) => {
|
|
|
62
62
|
toRemove.forEach(key => formData.delete(key))
|
|
63
63
|
|
|
64
64
|
let params = new URLSearchParams()
|
|
65
|
-
for(let [key, val] of formData.entries()){
|
|
65
|
+
for(let [key, val] of formData.entries()){
|
|
66
|
+
if(onlyNames.length === 0 || onlyNames.indexOf(key) >= 0){
|
|
67
|
+
params.append(key, val)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
66
70
|
for(let metaKey in meta){ params.append(metaKey, meta[metaKey]) }
|
|
67
71
|
|
|
68
72
|
return params.toString()
|
|
@@ -377,10 +381,13 @@ export default class View {
|
|
|
377
381
|
let destroyedCIDs = []
|
|
378
382
|
elements.forEach(parent => {
|
|
379
383
|
let components = DOM.all(parent, `[${PHX_COMPONENT}]`)
|
|
380
|
-
|
|
384
|
+
let hooks = DOM.all(parent, `[${this.binding(PHX_HOOK)}]`)
|
|
385
|
+
components.concat(parent).forEach(el => {
|
|
381
386
|
let cid = this.componentID(el)
|
|
382
387
|
if(isCid(cid) && destroyedCIDs.indexOf(cid) === -1){ destroyedCIDs.push(cid) }
|
|
383
|
-
|
|
388
|
+
})
|
|
389
|
+
hooks.concat(parent).forEach(hookEl => {
|
|
390
|
+
let hook = this.getHook(hookEl)
|
|
384
391
|
hook && this.destroyHook(hook)
|
|
385
392
|
})
|
|
386
393
|
})
|
|
@@ -449,7 +456,7 @@ export default class View {
|
|
|
449
456
|
}
|
|
450
457
|
|
|
451
458
|
update(diff, events){
|
|
452
|
-
if(this.isJoinPending() || this.liveSocket.hasPendingLink()){
|
|
459
|
+
if(this.isJoinPending() || (this.liveSocket.hasPendingLink() && !DOM.isPhxSticky(this.el))){
|
|
453
460
|
return this.pendingDiffs.push({diff, events})
|
|
454
461
|
}
|
|
455
462
|
|
|
@@ -635,7 +642,7 @@ export default class View {
|
|
|
635
642
|
}
|
|
636
643
|
|
|
637
644
|
displayError(){
|
|
638
|
-
if(this.isMain()){ DOM.dispatchEvent(window, "phx:page-loading-start", {to: this.href, kind: "error"}) }
|
|
645
|
+
if(this.isMain()){ DOM.dispatchEvent(window, "phx:page-loading-start", {detail: {to: this.href, kind: "error"}}) }
|
|
639
646
|
this.showLoader()
|
|
640
647
|
this.setContainerClasses(PHX_DISCONNECTED_CLASS, PHX_ERROR_CLASS)
|
|
641
648
|
}
|
|
@@ -821,7 +828,12 @@ export default class View {
|
|
|
821
828
|
let uploads
|
|
822
829
|
let cid = isCid(forceCid) ? forceCid : this.targetComponentID(inputEl.form, targetCtx)
|
|
823
830
|
let refGenerator = () => this.putRef([inputEl, inputEl.form], "change", opts)
|
|
824
|
-
let formData
|
|
831
|
+
let formData
|
|
832
|
+
if(inputEl.getAttribute(this.binding("change"))){
|
|
833
|
+
formData = serializeForm(inputEl.form, {_target: opts._target}, [inputEl.name])
|
|
834
|
+
} else {
|
|
835
|
+
formData = serializeForm(inputEl.form, {_target: opts._target})
|
|
836
|
+
}
|
|
825
837
|
if(DOM.isUploadInput(inputEl) && inputEl.files && inputEl.files.length > 0){
|
|
826
838
|
LiveUploader.trackFiles(inputEl, Array.from(inputEl.files))
|
|
827
839
|
}
|
|
@@ -930,7 +942,7 @@ export default class View {
|
|
|
930
942
|
}, onReply)
|
|
931
943
|
})
|
|
932
944
|
} else {
|
|
933
|
-
let formData = serializeForm(formEl)
|
|
945
|
+
let formData = serializeForm(formEl, {})
|
|
934
946
|
this.pushWithReply(refGenerator, "event", {
|
|
935
947
|
type: "form",
|
|
936
948
|
event: phxEvent,
|
|
@@ -985,7 +997,7 @@ export default class View {
|
|
|
985
997
|
let inputs = DOM.findUploadInputs(this.el).filter(el => el.name === name)
|
|
986
998
|
if(inputs.length === 0){ logError(`no live file inputs found matching the name "${name}"`) }
|
|
987
999
|
else if(inputs.length > 1){ logError(`duplicate live file inputs found matching the name "${name}"`) }
|
|
988
|
-
else { DOM.dispatchEvent(inputs[0], PHX_TRACK_UPLOADS, {files: filesOrBlobs}) }
|
|
1000
|
+
else { DOM.dispatchEvent(inputs[0], PHX_TRACK_UPLOADS, {detail: {files: filesOrBlobs}}) }
|
|
989
1001
|
}
|
|
990
1002
|
|
|
991
1003
|
pushFormRecovery(form, newCid, callback){
|
|
@@ -1000,8 +1012,9 @@ export default class View {
|
|
|
1000
1012
|
pushLinkPatch(href, targetEl, callback){
|
|
1001
1013
|
let linkRef = this.liveSocket.setPendingLink(href)
|
|
1002
1014
|
let refGen = targetEl ? () => this.putRef([targetEl], "click") : null
|
|
1015
|
+
let fallback = () => this.liveSocket.redirect(window.location.href)
|
|
1003
1016
|
|
|
1004
|
-
this.pushWithReply(refGen, "live_patch", {url: href}, resp => {
|
|
1017
|
+
let push = this.pushWithReply(refGen, "live_patch", {url: href}, resp => {
|
|
1005
1018
|
this.liveSocket.requestDOMUpdate(() => {
|
|
1006
1019
|
if(resp.link_redirect){
|
|
1007
1020
|
this.liveSocket.replaceMain(href, null, callback, linkRef)
|
|
@@ -1013,7 +1026,13 @@ export default class View {
|
|
|
1013
1026
|
callback && callback(linkRef)
|
|
1014
1027
|
}
|
|
1015
1028
|
})
|
|
1016
|
-
})
|
|
1029
|
+
})
|
|
1030
|
+
|
|
1031
|
+
if(push){
|
|
1032
|
+
push.receive("timeout", fallback)
|
|
1033
|
+
} else {
|
|
1034
|
+
fallback()
|
|
1035
|
+
}
|
|
1017
1036
|
}
|
|
1018
1037
|
|
|
1019
1038
|
formsForRecovery(html){
|