Skip to content

Commit e3eec7b

Browse files
committed
Readme and options updated.
1 parent 30068a5 commit e3eec7b

File tree

4 files changed

+119
-45
lines changed

4 files changed

+119
-45
lines changed

CHANGELOG.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,25 @@ Headlines: Added, Changed, Deprecated, Removed, Fixed, Security
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8-
## [0.8.0] - 2023-04-21
8+
## [0.8.0] - 2023-04-22
99

1010
### Changed
1111

12-
- Client-side validators now works in realtime, based on "reward early, validate late"; If no error, validate on `focusout`. If error exists, validate on `input`.
12+
- Client-side validators now works in realtime, based on "reward early, validate late": If no field error, validate on `blur`. If field error exists, validate on `input`.
1313

1414
### Removed
1515

1616
- The rarely used `update` function is removed. Use `form` instead, which now has an option for not tainting the affected fields.
1717

1818
### Fixed
1919

20-
- `tainted` wasn't updated correctly for array data.
20+
- `tainted` wasn't updated properly for array data.
2121
- `dataType: 'json'` now handles large (+1Mb) payloads.
2222

2323
### Added
2424

2525
- Added `validate` to `superForm`, which can be used to validate any field, at any time.
26+
- Client-side validation can be customized with the `validationMethod: 'auto' | 'oninput' | 'onblur' | 'submit-only'` option.
2627
- The option `{ taint: boolean | 'untaint' | 'untaint-all' }` has been added to `form.set` and `form.update`.
2728
- The `resetForm` option can now take an `async () => boolean` function to determine whether the form should be resetted or not.
2829

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,13 @@
2121

2222
<br/>
2323

24-
Superforms is a SvelteKit library that helps you with **server-side validation** and **client-side display** of forms.
24+
Making SvelteKit **validation** and **display** of forms easier than ever!
2525

2626
# Feature list
2727

