@holo-js/forms 0.3.16 → 0.3.18
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/dist/internal/client.d.ts +14 -4
- package/dist/internal/client.mjs +100 -101
- package/package.json +3 -3
|
@@ -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
|
-
|
|
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
|
|
package/dist/internal/client.mjs
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
303
|
-
this.
|
|
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.
|
|
528
|
+
return state.isDirty(path);
|
|
497
529
|
},
|
|
498
530
|
async set(value) {
|
|
499
|
-
|
|
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 =
|
|
533
|
+
const sequence = state.beginValidation();
|
|
508
534
|
const submission = await validateClientValues(
|
|
509
535
|
cloneFormValue(state.values),
|
|
510
536
|
schemaDefinition
|
|
511
537
|
);
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
}
|
|
538
|
+
state.applyValidation(sequence, submission.errors.flatten());
|
|
539
|
+
return;
|
|
515
540
|
}
|
|
516
|
-
|
|
541
|
+
state.notify();
|
|
517
542
|
},
|
|
518
543
|
async onInput(value) {
|
|
519
544
|
await this.set(value);
|
|
520
545
|
},
|
|
521
546
|
async onBlur() {
|
|
522
|
-
state.
|
|
547
|
+
state.touch(path);
|
|
523
548
|
if (validateOn === "blur") {
|
|
524
|
-
const sequence =
|
|
549
|
+
const sequence = state.beginValidation();
|
|
525
550
|
const submission = await validateClientValues(
|
|
526
551
|
cloneFormValue(state.values),
|
|
527
552
|
schemaDefinition
|
|
528
553
|
);
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
state.flattenedErrors,
|
|
532
|
-
path,
|
|
533
|
-
submission.errors.flatten()
|
|
534
|
-
);
|
|
535
|
-
}
|
|
554
|
+
state.applyFieldValidation(sequence, path, submission.errors.flatten());
|
|
555
|
+
return;
|
|
536
556
|
}
|
|
537
|
-
|
|
557
|
+
state.notify();
|
|
538
558
|
},
|
|
539
559
|
async validate() {
|
|
540
|
-
const sequence =
|
|
560
|
+
const sequence = state.beginValidation();
|
|
541
561
|
const submission = await validateClientValues(cloneFormValue(state.values), schemaDefinition);
|
|
542
|
-
|
|
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 =
|
|
748
|
+
const sequence = state.beginValidation();
|
|
732
749
|
const submission = await validateClientValues(input, schemaDefinition);
|
|
733
|
-
|
|
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.
|
|
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
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
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
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "0.3.18",
|
|
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.
|
|
31
|
+
"@holo-js/validation": "^0.3.18"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
|
-
"@holo-js/security": "^0.3.
|
|
34
|
+
"@holo-js/security": "^0.3.18"
|
|
35
35
|
},
|
|
36
36
|
"peerDependenciesMeta": {
|
|
37
37
|
"@holo-js/security": {
|