@holo-js/forms 0.3.16 → 0.3.17

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.
@@ -7,16 +7,26 @@ declare class FormClientState<TValues, TSuccess = unknown> {
7
7
  values: TValues;
8
8
  initialValues: TValues;
9
9
  flattenedErrors: Record<string, readonly string[]>;
10
- touched: Set<string>;
11
- dirty: Set<string>;
10
+ readonly touched: Set<string>;
12
11
  lastSubmission?: SerializedFormSubmission<TValues> | FormFailurePayload<TValues> | FormSuccessPayload<TSuccess>;
13
12
  readonly listeners: Set<() => void>;
14
- validationSequence: number;
15
13
  constructor(values: TValues, initialState?: SerializedFormSubmission<TValues> | FormFailurePayload<TValues>);
16
14
  get submitting(): boolean;
17
15
  get errors(): ValidationErrorBag<TValues>;
18
16
  get dirtyPaths(): readonly string[];
19
- replace(values: TValues, initialValues: TValues, errors: Record<string, readonly string[]>, touched: ReadonlySet<string>): void;
17
+ notify(): void;
18
+ invalidateEffects(): void;
19
+ beginValidation(): number;
20
+ isLatestValidation(sequence: number): boolean;
21
+ beginSubmissionEffects(): number;
22
+ ownsSubmissionEffects(sequence: number): boolean;
23
+ touch(path: string): void;
24
+ edit(path: string, value: unknown): void;
25
+ isDirty(path: string): boolean;
26
+ applyValidation(sequence: number, errors: Record<string, readonly string[]>, values?: Partial<TValues>): void;
27
+ applyFieldValidation(sequence: number, path: string, errors: Record<string, readonly string[]>): void;
28
+ applyServer(values: TValues, errors: Record<string, readonly string[]>, submission: FormClientState<TValues, TSuccess>['lastSubmission']): void;
29
+ reset(values?: Partial<TValues>): void;
20
30
  startSubmission(signal?: AbortSignal): () => void;
21
31
  }
22
32
 