2828
- Merging `PageData` and `ActionData` consistently - Forget about which one to use and how, just focus on your data.
2929
- Server-side data validation using [Zod](https://zod.dev), with output that can be used directly on the client.
30-
- [Auto-centering and auto-focusing](https://superforms.vercel.app/concepts/error-handling) on invalid form fields.
30+
- [Auto-centering and auto-focusing](https://superforms.vercel.app/concepts/error-handling#usage-client) on invalid form fields.
3131
- [Tainted form detection](https://superforms.vercel.app/concepts/tainted), prevents the user from losing data if navigating away from an unsaved form.
3232
- No JS required as default, but full support for [progressive enhancement](https://superforms.vercel.app/concepts/enhance).
3333
- Automatically coerces the string data from `FormData` into correct types.
@@ -61,4 +61,4 @@ You can also watch this excellent introduction video to see what's possible: htt
6161

6262
# Feedback wanted!
6363

64-
Ideas, feedback, bug reports, PR:s, etc, are very welcome as a Github [issue](https://github.com/ciscoheat/sveltekit-superforms/issues) or [discussion](https://github.com/ciscoheat/sveltekit-superforms/discussions)!
64+
Ideas, general feedback, bug reports, PR:s, etc, are very welcome as a Github [issue](https://github.com/ciscoheat/sveltekit-superforms/issues), [discussion](https://github.com/ciscoheat/sveltekit-superforms/discussions), or on the [Discord server](https://discord.gg/AptebvVuhB)!

src/lib/client/index.ts

Lines changed: 72 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ export type FormOptions<T extends ZodValidation<AnyZodObject>, M> = Partial<{
126126
| ZodEffects<ZodEffects<ZodEffects<T>>>
127127
| ZodEffects<ZodEffects<ZodEffects<ZodEffects<T>>>>
128128
| ZodEffects<ZodEffects<ZodEffects<ZodEffects<ZodEffects<T>>>>>;
129+
validationMethod: 'auto' | 'oninput' | 'onblur' | 'submit-only';
129130
defaultValidator: 'keep' | 'clear';
130131
clearOnSubmit: 'errors' | 'message' | 'errors-and-message' | 'none';
131132
delayMs: number;
@@ -182,7 +183,8 @@ const defaultFormOptions = {
182183
timeoutMs: 8000,
183184
multipleSubmits: 'prevent',
184185
validation: undefined,
185-
SPA: undefined
186+
SPA: undefined,
187+
validateMethod: 'auto'
186188
};
187189

188190
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -202,9 +204,12 @@ type SuperFormEventList<T extends AnyZodObject, M> = {
202204
>[];
203205
};
204206

207+
type TaintOption = boolean | 'untaint' | 'untaint-all';
208+
205209
type ValidateOptions<V> = Partial<{
206210
value: V;
207-
update: boolean;
211+
update: boolean | 'errors' | 'value';
212+
taint: TaintOption;
208213
errors: string | string[];
209214
}>;
210215

@@ -224,12 +229,12 @@ export type SuperForm<T extends ZodValidation<AnyZodObject>, M = any> = {
224229
set(
225230
this: void,
226231
value: z.infer<T>,
227-
options?: { taint?: boolean | 'untaint' | 'untaint-all' }
232+
options?: { taint?: TaintOption }
228233
): void;
229234
update(
230235
this: void,
231236
updater: Updater<z.infer<T>>,
232-
options?: { taint?: boolean | 'untaint' | 'untaint-all' }
237+
options?: { taint?: TaintOption }
233238
): void;
234239
};
235240
formId: Writable<string | undefined>;
@@ -414,28 +419,23 @@ export function superForm<
414419
let taintedFormState: typeof initialForm.data = clone(initialForm.data);
415420

416421
const _formData = writable(storeForm.data);
417-
// TODO: Is get(Submitting) really needed?
418422
const Form = {
419423
subscribe: _formData.subscribe,
420424
set: (
421425
value: Parameters<typeof _formData.set>[0],
422-
options: { taint?: boolean | 'untaint' | 'untaint-all' } = {}
426+
options: { taint?: TaintOption } = {}
423427
) => {
424-
//if (!get(Submitting)) {
425428
checkTainted(value, taintedFormState, options.taint ?? true);
426-
//}
427429
taintedFormState = clone(value);
428430
return _formData.set(value);
429431
},
430432
update: (
431433
updater: Parameters<typeof _formData.update>[0],
432-
options: { taint?: boolean | 'untaint' | 'untaint-all' } = {}
434+
options: { taint?: TaintOption } = {}
433435
) => {
434436
return _formData.update((value) => {
435437
const output = updater(value);
436-
//if (!get(Submitting)) {
437438
checkTainted(output, taintedFormState, options.taint ?? true);
438-
//}
439439
taintedFormState = clone(value);
440440
return output;
441441
});
@@ -447,7 +447,7 @@ export function superForm<
447447
function checkTainted(
448448
newObj: unknown,
449449
compareAgainst: unknown,
450-
options: boolean | 'untaint' | 'untaint-all'
450+
options: TaintOption
451451
) {
452452
if (options === false) {
453453
return;
@@ -764,7 +764,7 @@ export function superForm<
764764
(Array.isArray(path) ? path : [path]) as string[],
765765
options.validators,
766766
options.defaultValidator,
767-
get(Form),
767+
Form,
768768
Errors,
769769
opts
770770
);
@@ -837,14 +837,17 @@ async function validateField<T extends AnyZodObject, M>(
837837
path: string[],
838838
validators: FormOptions<T, M>['validators'],
839839
defaultValidator: FormOptions<T, M>['defaultValidator'],
840-
data: z.infer<T>,
840+
data: SuperForm<T, M>['form'],
841841
errors: SuperForm<T, M>['errors'],
842842
options: ValidateOptions<unknown> = {}
843843
): Promise<string[] | undefined> {
844+
if (options.update === undefined) options.update = true;
845+
if (options.taint === undefined) options.taint = false;
846+
844847
function setError(errorMsgs: null | undefined | string | string[]) {
845848
if (typeof errorMsgs === 'string') errorMsgs = [errorMsgs];
846849

847-
if (options.update !== false) {
850+
if (options.update === true || options.update == 'errors') {
848851
errors.update((errors) => {
849852
const error = traversePath(
850853
errors,
@@ -879,19 +882,29 @@ async function validateField<T extends AnyZodObject, M>(
879882
}
880883

881884
let value = options.value;
885+
const currentData = get(data);
882886

883-
if (value === undefined) {
887+
if (!('value' in options)) {
884888
const dataToValidate = traversePath(
885-
data,
886-
path as FieldPath<typeof data>
889+
currentData,
890+
path as FieldPath<typeof currentData>
887891
);
888892

889893
if (!dataToValidate) {
890894
throw new SuperFormError('Validation data not found: ' + path);
891895
}
892896

893897
value = dataToValidate.value;
898+
} else if (options.update === true || options.update === 'value') {
899+
data.update(
900+
($data) => {
901+
setPaths($data, [path], value);
902+
return $data;
903+
},
904+
{ taint: options.taint }
905+
);
894906
}
907+
895908
//console.log('🚀 ~ file: index.ts:871 ~ validate:', path, value);
896909

897910
if (typeof validators !== 'object') {
@@ -998,8 +1011,22 @@ function formEnhance<T extends AnyZodObject, M>(
9981011
// Add blur event, to check tainted
9991012
let lastBlur: string[][] = [];
10001013
function checkBlur() {
1014+
if (
1015+
options.validationMethod == 'oninput' ||
1016+
options.validationMethod == 'submit-only'
1017+
) {
1018+
return;
1019+
}
1020+
10011021
const newChanges = get(lastChanges);
1002-
if (equal(newChanges, lastBlur)) return;
1022+
1023+
if (
1024+
options.validationMethod != 'onblur' &&
1025+
equal(newChanges, lastBlur)
1026+
) {
1027+
return;
1028+
}
1029+
10031030
lastBlur = newChanges;
10041031

10051032
for (const change of newChanges) {
@@ -1008,7 +1035,7 @@ function formEnhance<T extends AnyZodObject, M>(
10081035
change,
10091036
options.validators,
10101037
options.defaultValidator,
1011-
get(data),
1038+
data,
10121039
errors
10131040
);
10141041
}
@@ -1017,30 +1044,43 @@ function formEnhance<T extends AnyZodObject, M>(
10171044

10181045
// Add input event, to check tainted
10191046
function checkInput() {
1047+
if (
1048+
options.validationMethod == 'onblur' ||
1049+
options.validationMethod == 'submit-only'
1050+
) {
1051+
return;
1052+
}
1053+
10201054
const errorContent = get(errors);
10211055
const taintedContent = get(tainted);
10221056

10231057
for (const change of get(lastChanges)) {
1024-
const isTainted =
1025-
taintedContent &&
1026-
pathExists(taintedContent, change, (value) => value === true);
1058+
let shouldValidate = options.validationMethod === 'oninput';
10271059

1028-
const errorNode = errorContent
1029-
? pathExists(errorContent, change)
1030-
: undefined;
1060+
if (!shouldValidate) {
1061+
const isTainted =
1062+
taintedContent &&
1063+
pathExists(taintedContent, change, (value) => value === true);
10311064

1032-
// Need a special check here, since if the error has never existed,
1033-
// there won't be a key for the error. But if it existed and was cleared,
1034-
// the key exists with the value undefined.
1035-
const hasError = errorNode && errorNode.key in errorNode.parent;
1065+
const errorNode = errorContent
1066+
? pathExists(errorContent, change)
1067+
: undefined;
1068+
1069+
// Need a special check here, since if the error has never existed,
1070+
// there won't be a key for the error. But if it existed and was cleared,
1071+
// the key exists with the value undefined.
1072+
const hasError = errorNode && errorNode.key in errorNode.parent;
1073+
1074+
shouldValidate = !!isTainted && !!hasError;
1075+
}
10361076

1037-
if (isTainted && hasError) {
1077+
if (shouldValidate) {
10381078
//console.log('🚀 ~ file: index.ts:920 ~ INPUT with error:', change);
10391079
validateField(
10401080
change,
10411081
options.validators,
10421082
options.defaultValidator,
1043-
get(data),
1083+
data,
10441084
errors
10451085
);
10461086
}

src/routes/nested-validation/TagForm.svelte

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
1414
export let output: (string[] | undefined)[] = [];
1515
16+
$: testMode = $page.url.searchParams.has('test');
17+
1618
const superFormValidator: FormOptions<
1719
typeof schema,
1820
unknown
@@ -54,30 +56,61 @@
5456
5557
// validate tests
5658
onMount(async () => {
57-
if (!$page.url.searchParams.has('test')) return;
59+
if (!testMode) return;
60+
61+
await validate(['tags', 0, 'name'], {
62+
value: 'p',
63+
update: 'errors',
64+
errors: 'Custom error'
65+
});
5866
59-
validate(['tags', 0, 'name'], { value: 'p', errors: 'Custom error' });
6067
output = [...output, await validate('name')];
6168
6269
output = [
6370
...output,
64-
await validate(['tags', 0, 'id'], {
65-
update: false,
66-
value: 1
71+
await validate(['tags', 2, 'id'], {
72+
value: 2
6773
})
6874
];
6975
7076
output = [
7177
...output,
7278
(await validate(['tags', 0, 'id'], {
73-
update: false,
79+
update: 'errors',
7480
value: 7
75-
})) ?? ['OK']
81+
})) ?? ['Update errors OK']
7682
];
83+
84+
const errors = await validate(['tags', 1, 'id']);
85+
if (
86+
errors?.length == 1 &&
87+
errors[0] == 'Number must be greater than or equal to 3'
88+
) {
89+
output = [...output, ['Error check OK']];
90+
} else {
91+
output = [...output, ['FAIL']];
92+
}
93+
94+
const errors2 = await validate(['tags', 1, 'id'], {
95+
value: 0,
96+
update: false
97+
});
98+
if (
99+
errors2?.length == 1 &&
100+
errors2[0] == 'Number must be greater than or equal to 3'
101+
) {
102+
output = [...output, ['Error check 2 OK']];
103+
} else {
104+
output = [...output, ['FAIL']];
105+
}
77106
});
78107
</script>
79108

80109
<form method="POST" use:enhance>
110+
{#if testMode}
111+
<SuperDebug data={$tainted} />
112+
{/if}
113+
81114
{#if $message}<h4>{$message}</h4>{/if}
82115
<input type="hidden" name="id" value={validator} />
83116
<small>{validator} validation</small>

0 commit comments

Comments
 (0)