phoenix_live_view 1.0.5 → 1.0.7

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.
@@ -67,6 +67,7 @@ export const PHX_RELOAD_STATUS = "__phoenix_reload_status__"
67
67
  export const LOADER_TIMEOUT = 1
68
68
  export const MAX_CHILD_JOIN_ATTEMPTS = 3
69
69
  export const BEFORE_UNLOAD_LOADER_TIMEOUT = 200
70
+ export const DISCONNECTED_TIMEOUT = 500
70
71
  export const BINDING_PREFIX = "phx-"
71
72
  export const PUSH_TIMEOUT = 30000
72
73
  export const LINK_HEADER = "x-requested-with"
@@ -234,10 +234,11 @@ let DOM = {
234
234
  case null: return callback()
235
235
 
236
236
  case "blur":
237
+ this.incCycle(el, "debounce-blur-cycle", () => {
238
+ if(asyncFilter()){ callback() }
239
+ })
237
240
  if(this.once(el, "debounce-blur")){
238
- el.addEventListener("blur", () => {
239
- if(asyncFilter()){ callback() }
240
- })
241
+ el.addEventListener("blur", () => this.triggerCycle(el, "debounce-blur-cycle"))
241
242
  }
242
243
  return
243
244
 
@@ -28,6 +28,8 @@
28
28
  * @param {Object} [opts.uploaders] - The optional object for referencing LiveView uploader callbacks.
29
29
  * @param {integer} [opts.loaderTimeout] - The optional delay in milliseconds to wait before apply
30
30
  * loading states.
31
+ * @param {integer} [opts.disconnectedTimeout] - The delay in milliseconds to wait before
32
+ * executing phx-disconnected commands. Defaults to 500.
31
33
  * @param {integer} [opts.maxReloads] - The maximum reloads before entering failsafe mode.
32
34
  * @param {integer} [opts.reloadJitterMin] - The minimum time between normal reload attempts.
33
35
  * @param {integer} [opts.reloadJitterMax] - The maximum time between normal reload attempts.
@@ -78,6 +80,7 @@ import {
78
80
  DEFAULTS,
79
81
  FAILSAFE_JITTER,
80
82
  LOADER_TIMEOUT,
83
+ DISCONNECTED_TIMEOUT,
81
84
  MAX_RELOADS,
82
85
  PHX_DEBOUNCE,
83
86
  PHX_DROP_TARGET,
@@ -152,6 +155,7 @@ export default class LiveSocket {
152
155
  this.hooks = opts.hooks || {}
153
156
  this.uploaders = opts.uploaders || {}
154
157
  this.loaderTimeout = opts.loaderTimeout || LOADER_TIMEOUT
158
+ this.disconnectedTimeout = opts.disconnectedTimeout || DISCONNECTED_TIMEOUT
155
159
  this.reloadWithJitterTimer = null
156
160
  this.maxReloads = opts.maxReloads || MAX_RELOADS
157
161
  this.reloadJitterMin = opts.reloadJitterMin || RELOAD_JITTER_MIN
@@ -67,8 +67,8 @@ export let prependFormDataKey = (key, prefix) => {
67
67
  return baseKey
68
68
  }
69
69
 
70
- let serializeForm = (form, metadata, onlyNames = []) => {
71
- const {submitter, ...meta} = metadata
70
+ let serializeForm = (form, opts, onlyNames = []) => {
71
+ const {submitter} = opts
72
72
 
73
73
  // We must inject the submitter in the order that it exists in the DOM
74
74
  // relative to other inputs. For example, for checkbox groups, the order must be maintained.
@@ -100,12 +100,26 @@ let serializeForm = (form, metadata, onlyNames = []) => {
100
100
 
101
101
  const params = new URLSearchParams()
102
102
 
103
- let elements = Array.from(form.elements)
103
+ const {inputsUnused, onlyHiddenInputs} = Array.from(form.elements).reduce((acc, input) => {
104
+ const {inputsUnused, onlyHiddenInputs} = acc
105
+ const key = input.name
106
+ if(!key){ return acc }
107
+
108
+ if(inputsUnused[key] === undefined){ inputsUnused[key] = true }
109
+ if(onlyHiddenInputs[key] === undefined){ onlyHiddenInputs[key] = true }
110
+
111
+ const isUsed = DOM.private(input, PHX_HAS_FOCUSED) || DOM.private(input, PHX_HAS_SUBMITTED)
112
+ const isHidden = input.type === "hidden"
113
+ inputsUnused[key] = inputsUnused[key] && !isUsed
114
+ onlyHiddenInputs[key] = onlyHiddenInputs[key] && isHidden
115
+
116
+ return acc
117
+ }, {inputsUnused: {}, onlyHiddenInputs: {}})
118
+
104
119
  for(let [key, val] of formData.entries()){
105
120
  if(onlyNames.length === 0 || onlyNames.indexOf(key) >= 0){
106
- let inputs = elements.filter(input => input.name === key)
107
- let isUnused = !inputs.some(input => (DOM.private(input, PHX_HAS_FOCUSED) || DOM.private(input, PHX_HAS_SUBMITTED)))
108
- let hidden = inputs.every(input => input.type === "hidden")
121
+ let isUnused = inputsUnused[key]
122
+ let hidden = onlyHiddenInputs[key]
109
123
  if(isUnused && !(submitter && submitter.name == key) && !hidden){
110
124
  params.append(prependFormDataKey(key, "_unused_"), "")
111
125
  }
@@ -119,8 +133,6 @@ let serializeForm = (form, metadata, onlyNames = []) => {
119
133
  submitter.parentElement.removeChild(injectedElement)
120
134
  }
121
135
 
122
- for(let metaKey in meta){ params.append(metaKey, meta[metaKey]) }
123
-
124
136
  return params.toString()
125
137
  }
126
138
 
@@ -143,6 +155,7 @@ export default class View {
143
155
  this.lastAckRef = null
144
156
  this.childJoins = 0
145
157
  this.loaderTimer = null
158
+ this.disconnectedTimer = null
146
159
  this.pendingDiffs = []
147
160
  this.pendingForms = new Set()
148
161
  this.redirect = false
@@ -254,6 +267,7 @@ export default class View {
254
267
 
255
268
  hideLoader(){
256
269
  clearTimeout(this.loaderTimer)
270
+ clearTimeout(this.disconnectedTimer)
257
271
  this.setContainerClasses(PHX_CONNECTED_CLASS)
258
272
  this.execAll(this.binding("connected"))
259
273
  }
@@ -740,6 +754,12 @@ export default class View {
740
754
  }
741
755
 
742
756
  applyPendingUpdates(){
757
+ // prevent race conditions where we might still be pending a new
758
+ // navigation after applying the current one;
759
+ // if we call update and a pendingDiff is not applied, it would
760
+ // be silently dropped otherwise, as update would push it back to
761
+ // pendingDiffs, but we clear it immediately after
762
+ if(this.liveSocket.hasPendingLink() && this.root.isMain()){ return }
743
763
  this.pendingDiffs.forEach(({diff, events}) => this.update(diff, events))
744
764
  this.pendingDiffs = []
745
765
  this.eachChild(child => child.applyPendingUpdates())
@@ -891,7 +911,13 @@ export default class View {
891
911
  if(this.isMain()){ DOM.dispatchEvent(window, "phx:page-loading-start", {detail: {to: this.href, kind: "error"}}) }
892
912
  this.showLoader()
893
913
  this.setContainerClasses(...classes)
894
- this.execAll(this.binding("disconnected"))
914
+ this.delayedDisconnected()
915
+ }
916
+
917
+ delayedDisconnected(){
918
+ this.disconnectedTimer = setTimeout(() => {
919
+ this.execAll(this.binding("disconnected"))
920
+ }, this.liveSocket.disconnectedTimeout)
895
921
  }
896
922
 
897
923
  wrapPush(callerPush, receives){
@@ -1171,12 +1197,13 @@ export default class View {
1171
1197
  ], phxEvent, "change", opts)
1172
1198
  }
1173
1199
  let formData
1174
- let meta = this.extractMeta(inputEl.form)
1175
- if(inputEl instanceof HTMLButtonElement){ meta.submitter = inputEl }
1200
+ let meta = this.extractMeta(inputEl.form, {}, opts.value)
1201
+ let serializeOpts = {}
1202
+ if(inputEl instanceof HTMLButtonElement){ serializeOpts.submitter = inputEl }
1176
1203
  if(inputEl.getAttribute(this.binding("change"))){
1177
- formData = serializeForm(inputEl.form, {_target: opts._target, ...meta}, [inputEl.name])
1204
+ formData = serializeForm(inputEl.form, serializeOpts, [inputEl.name])
1178
1205
  } else {
1179
- formData = serializeForm(inputEl.form, {_target: opts._target, ...meta})
1206
+ formData = serializeForm(inputEl.form, serializeOpts)
1180
1207
  }
1181
1208
  if(DOM.isUploadInput(inputEl) && inputEl.files && inputEl.files.length > 0){
1182
1209
  LiveUploader.trackFiles(inputEl, Array.from(inputEl.files))
@@ -1187,6 +1214,7 @@ export default class View {
1187
1214
  type: "form",
1188
1215
  event: phxEvent,
1189
1216
  value: formData,
1217
+ meta: {_target: opts._target, ...meta},
1190
1218
  uploads: uploads,
1191
1219
  cid: cid
1192
1220
  }
@@ -1300,22 +1328,24 @@ export default class View {
1300
1328
  if(LiveUploader.inputsAwaitingPreflight(formEl).length > 0){
1301
1329
  return this.undoRefs(ref, phxEvent)
1302
1330
  }
1303
- let meta = this.extractMeta(formEl)
1304
- let formData = serializeForm(formEl, {submitter, ...meta})
1331
+ let meta = this.extractMeta(formEl, {}, opts.value)
1332
+ let formData = serializeForm(formEl, {submitter})
1305
1333
  this.pushWithReply(proxyRefGen, "event", {
1306
1334
  type: "form",
1307
1335
  event: phxEvent,
1308
1336
  value: formData,
1337
+ meta: meta,
1309
1338
  cid: cid
1310
1339
  }).then(({resp}) => onReply(resp))
1311
1340
  })
1312
1341
  } else if(!(formEl.hasAttribute(PHX_REF_SRC) && formEl.classList.contains("phx-submit-loading"))){
1313
- let meta = this.extractMeta(formEl)
1314
- let formData = serializeForm(formEl, {submitter, ...meta})
1342
+ let meta = this.extractMeta(formEl, {}, opts.value)
1343
+ let formData = serializeForm(formEl, {submitter})
1315
1344
  this.pushWithReply(refGenerator, "event", {
1316
1345
  type: "form",
1317
1346
  event: phxEvent,
1318
1347
  value: formData,
1348
+ meta: meta,
1319
1349
  cid: cid
1320
1350
  }).then(({resp}) => onReply(resp))
1321
1351
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phoenix_live_view",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "The Phoenix LiveView JavaScript client.",
5
5
  "license": "MIT",
6
6
  "module": "./priv/static/phoenix_live_view.esm.js",
@@ -100,6 +100,7 @@ var PHX_RELOAD_STATUS = "__phoenix_reload_status__";
100
100
  var LOADER_TIMEOUT = 1;
101
101
  var MAX_CHILD_JOIN_ATTEMPTS = 3;
102
102
  var BEFORE_UNLOAD_LOADER_TIMEOUT = 200;
103
+ var DISCONNECTED_TIMEOUT = 500;
103
104
  var BINDING_PREFIX = "phx-";
104
105
  var PUSH_TIMEOUT = 3e4;
105
106
  var DEBOUNCE_TRIGGER = "debounce-trigger";
@@ -515,12 +516,13 @@ var DOM = {
515
516
  case null:
516
517
  return callback();
517
518
  case "blur":
519
+ this.incCycle(el, "debounce-blur-cycle", () => {
520
+ if (asyncFilter()) {
521
+ callback();
522
+ }
523
+ });
518
524
  if (this.once(el, "debounce-blur")) {
519
- el.addEventListener("blur", () => {
520
- if (asyncFilter()) {
521
- callback();
522
- }
523
- });
525
+ el.addEventListener("blur", () => this.triggerCycle(el, "debounce-blur-cycle"));
524
526
  }
525
527
  return;
526
528
  default:
@@ -3358,8 +3360,8 @@ var prependFormDataKey = (key, prefix) => {
3358
3360
  }
3359
3361
  return baseKey;
3360
3362
  };
3361
- var serializeForm = (form, metadata, onlyNames = []) => {
3362
- const { submitter, ...meta } = metadata;
3363
+ var serializeForm = (form, opts, onlyNames = []) => {
3364
+ const { submitter } = opts;
3363
3365
  let injectedElement;
3364
3366
  if (submitter && submitter.name) {
3365
3367
  const input = document.createElement("input");
@@ -3382,12 +3384,28 @@ var serializeForm = (form, metadata, onlyNames = []) => {
3382
3384
  });
3383
3385
  toRemove.forEach((key) => formData.delete(key));
3384
3386
  const params = new URLSearchParams();
3385
- let elements = Array.from(form.elements);
3387
+ const { inputsUnused, onlyHiddenInputs } = Array.from(form.elements).reduce((acc, input) => {
3388
+ const { inputsUnused: inputsUnused2, onlyHiddenInputs: onlyHiddenInputs2 } = acc;
3389
+ const key = input.name;
3390
+ if (!key) {
3391
+ return acc;
3392
+ }
3393
+ if (inputsUnused2[key] === void 0) {
3394
+ inputsUnused2[key] = true;
3395
+ }
3396
+ if (onlyHiddenInputs2[key] === void 0) {
3397
+ onlyHiddenInputs2[key] = true;
3398
+ }
3399
+ const isUsed = dom_default.private(input, PHX_HAS_FOCUSED) || dom_default.private(input, PHX_HAS_SUBMITTED);
3400
+ const isHidden = input.type === "hidden";
3401
+ inputsUnused2[key] = inputsUnused2[key] && !isUsed;
3402
+ onlyHiddenInputs2[key] = onlyHiddenInputs2[key] && isHidden;
3403
+ return acc;
3404
+ }, { inputsUnused: {}, onlyHiddenInputs: {} });
3386
3405
  for (let [key, val] of formData.entries()) {
3387
3406
  if (onlyNames.length === 0 || onlyNames.indexOf(key) >= 0) {
3388
- let inputs = elements.filter((input) => input.name === key);
3389
- let isUnused = !inputs.some((input) => dom_default.private(input, PHX_HAS_FOCUSED) || dom_default.private(input, PHX_HAS_SUBMITTED));
3390
- let hidden = inputs.every((input) => input.type === "hidden");
3407
+ let isUnused = inputsUnused[key];
3408
+ let hidden = onlyHiddenInputs[key];
3391
3409
  if (isUnused && !(submitter && submitter.name == key) && !hidden) {
3392
3410
  params.append(prependFormDataKey(key, "_unused_"), "");
3393
3411
  }
@@ -3397,9 +3415,6 @@ var serializeForm = (form, metadata, onlyNames = []) => {
3397
3415
  if (submitter && injectedElement) {
3398
3416
  submitter.parentElement.removeChild(injectedElement);
3399
3417
  }
3400
- for (let metaKey in meta) {
3401
- params.append(metaKey, meta[metaKey]);
3402
- }
3403
3418
  return params.toString();
3404
3419
  };
3405
3420
  var View = class _View {
@@ -3420,6 +3435,7 @@ var View = class _View {
3420
3435
  this.lastAckRef = null;
3421
3436
  this.childJoins = 0;
3422
3437
  this.loaderTimer = null;
3438
+ this.disconnectedTimer = null;
3423
3439
  this.pendingDiffs = [];
3424
3440
  this.pendingForms = /* @__PURE__ */ new Set();
3425
3441
  this.redirect = false;
@@ -3528,6 +3544,7 @@ var View = class _View {
3528
3544
  }
3529
3545
  hideLoader() {
3530
3546
  clearTimeout(this.loaderTimer);
3547
+ clearTimeout(this.disconnectedTimer);
3531
3548
  this.setContainerClasses(PHX_CONNECTED_CLASS);
3532
3549
  this.execAll(this.binding("connected"));
3533
3550
  }
@@ -3947,6 +3964,9 @@ var View = class _View {
3947
3964
  delete this.viewHooks[hookId];
3948
3965
  }
3949
3966
  applyPendingUpdates() {
3967
+ if (this.liveSocket.hasPendingLink() && this.root.isMain()) {
3968
+ return;
3969
+ }
3950
3970
  this.pendingDiffs.forEach(({ diff, events }) => this.update(diff, events));
3951
3971
  this.pendingDiffs = [];
3952
3972
  this.eachChild((child) => child.applyPendingUpdates());
@@ -4102,7 +4122,12 @@ var View = class _View {
4102
4122
  }
4103
4123
  this.showLoader();
4104
4124
  this.setContainerClasses(...classes);
4105
- this.execAll(this.binding("disconnected"));
4125
+ this.delayedDisconnected();
4126
+ }
4127
+ delayedDisconnected() {
4128
+ this.disconnectedTimer = setTimeout(() => {
4129
+ this.execAll(this.binding("disconnected"));
4130
+ }, this.liveSocket.disconnectedTimeout);
4106
4131
  }
4107
4132
  wrapPush(callerPush, receives) {
4108
4133
  let latency = this.liveSocket.getLatencySim();
@@ -4392,14 +4417,15 @@ var View = class _View {
4392
4417
  ], phxEvent, "change", opts);
4393
4418
  };
4394
4419
  let formData;
4395
- let meta = this.extractMeta(inputEl.form);
4420
+ let meta = this.extractMeta(inputEl.form, {}, opts.value);
4421
+ let serializeOpts = {};
4396
4422
  if (inputEl instanceof HTMLButtonElement) {
4397
- meta.submitter = inputEl;
4423
+ serializeOpts.submitter = inputEl;
4398
4424
  }
4399
4425
  if (inputEl.getAttribute(this.binding("change"))) {
4400
- formData = serializeForm(inputEl.form, { _target: opts._target, ...meta }, [inputEl.name]);
4426
+ formData = serializeForm(inputEl.form, serializeOpts, [inputEl.name]);
4401
4427
  } else {
4402
- formData = serializeForm(inputEl.form, { _target: opts._target, ...meta });
4428
+ formData = serializeForm(inputEl.form, serializeOpts);
4403
4429
  }
4404
4430
  if (dom_default.isUploadInput(inputEl) && inputEl.files && inputEl.files.length > 0) {
4405
4431
  LiveUploader.trackFiles(inputEl, Array.from(inputEl.files));
@@ -4409,6 +4435,7 @@ var View = class _View {
4409
4435
  type: "form",
4410
4436
  event: phxEvent,
4411
4437
  value: formData,
4438
+ meta: { _target: opts._target, ...meta },
4412
4439
  uploads,
4413
4440
  cid
4414
4441
  };
@@ -4507,22 +4534,24 @@ var View = class _View {
4507
4534
  if (LiveUploader.inputsAwaitingPreflight(formEl).length > 0) {
4508
4535
  return this.undoRefs(ref, phxEvent);
4509
4536
  }
4510
- let meta = this.extractMeta(formEl);
4511
- let formData = serializeForm(formEl, { submitter, ...meta });
4537
+ let meta = this.extractMeta(formEl, {}, opts.value);
4538
+ let formData = serializeForm(formEl, { submitter });
4512
4539
  this.pushWithReply(proxyRefGen, "event", {
4513
4540
  type: "form",
4514
4541
  event: phxEvent,
4515
4542
  value: formData,
4543
+ meta,
4516
4544
  cid
4517
4545
  }).then(({ resp }) => onReply(resp));
4518
4546
  });
4519
4547
  } else if (!(formEl.hasAttribute(PHX_REF_SRC) && formEl.classList.contains("phx-submit-loading"))) {
4520
- let meta = this.extractMeta(formEl);
4521
- let formData = serializeForm(formEl, { submitter, ...meta });
4548
+ let meta = this.extractMeta(formEl, {}, opts.value);
4549
+ let formData = serializeForm(formEl, { submitter });
4522
4550
  this.pushWithReply(refGenerator, "event", {
4523
4551
  type: "form",
4524
4552
  event: phxEvent,
4525
4553
  value: formData,
4554
+ meta,
4526
4555
  cid
4527
4556
  }).then(({ resp }) => onReply(resp));
4528
4557
  }
@@ -4742,6 +4771,7 @@ var LiveSocket = class {
4742
4771
  this.hooks = opts.hooks || {};
4743
4772
  this.uploaders = opts.uploaders || {};
4744
4773
  this.loaderTimeout = opts.loaderTimeout || LOADER_TIMEOUT;
4774
+ this.disconnectedTimeout = opts.disconnectedTimeout || DISCONNECTED_TIMEOUT;
4745
4775
  this.reloadWithJitterTimer = null;
4746
4776
  this.maxReloads = opts.maxReloads || MAX_RELOADS;
4747
4777
  this.reloadJitterMin = opts.reloadJitterMin || RELOAD_JITTER_MIN;
@@ -4775,7 +4805,7 @@ var LiveSocket = class {
4775
4805
  }
4776
4806
  // public
4777
4807
  version() {
4778
- return "1.0.5";
4808
+ return "1.0.7";
4779
4809
  }
4780
4810
  isProfileEnabled() {
4781
4811
  return this.sessionStorage.getItem(PHX_LV_PROFILE) === "true";