phoenix_live_view 0.17.10 → 0.17.11

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,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.17.11 (2022-07-11)
4
+
5
+ ### Enhancements
6
+ - Add `replaceTransport` to LiveSocket
7
+
8
+ ### Bug fixes
9
+ - Cancel debounced events from firing after a live navigation event
10
+ - Fix hash anchor failing to scroll to anchor element on live navigation
11
+ - Do not debounce `phx-blur` events
12
+
13
+
3
14
  ## 0.17.10 (2022-05-25)
4
15
 
5
16
  ### Bug fixes
@@ -1,8 +1,8 @@
1
1
 
2
2
  export const CONSECUTIVE_RELOADS = "consecutive-reloads"
3
3
  export const MAX_RELOADS = 10
4
- export const RELOAD_JITTER_MIN = 1000
5
- export const RELOAD_JITTER_MAX = 3000
4
+ export const RELOAD_JITTER_MIN = 5000
5
+ export const RELOAD_JITTER_MAX = 10000
6
6
  export const FAILSAFE_JITTER = 30000
7
7
  export const PHX_EVENT_CLASSES = [
8
8
  "phx-click-loading", "phx-change-loading", "phx-submit-loading",
@@ -141,7 +141,7 @@ let DOM = {
141
141
  document.title = `${prefix || ""}${str}${suffix || ""}`
142
142
  },
143
143
 
144
- debounce(el, event, phxDebounce, defaultDebounce, phxThrottle, defaultThrottle, callback){
144
+ debounce(el, event, phxDebounce, defaultDebounce, phxThrottle, defaultThrottle, asyncFilter, callback){
145
145
  let debounce = el.getAttribute(phxDebounce)
146
146
  let throttle = el.getAttribute(phxThrottle)
147
147
  if(debounce === ""){ debounce = defaultDebounce }
@@ -174,13 +174,16 @@ let DOM = {
174
174
  } else {
175
175
  callback()
176
176
  this.putPrivate(el, THROTTLED, true)
177
- setTimeout(() => this.triggerCycle(el, DEBOUNCE_TRIGGER), timeout)
177
+ setTimeout(() => {
178
+ if(asyncFilter()){ this.triggerCycle(el, DEBOUNCE_TRIGGER) }
179
+ }, timeout)
178
180
  }
179
181
  } else {
180
- setTimeout(() => this.triggerCycle(el, DEBOUNCE_TRIGGER, currentCycle), timeout)
182
+ setTimeout(() => {
183
+ if(asyncFilter()){ this.triggerCycle(el, DEBOUNCE_TRIGGER, currentCycle) }
184
+ }, timeout)
181
185
  }
182
186
 
183
-
184
187
  let form = el.form
185
188
  if(form && this.once(form, "bind-debounce")){
186
189
  form.addEventListener("submit", () => {
@@ -148,6 +148,7 @@ export default class LiveSocket {
148
148
  this.hooks = opts.hooks || {}
149
149
  this.uploaders = opts.uploaders || {}
150
150
  this.loaderTimeout = opts.loaderTimeout || LOADER_TIMEOUT
151
+ this.reloadWithJitterTimer = null
151
152
  this.maxReloads = opts.maxReloads || MAX_RELOADS
152
153
  this.reloadJitterMin = opts.reloadJitterMin || RELOAD_JITTER_MIN
153
154
  this.reloadJitterMax = opts.reloadJitterMax || RELOAD_JITTER_MAX
@@ -206,6 +207,8 @@ export default class LiveSocket {
206
207
  if(this.joinRootViews()){
207
208
  this.bindTopLevelEvents()
208
209
  this.socket.connect()
210
+ } else if(this.main){
211
+ this.socket.connect()
209
212
  }
210
213
  }
211
214
  if(["complete", "loaded", "interactive"].indexOf(document.readyState) >= 0){
@@ -215,7 +218,16 @@ export default class LiveSocket {
215
218
  }
216
219
  }
217
220
 
218
- disconnect(callback){ this.socket.disconnect(callback) }
221
+ disconnect(callback){
222
+ clearTimeout(this.reloadWithJitterTimer)
223
+ this.socket.disconnect(callback)
224
+ }
225
+
226
+ replaceTransport(transport){
227
+ clearTimeout(this.reloadWithJitterTimer)
228
+ this.socket.replaceTransport(transport)
229
+ this.connect()
230
+ }
219
231
 
220
232
  execJS(el, encodedJS, eventType = null){
221
233
  this.owner(el, view => JS.exec(eventType, encodedJS, view, el))
@@ -293,18 +305,23 @@ export default class LiveSocket {
293
305
  }
294
306
 
295
307
  reloadWithJitter(view, log){
296
- view.destroy()
308
+ clearTimeout(this.reloadWithJitterTimer)
297
309
  this.disconnect()
298
310
  let minMs = this.reloadJitterMin
299
311
  let maxMs = this.reloadJitterMax
300
312
  let afterMs = Math.floor(Math.random() * (maxMs - minMs + 1)) + minMs
301
313
  let tries = Browser.updateLocal(this.localStorage, window.location.pathname, CONSECUTIVE_RELOADS, 0, count => count + 1)
302
- log ? log() : this.log(view, "join", () => [`encountered ${tries} consecutive reloads`])
303
314
  if(tries > this.maxReloads){
304
- this.log(view, "join", () => [`exceeded ${this.maxReloads} consecutive reloads. Entering failsafe mode`])
305
315
  afterMs = this.failsafeJitter
306
316
  }
307
- setTimeout(() => {
317
+ this.reloadWithJitterTimer = setTimeout(() => {
318
+ // if view has recovered, such as transport replaced, then cancel
319
+ if(view.isDestroyed() || view.isConnected()){ return }
320
+ view.destroy()
321
+ log ? log() : this.log(view, "join", () => [`encountered ${tries} consecutive reloads`])
322
+ if(tries > this.maxReloads){
323
+ this.log(view, "join", () => [`exceeded ${this.maxReloads} consecutive reloads. Entering failsafe mode`])
324
+ }
308
325
  if(this.hasPendingLink()){
309
326
  window.location = this.pendingLink
310
327
  } else {
@@ -361,7 +378,7 @@ export default class LiveSocket {
361
378
  DOM.findPhxSticky(document).forEach(el => newMainEl.appendChild(el))
362
379
  this.outgoingMainEl.replaceWith(newMainEl)
363
380
  this.outgoingMainEl = null
364
- callback && callback()
381
+ callback && requestAnimationFrame(callback)
365
382
  onDone()
366
383
  })
367
384
  }
@@ -407,6 +424,7 @@ export default class LiveSocket {
407
424
  this.roots[id].destroy()
408
425
  delete this.roots[id]
409
426
  }
427
+ this.main = null
410
428
  }
411
429
 
412
430
  destroyViewByEl(el){
@@ -555,7 +573,7 @@ export default class LiveSocket {
555
573
  let windowBinding = this.binding(`window-${event}`)
556
574
  let targetPhxEvent = e.target.getAttribute && e.target.getAttribute(binding)
557
575
  if(targetPhxEvent){
558
- this.debounce(e.target, e, () => {
576
+ this.debounce(e.target, e, browserEventName, () => {
559
577
  this.withinOwners(e.target, view => {
560
578
  callback(e, event, view, e.target, targetPhxEvent, null)
561
579
  })
@@ -563,7 +581,7 @@ export default class LiveSocket {
563
581
  } else {
564
582
  DOM.all(document, `[${windowBinding}]`, el => {
565
583
  let phxEvent = el.getAttribute(windowBinding)
566
- this.debounce(el, e, () => {
584
+ this.debounce(el, e, browserEventName, () => {
567
585
  this.withinOwners(el, view => {
568
586
  callback(e, event, view, el, phxEvent, "window")
569
587
  })
@@ -596,7 +614,7 @@ export default class LiveSocket {
596
614
  if(!phxEvent){ return }
597
615
  if(target.getAttribute("href") === "#"){ e.preventDefault() }
598
616
 
599
- this.debounce(target, e, () => {
617
+ this.debounce(target, e, "click", () => {
600
618
  this.withinOwners(target, view => {
601
619
  JS.exec("click", phxEvent, view, target, ["push", {data: this.eventMeta("click", e, target)}])
602
620
  })
@@ -758,7 +776,7 @@ export default class LiveSocket {
758
776
 
759
777
  DOM.putPrivate(input, "prev-iteration", {at: currentIterations, type: type})
760
778
 
761
- this.debounce(input, e, () => {
779
+ this.debounce(input, e, type, () => {
762
780
  this.withinOwners(dispatcher, view => {
763
781
  DOM.putPrivate(input, PHX_HAS_FOCUSED, true)
764
782
  if(!DOM.isTextualInput(input)){
@@ -771,12 +789,20 @@ export default class LiveSocket {
771
789
  }
772
790
  }
773
791
 
774
- debounce(el, event, callback){
792
+ debounce(el, event, eventType, callback){
793
+ if(eventType === "blur" || eventType === "focusout"){ return callback() }
794
+
775
795
  let phxDebounce = this.binding(PHX_DEBOUNCE)
776
796
  let phxThrottle = this.binding(PHX_THROTTLE)
777
797
  let defaultDebounce = this.defaults.debounce.toString()
778
798
  let defaultThrottle = this.defaults.throttle.toString()
779
- DOM.debounce(el, event, phxDebounce, defaultDebounce, phxThrottle, defaultThrottle, callback)
799
+
800
+ this.withinOwners(el, view => {
801
+ let asyncFilter = () => !view.isDestroyed() && document.body.contains(el)
802
+ DOM.debounce(el, event, phxDebounce, defaultDebounce, phxThrottle, defaultThrottle, asyncFilter, () => {
803
+ callback()
804
+ })
805
+ })
780
806
  }
781
807
 
782
808
  silenceEvents(callback){
@@ -616,14 +616,12 @@ export default class View {
616
616
  if(resp.redirect){ return this.onRedirect(resp.redirect) }
617
617
  if(resp.live_redirect){ return this.onLiveRedirect(resp.live_redirect) }
618
618
  this.log("error", () => ["unable to join", resp])
619
- return this.liveSocket.reloadWithJitter(this)
619
+ if(this.liveSocket.isConnected()){ this.liveSocket.reloadWithJitter(this) }
620
620
  }
621
621
 
622
622
  onClose(reason){
623
623
  if(this.isDestroyed()){ return }
624
- if((this.isJoinPending() && document.visibilityState !== "hidden") ||
625
- (this.liveSocket.hasPendingLink() && reason !== "leave")){
626
-
624
+ if(this.liveSocket.hasPendingLink() && reason !== "leave"){
627
625
  return this.liveSocket.reloadWithJitter(this)
628
626
  }
629
627
  this.destroyAllChildren()
@@ -637,7 +635,7 @@ export default class View {
637
635
 
638
636
  onError(reason){
639
637
  this.onClose(reason)
640
- this.log("error", () => ["view crashed", reason])
638
+ if(this.liveSocket.isConnected()){ this.log("error", () => ["view crashed", reason]) }
641
639
  if(!this.liveSocket.isUnloaded()){ this.displayError() }
642
640
  }
643
641
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phoenix_live_view",
3
- "version": "0.17.10",
3
+ "version": "0.17.11",
4
4
  "description": "The Phoenix LiveView JavaScript client.",
5
5
  "license": "MIT",
6
6
  "module": "./priv/static/phoenix_live_view.esm.js",
@@ -14,8 +14,8 @@ __export(exports, {
14
14
  // js/phoenix_live_view/constants.js
15
15
  var CONSECUTIVE_RELOADS = "consecutive-reloads";
16
16
  var MAX_RELOADS = 10;
17
- var RELOAD_JITTER_MIN = 1e3;
18
- var RELOAD_JITTER_MAX = 3e3;
17
+ var RELOAD_JITTER_MIN = 5e3;
18
+ var RELOAD_JITTER_MAX = 1e4;
19
19
  var FAILSAFE_JITTER = 3e4;
20
20
  var PHX_EVENT_CLASSES = [
21
21
  "phx-click-loading",
@@ -379,7 +379,7 @@ var DOM = {
379
379
  let { prefix, suffix } = titleEl.dataset;
380
380
  document.title = `${prefix || ""}${str}${suffix || ""}`;
381
381
  },
382
- debounce(el, event, phxDebounce, defaultDebounce, phxThrottle, defaultThrottle, callback) {
382
+ debounce(el, event, phxDebounce, defaultDebounce, phxThrottle, defaultThrottle, asyncFilter, callback) {
383
383
  let debounce = el.getAttribute(phxDebounce);
384
384
  let throttle = el.getAttribute(phxThrottle);
385
385
  if (debounce === "") {
@@ -416,10 +416,18 @@ var DOM = {
416
416
  } else {
417
417
  callback();
418
418
  this.putPrivate(el, THROTTLED, true);
419
- setTimeout(() => this.triggerCycle(el, DEBOUNCE_TRIGGER), timeout);
419
+ setTimeout(() => {
420
+ if (asyncFilter()) {
421
+ this.triggerCycle(el, DEBOUNCE_TRIGGER);
422
+ }
423
+ }, timeout);
420
424
  }
421
425
  } else {
422
- setTimeout(() => this.triggerCycle(el, DEBOUNCE_TRIGGER, currentCycle), timeout);
426
+ setTimeout(() => {
427
+ if (asyncFilter()) {
428
+ this.triggerCycle(el, DEBOUNCE_TRIGGER, currentCycle);
429
+ }
430
+ }, timeout);
423
431
  }
424
432
  let form = el.form;
425
433
  if (form && this.once(form, "bind-debounce")) {
@@ -2679,13 +2687,15 @@ var View = class {
2679
2687
  return this.onLiveRedirect(resp.live_redirect);
2680
2688
  }
2681
2689
  this.log("error", () => ["unable to join", resp]);
2682
- return this.liveSocket.reloadWithJitter(this);
2690
+ if (this.liveSocket.isConnected()) {
2691
+ this.liveSocket.reloadWithJitter(this);
2692
+ }
2683
2693
  }
2684
2694
  onClose(reason) {
2685
2695
  if (this.isDestroyed()) {
2686
2696
  return;
2687
2697
  }
2688
- if (this.isJoinPending() && document.visibilityState !== "hidden" || this.liveSocket.hasPendingLink() && reason !== "leave") {
2698
+ if (this.liveSocket.hasPendingLink() && reason !== "leave") {
2689
2699
  return this.liveSocket.reloadWithJitter(this);
2690
2700
  }
2691
2701
  this.destroyAllChildren();
@@ -2699,7 +2709,9 @@ var View = class {
2699
2709
  }
2700
2710
  onError(reason) {
2701
2711
  this.onClose(reason);
2702
- this.log("error", () => ["view crashed", reason]);
2712
+ if (this.liveSocket.isConnected()) {
2713
+ this.log("error", () => ["view crashed", reason]);
2714
+ }
2703
2715
  if (!this.liveSocket.isUnloaded()) {
2704
2716
  this.displayError();
2705
2717
  }
@@ -3184,6 +3196,7 @@ var LiveSocket = class {
3184
3196
  this.hooks = opts.hooks || {};
3185
3197
  this.uploaders = opts.uploaders || {};
3186
3198
  this.loaderTimeout = opts.loaderTimeout || LOADER_TIMEOUT;
3199
+ this.reloadWithJitterTimer = null;
3187
3200
  this.maxReloads = opts.maxReloads || MAX_RELOADS;
3188
3201
  this.reloadJitterMin = opts.reloadJitterMin || RELOAD_JITTER_MIN;
3189
3202
  this.reloadJitterMax = opts.reloadJitterMax || RELOAD_JITTER_MAX;
@@ -3246,6 +3259,8 @@ var LiveSocket = class {
3246
3259
  if (this.joinRootViews()) {
3247
3260
  this.bindTopLevelEvents();
3248
3261
  this.socket.connect();
3262
+ } else if (this.main) {
3263
+ this.socket.connect();
3249
3264
  }
3250
3265
  };
3251
3266
  if (["complete", "loaded", "interactive"].indexOf(document.readyState) >= 0) {
@@ -3255,8 +3270,14 @@ var LiveSocket = class {
3255
3270
  }
3256
3271
  }
3257
3272
  disconnect(callback) {
3273
+ clearTimeout(this.reloadWithJitterTimer);
3258
3274
  this.socket.disconnect(callback);
3259
3275
  }
3276
+ replaceTransport(transport) {
3277
+ clearTimeout(this.reloadWithJitterTimer);
3278
+ this.socket.replaceTransport(transport);
3279
+ this.connect();
3280
+ }
3260
3281
  execJS(el, encodedJS, eventType = null) {
3261
3282
  this.owner(el, (view) => js_default.exec(eventType, encodedJS, view, el));
3262
3283
  }
@@ -3331,18 +3352,24 @@ var LiveSocket = class {
3331
3352
  return fakePush;
3332
3353
  }
3333
3354
  reloadWithJitter(view, log) {
3334
- view.destroy();
3355
+ clearTimeout(this.reloadWithJitterTimer);
3335
3356
  this.disconnect();
3336
3357
  let minMs = this.reloadJitterMin;
3337
3358
  let maxMs = this.reloadJitterMax;
3338
3359
  let afterMs = Math.floor(Math.random() * (maxMs - minMs + 1)) + minMs;
3339
3360
  let tries = browser_default.updateLocal(this.localStorage, window.location.pathname, CONSECUTIVE_RELOADS, 0, (count) => count + 1);
3340
- log ? log() : this.log(view, "join", () => [`encountered ${tries} consecutive reloads`]);
3341
3361
  if (tries > this.maxReloads) {
3342
- this.log(view, "join", () => [`exceeded ${this.maxReloads} consecutive reloads. Entering failsafe mode`]);
3343
3362
  afterMs = this.failsafeJitter;
3344
3363
  }
3345
- setTimeout(() => {
3364
+ this.reloadWithJitterTimer = setTimeout(() => {
3365
+ if (view.isDestroyed() || view.isConnected()) {
3366
+ return;
3367
+ }
3368
+ view.destroy();
3369
+ log ? log() : this.log(view, "join", () => [`encountered ${tries} consecutive reloads`]);
3370
+ if (tries > this.maxReloads) {
3371
+ this.log(view, "join", () => [`exceeded ${this.maxReloads} consecutive reloads. Entering failsafe mode`]);
3372
+ }
3346
3373
  if (this.hasPendingLink()) {
3347
3374
  window.location = this.pendingLink;
3348
3375
  } else {
@@ -3401,7 +3428,7 @@ var LiveSocket = class {
3401
3428
  dom_default.findPhxSticky(document).forEach((el) => newMainEl.appendChild(el));
3402
3429
  this.outgoingMainEl.replaceWith(newMainEl);
3403
3430
  this.outgoingMainEl = null;
3404
- callback && callback();
3431
+ callback && requestAnimationFrame(callback);
3405
3432
  onDone();
3406
3433
  });
3407
3434
  }
@@ -3445,6 +3472,7 @@ var LiveSocket = class {
3445
3472
  this.roots[id].destroy();
3446
3473
  delete this.roots[id];
3447
3474
  }
3475
+ this.main = null;
3448
3476
  }
3449
3477
  destroyViewByEl(el) {
3450
3478
  let root = this.getRootById(el.getAttribute(PHX_ROOT_ID));
@@ -3592,7 +3620,7 @@ var LiveSocket = class {
3592
3620
  let windowBinding = this.binding(`window-${event}`);
3593
3621
  let targetPhxEvent = e.target.getAttribute && e.target.getAttribute(binding);
3594
3622
  if (targetPhxEvent) {
3595
- this.debounce(e.target, e, () => {
3623
+ this.debounce(e.target, e, browserEventName, () => {
3596
3624
  this.withinOwners(e.target, (view) => {
3597
3625
  callback(e, event, view, e.target, targetPhxEvent, null);
3598
3626
  });
@@ -3600,7 +3628,7 @@ var LiveSocket = class {
3600
3628
  } else {
3601
3629
  dom_default.all(document, `[${windowBinding}]`, (el) => {
3602
3630
  let phxEvent = el.getAttribute(windowBinding);
3603
- this.debounce(el, e, () => {
3631
+ this.debounce(el, e, browserEventName, () => {
3604
3632
  this.withinOwners(el, (view) => {
3605
3633
  callback(e, event, view, el, phxEvent, "window");
3606
3634
  });
@@ -3634,7 +3662,7 @@ var LiveSocket = class {
3634
3662
  if (target.getAttribute("href") === "#") {
3635
3663
  e.preventDefault();
3636
3664
  }
3637
- this.debounce(target, e, () => {
3665
+ this.debounce(target, e, "click", () => {
3638
3666
  this.withinOwners(target, (view) => {
3639
3667
  js_default.exec("click", phxEvent, view, target, ["push", { data: this.eventMeta("click", e, target) }]);
3640
3668
  });
@@ -3798,7 +3826,7 @@ var LiveSocket = class {
3798
3826
  return;
3799
3827
  }
3800
3828
  dom_default.putPrivate(input, "prev-iteration", { at: currentIterations, type });
3801
- this.debounce(input, e, () => {
3829
+ this.debounce(input, e, type, () => {
3802
3830
  this.withinOwners(dispatcher, (view) => {
3803
3831
  dom_default.putPrivate(input, PHX_HAS_FOCUSED, true);
3804
3832
  if (!dom_default.isTextualInput(input)) {
@@ -3810,12 +3838,20 @@ var LiveSocket = class {
3810
3838
  }, false);
3811
3839
  }
3812
3840
  }
3813
- debounce(el, event, callback) {
3841
+ debounce(el, event, eventType, callback) {
3842
+ if (eventType === "blur" || eventType === "focusout") {
3843
+ return callback();
3844
+ }
3814
3845
  let phxDebounce = this.binding(PHX_DEBOUNCE);
3815
3846
  let phxThrottle = this.binding(PHX_THROTTLE);
3816
3847
  let defaultDebounce = this.defaults.debounce.toString();
3817
3848
  let defaultThrottle = this.defaults.throttle.toString();
3818
- dom_default.debounce(el, event, phxDebounce, defaultDebounce, phxThrottle, defaultThrottle, callback);
3849
+ this.withinOwners(el, (view) => {
3850
+ let asyncFilter = () => !view.isDestroyed() && document.body.contains(el);
3851
+ dom_default.debounce(el, event, phxDebounce, defaultDebounce, phxThrottle, defaultThrottle, asyncFilter, () => {
3852
+ callback();
3853
+ });
3854
+ });
3819
3855
  }
3820
3856
  silenceEvents(callback) {
3821
3857
  this.silenced = true;