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.
@@ -90,8 +90,8 @@ import {
90
90
  PHX_ROOT_ID,
91
91
  PHX_THROTTLE,
92
92
  PHX_TRACK_UPLOADS,
93
- RELOAD_JITTER
94
-
93
+ PHX_SESSION,
94
+ RELOAD_JITTER,
95
95
  } from "./constants"
96
96
 
97
97
  import {
@@ -99,6 +99,7 @@ import {
99
99
  closestPhxBinding,
100
100
  closure,
101
101
  debug,
102
+ isObject,
102
103
  maybe
103
104
  } from "./utils"
104
105
 
@@ -107,7 +108,7 @@ import DOM from "./dom"
107
108
  import Hooks from "./hooks"
108
109
  import LiveUploader from "./live_uploader"
109
110
  import View from "./view"
110
- import {PHX_SESSION} from "./constants"
111
+ import JS from "./js"
111
112
 
112
113
  export default class LiveSocket {
113
114
  constructor(url, phxSocket, opts = {}){
@@ -144,6 +145,7 @@ export default class LiveSocket {
144
145
  this.sessionStorage = opts.sessionStorage || window.sessionStorage
145
146
  this.boundTopLevelEvents = false
146
147
  this.domCallbacks = Object.assign({onNodeAdded: closure(), onBeforeElUpdated: closure()}, opts.dom || {})
148
+ this.transitions = new TransitionSet()
147
149
  window.addEventListener("pagehide", _e => {
148
150
  this.unloaded = true
149
151
  })
@@ -200,6 +202,10 @@ export default class LiveSocket {
200
202
 
201
203
  disconnect(callback){ this.socket.disconnect(callback) }
202
204
 
205
+ execJS(el, encodedJS, eventType = null){
206
+ this.owner(el, view => JS.exec(eventType, encodedJS, view, el))
207
+ }
208
+
203
209
  // private
204
210
 
205
211
  triggerDOM(kind, args){ this.domCallbacks[kind](...args) }
@@ -222,6 +228,14 @@ export default class LiveSocket {
222
228
  }
223
229
  }
224
230
 
231
+ requestDOMUpdate(callback){
232
+ this.transitions.after(callback)
233
+ }
234
+
235
+ transition(time, onDone = function(){}){
236
+ this.transitions.addTransition(time, onDone)
237
+ }
238
+
225
239
  onChannel(channel, event, cb){
226
240
  channel.on(event, data => {
227
241
  let latency = this.getLatencySim()
@@ -324,10 +338,24 @@ export default class LiveSocket {
324
338
 
325
339
  this.main = this.newRootView(newMainEl, flash)
326
340
  this.main.setRedirect(href)
327
- this.main.join(joinCount => {
341
+ this.transitionRemoves()
342
+ this.main.join((joinCount, onDone) => {
328
343
  if(joinCount === 1 && this.commitPendingLink(linkRef)){
329
- oldMainEl.replaceWith(newMainEl)
330
- callback && callback()
344
+ this.requestDOMUpdate(() => {
345
+ oldMainEl.replaceWith(newMainEl)
346
+ callback && callback()
347
+ onDone()
348
+ })
349
+ }
350
+ })
351
+ }
352
+
353
+ transitionRemoves(elements){
354
+ let removeAttr = this.binding("remove")
355
+ elements = elements || DOM.all(document, `[${removeAttr}]`)
356
+ elements.forEach(el => {
357
+ if(document.body.contains(el)){ // skip children already removed
358
+ this.execJS(el, el.getAttribute(removeAttr), "remove")
331
359
  }
332
360
  })
333
361
  }
@@ -346,14 +374,7 @@ export default class LiveSocket {
346
374
  }
347
375
 
348
376
  withinOwners(childEl, callback){
349
- this.owner(childEl, view => {
350
- let phxTarget = childEl.getAttribute(this.binding("target"))
351
- if(phxTarget === null){
352
- callback(view, childEl)
353
- } else {
354
- view.withinTargets(phxTarget, callback)
355
- }
356
- })
377
+ this.owner(childEl, view => callback(view, childEl))
357
378
  }
358
379
 
359
380
  getViewByEl(el){
@@ -428,22 +449,25 @@ export default class LiveSocket {
428
449
  this.bindNav()
429
450
  this.bindClicks()
430
451
  this.bindForms()
431
- this.bind({keyup: "keyup", keydown: "keydown"}, (e, type, view, target, targetCtx, phxEvent, _phxTarget) => {
432
- let matchKey = target.getAttribute(this.binding(PHX_KEY))
452
+ this.bind({keyup: "keyup", keydown: "keydown"}, (e, type, view, targetEl, phxEvent, eventTarget) => {
453
+ let matchKey = targetEl.getAttribute(this.binding(PHX_KEY))
433
454
  let pressedKey = e.key && e.key.toLowerCase() // chrome clicked autocompletes send a keydown without key
434
455
  if(matchKey && matchKey.toLowerCase() !== pressedKey){ return }
435
456
 
436
- view.pushKey(target, targetCtx, type, phxEvent, {key: e.key, ...this.eventMeta(type, e, target)})
457
+ let data = {key: e.key, ...this.eventMeta(type, e, targetEl)}
458
+ JS.exec(type, phxEvent, view, targetEl, ["push", {data}])
437
459
  })
438
- this.bind({blur: "focusout", focus: "focusin"}, (e, type, view, targetEl, targetCtx, phxEvent, phxTarget) => {
439
- if(!phxTarget){
440
- view.pushEvent(type, targetEl, targetCtx, phxEvent, this.eventMeta(type, e, targetEl))
460
+ this.bind({blur: "focusout", focus: "focusin"}, (e, type, view, targetEl, phxEvent, eventTarget) => {
461
+ if(!eventTarget){
462
+ let data = {key: e.key, ...this.eventMeta(type, e, targetEl)}
463
+ JS.exec(type, phxEvent, view, targetEl, ["push", {data}])
441
464
  }
442
465
  })
443
466
  this.bind({blur: "blur", focus: "focus"}, (e, type, view, targetEl, targetCtx, phxEvent, phxTarget) => {
444
467
  // blur and focus are triggered on document and window. Discard one to avoid dups
445
- if(phxTarget && !phxTarget !== "window"){
446
- view.pushEvent(type, targetEl, targetCtx, phxEvent, this.eventMeta(type, e, targetEl))
468
+ if(phxTarget === "window"){
469
+ let data = this.eventMeta(type, e, targetEl)
470
+ JS.exec(type, phxEvent, view, targetEl, ["push", {data}])
447
471
  }
448
472
  })
449
473
  window.addEventListener("dragover", e => e.preventDefault())
@@ -503,16 +527,16 @@ export default class LiveSocket {
503
527
  let targetPhxEvent = e.target.getAttribute && e.target.getAttribute(binding)
504
528
  if(targetPhxEvent){
505
529
  this.debounce(e.target, e, () => {
506
- this.withinOwners(e.target, (view, targetCtx) => {
507
- callback(e, event, view, e.target, targetCtx, targetPhxEvent, null)
530
+ this.withinOwners(e.target, view => {
531
+ callback(e, event, view, e.target, targetPhxEvent, null)
508
532
  })
509
533
  })
510
534
  } else {
511
535
  DOM.all(document, `[${windowBinding}]`, el => {
512
536
  let phxEvent = el.getAttribute(windowBinding)
513
537
  this.debounce(el, e, () => {
514
- this.withinOwners(el, (view, targetCtx) => {
515
- callback(e, event, view, el, targetCtx, phxEvent, "window")
538
+ this.withinOwners(el, view => {
539
+ callback(e, event, view, el, phxEvent, "window")
516
540
  })
517
541
  })
518
542
  })
@@ -535,19 +559,32 @@ export default class LiveSocket {
535
559
  target = e.target.matches(`[${click}]`) ? e.target : e.target.querySelector(`[${click}]`)
536
560
  } else {
537
561
  target = closestPhxBinding(e.target, click)
562
+ this.dispatchClickAway(e)
538
563
  }
539
564
  let phxEvent = target && target.getAttribute(click)
540
565
  if(!phxEvent){ return }
541
566
  if(target.getAttribute("href") === "#"){ e.preventDefault() }
542
567
 
543
568
  this.debounce(target, e, () => {
544
- this.withinOwners(target, (view, targetCtx) => {
545
- view.pushEvent("click", target, targetCtx, phxEvent, this.eventMeta("click", e, target))
569
+ this.withinOwners(target, view => {
570
+ JS.exec("click", phxEvent, view, target, ["push", {data: this.eventMeta("click", e, target)}])
546
571
  })
547
572
  })
548
573
  }, capture)
549
574
  }
550
575
 
576
+ dispatchClickAway(e){
577
+ let binding = this.binding("click-away")
578
+ DOM.all(document, `[${binding}]`, el => {
579
+ if(!(el.isSameNode(e.target) || el.contains(e.target))){
580
+ this.withinOwners(e.target, view => {
581
+ let phxEvent = el.getAttribute(binding)
582
+ JS.exec("click", phxEvent, view, e.target, ["push", {data: this.eventMeta("click", e, e.target)}])
583
+ })
584
+ }
585
+ })
586
+ }
587
+
551
588
  bindNav(){
552
589
  if(!Browser.canPushState()){ return }
553
590
  if(history.scrollRestoration){ history.scrollRestoration = "manual" }
@@ -563,18 +600,20 @@ export default class LiveSocket {
563
600
  let {type, id, root, scroll} = event.state || {}
564
601
  let href = window.location.href
565
602
 
566
- if(this.main.isConnected() && (type === "patch" && id === this.main.id)){
567
- this.main.pushLinkPatch(href, null)
568
- } else {
569
- this.replaceMain(href, null, () => {
570
- if(root){ this.replaceRootHistory() }
571
- if(typeof(scroll) === "number"){
572
- setTimeout(() => {
573
- window.scrollTo(0, scroll)
574
- }, 0) // the body needs to render before we scroll.
575
- }
576
- })
577
- }
603
+ this.requestDOMUpdate(() => {
604
+ if(this.main.isConnected() && (type === "patch" && id === this.main.id)){
605
+ this.main.pushLinkPatch(href, null)
606
+ } else {
607
+ this.replaceMain(href, null, () => {
608
+ if(root){ this.replaceRootHistory() }
609
+ if(typeof(scroll) === "number"){
610
+ setTimeout(() => {
611
+ window.scrollTo(0, scroll)
612
+ }, 0) // the body needs to render before we scroll.
613
+ }
614
+ })
615
+ }
616
+ })
578
617
  }, false)
579
618
  window.addEventListener("click", e => {
580
619
  let target = closestPhxBinding(e.target, PHX_LIVE_LINK)
@@ -586,16 +625,26 @@ export default class LiveSocket {
586
625
  e.preventDefault()
587
626
  if(this.pendingLink === href){ return }
588
627
 
589
- if(type === "patch"){
590
- this.pushHistoryPatch(href, linkState, target)
591
- } else if(type === "redirect"){
592
- this.historyRedirect(href, linkState)
593
- } else {
594
- throw new Error(`expected ${PHX_LIVE_LINK} to be "patch" or "redirect", got: ${type}`)
595
- }
628
+ this.requestDOMUpdate(() => {
629
+ if(type === "patch"){
630
+ this.pushHistoryPatch(href, linkState, target)
631
+ } else if(type === "redirect"){
632
+ this.historyRedirect(href, linkState)
633
+ } else {
634
+ throw new Error(`expected ${PHX_LIVE_LINK} to be "patch" or "redirect", got: ${type}`)
635
+ }
636
+ })
596
637
  }, false)
597
638
  }
598
639
 
640
+ dispatchEvent(event, payload = {}){
641
+ DOM.dispatchEvent(window, `phx:${event}`, payload)
642
+ }
643
+
644
+ dispatchEvents(events){
645
+ events.forEach(([event, payload]) => this.dispatchEvent(event, payload))
646
+ }
647
+
599
648
  withPageLoading(info, callback){
600
649
  DOM.dispatchEvent(window, "phx:page-loading-start", info)
601
650
  let done = () => DOM.dispatchEvent(window, "phx:page-loading-stop", info)
@@ -650,7 +699,9 @@ export default class LiveSocket {
650
699
  if(!phxEvent){ return }
651
700
  e.preventDefault()
652
701
  e.target.disabled = true
653
- this.withinOwners(e.target, (view, targetCtx) => view.submitForm(e.target, targetCtx, phxEvent))
702
+ this.withinOwners(e.target, view => {
703
+ JS.exec("submit", phxEvent, view, e.target, ["push", {}])
704
+ })
654
705
  }, false)
655
706
 
656
707
  for(let type of ["change", "input"]){
@@ -668,12 +719,12 @@ export default class LiveSocket {
668
719
  DOM.putPrivate(input, "prev-iteration", {at: currentIterations, type: type})
669
720
 
670
721
  this.debounce(input, e, () => {
671
- this.withinOwners(input.form, (view, targetCtx) => {
722
+ this.withinOwners(input.form, view => {
672
723
  DOM.putPrivate(input, PHX_HAS_FOCUSED, true)
673
724
  if(!DOM.isTextualInput(input)){
674
725
  this.setActiveElement(input)
675
726
  }
676
- view.pushInput(input, targetCtx, null, phxEvent, e.target)
727
+ JS.exec("change", phxEvent, view, input, ["push", {_target: e.target.name}])
677
728
  })
678
729
  })
679
730
  }, false)
@@ -700,3 +751,45 @@ export default class LiveSocket {
700
751
  })
701
752
  }
702
753
  }
754
+
755
+ class TransitionSet {
756
+ constructor(){
757
+ this.transitions = new Set()
758
+ this.pendingOps = []
759
+ this.reset()
760
+ }
761
+
762
+ reset(){
763
+ this.transitions.forEach(timer => {
764
+ cancelTimeout(timer)
765
+ this.transitions.delete(timer)
766
+ })
767
+ this.flushPendingOps()
768
+ }
769
+
770
+ after(callback){
771
+ if(this.size() === 0){
772
+ callback()
773
+ } else {
774
+ this.pushPendingOp(callback)
775
+ }
776
+ }
777
+
778
+ addTransition(time, onDone){
779
+ let timer = setTimeout(() => {
780
+ this.transitions.delete(timer)
781
+ onDone()
782
+ if(this.size() === 0){ this.flushPendingOps() }
783
+ }, time)
784
+ this.transitions.add(timer)
785
+ }
786
+
787
+ pushPendingOp(op){ this.pendingOps.push(op) }
788
+
789
+ size(){ return this.transitions.size }
790
+
791
+ flushPendingOps(){
792
+ this.pendingOps.forEach(op => op())
793
+ this.pendingOps = []
794
+ }
795
+ }