phoenix_live_view 0.18.3 → 0.18.5

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.
@@ -58,6 +58,11 @@ let DOM = {
58
58
  return node.id && DOM.private(node, "destroyed") ? true : false
59
59
  },
60
60
 
61
+ isExternalClick(e){
62
+ return(e.ctrlKey || e.shiftKey || e.metaKey || (e.button && e.button === 1)
63
+ || e.target.getAttribute("target") === "_blank")
64
+ },
65
+
61
66
  markPhxChildDestroyed(el){
62
67
  if(this.isPhxChild(el)){ el.setAttribute(PHX_SESSION, "") }
63
68
  this.putPrivate(el, "destroyed", true)
@@ -231,7 +236,7 @@ let DOM = {
231
236
  let input = field && container.querySelector(`[id="${field}"], [name="${field}"], [name="${field}[]"]`)
232
237
  if(!input){ return }
233
238
 
234
- if(!(this.private(input, PHX_HAS_FOCUSED) || this.private(input.form, PHX_HAS_SUBMITTED))){
239
+ if(!(this.private(input, PHX_HAS_FOCUSED) || this.private(input, PHX_HAS_SUBMITTED))){
235
240
  el.classList.add(PHX_NO_FEEDBACK_CLASS)
236
241
  }
237
242
  },
@@ -176,7 +176,7 @@ export default class DOMPatch {
176
176
  DOM.discardError(targetContainer, toEl, phxFeedbackFor)
177
177
 
178
178
  let isFocusedFormEl = focused && fromEl.isSameNode(focused) && DOM.isFormInput(fromEl)
179
- if(isFocusedFormEl){
179
+ if(isFocusedFormEl && fromEl.type !== "hidden"){
180
180
  this.trackBefore("updated", fromEl, toEl)
181
181
  DOM.mergeFocusedInput(fromEl, toEl)
182
182
  DOM.syncAttrsToProps(fromEl)
@@ -222,7 +222,7 @@ export default class DOMPatch {
222
222
  }
223
223
 
224
224
  if(externalFormTriggered){
225
- liveSocket.disconnect()
225
+ liveSocket.unload()
226
226
  externalFormTriggered.submit()
227
227
  }
228
228
  return true
@@ -43,7 +43,7 @@ let JS = {
43
43
  view.withinTargets(phxTarget, (targetView, targetCtx) => {
44
44
  if(eventType === "change"){
45
45
  let {newCid, _target, callback} = args
46
- _target = _target || (sourceEl instanceof HTMLInputElement ? sourceEl.name : undefined)
46
+ _target = _target || (DOM.isFormInput(sourceEl) ? sourceEl.name : undefined)
47
47
  if(_target){ pushOpts._target = _target }
48
48
  targetView.pushInput(sourceEl, targetCtx, newCid, event || phxEvent, pushOpts, callback)
49
49
  } else if(eventType === "submit"){
@@ -210,8 +210,9 @@ export default class LiveSocket {
210
210
  } else if(this.main){
211
211
  this.socket.connect()
212
212
  } else {
213
- this.joinDeadView()
213
+ this.bindTopLevelEvents({dead: true})
214
214
  }
215
+ this.joinDeadView()
215
216
  }
216
217
  if(["complete", "loaded", "interactive"].indexOf(document.readyState) >= 0){
217
218
  doConnect()
@@ -237,6 +238,14 @@ export default class LiveSocket {
237
238
 
238
239
  // private
239
240
 
241
+ unload(){
242
+ if(this.unloaded){ return }
243
+ if(this.main && this.isConnected()){ this.log(this.main, "socket", () => ["disconnect for page nav"]) }
244
+ this.unloaded = true
245
+ this.destroyAllViews()
246
+ this.disconnect()
247
+ }
248
+
240
249
  triggerDOM(kind, args){ this.domCallbacks[kind](...args) }
241
250
 
242
251
  time(name, func){
@@ -345,12 +354,14 @@ export default class LiveSocket {
345
354
  channel(topic, params){ return this.socket.channel(topic, params) }
346
355
 
347
356
  joinDeadView(){
348
- this.bindTopLevelEvents({dead: true})
349
- let view = this.newRootView(document.body)
350
- view.setHref(this.getHref())
351
- view.joinDead()
352
- this.main = view
353
- window.requestAnimationFrame(() => view.execNewMounted())
357
+ let body = document.body
358
+ if(body && !this.isPhxView(body) && !this.isPhxView(document.firstElementChild)){
359
+ let view = this.newRootView(body)
360
+ view.setHref(this.getHref())
361
+ view.joinDead()
362
+ if(!this.main){ this.main = view }
363
+ window.requestAnimationFrame(() => view.execNewMounted())
364
+ }
354
365
  }
355
366
 
356
367
  joinRootViews(){
@@ -360,7 +371,7 @@ export default class LiveSocket {
360
371
  let view = this.newRootView(rootEl)
361
372
  view.setHref(this.getHref())
362
373
  view.join()
363
- if(rootEl.getAttribute(PHX_MAIN)){ this.main = view }
374
+ if(rootEl.hasAttribute(PHX_MAIN)){ this.main = view }
364
375
  }
365
376
  rootsFound = true
366
377
  })
@@ -491,9 +502,10 @@ export default class LiveSocket {
491
502
  this.boundTopLevelEvents = true
492
503
  // enter failsafe reload if server has gone away intentionally, such as "disconnect" broadcast
493
504
  this.socket.onClose(event => {
494
- if(event && event.code === 1000 && this.main){
495
- this.reloadWithJitter(this.main)
496
- }
505
+ // unload when navigating href or form submit (such as for firefox)
506
+ if(event && event.code === 1001){ return this.unload() }
507
+ // failsafe reload if normal closure and we still have a main LV
508
+ if(event && event.code === 1000 && this.main){ return this.reloadWithJitter(this.main) }
497
509
  })
498
510
  document.body.addEventListener("click", function (){ }) // ensure all click events bubble for mobile Safari
499
511
  window.addEventListener("pageshow", e => {
@@ -621,7 +633,10 @@ export default class LiveSocket {
621
633
  this.clickStartedAtTarget = null
622
634
  }
623
635
  let phxEvent = target && target.getAttribute(click)
624
- if(!phxEvent){ return }
636
+ if(!phxEvent){
637
+ if(!capture && e.target.href !== undefined && DOM.isExternalClick(e)){ this.unload() }
638
+ return
639
+ }
625
640
  if(target.getAttribute("href") === "#"){ e.preventDefault() }
626
641
 
627
642
  this.debounce(target, e, "click", () => {
@@ -739,7 +754,7 @@ export default class LiveSocket {
739
754
  historyRedirect(href, linkState, flash){
740
755
  // convert to full href if only path prefix
741
756
  if(!this.isConnected()){ return Browser.redirect(href, flash) }
742
- if(/^\/[^\/]+.*$/.test(href)){
757
+ if(/^\/$|^\/[^\/]+.*$/.test(href)){
743
758
  let {protocol, host} = window.location
744
759
  href = `${protocol}//${host}${href}`
745
760
  }
@@ -778,6 +793,7 @@ export default class LiveSocket {
778
793
  if(!externalFormSubmitted && phxChange && !phxSubmit){
779
794
  externalFormSubmitted = true
780
795
  e.preventDefault()
796
+ this.unload()
781
797
  this.withinOwners(e.target, view => {
782
798
  view.disableForm(e.target)
783
799
  window.requestAnimationFrame(() => e.target.submit()) // safari needs next tick
@@ -787,7 +803,7 @@ export default class LiveSocket {
787
803
 
788
804
  this.on("submit", e => {
789
805
  let phxEvent = e.target.getAttribute(this.binding("submit"))
790
- if(!phxEvent){ return }
806
+ if(!phxEvent){ return this.unload() }
791
807
  e.preventDefault()
792
808
  e.target.disabled = true
793
809
  this.withinOwners(e.target, view => {
@@ -119,7 +119,7 @@ export default class View {
119
119
  this.href = href
120
120
  }
121
121
 
122
- isMain(){ return this.el.getAttribute(PHX_MAIN) !== null }
122
+ isMain(){ return this.el.hasAttribute(PHX_MAIN) }
123
123
 
124
124
  connectParams(liveReferer){
125
125
  let params = this.liveSocket.params(this.el)
@@ -228,9 +228,8 @@ export default class View {
228
228
  applyDiff(type, rawDiff, callback){
229
229
  this.log(type, () => ["", clone(rawDiff)])
230
230
  let {diff, reply, events, title} = Rendered.extract(rawDiff)
231
- if(title){ DOM.putTitle(title) }
232
-
233
231
  callback({diff, reply, events})
232
+ if(title){ window.requestAnimationFrame(() => DOM.putTitle(title)) }
234
233
  }
235
234
 
236
235
  onJoin(resp){
@@ -553,6 +552,12 @@ export default class View {
553
552
  applyPendingUpdates(){
554
553
  this.pendingDiffs.forEach(({diff, events}) => this.update(diff, events))
555
554
  this.pendingDiffs = []
555
+ this.eachChild(child => child.applyPendingUpdates())
556
+ }
557
+
558
+ eachChild(callback){
559
+ let children = this.root.children[this.id] || {}
560
+ for(let id in children){ callback(this.getChildById(id)) }
556
561
  }
557
562
 
558
563
  onChannel(event, cb){
@@ -580,11 +585,7 @@ export default class View {
580
585
  this.channel.onClose(reason => this.onClose(reason))
581
586
  }
582
587
 
583
- destroyAllChildren(){
584
- for(let id in this.root.children[this.id]){
585
- this.getChildById(id).destroy()
586
- }
587
- }
588
+ destroyAllChildren(){ this.eachChild(child => child.destroy()) }
588
589
 
589
590
  onLiveRedirect(redir){
590
591
  let {to, kind, flash} = redir
@@ -1115,14 +1116,17 @@ export default class View {
1115
1116
  }
1116
1117
 
1117
1118
  ownsElement(el){
1118
- return this.isDead || el.getAttribute(PHX_PARENT_ID) === this.id ||
1119
- maybe(el.closest(PHX_VIEW_SELECTOR), node => node.id) === this.id
1119
+ let parentViewEl = el.closest(PHX_VIEW_SELECTOR)
1120
+ return el.getAttribute(PHX_PARENT_ID) === this.id ||
1121
+ (parentViewEl && parentViewEl.id === this.id) ||
1122
+ (!parentViewEl && this.isDead)
1120
1123
  }
1121
1124
 
1122
1125
  submitForm(form, targetCtx, phxEvent, opts = {}){
1123
1126
  DOM.putPrivate(form, PHX_HAS_SUBMITTED, true)
1124
1127
  let phxFeedback = this.liveSocket.binding(PHX_FEEDBACK_FOR)
1125
1128
  let inputs = Array.from(form.elements)
1129
+ inputs.forEach(input => DOM.putPrivate(input, PHX_HAS_SUBMITTED, true))
1126
1130
  this.liveSocket.blurActiveElement(this)
1127
1131
  this.pushFormSubmit(form, targetCtx, phxEvent, opts, () => {
1128
1132
  inputs.forEach(input => DOM.showError(input, phxFeedback))
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "phoenix_live_view",
3
+ "version": "0.18.5",
4
+ "description": "The Phoenix LiveView JavaScript client.",
5
+ "license": "MIT",
6
+ "repository": {},
7
+ "scripts": {
8
+ "test": "jest",
9
+ "test.coverage": "jest --coverage",
10
+ "test.watch": "jest --watch"
11
+ },
12
+ "dependencies": {
13
+ "morphdom": "2.6.1"
14
+ },
15
+ "devDependencies": {
16
+ "@babel/cli": "7.14.3",
17
+ "@babel/core": "7.14.3",
18
+ "@babel/preset-env": "7.14.2",
19
+ "eslint": "7.27.0",
20
+ "eslint-plugin-jest": "24.3.6",
21
+ "jest": "^27.0.1",
22
+ "phoenix": "1.5.9"
23
+ }
24
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phoenix_live_view",
3
- "version": "0.18.3",
3
+ "version": "0.18.5",
4
4
  "description": "The Phoenix LiveView JavaScript client.",
5
5
  "license": "MIT",
6
6
  "module": "./priv/static/phoenix_live_view.esm.js",
@@ -302,6 +302,9 @@ var DOM = {
302
302
  isPhxDestroyed(node) {
303
303
  return node.id && DOM.private(node, "destroyed") ? true : false;
304
304
  },
305
+ isExternalClick(e) {
306
+ return e.ctrlKey || e.shiftKey || e.metaKey || e.button && e.button === 1 || e.target.getAttribute("target") === "_blank";
307
+ },
305
308
  markPhxChildDestroyed(el) {
306
309
  if (this.isPhxChild(el)) {
307
310
  el.setAttribute(PHX_SESSION, "");
@@ -476,7 +479,7 @@ var DOM = {
476
479
  if (!input) {
477
480
  return;
478
481
  }
479
- if (!(this.private(input, PHX_HAS_FOCUSED) || this.private(input.form, PHX_HAS_SUBMITTED))) {
482
+ if (!(this.private(input, PHX_HAS_FOCUSED) || this.private(input, PHX_HAS_SUBMITTED))) {
480
483
  el.classList.add(PHX_NO_FEEDBACK_CLASS);
481
484
  }
482
485
  },
@@ -1669,7 +1672,7 @@ var DOMPatch = class {
1669
1672
  dom_default.copyPrivates(toEl, fromEl);
1670
1673
  dom_default.discardError(targetContainer, toEl, phxFeedbackFor);
1671
1674
  let isFocusedFormEl = focused && fromEl.isSameNode(focused) && dom_default.isFormInput(fromEl);
1672
- if (isFocusedFormEl) {
1675
+ if (isFocusedFormEl && fromEl.type !== "hidden") {
1673
1676
  this.trackBefore("updated", fromEl, toEl);
1674
1677
  dom_default.mergeFocusedInput(fromEl, toEl);
1675
1678
  dom_default.syncAttrsToProps(fromEl);
@@ -1714,7 +1717,7 @@ var DOMPatch = class {
1714
1717
  });
1715
1718
  }
1716
1719
  if (externalFormTriggered) {
1717
- liveSocket.disconnect();
1720
+ liveSocket.unload();
1718
1721
  externalFormTriggered.submit();
1719
1722
  }
1720
1723
  return true;
@@ -2084,7 +2087,7 @@ var JS = {
2084
2087
  view.withinTargets(phxTarget, (targetView, targetCtx) => {
2085
2088
  if (eventType === "change") {
2086
2089
  let { newCid, _target, callback } = args;
2087
- _target = _target || (sourceEl instanceof HTMLInputElement ? sourceEl.name : void 0);
2090
+ _target = _target || (dom_default.isFormInput(sourceEl) ? sourceEl.name : void 0);
2088
2091
  if (_target) {
2089
2092
  pushOpts._target = _target;
2090
2093
  }
@@ -2321,7 +2324,7 @@ var View = class {
2321
2324
  this.href = href;
2322
2325
  }
2323
2326
  isMain() {
2324
- return this.el.getAttribute(PHX_MAIN) !== null;
2327
+ return this.el.hasAttribute(PHX_MAIN);
2325
2328
  }
2326
2329
  connectParams(liveReferer) {
2327
2330
  let params = this.liveSocket.params(this.el);
@@ -2419,10 +2422,10 @@ var View = class {
2419
2422
  applyDiff(type, rawDiff, callback) {
2420
2423
  this.log(type, () => ["", clone(rawDiff)]);
2421
2424
  let { diff, reply, events, title } = Rendered.extract(rawDiff);
2425
+ callback({ diff, reply, events });
2422
2426
  if (title) {
2423
- dom_default.putTitle(title);
2427
+ window.requestAnimationFrame(() => dom_default.putTitle(title));
2424
2428
  }
2425
- callback({ diff, reply, events });
2426
2429
  }
2427
2430
  onJoin(resp) {
2428
2431
  let { rendered, container } = resp;
@@ -2724,6 +2727,13 @@ var View = class {
2724
2727
  applyPendingUpdates() {
2725
2728
  this.pendingDiffs.forEach(({ diff, events }) => this.update(diff, events));
2726
2729
  this.pendingDiffs = [];
2730
+ this.eachChild((child) => child.applyPendingUpdates());
2731
+ }
2732
+ eachChild(callback) {
2733
+ let children = this.root.children[this.id] || {};
2734
+ for (let id in children) {
2735
+ callback(this.getChildById(id));
2736
+ }
2727
2737
  }
2728
2738
  onChannel(event, cb) {
2729
2739
  this.liveSocket.onChannel(this.channel, event, (resp) => {
@@ -2747,9 +2757,7 @@ var View = class {
2747
2757
  this.channel.onClose((reason) => this.onClose(reason));
2748
2758
  }
2749
2759
  destroyAllChildren() {
2750
- for (let id in this.root.children[this.id]) {
2751
- this.getChildById(id).destroy();
2752
- }
2760
+ this.eachChild((child) => child.destroy());
2753
2761
  }
2754
2762
  onLiveRedirect(redir) {
2755
2763
  let { to, kind, flash } = redir;
@@ -3271,12 +3279,14 @@ var View = class {
3271
3279
  }
3272
3280
  }
3273
3281
  ownsElement(el) {
3274
- return this.isDead || el.getAttribute(PHX_PARENT_ID) === this.id || maybe(el.closest(PHX_VIEW_SELECTOR), (node) => node.id) === this.id;
3282
+ let parentViewEl = el.closest(PHX_VIEW_SELECTOR);
3283
+ return el.getAttribute(PHX_PARENT_ID) === this.id || parentViewEl && parentViewEl.id === this.id || !parentViewEl && this.isDead;
3275
3284
  }
3276
3285
  submitForm(form, targetCtx, phxEvent, opts = {}) {
3277
3286
  dom_default.putPrivate(form, PHX_HAS_SUBMITTED, true);
3278
3287
  let phxFeedback = this.liveSocket.binding(PHX_FEEDBACK_FOR);
3279
3288
  let inputs = Array.from(form.elements);
3289
+ inputs.forEach((input) => dom_default.putPrivate(input, PHX_HAS_SUBMITTED, true));
3280
3290
  this.liveSocket.blurActiveElement(this);
3281
3291
  this.pushFormSubmit(form, targetCtx, phxEvent, opts, () => {
3282
3292
  inputs.forEach((input) => dom_default.showError(input, phxFeedback));
@@ -3388,8 +3398,9 @@ var LiveSocket = class {
3388
3398
  } else if (this.main) {
3389
3399
  this.socket.connect();
3390
3400
  } else {
3391
- this.joinDeadView();
3401
+ this.bindTopLevelEvents({ dead: true });
3392
3402
  }
3403
+ this.joinDeadView();
3393
3404
  };
3394
3405
  if (["complete", "loaded", "interactive"].indexOf(document.readyState) >= 0) {
3395
3406
  doConnect();
@@ -3409,6 +3420,17 @@ var LiveSocket = class {
3409
3420
  execJS(el, encodedJS, eventType = null) {
3410
3421
  this.owner(el, (view) => js_default.exec(eventType, encodedJS, view, el));
3411
3422
  }
3423
+ unload() {
3424
+ if (this.unloaded) {
3425
+ return;
3426
+ }
3427
+ if (this.main && this.isConnected()) {
3428
+ this.log(this.main, "socket", () => ["disconnect for page nav"]);
3429
+ }
3430
+ this.unloaded = true;
3431
+ this.destroyAllViews();
3432
+ this.disconnect();
3433
+ }
3412
3434
  triggerDOM(kind, args) {
3413
3435
  this.domCallbacks[kind](...args);
3414
3436
  }
@@ -3522,12 +3544,16 @@ var LiveSocket = class {
3522
3544
  return this.socket.channel(topic, params);
3523
3545
  }
3524
3546
  joinDeadView() {
3525
- this.bindTopLevelEvents({ dead: true });
3526
- let view = this.newRootView(document.body);
3527
- view.setHref(this.getHref());
3528
- view.joinDead();
3529
- this.main = view;
3530
- window.requestAnimationFrame(() => view.execNewMounted());
3547
+ let body = document.body;
3548
+ if (body && !this.isPhxView(body) && !this.isPhxView(document.firstElementChild)) {
3549
+ let view = this.newRootView(body);
3550
+ view.setHref(this.getHref());
3551
+ view.joinDead();
3552
+ if (!this.main) {
3553
+ this.main = view;
3554
+ }
3555
+ window.requestAnimationFrame(() => view.execNewMounted());
3556
+ }
3531
3557
  }
3532
3558
  joinRootViews() {
3533
3559
  let rootsFound = false;
@@ -3536,7 +3562,7 @@ var LiveSocket = class {
3536
3562
  let view = this.newRootView(rootEl);
3537
3563
  view.setHref(this.getHref());
3538
3564
  view.join();
3539
- if (rootEl.getAttribute(PHX_MAIN)) {
3565
+ if (rootEl.hasAttribute(PHX_MAIN)) {
3540
3566
  this.main = view;
3541
3567
  }
3542
3568
  }
@@ -3662,8 +3688,11 @@ var LiveSocket = class {
3662
3688
  }
3663
3689
  this.boundTopLevelEvents = true;
3664
3690
  this.socket.onClose((event) => {
3691
+ if (event && event.code === 1001) {
3692
+ return this.unload();
3693
+ }
3665
3694
  if (event && event.code === 1e3 && this.main) {
3666
- this.reloadWithJitter(this.main);
3695
+ return this.reloadWithJitter(this.main);
3667
3696
  }
3668
3697
  });
3669
3698
  document.body.addEventListener("click", function() {
@@ -3796,6 +3825,9 @@ var LiveSocket = class {
3796
3825
  }
3797
3826
  let phxEvent = target && target.getAttribute(click);
3798
3827
  if (!phxEvent) {
3828
+ if (!capture && e.target.href !== void 0 && dom_default.isExternalClick(e)) {
3829
+ this.unload();
3830
+ }
3799
3831
  return;
3800
3832
  }
3801
3833
  if (target.getAttribute("href") === "#") {
@@ -3920,7 +3952,7 @@ var LiveSocket = class {
3920
3952
  if (!this.isConnected()) {
3921
3953
  return browser_default.redirect(href, flash);
3922
3954
  }
3923
- if (/^\/[^\/]+.*$/.test(href)) {
3955
+ if (/^\/$|^\/[^\/]+.*$/.test(href)) {
3924
3956
  let { protocol, host } = window.location;
3925
3957
  href = `${protocol}//${host}${href}`;
3926
3958
  }
@@ -3954,6 +3986,7 @@ var LiveSocket = class {
3954
3986
  if (!externalFormSubmitted && phxChange && !phxSubmit) {
3955
3987
  externalFormSubmitted = true;
3956
3988
  e.preventDefault();
3989
+ this.unload();
3957
3990
  this.withinOwners(e.target, (view) => {
3958
3991
  view.disableForm(e.target);
3959
3992
  window.requestAnimationFrame(() => e.target.submit());
@@ -3963,7 +3996,7 @@ var LiveSocket = class {
3963
3996
  this.on("submit", (e) => {
3964
3997
  let phxEvent = e.target.getAttribute(this.binding("submit"));
3965
3998
  if (!phxEvent) {
3966
- return;
3999
+ return this.unload();
3967
4000
  }
3968
4001
  e.preventDefault();
3969
4002
  e.target.disabled = true;