phoenix_live_view 0.18.2 → 0.18.4

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.
@@ -231,7 +231,7 @@ let DOM = {
231
231
  let input = field && container.querySelector(`[id="${field}"], [name="${field}"], [name="${field}[]"]`)
232
232
  if(!input){ return }
233
233
 
234
- if(!(this.private(input, PHX_HAS_FOCUSED) || this.private(input.form, PHX_HAS_SUBMITTED))){
234
+ if(!(this.private(input, PHX_HAS_FOCUSED) || this.private(input, PHX_HAS_SUBMITTED))){
235
235
  el.classList.add(PHX_NO_FEEDBACK_CLASS)
236
236
  }
237
237
  },
@@ -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){ 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.4",
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.2",
3
+ "version": "0.18.4",
4
4
  "description": "The Phoenix LiveView JavaScript client.",
5
5
  "license": "MIT",
6
6
  "module": "./priv/static/phoenix_live_view.esm.js",
@@ -476,7 +476,7 @@ var DOM = {
476
476
  if (!input) {
477
477
  return;
478
478
  }
479
- if (!(this.private(input, PHX_HAS_FOCUSED) || this.private(input.form, PHX_HAS_SUBMITTED))) {
479
+ if (!(this.private(input, PHX_HAS_FOCUSED) || this.private(input, PHX_HAS_SUBMITTED))) {
480
480
  el.classList.add(PHX_NO_FEEDBACK_CLASS);
481
481
  }
482
482
  },
@@ -1669,7 +1669,7 @@ var DOMPatch = class {
1669
1669
  dom_default.copyPrivates(toEl, fromEl);
1670
1670
  dom_default.discardError(targetContainer, toEl, phxFeedbackFor);
1671
1671
  let isFocusedFormEl = focused && fromEl.isSameNode(focused) && dom_default.isFormInput(fromEl);
1672
- if (isFocusedFormEl) {
1672
+ if (isFocusedFormEl && fromEl.type !== "hidden") {
1673
1673
  this.trackBefore("updated", fromEl, toEl);
1674
1674
  dom_default.mergeFocusedInput(fromEl, toEl);
1675
1675
  dom_default.syncAttrsToProps(fromEl);
@@ -1714,7 +1714,7 @@ var DOMPatch = class {
1714
1714
  });
1715
1715
  }
1716
1716
  if (externalFormTriggered) {
1717
- liveSocket.disconnect();
1717
+ liveSocket.unload();
1718
1718
  externalFormTriggered.submit();
1719
1719
  }
1720
1720
  return true;
@@ -2084,7 +2084,7 @@ var JS = {
2084
2084
  view.withinTargets(phxTarget, (targetView, targetCtx) => {
2085
2085
  if (eventType === "change") {
2086
2086
  let { newCid, _target, callback } = args;
2087
- _target = _target || (sourceEl instanceof HTMLInputElement ? sourceEl.name : void 0);
2087
+ _target = _target || (dom_default.isFormInput(sourceEl) ? sourceEl.name : void 0);
2088
2088
  if (_target) {
2089
2089
  pushOpts._target = _target;
2090
2090
  }
@@ -2321,7 +2321,7 @@ var View = class {
2321
2321
  this.href = href;
2322
2322
  }
2323
2323
  isMain() {
2324
- return this.el.getAttribute(PHX_MAIN) !== null;
2324
+ return this.el.hasAttribute(PHX_MAIN);
2325
2325
  }
2326
2326
  connectParams(liveReferer) {
2327
2327
  let params = this.liveSocket.params(this.el);
@@ -2419,10 +2419,10 @@ var View = class {
2419
2419
  applyDiff(type, rawDiff, callback) {
2420
2420
  this.log(type, () => ["", clone(rawDiff)]);
2421
2421
  let { diff, reply, events, title } = Rendered.extract(rawDiff);
2422
+ callback({ diff, reply, events });
2422
2423
  if (title) {
2423
- dom_default.putTitle(title);
2424
+ window.requestAnimationFrame(() => dom_default.putTitle(title));
2424
2425
  }
2425
- callback({ diff, reply, events });
2426
2426
  }
2427
2427
  onJoin(resp) {
2428
2428
  let { rendered, container } = resp;
@@ -2724,6 +2724,13 @@ var View = class {
2724
2724
  applyPendingUpdates() {
2725
2725
  this.pendingDiffs.forEach(({ diff, events }) => this.update(diff, events));
2726
2726
  this.pendingDiffs = [];
2727
+ this.eachChild((child) => child.applyPendingUpdates());
2728
+ }
2729
+ eachChild(callback) {
2730
+ let children = this.root.children[this.id] || {};
2731
+ for (let id in children) {
2732
+ callback(this.getChildById(id));
2733
+ }
2727
2734
  }
2728
2735
  onChannel(event, cb) {
2729
2736
  this.liveSocket.onChannel(this.channel, event, (resp) => {
@@ -2747,9 +2754,7 @@ var View = class {
2747
2754
  this.channel.onClose((reason) => this.onClose(reason));
2748
2755
  }
2749
2756
  destroyAllChildren() {
2750
- for (let id in this.root.children[this.id]) {
2751
- this.getChildById(id).destroy();
2752
- }
2757
+ this.eachChild((child) => child.destroy());
2753
2758
  }
2754
2759
  onLiveRedirect(redir) {
2755
2760
  let { to, kind, flash } = redir;
@@ -3271,12 +3276,14 @@ var View = class {
3271
3276
  }
3272
3277
  }
3273
3278
  ownsElement(el) {
3274
- return this.isDead || el.getAttribute(PHX_PARENT_ID) === this.id || maybe(el.closest(PHX_VIEW_SELECTOR), (node) => node.id) === this.id;
3279
+ let parentViewEl = el.closest(PHX_VIEW_SELECTOR);
3280
+ return el.getAttribute(PHX_PARENT_ID) === this.id || parentViewEl && parentViewEl.id === this.id || !parentViewEl && this.isDead;
3275
3281
  }
3276
3282
  submitForm(form, targetCtx, phxEvent, opts = {}) {
3277
3283
  dom_default.putPrivate(form, PHX_HAS_SUBMITTED, true);
3278
3284
  let phxFeedback = this.liveSocket.binding(PHX_FEEDBACK_FOR);
3279
3285
  let inputs = Array.from(form.elements);
3286
+ inputs.forEach((input) => dom_default.putPrivate(input, PHX_HAS_SUBMITTED, true));
3280
3287
  this.liveSocket.blurActiveElement(this);
3281
3288
  this.pushFormSubmit(form, targetCtx, phxEvent, opts, () => {
3282
3289
  inputs.forEach((input) => dom_default.showError(input, phxFeedback));
@@ -3388,8 +3395,9 @@ var LiveSocket = class {
3388
3395
  } else if (this.main) {
3389
3396
  this.socket.connect();
3390
3397
  } else {
3391
- this.joinDeadView();
3398
+ this.bindTopLevelEvents({ dead: true });
3392
3399
  }
3400
+ this.joinDeadView();
3393
3401
  };
3394
3402
  if (["complete", "loaded", "interactive"].indexOf(document.readyState) >= 0) {
3395
3403
  doConnect();
@@ -3409,6 +3417,17 @@ var LiveSocket = class {
3409
3417
  execJS(el, encodedJS, eventType = null) {
3410
3418
  this.owner(el, (view) => js_default.exec(eventType, encodedJS, view, el));
3411
3419
  }
3420
+ unload() {
3421
+ if (this.unloaded) {
3422
+ return;
3423
+ }
3424
+ if (this.main && this.isConnected()) {
3425
+ this.log(this.main, "socket", () => ["disconnect for page nav"]);
3426
+ }
3427
+ this.unloaded = true;
3428
+ this.destroyAllViews();
3429
+ this.disconnect();
3430
+ }
3412
3431
  triggerDOM(kind, args) {
3413
3432
  this.domCallbacks[kind](...args);
3414
3433
  }
@@ -3522,12 +3541,16 @@ var LiveSocket = class {
3522
3541
  return this.socket.channel(topic, params);
3523
3542
  }
3524
3543
  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());
3544
+ let body = document.body;
3545
+ if (body && !this.isPhxView(body) && !this.isPhxView(document.firstElementChild)) {
3546
+ let view = this.newRootView(body);
3547
+ view.setHref(this.getHref());
3548
+ view.joinDead();
3549
+ if (!this.main) {
3550
+ this.main = view;
3551
+ }
3552
+ window.requestAnimationFrame(() => view.execNewMounted());
3553
+ }
3531
3554
  }
3532
3555
  joinRootViews() {
3533
3556
  let rootsFound = false;
@@ -3536,7 +3559,7 @@ var LiveSocket = class {
3536
3559
  let view = this.newRootView(rootEl);
3537
3560
  view.setHref(this.getHref());
3538
3561
  view.join();
3539
- if (rootEl.getAttribute(PHX_MAIN)) {
3562
+ if (rootEl.hasAttribute(PHX_MAIN)) {
3540
3563
  this.main = view;
3541
3564
  }
3542
3565
  }
@@ -3662,8 +3685,11 @@ var LiveSocket = class {
3662
3685
  }
3663
3686
  this.boundTopLevelEvents = true;
3664
3687
  this.socket.onClose((event) => {
3688
+ if (event && event.code === 1001) {
3689
+ return this.unload();
3690
+ }
3665
3691
  if (event && event.code === 1e3 && this.main) {
3666
- this.reloadWithJitter(this.main);
3692
+ return this.reloadWithJitter(this.main);
3667
3693
  }
3668
3694
  });
3669
3695
  document.body.addEventListener("click", function() {
@@ -3796,6 +3822,9 @@ var LiveSocket = class {
3796
3822
  }
3797
3823
  let phxEvent = target && target.getAttribute(click);
3798
3824
  if (!phxEvent) {
3825
+ if (!capture && e.target.href !== void 0) {
3826
+ this.unload();
3827
+ }
3799
3828
  return;
3800
3829
  }
3801
3830
  if (target.getAttribute("href") === "#") {
@@ -3920,7 +3949,7 @@ var LiveSocket = class {
3920
3949
  if (!this.isConnected()) {
3921
3950
  return browser_default.redirect(href, flash);
3922
3951
  }
3923
- if (/^\/[^\/]+.*$/.test(href)) {
3952
+ if (/^\/$|^\/[^\/]+.*$/.test(href)) {
3924
3953
  let { protocol, host } = window.location;
3925
3954
  href = `${protocol}//${host}${href}`;
3926
3955
  }
@@ -3954,6 +3983,7 @@ var LiveSocket = class {
3954
3983
  if (!externalFormSubmitted && phxChange && !phxSubmit) {
3955
3984
  externalFormSubmitted = true;
3956
3985
  e.preventDefault();
3986
+ this.unload();
3957
3987
  this.withinOwners(e.target, (view) => {
3958
3988
  view.disableForm(e.target);
3959
3989
  window.requestAnimationFrame(() => e.target.submit());
@@ -3963,7 +3993,7 @@ var LiveSocket = class {
3963
3993
  this.on("submit", (e) => {
3964
3994
  let phxEvent = e.target.getAttribute(this.binding("submit"));
3965
3995
  if (!phxEvent) {
3966
- return;
3996
+ return this.unload();
3967
3997
  }
3968
3998
  e.preventDefault();
3969
3999
  e.target.disabled = true;