@@ -275,10 +275,10 @@ var FormClientState = class {
275
275
  initialValues;
276
276
  flattenedErrors;
277
277
  touched = /* @__PURE__ */ new Set();
278
- dirty = /* @__PURE__ */ new Set();
279
278
  lastSubmission;
280
279
  listeners = /* @__PURE__ */ new Set();
281
- validationSequence = 0;
280
+ #validationSequence = 0;
281
+ #submissionSequence = 0;
282
282
  #submissions = /* @__PURE__ */ new Set();
283
283
  constructor(values, initialState) {
284
284
  this.values = cloneFormValue(values);
@@ -295,12 +295,68 @@ var FormClientState = class {
295
295
  get dirtyPaths() {
296
296
  return collectFormDirtyPaths(this.values, this.initialValues);
297
297
  }
298
- replace(values, initialValues, errors, touched) {
298
+ notify() {
299
+ for (const listener of this.listeners) listener();
300
+ }
301
+ invalidateEffects() {
302
+ this.#validationSequence += 1;
303
+ this.#submissionSequence += 1;
304
+ }
305
+ beginValidation() {
306
+ return ++this.#validationSequence;
307
+ }
308
+ isLatestValidation(sequence) {
309
+ return sequence === this.#validationSequence;
310
+ }
311
+ beginSubmissionEffects() {
312
+ this.#validationSequence += 1;
313
+ return ++this.#submissionSequence;
314
+ }
315
+ ownsSubmissionEffects(sequence) {
316
+ return sequence === this.#submissionSequence;
317
+ }
318
+ touch(path) {
319
+ this.touched.add(path);
320
+ }
321
+ edit(path, value) {
322
+ this.invalidateEffects();
323
+ setFormValueAtPath(this.values, path, value);
324
+ this.touch(path);
325
+ }
326
+ isDirty(path) {
327
+ return !areFormValuesEqual(getFormValueAtPath(this.values, path), getFormValueAtPath(this.initialValues, path));
328
+ }
329
+ applyValidation(sequence, errors, values) {
330
+ if (!this.isLatestValidation(sequence)) return;
331
+ if (values) this.values = mergeFormValues(this.values, values);
332
+ this.flattenedErrors = errors;
333
+ this.notify();
334
+ }
335
+ applyFieldValidation(sequence, path, errors) {
336
+ if (!this.isLatestValidation(sequence)) return;
337
+ const prefix = `${path}.`;
338
+ const merged = Object.fromEntries(Object.entries(this.flattenedErrors).filter(([key]) => key !== path && !key.startsWith(prefix)));
339
+ for (const [key, messages] of Object.entries(errors)) {
340
+ if (key === path || key.startsWith(prefix)) merged[key] = messages;
341
+ }
342
+ this.applyValidation(sequence, merged);
343
+ }
344
+ applyServer(values, errors, submission) {
345
+ this.#validationSequence += 1;
299
346
  this.values = values;
300
- this.initialValues = initialValues;
301
347
  this.flattenedErrors = errors;
302
- this.touched = new Set(touched);
303
- this.dirty = new Set(this.dirtyPaths);
348
+ this.lastSubmission = submission;
349
+ this.notify();
350
+ }
351
+ reset(values) {
352
+ const next = mergeFormValues(this.initialValues, values);
353
+ this.invalidateEffects();
354
+ this.values = cloneFormValue(next);
355
+ this.initialValues = cloneFormValue(next);
356
+ this.flattenedErrors = {};
357
+ this.touched.clear();
358
+ this.lastSubmission = void 0;
359
+ this.notify();
304
360
  }
305
361
  startSubmission(signal) {
306
362
  const id = /* @__PURE__ */ Symbol();
@@ -446,36 +502,12 @@ async function runWithBrowserFormElement(formClient, form) {
446
502
  }
447
503
  return await formClient.submit();
448
504
  }
449
- function notifyListeners(state) {
450
- for (const listener of state.listeners) {
451
- listener();
452
- }
453
- }
454
- function nextValidationSequence(state) {
455
- state.validationSequence += 1;
456
- return state.validationSequence;
457
- }
458
- function isLatestValidation(state, sequence) {
459
- return sequence === state.validationSequence;
460
- }
461
505
  function collectErrorsForPath(flattenedErrors, path) {
462
506
  const direct = flattenedErrors[path] ?? [];
463
507
  const prefix = `${path}.`;
464
508
  const nested = Object.entries(flattenedErrors).filter(([key]) => key.startsWith(prefix)).flatMap(([, messages]) => messages);
465
509
  return direct.length > 0 || nested.length > 0 ? [...direct, ...nested] : [];
466
510
  }
467
- function replaceErrorsForPath(flattenedErrors, path, nextErrors) {
468
- const prefix = `${path}.`;
469
- const merged = Object.fromEntries(
470
- Object.entries(flattenedErrors).filter(([key]) => key !== path && !key.startsWith(prefix))
471
- );
472
- for (const [key, messages] of Object.entries(nextErrors)) {
473
- if (key === path || key.startsWith(prefix)) {
474
- merged[key] = messages;
475
- }
476
- }
477
- return merged;
478
- }
479
511
  function buildFieldsTree(state, schemaDefinition, source, validateOn, prefix = "") {
480
512
  if (isFormLeafValue(source)) {
481
513
  const path = prefix;
@@ -493,56 +525,41 @@ function buildFieldsTree(state, schemaDefinition, source, validateOn, prefix = "
493
525
  return state.touched.has(path);
494
526
  },
495
527
  get dirty() {
496
- return state.dirty.has(path);
528
+ return state.isDirty(path);
497
529
  },
498
530
  async set(value) {
499
- setFormValueAtPath(state.values, path, value);
500
- state.touched.add(path);
501
- if (!areFormValuesEqual(getFormValueAtPath(state.initialValues, path), value)) {
502
- state.dirty.add(path);
503
- } else {
504
- state.dirty.delete(path);
505
- }
531
+ state.edit(path, value);
506
532
  if (validateOn === "change") {
507
- const sequence = nextValidationSequence(state);
533
+ const sequence = state.beginValidation();
508
534
  const submission = await validateClientValues(
509
535
  cloneFormValue(state.values),
510
536
  schemaDefinition
511
537
  );
512
- if (isLatestValidation(state, sequence)) {
513
- state.flattenedErrors = submission.errors.flatten();
514
- }
538
+ state.applyValidation(sequence, submission.errors.flatten());
539
+ return;
515
540
  }
516
- notifyListeners(state);
541
+ state.notify();
517
542
  },
518
543
  async onInput(value) {
519
544
  await this.set(value);
520
545
  },
521
546
  async onBlur() {
522
- state.touched.add(path);
547
+ state.touch(path);
523
548
  if (validateOn === "blur") {
524
- const sequence = nextValidationSequence(state);
549
+ const sequence = state.beginValidation();
525
550
  const submission = await validateClientValues(
526
551
  cloneFormValue(state.values),
527
552
  schemaDefinition
528
553
  );
529
- if (isLatestValidation(state, sequence)) {
530
- state.flattenedErrors = replaceErrorsForPath(
531
- state.flattenedErrors,
532
- path,
533
- submission.errors.flatten()
534
- );
535
- }
554
+ state.applyFieldValidation(sequence, path, submission.errors.flatten());
555
+ return;
536
556
  }
537
- notifyListeners(state);
557
+ state.notify();
538
558
  },
539
559
  async validate() {
540
- const sequence = nextValidationSequence(state);
560
+ const sequence = state.beginValidation();
541
561
  const submission = await validateClientValues(cloneFormValue(state.values), schemaDefinition);
542
- if (isLatestValidation(state, sequence)) {
543
- state.flattenedErrors = submission.errors.flatten();
544
- notifyListeners(state);
545
- }
562
+ state.applyValidation(sequence, submission.errors.flatten());
546
563
  return state.flattenedErrors[path] ?? [];
547
564
  }
548
565
  });
@@ -728,28 +745,21 @@ function createFormClient(schemaDefinition, options = {}) {
728
745
  return await runValidationForInput(cloneFormValue(state.values));
729
746
  }
730
747
  async function runValidationForInput(input) {
731
- const sequence = nextValidationSequence(state);
748
+ const sequence = state.beginValidation();
732
749
  const submission = await validateClientValues(input, schemaDefinition);
733
- if (isLatestValidation(state, sequence)) {
734
- state.values = mergeFormValues(
735
- state.values,
736
- submission.valid ? submission.data : submission.values
737
- );
738
- state.flattenedErrors = submission.errors.flatten();
739
- notifyListeners(state);
740
- }
750
+ state.applyValidation(sequence, submission.errors.flatten(), submission.valid ? submission.data : submission.values);
741
751
  return submission;
742
752
  }
743
- function applyServerState(result) {
753
+ function applyServerState(result, submissionSequence) {
744
754
  const normalized = normalizeSubmissionLike(
745
755
  schemaDefinition,
746
756
  state.values,
747
757
  result
748
758
  );
759
+ const applies = submissionSequence === void 0 || state.ownsSubmissionEffects(submissionSequence);
760
+ if (submissionSequence === void 0) state.invalidateEffects();
749
761
  if ("ok" in normalized && normalized.ok === true) {
750
- state.lastSubmission = normalized;
751
- state.flattenedErrors = {};
752
- notifyListeners(state);
762
+ if (applies) state.applyServer(state.values, {}, normalized);
753
763
  return normalized;
754
764
  }
755
765
  if ("ok" in normalized && normalized.ok === false) {
@@ -757,24 +767,25 @@ function createFormClient(schemaDefinition, options = {}) {
757
767
  ...normalized,
758
768
  values: sanitizeFlashedInput(normalized.values, schemaDefinition)
759
769
  };
760
- state.values = mergeFormValues(state.values, sanitized.values);
761
- clearSensitiveInputValues(state.values, schemaDefinition);
762
- state.flattenedErrors = sanitized.errors;
763
- state.lastSubmission = sanitized;
764
- notifyListeners(state);
770
+ if (applies) {
771
+ const values = mergeFormValues(state.values, sanitized.values);
772
+ clearSensitiveInputValues(values, schemaDefinition);
773
+ state.applyServer(values, sanitized.errors, sanitized);
774
+ }
765
775
  return sanitized;
766
776
  }
767
777
  const normalizedSubmission = normalized;
768
- state.values = mergeFormValues(state.values, normalizedSubmission.values);
769
- clearSensitiveInputValues(state.values, schemaDefinition);
770
- state.flattenedErrors = normalizedSubmission.errors.flatten();
771
- state.lastSubmission = normalizedSubmission.valid ? void 0 : normalizedSubmission.fail();
772
- notifyListeners(state);
778
+ if (applies) {
779
+ const values = mergeFormValues(state.values, normalizedSubmission.values);
780
+ clearSensitiveInputValues(values, schemaDefinition);
781
+ state.applyServer(values, normalizedSubmission.errors.flatten(), normalizedSubmission.valid ? void 0 : normalizedSubmission.fail());
782
+ }
773
783
  return normalizedSubmission;
774
784
  }
775
785
  async function submit(browserForm) {
786
+ const submissionSequence = state.beginSubmissionEffects();
776
787
  const finishSubmission = state.startSubmission();
777
- notifyListeners(state);
788
+ state.notify();
778
789
  try {
779
790
  const submitter = options.submitter ?? ((context) => defaultSubmitter(context, options));
780
791
  const liveForm = browserForm ?? getActiveElementBrowserForm();
@@ -805,17 +816,18 @@ function createFormClient(schemaDefinition, options = {}) {
805
816
  throw error;
806
817
  }
807
818
  return applyServerState(
808
- validationExceptionToFailure(error, state.values) ?? createTransportFailure(state.values)
819
+ validationExceptionToFailure(error, localSubmission.data) ?? createTransportFailure(localSubmission.data),
820
+ submissionSequence
809
821
  );
810
822
  }
811
823
  return applyServerState(response ?? {
812
824
  ok: true,
813
825
  status: 204,
814
826
  data: void 0
815
- });
827
+ }, submissionSequence);
816
828
  } finally {
817
829
  finishSubmission();
818
- notifyListeners(state);
830
+ state.notify();
819
831
  }
820
832
  }
821
833
  return Object.freeze({
@@ -855,28 +867,15 @@ function createFormClient(schemaDefinition, options = {}) {
855
867
  return submit(form);
856
868
  },
857
869
  reset(values) {
858
- const next = mergeFormValues(state.initialValues, values);
859
- state.values = cloneFormValue(next);
860
- state.initialValues = cloneFormValue(next);
861
- state.flattenedErrors = {};
862
- state.touched.clear();
863
- state.dirty.clear();
864
- state.lastSubmission = void 0;
865
- notifyListeners(state);
870
+ state.reset(values);
866
871
  },
867
872
  async setValue(path, value) {
868
- setFormValueAtPath(state.values, path, value);
869
- state.touched.add(path);
870
- if (!areFormValuesEqual(getFormValueAtPath(state.initialValues, path), value)) {
871
- state.dirty.add(path);
872
- } else {
873
- state.dirty.delete(path);
874
- }
873
+ state.edit(path, value);
875
874
  if (validateOn === "change" && fieldPaths.includes(path)) {
876
875
  await runValidation();
877
876
  return;
878
877
  }
879
- notifyListeners(state);
878
+ state.notify();
880
879
  },
881
880
  applyServerState(result) {
882
881
  return applyServerState(result);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@holo-js/forms",
3
- "version": "0.3.16",
3
+ "version": "0.3.17",
4
4
  "description": "Holo-JS Framework - forms-first validation and submission contracts",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -28,10 +28,10 @@
28
28
  "test": "vitest --run"
29
29
  },
30
30
  "dependencies": {
31
- "@holo-js/validation": "^0.3.16"
31
+ "@holo-js/validation": "^0.3.17"
32
32
  },
33
33
  "peerDependencies": {
34
- "@holo-js/security": "^0.3.16"
34
+ "@holo-js/security": "^0.3.17"
35
35
  },
36
36
  "peerDependenciesMeta": {
37
37
  "@holo-js/security": {