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 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
- ## # Security Considerations Upgrading from 0.15
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
- ```javascript
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
- ```html
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
- ```html
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
- ```javascript
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
- ```javascript
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
- ```javascript
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, eventString, detail = {}){
254
- let event = new CustomEvent(eventString, {bubbles: true, cancelable: true, detail: detail})
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, {except: ["value"]}) }
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
- DOM.dispatchEvent(el, event, detail)
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
- let {event, data, target, page_loading, loading, value} = args
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" ? sourceEl.form : sourceEl
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
- let keepSets = sets.filter(([attr, _val]) => !this.hasSet(prevSets, attr) && !el.attributes.getNamedItem(attr))
174
- let keepRemoves = removes.filter(attr => prevRemoves.indexOf(attr) < 0 && el.attributes.getNamedItem(attr))
175
- let newSets = prevSets.filter(([attr, _val]) => removes.indexOf(attr) < 0).concat(keepSets)
176
- let newRemoves = prevRemoves.filter(attr => !this.hasSet(sets, attr)).concat(keepRemoves)
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.removeItem(PHX_LV_DEBUG) }
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
- let oldMainEl = this.main.el
346
- let newMainEl = DOM.cloneNode(oldMainEl, "")
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
- oldMainEl.replaceWith(newMainEl)
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
- target = closestPhxBinding(e.target, click)
587
- this.dispatchClickAway(e, clickRefWas)
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, clickRefWas){
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(e.target) || el.contains(e.target))){
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 phxEvent = input.form && input.form.getAttribute(this.binding("change"))
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(input.form, view => {
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()){ params.append(key, val) }
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
- components.concat(parent).forEach(el => {
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
- let hook = this.getHook(el)
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 = serializeForm(inputEl.form, {_target: opts._target})
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
- }).receive("timeout", () => this.liveSocket.redirect(window.location.href))
1029
+ })
1030
+
1031
+ if(push){
1032
+ push.receive("timeout", fallback)
1033
+ } else {
1034
+ fallback()
1035
+ }
1017
1036
  }
1018
1037
 
1019
1038
  formsForRecovery(html){
@@ -5,7 +5,7 @@ export default class ViewHook {
5
5
 
6
6
  constructor(view, el, callbacks){
7
7
  this.__view = view
8
- this.__liveSocket = view.liveSocket
8
+ this.liveSocket = view.liveSocket
9
9
  this.__callbacks = callbacks
10
10
  this.__listeners = new Set()
11
11
  this.__isDisconnected = false
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phoenix_live_view",
3
- "version": "0.17.6",
3
+ "version": "0.17.9",
4
4
  "description": "The Phoenix LiveView JavaScript client.",
5
5
  "license": "MIT",
6
6
  "module": "./priv/static/phoenix_live_view.esm.js",