phoenix_live_view 0.16.3 → 0.17.2

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.
@@ -46,6 +46,7 @@ import DOMPatch from "./dom_patch"
46
46
  import LiveUploader from "./live_uploader"
47
47
  import Rendered from "./rendered"
48
48
  import ViewHook from "./view_hook"
49
+ import JS from "./js"
49
50
 
50
51
  let serializeForm = (form, meta = {}) => {
51
52
  let formData = new FormData(form)
@@ -83,8 +84,8 @@ export default class View {
83
84
  this.joinCount = this.parent ? this.parent.joinCount - 1 : 0
84
85
  this.joinPending = true
85
86
  this.destroyed = false
86
- this.joinCallback = function (){ }
87
- this.stopCallback = function (){ }
87
+ this.joinCallback = function(onDone){ onDone && onDone() }
88
+ this.stopCallback = function(){ }
88
89
  this.pendingJoinOps = this.parent ? null : []
89
90
  this.viewHooks = {}
90
91
  this.uploaders = {}
@@ -166,8 +167,6 @@ export default class View {
166
167
  this.el.classList.add(...classes)
167
168
  }
168
169
 
169
- isLoading(){ return this.el.classList.contains(PHX_DISCONNECTED_CLASS) }
170
-
171
170
  showLoader(timeout){
172
171
  clearTimeout(this.loaderTimer)
173
172
  if(timeout){
@@ -191,17 +190,21 @@ export default class View {
191
190
  this.liveSocket.log(this, kind, msgCallback)
192
191
  }
193
192
 
193
+ transition(time, onDone = function(){}){
194
+ this.liveSocket.transition(time, onDone)
195
+ }
196
+
194
197
  withinTargets(phxTarget, callback){
195
- if(phxTarget instanceof HTMLElement){
198
+ if(phxTarget instanceof HTMLElement || phxTarget instanceof SVGElement){
196
199
  return this.liveSocket.owner(phxTarget, view => callback(view, phxTarget))
197
200
  }
198
201
 
199
- if(/^(0|[1-9]\d*)$/.test(phxTarget)){
202
+ if(typeof(phxTarget) === "number" || /^(0|[1-9]\d*)$/.test(phxTarget)){
200
203
  let targets = DOM.findComponentNodeList(this.el, phxTarget)
201
204
  if(targets.length === 0){
202
205
  logError(`no component found matching phx-target of ${phxTarget}`)
203
206
  } else {
204
- callback(this, targets[0])
207
+ callback(this, parseInt(phxTarget))
205
208
  }
206
209
  } else {
207
210
  let targets = Array.from(document.querySelectorAll(phxTarget))
@@ -289,12 +292,6 @@ export default class View {
289
292
  this.el.setAttribute(PHX_ROOT_ID, this.root.id)
290
293
  }
291
294
 
292
- dispatchEvents(events){
293
- events.forEach(([event, payload]) => {
294
- window.dispatchEvent(new CustomEvent(`phx:hook:${event}`, {detail: payload}))
295
- })
296
- }
297
-
298
295
  applyJoinPatch(live_patch, html, events){
299
296
  this.attachTrueDocEl()
300
297
  let patch = new DOMPatch(this, this.el, this.id, html, null)
@@ -307,7 +304,7 @@ export default class View {
307
304
  })
308
305
 
309
306
  this.joinPending = false
310
- this.dispatchEvents(events)
307
+ this.liveSocket.dispatchEvents(events)
311
308
  this.applyPendingUpdates()
312
309
 
313
310
  if(live_patch){
@@ -330,7 +327,7 @@ export default class View {
330
327
  }
331
328
 
332
329
  performPatch(patch, pruneCids){
333
- let destroyedCIDs = []
330
+ let removedEls = []
334
331
  let phxChildrenAdded = false
335
332
  let updatedHookIds = new Set()
336
333
 
@@ -353,22 +350,33 @@ export default class View {
353
350
  })
354
351
 
355
352
  patch.after("discarded", (el) => {
356
- let cid = this.componentID(el)
357
- if(isCid(cid) && destroyedCIDs.indexOf(cid) === -1){ destroyedCIDs.push(cid) }
358
- let hook = this.getHook(el)
359
- hook && this.destroyHook(hook)
353
+ if(el.nodeType === Node.ELEMENT_NODE){ removedEls.push(el) }
360
354
  })
361
355
 
356
+ patch.after("transitionsDiscarded", els => this.afterElementsRemoved(els, pruneCids))
362
357
  patch.perform()
358
+ this.afterElementsRemoved(removedEls, pruneCids)
359
+
360
+ return phxChildrenAdded
361
+ }
363
362
 
363
+ afterElementsRemoved(elements, pruneCids){
364
+ let destroyedCIDs = []
365
+ elements.forEach(parent => {
366
+ let components = DOM.all(parent, `[${PHX_COMPONENT}]`)
367
+ components.concat(parent).forEach(el => {
368
+ let cid = this.componentID(el)
369
+ if(isCid(cid) && destroyedCIDs.indexOf(cid) === -1){ destroyedCIDs.push(cid) }
370
+ let hook = this.getHook(el)
371
+ hook && this.destroyHook(hook)
372
+ })
373
+ })
364
374
  // We should not pruneCids on joins. Otherwise, in case of
365
375
  // rejoins, we may notify cids that no longer belong to the
366
376
  // current LiveView to be removed.
367
377
  if(pruneCids){
368
378
  this.maybePushComponentsDestroyed(destroyedCIDs)
369
379
  }
370
-
371
- return phxChildrenAdded
372
380
  }
373
381
 
374
382
  joinNewChildren(){
@@ -419,11 +427,12 @@ export default class View {
419
427
  }
420
428
 
421
429
  onAllChildJoinsComplete(){
422
- this.joinCallback()
423
- this.pendingJoinOps.forEach(([view, op]) => {
424
- if(!view.isDestroyed()){ op() }
430
+ this.joinCallback(() => {
431
+ this.pendingJoinOps.forEach(([view, op]) => {
432
+ if(!view.isDestroyed()){ op() }
433
+ })
434
+ this.pendingJoinOps = []
425
435
  })
426
- this.pendingJoinOps = []
427
436
  }
428
437
 
429
438
  update(diff, events){
@@ -452,7 +461,7 @@ export default class View {
452
461
  })
453
462
  }
454
463
 
455
- this.dispatchEvents(events)
464
+ this.liveSocket.dispatchEvents(events)
456
465
  if(phxChildrenAdded){ this.joinNewChildren() }
457
466
  }
458
467
 
@@ -509,7 +518,7 @@ export default class View {
509
518
  if(this.isJoinPending()){
510
519
  this.root.pendingJoinOps.push([this, () => cb(resp)])
511
520
  } else {
512
- cb(resp)
521
+ this.liveSocket.requestDOMUpdate(() => cb(resp))
513
522
  }
514
523
  })
515
524
  }
@@ -518,7 +527,9 @@ export default class View {
518
527
  // The diff event should be handled by the regular update operations.
519
528
  // All other operations are queued to be applied only after join.
520
529
  this.liveSocket.onChannel(this.channel, "diff", (rawDiff) => {
521
- this.applyDiff("update", rawDiff, ({diff, events}) => this.update(diff, events))
530
+ this.liveSocket.requestDOMUpdate(() => {
531
+ this.applyDiff("update", rawDiff, ({diff, events}) => this.update(diff, events))
532
+ })
522
533
  })
523
534
  this.onChannel("redirect", ({to, flash}) => this.onRedirect({to, flash}))
524
535
  this.onChannel("live_patch", (redir) => this.onLivePatch(redir))
@@ -557,10 +568,17 @@ export default class View {
557
568
  if(!this.parent){
558
569
  this.stopCallback = this.liveSocket.withPageLoading({to: this.href, kind: "initial"})
559
570
  }
560
- this.joinCallback = () => callback && callback(this.joinCount)
571
+ this.joinCallback = (onDone) => {
572
+ onDone = onDone || function(){}
573
+ callback ? callback(this.joinCount, onDone) : onDone()
574
+ }
561
575
  this.liveSocket.wrapPush(this, {timeout: false}, () => {
562
576
  return this.channel.join()
563
- .receive("ok", data => !this.isDestroyed() && this.onJoin(data))
577
+ .receive("ok", data => {
578
+ if(!this.isDestroyed()){
579
+ this.liveSocket.requestDOMUpdate(() => this.onJoin(data))
580
+ }
581
+ })
564
582
  .receive("error", resp => !this.isDestroyed() && this.onJoinError(resp))
565
583
  .receive("timeout", () => !this.isDestroyed() && this.onJoinError({reason: "timeout"}))
566
584
  })
@@ -612,9 +630,9 @@ export default class View {
612
630
  pushWithReply(refGenerator, event, payload, onReply = function (){ }){
613
631
  if(!this.isConnected()){ return }
614
632
 
615
- let [ref, [el]] = refGenerator ? refGenerator() : [null, []]
616
- let onLoadingDone = function (){ }
617
- if(el && (el.getAttribute(this.binding(PHX_PAGE_LOADING)) !== null)){
633
+ let [ref, [el], opts] = refGenerator ? refGenerator() : [null, [], {}]
634
+ let onLoadingDone = function(){ }
635
+ if(opts.page_loading || (el && (el.getAttribute(this.binding(PHX_PAGE_LOADING)) !== null))){
618
636
  onLoadingDone = this.liveSocket.withPageLoading({kind: "element", target: el})
619
637
  }
620
638
 
@@ -622,18 +640,20 @@ export default class View {
622
640
  return (
623
641
  this.liveSocket.wrapPush(this, {timeout: true}, () => {
624
642
  return this.channel.push(event, payload, PUSH_TIMEOUT).receive("ok", resp => {
625
- let hookReply = null
626
- if(ref !== null){ this.undoRefs(ref) }
627
- if(resp.diff){
628
- hookReply = this.applyDiff("update", resp.diff, ({diff, events}) => {
629
- this.update(diff, events)
630
- })
631
- }
632
- if(resp.redirect){ this.onRedirect(resp.redirect) }
633
- if(resp.live_patch){ this.onLivePatch(resp.live_patch) }
634
- if(resp.live_redirect){ this.onLiveRedirect(resp.live_redirect) }
635
- onLoadingDone()
636
- onReply(resp, hookReply)
643
+ this.liveSocket.requestDOMUpdate(() => {
644
+ let hookReply = null
645
+ if(ref !== null){ this.undoRefs(ref) }
646
+ if(resp.diff){
647
+ hookReply = this.applyDiff("update", resp.diff, ({diff, events}) => {
648
+ this.update(diff, events)
649
+ })
650
+ }
651
+ if(resp.redirect){ this.onRedirect(resp.redirect) }
652
+ if(resp.live_patch){ this.onLivePatch(resp.live_patch) }
653
+ if(resp.live_redirect){ this.onLiveRedirect(resp.live_redirect) }
654
+ onLoadingDone()
655
+ onReply(resp, hookReply)
656
+ })
637
657
  })
638
658
  })
639
659
  )
@@ -671,9 +691,10 @@ export default class View {
671
691
  })
672
692
  }
673
693
 
674
- putRef(elements, event){
694
+ putRef(elements, event, opts = {}){
675
695
  let newRef = this.ref++
676
696
  let disableWith = this.binding(PHX_DISABLE_WITH)
697
+ if(opts.loading){ elements = elements.concat(DOM.all(document, opts.loading))}
677
698
 
678
699
  elements.forEach(el => {
679
700
  el.classList.add(`phx-${event}-loading`)
@@ -686,7 +707,7 @@ export default class View {
686
707
  el.innerText = disableText
687
708
  }
688
709
  })
689
- return [newRef, elements]
710
+ return [newRef, elements, opts]
690
711
  }
691
712
 
692
713
  componentID(el){
@@ -695,7 +716,9 @@ export default class View {
695
716
  }
696
717
 
697
718
  targetComponentID(target, targetCtx){
698
- if(target.getAttribute(this.binding("target"))){
719
+ if(isCid(targetCtx)){
720
+ return targetCtx
721
+ } else if(target.getAttribute(this.binding("target"))){
699
722
  return this.closestComponentID(targetCtx)
700
723
  } else {
701
724
  return null
@@ -703,7 +726,9 @@ export default class View {
703
726
  }
704
727
 
705
728
  closestComponentID(targetCtx){
706
- if(targetCtx){
729
+ if(isCid(targetCtx)){
730
+ return targetCtx
731
+ } else if(targetCtx){
707
732
  return maybe(targetCtx.closest(`[${PHX_COMPONENT}]`), el => this.ownsElement(el) && this.componentID(el))
708
733
  } else {
709
734
  return null
@@ -715,8 +740,8 @@ export default class View {
715
740
  this.log("hook", () => ["unable to push hook event. LiveView not connected", event, payload])
716
741
  return false
717
742
  }
718
- let [ref, els] = this.putRef([], "hook")
719
- this.pushWithReply(() => [ref, els], "event", {
743
+ let [ref, els, opts] = this.putRef([], "hook")
744
+ this.pushWithReply(() => [ref, els, opts], "event", {
720
745
  type: "hook",
721
746
  event: event,
722
747
  value: payload,
@@ -726,40 +751,37 @@ export default class View {
726
751
  return ref
727
752
  }
728
753
 
729
- extractMeta(el, meta){
754
+ extractMeta(el, meta, value){
730
755
  let prefix = this.binding("value-")
731
756
  for(let i = 0; i < el.attributes.length; i++){
757
+ if(!meta){ meta = {} }
732
758
  let name = el.attributes[i].name
733
759
  if(name.startsWith(prefix)){ meta[name.replace(prefix, "")] = el.getAttribute(name) }
734
760
  }
735
761
  if(el.value !== undefined){
762
+ if(!meta){ meta = {} }
736
763
  meta.value = el.value
737
764
 
738
765
  if(el.tagName === "INPUT" && CHECKABLE_INPUTS.indexOf(el.type) >= 0 && !el.checked){
739
766
  delete meta.value
740
767
  }
741
768
  }
769
+ if(value){
770
+ if(!meta){ meta = {} }
771
+ for(let key in value){ meta[key] = value[key] }
772
+ }
742
773
  return meta
743
774
  }
744
775
 
745
- pushEvent(type, el, targetCtx, phxEvent, meta){
746
- this.pushWithReply(() => this.putRef([el], type), "event", {
776
+ pushEvent(type, el, targetCtx, phxEvent, meta, opts = {}){
777
+ this.pushWithReply(() => this.putRef([el], type, opts), "event", {
747
778
  type: type,
748
779
  event: phxEvent,
749
- value: this.extractMeta(el, meta),
780
+ value: this.extractMeta(el, meta, opts.value),
750
781
  cid: this.targetComponentID(el, targetCtx)
751
782
  })
752
783
  }
753
784
 
754
- pushKey(keyElement, targetCtx, kind, phxEvent, meta){
755
- this.pushWithReply(() => this.putRef([keyElement], kind), "event", {
756
- type: kind,
757
- event: phxEvent,
758
- value: this.extractMeta(keyElement, meta),
759
- cid: this.targetComponentID(keyElement, targetCtx)
760
- })
761
- }
762
-
763
785
  pushFileProgress(fileEl, entryRef, progress, onReply = function (){ }){
764
786
  this.liveSocket.withinOwners(fileEl.form, (view, targetCtx) => {
765
787
  view.pushWithReply(null, "progress", {
@@ -772,12 +794,12 @@ export default class View {
772
794
  })
773
795
  }
774
796
 
775
- pushInput(inputEl, targetCtx, forceCid, phxEvent, eventTarget, callback){
797
+ pushInput(inputEl, targetCtx, forceCid, phxEvent, opts, callback){
776
798
  let uploads
777
799
  let cid = isCid(forceCid) ? forceCid : this.targetComponentID(inputEl.form, targetCtx)
778
- let refGenerator = () => this.putRef([inputEl, inputEl.form], "change")
779
- let formData = serializeForm(inputEl.form, {_target: eventTarget.name})
780
- if(inputEl.files && inputEl.files.length > 0){
800
+ let refGenerator = () => this.putRef([inputEl, inputEl.form], "change", opts)
801
+ let formData = serializeForm(inputEl.form, {_target: opts._target})
802
+ if(DOM.isUploadInput(inputEl) && inputEl.files && inputEl.files.length > 0){
781
803
  LiveUploader.trackFiles(inputEl, Array.from(inputEl.files))
782
804
  }
783
805
  uploads = LiveUploader.serializeUploads(inputEl)
@@ -807,19 +829,19 @@ export default class View {
807
829
  triggerAwaitingSubmit(formEl){
808
830
  let awaitingSubmit = this.getScheduledSubmit(formEl)
809
831
  if(awaitingSubmit){
810
- let [_el, _ref, callback] = awaitingSubmit
832
+ let [_el, _ref, _opts, callback] = awaitingSubmit
811
833
  this.cancelSubmit(formEl)
812
834
  callback()
813
835
  }
814
836
  }
815
837
 
816
838
  getScheduledSubmit(formEl){
817
- return this.formSubmits.find(([el, _callback]) => el.isSameNode(formEl))
839
+ return this.formSubmits.find(([el, _ref, _opts, _callback]) => el.isSameNode(formEl))
818
840
  }
819
841
 
820
- scheduleSubmit(formEl, ref, callback){
842
+ scheduleSubmit(formEl, ref, opts, callback){
821
843
  if(this.getScheduledSubmit(formEl)){ return true }
822
- this.formSubmits.push([formEl, ref, callback])
844
+ this.formSubmits.push([formEl, ref, opts, callback])
823
845
  }
824
846
 
825
847
  cancelSubmit(formEl){
@@ -833,7 +855,7 @@ export default class View {
833
855
  })
834
856
  }
835
857
 
836
- pushFormSubmit(formEl, targetCtx, phxEvent, onReply){
858
+ pushFormSubmit(formEl, targetCtx, phxEvent, opts, onReply){
837
859
  let filterIgnored = el => {
838
860
  let userIgnored = closestPhxBinding(el, `${this.binding(PHX_UPDATE)}=ignore`, el.form)
839
861
  return !(userIgnored || closestPhxBinding(el, "data-phx-update=ignore", el.form))
@@ -864,16 +886,17 @@ export default class View {
864
886
  }
865
887
  })
866
888
  formEl.setAttribute(this.binding(PHX_PAGE_LOADING), "")
867
- return this.putRef([formEl].concat(disables).concat(buttons).concat(inputs), "submit")
889
+ return this.putRef([formEl].concat(disables).concat(buttons).concat(inputs), "submit", opts)
868
890
  }
869
891
 
870
892
  let cid = this.targetComponentID(formEl, targetCtx)
871
893
  if(LiveUploader.hasUploadsInProgress(formEl)){
872
894
  let [ref, _els] = refGenerator()
873
- return this.scheduleSubmit(formEl, ref, () => this.pushFormSubmit(formEl, targetCtx, phxEvent, onReply))
895
+ let push = () => this.pushFormSubmit(formEl, targetCtx, phxEvent, opts, onReply)
896
+ return this.scheduleSubmit(formEl, ref, opts, push)
874
897
  } else if(LiveUploader.inputsAwaitingPreflight(formEl).length > 0){
875
898
  let [ref, els] = refGenerator()
876
- let proxyRefGen = () => [ref, els]
899
+ let proxyRefGen = () => [ref, els, opts]
877
900
  this.uploadFiles(formEl, targetCtx, ref, cid, (_uploads) => {
878
901
  let formData = serializeForm(formEl, {})
879
902
  this.pushWithReply(proxyRefGen, "event", {
@@ -946,7 +969,8 @@ export default class View {
946
969
  this.liveSocket.withinOwners(form, (view, targetCtx) => {
947
970
  let input = form.elements[0]
948
971
  let phxEvent = form.getAttribute(this.binding(PHX_AUTO_RECOVER)) || form.getAttribute(this.binding("change"))
949
- view.pushInput(input, targetCtx, newCid, phxEvent, input, callback)
972
+
973
+ JS.exec("change", phxEvent, view, input, ["push", {_target: input.name, newCid: newCid, callback: callback}])
950
974
  })
951
975
  }
952
976
 
@@ -955,15 +979,17 @@ export default class View {
955
979
  let refGen = targetEl ? () => this.putRef([targetEl], "click") : null
956
980
 
957
981
  this.pushWithReply(refGen, "live_patch", {url: href}, resp => {
958
- if(resp.link_redirect){
959
- this.liveSocket.replaceMain(href, null, callback, linkRef)
960
- } else {
961
- if(this.liveSocket.commitPendingLink(linkRef)){
962
- this.href = href
982
+ this.liveSocket.requestDOMUpdate(() => {
983
+ if(resp.link_redirect){
984
+ this.liveSocket.replaceMain(href, null, callback, linkRef)
985
+ } else {
986
+ if(this.liveSocket.commitPendingLink(linkRef)){
987
+ this.href = href
988
+ }
989
+ this.applyPendingUpdates()
990
+ callback && callback(linkRef)
963
991
  }
964
- this.applyPendingUpdates()
965
- callback && callback(linkRef)
966
- }
992
+ })
967
993
  }).receive("timeout", () => this.liveSocket.redirect(window.location.href))
968
994
  }
969
995
 
@@ -1023,10 +1049,10 @@ export default class View {
1023
1049
  maybe(el.closest(PHX_VIEW_SELECTOR), node => node.id) === this.id
1024
1050
  }
1025
1051
 
1026
- submitForm(form, targetCtx, phxEvent){
1052
+ submitForm(form, targetCtx, phxEvent, opts = {}){
1027
1053
  DOM.putPrivate(form, PHX_HAS_SUBMITTED, true)
1028
1054
  this.liveSocket.blurActiveElement(this)
1029
- this.pushFormSubmit(form, targetCtx, phxEvent, () => {
1055
+ this.pushFormSubmit(form, targetCtx, phxEvent, opts, () => {
1030
1056
  this.liveSocket.restorePreviouslyActiveFocus()
1031
1057
  })
1032
1058
  }
@@ -41,14 +41,14 @@ export default class ViewHook {
41
41
 
42
42
  handleEvent(event, callback){
43
43
  let callbackRef = (customEvent, bypass) => bypass ? event : callback(customEvent.detail)
44
- window.addEventListener(`phx:hook:${event}`, callbackRef)
44
+ window.addEventListener(`phx:${event}`, callbackRef)
45
45
  this.__listeners.add(callbackRef)
46
46
  return callbackRef
47
47
  }
48
48
 
49
49
  removeHandleEvent(callbackRef){
50
50
  let event = callbackRef(null, true)
51
- window.removeEventListener(`phx:hook:${event}`, callbackRef)
51
+ window.removeEventListener(`phx:${event}`, callbackRef)
52
52
  this.__listeners.delete(callbackRef)
53
53
  }
54
54
 
package/package.json CHANGED
@@ -1,13 +1,15 @@
1
1
  {
2
2
  "name": "phoenix_live_view",
3
- "version": "0.16.3",
3
+ "version": "0.17.2",
4
4
  "description": "The Phoenix LiveView JavaScript client.",
5
5
  "license": "MIT",
6
6
  "module": "./priv/static/phoenix_live_view.esm.js",
7
+ "main": "./priv/static/phoenix_live_view.cjs.js",
7
8
  "unpkg": "./priv/static/phoenix_live_view.min.js",
8
9
  "jsdelivr": "./priv/static/phoenix_live_view.min.js",
9
10
  "exports": {
10
- "import": "./priv/static/phoenix_live_view.esm.js"
11
+ "import": "./priv/static/phoenix_live_view.esm.js",
12
+ "require": "./priv/static/phoenix_live_view.cjs.js"
11
13
  },
12
14
  "author": "Chris McCord <chris@chrismccord.com> (http://www.phoenixframework.org)",
13
15
  "repository": {
@@ -21,4 +23,4 @@
21
23
  "priv/static/*",
22
24
  "assets/js/phoenix_live_view/*"
23
25
  ]
24
- }
26
+ }