@valfuse-node/form 0.2.0 → 0.3.1

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/README.md CHANGED
@@ -41,12 +41,7 @@ import { createSchema, validateSchema, transformValues, t } from "@valfuse-node/
41
41
  ## Quick Start
42
42
 
43
43
  ```ts
44
- import {
45
- createSchema,
46
- validateSchema,
47
- transformValues,
48
- t,
49
- } from "@valfuse-node/form";
44
+ import { createSchema, validateSchema, transformValues, t } from "@valfuse-node/form";
50
45
 
51
46
  const schema = createSchema({
52
47
  email: {
@@ -54,14 +49,14 @@ const schema = createSchema({
54
49
  transform: t.pipe(t.trim, t.toLowerCase),
55
50
  rules: [
56
51
  { name: "required", error: { message: "Email is required", code: "email.required" } },
57
- { name: "email", error: { message: "Invalid email", code: "email.invalid" } },
52
+ { name: "email", error: { message: "Invalid email", code: "email.invalid" } },
58
53
  ],
59
54
  },
60
55
  age: {
61
56
  type: "number",
62
57
  transform: t.toInteger,
63
58
  rules: [
64
- { name: "required", error: { message: "Required", code: "age.required" } },
59
+ { name: "required", error: { message: "Required", code: "age.required" } },
65
60
  { name: "min", value: 18, error: { message: "Must be 18+", code: "age.min" } },
66
61
  ],
67
62
  },
@@ -102,7 +97,7 @@ const schema = createSchema({
102
97
  // ─── number fields ──────────────────────────────────────────────────────────
103
98
  age: {
104
99
  type: "number",
105
- transform: t.toInteger, // optional pre-validation coercion
100
+ transform: t.toInteger, // optional pre-validation coercion
106
101
  rules: [
107
102
  { name: "min", value: 0, error: { message: "Must be ≥ 0" } },
108
103
  { name: "max", value: 120, error: { message: "Must be ≤ 120" } },
@@ -113,9 +108,7 @@ const schema = createSchema({
113
108
  // ─── boolean fields ─────────────────────────────────────────────────────────
114
109
  agreed: {
115
110
  type: "boolean",
116
- rules: [
117
- { name: "accepted", error: { message: "You must accept the terms" } },
118
- ],
111
+ rules: [{ name: "accepted", error: { message: "You must accept the terms" } }],
119
112
  },
120
113
 
121
114
  // ─── array fields ───────────────────────────────────────────────────────────
@@ -130,22 +123,20 @@ const schema = createSchema({
130
123
  // ─── object fields (nested) ─────────────────────────────────────────────────
131
124
  address: {
132
125
  type: "object",
133
- rules: [
134
- { name: "required", error: { message: "Address is required" } },
135
- ],
126
+ rules: [{ name: "required", error: { message: "Address is required" } }],
136
127
  },
137
128
  });
138
129
  ```
139
130
 
140
131
  ### Supported field types
141
132
 
142
- | Type | Description |
143
- |---|---|
144
- | `"string"` | Free text, validated by string rules |
145
- | `"number"` | Coerced numeric value (use `transform: t.toNumber` to coerce raw strings) |
146
- | `"boolean"` | `true` / `false` |
147
- | `"array"` | Any array (length-based rules only — element validation is a `custom` rule) |
148
- | `"object"` | Any object (shape and presence rules only) |
133
+ | Type | Description |
134
+ | ----------- | --------------------------------------------------------------------------- |
135
+ | `"string"` | Free text, validated by string rules |
136
+ | `"number"` | Coerced numeric value (use `transform: t.toNumber` to coerce raw strings) |
137
+ | `"boolean"` | `true` / `false` |
138
+ | `"array"` | Any array (length-based rules only — element validation is a `custom` rule) |
139
+ | `"object"` | Any object (shape and presence rules only) |
149
140
 
150
141
  ---
151
142
 
@@ -153,79 +144,84 @@ const schema = createSchema({
153
144
 
154
145
  ### String rules
155
146
 
156
- | Rule | Value | Example |
157
- |---|---|---|
158
- | `required` | — | `{ name: "required", error: { message: "Required" } }` |
159
- | `min` | `number` (length) | `{ name: "min", value: 3, error: … }` |
160
- | `max` | `number` (length) | `{ name: "max", value: 100, error: … }` |
161
- | `length` | `number` (exact length) | `{ name: "length", value: 10, error: … }` |
162
- | `email` | — | `{ name: "email", error: … }` |
163
- | `url` | — | `{ name: "url", error: … }` |
164
- | `uuid` | — | `{ name: "uuid", error: … }` |
165
- | `regex` | `RegExp` or `{ pattern, flags }` | `{ name: "regex", value: /^[a-z]+$/, error: … }` |
166
- | `includes` | `string` | `{ name: "includes", value: "@", error: … }` |
167
- | `startsWith` | `string` | `{ name: "startsWith", value: "https://", error: … }` |
168
- | `endsWith` | `string` | `{ name: "endsWith", value: ".com", error: … }` |
147
+ | Rule | Value | Example |
148
+ | ------------ | -------------------------------- | ------------------------------------------------------ |
149
+ | `required` | — | `{ name: "required", error: { message: "Required" } }` |
150
+ | `min` | `number` (length) | `{ name: "min", value: 3, error: … }` |
151
+ | `max` | `number` (length) | `{ name: "max", value: 100, error: … }` |
152
+ | `length` | `number` (exact length) | `{ name: "length", value: 10, error: … }` |
153
+ | `email` | — | `{ name: "email", error: … }` |
154
+ | `url` | — | `{ name: "url", error: … }` |
155
+ | `uuid` | — | `{ name: "uuid", error: … }` |
156
+ | `regex` | `RegExp` or `{ pattern, flags }` | `{ name: "regex", value: /^[a-z]+$/, error: … }` |
157
+ | `includes` | `string` | `{ name: "includes", value: "@", error: … }` |
158
+ | `startsWith` | `string` | `{ name: "startsWith", value: "https://", error: … }` |
159
+ | `endsWith` | `string` | `{ name: "endsWith", value: ".com", error: … }` |
169
160
 
170
161
  ### Number rules
171
162
 
172
- | Rule | Value | Notes |
173
- |---|---|---|
174
- | `required` | — | Rejects `null`, `undefined`, `NaN` |
175
- | `min` | `number` | Inclusive lower bound |
176
- | `max` | `number` | Inclusive upper bound |
177
- | `gt` | `number` | Strictly greater than |
178
- | `gte` | `number` | Greater than or equal |
179
- | `lt` | `number` | Strictly less than |
180
- | `lte` | `number` | Less than or equal |
181
- | `int` | — | Rejects non-integers |
182
- | `positive` | — | `> 0` |
183
- | `nonnegative` | — | `≥ 0` |
184
- | `negative` | — | `< 0` |
185
- | `nonpositive` | — | `≤ 0` |
186
- | `multipleOf` | `number` | `value % multipleOf === 0` |
163
+ | Rule | Value | Notes |
164
+ | ------------- | -------- | ---------------------------------- |
165
+ | `required` | — | Rejects `null`, `undefined`, `NaN` |
166
+ | `min` | `number` | Inclusive lower bound |
167
+ | `max` | `number` | Inclusive upper bound |
168
+ | `gt` | `number` | Strictly greater than |
169
+ | `gte` | `number` | Greater than or equal |
170
+ | `lt` | `number` | Strictly less than |
171
+ | `lte` | `number` | Less than or equal |
172
+ | `int` | — | Rejects non-integers |
173
+ | `positive` | — | `> 0` |
174
+ | `nonnegative` | — | `≥ 0` |
175
+ | `negative` | — | `< 0` |
176
+ | `nonpositive` | — | `≤ 0` |
177
+ | `multipleOf` | `number` | `value % multipleOf === 0` |
187
178
 
188
179
  ### Boolean rules
189
180
 
190
- | Rule | Value | Notes |
191
- |---|---|---|
192
- | `required` | — | Rejects `null`, `undefined`, `false` |
193
- | `literal` | `boolean` | Must match exactly |
194
- | `accepted` | — | Sugar for `literal: true` (terms-of-service pattern) |
181
+ | Rule | Value | Notes |
182
+ | ---------- | --------- | ---------------------------------------------------- |
183
+ | `required` | — | Rejects `null`, `undefined`, `false` |
184
+ | `literal` | `boolean` | Must match exactly |
185
+ | `accepted` | — | Sugar for `literal: true` (terms-of-service pattern) |
195
186
 
196
187
  ### Array rules
197
188
 
198
- | Rule | Value |
199
- |---|---|
200
- | `required` | — |
201
- | `min` | `number` (min length) |
202
- | `max` | `number` (max length) |
203
- | `length` | `number` (exact length) |
204
- | `nonempty` | — (length ≥ 1) |
189
+ | Rule | Value |
190
+ | ---------- | ----------------------- |
191
+ | `required` | — |
192
+ | `min` | `number` (min length) |
193
+ | `max` | `number` (max length) |
194
+ | `length` | `number` (exact length) |
195
+ | `nonempty` | — (length ≥ 1) |
205
196
 
206
197
  ### Object rules
207
198
 
208
- | Rule | Value |
209
- |---|---|
210
- | `required` | — (rejects `null` / `undefined`) |
211
- | `shape` | `Record<string, unknown>` (key set must match) |
199
+ | Rule | Value |
200
+ | ---------- | ---------------------------------------------- |
201
+ | `required` | — (rejects `null` / `undefined`) |
202
+ | `shape` | `Record<string, unknown>` (key set must match) |
212
203
 
213
204
  ### Generic (all types)
214
205
 
215
- | Rule | Shape | Use |
216
- |---|---|---|
217
- | `custom` | `{ name: "custom", validate: (v, all) => boolean, error }` | Ad-hoc validator with access to all values |
218
- | `refine` | Same as `custom` | Alias — same implementation, different intent name |
219
- | `matchField` | `{ name: "matchField", value: "<other-field-name>", error }` | Cross-field equality (e.g. password confirmation) |
220
- | `oneOf` | `{ name: "oneOf", value: unknown[], error }` | Value must be in the list |
221
- | `notOneOf` | `{ name: "notOneOf", value: unknown[], error }` | Value must NOT be in the list |
206
+ | Rule | Shape | Use |
207
+ | ------------ | ------------------------------------------------------------ | -------------------------------------------------- |
208
+ | `custom` | `{ name: "custom", validate: (v, all) => boolean, error }` | Ad-hoc validator with access to all values |
209
+ | `refine` | Same as `custom` | Alias — same implementation, different intent name |
210
+ | `matchField` | `{ name: "matchField", value: "<other-field-name>", error }` | Cross-field equality (e.g. password confirmation) |
211
+ | `oneOf` | `{ name: "oneOf", value: unknown[], error }` | Value must be in the list |
212
+ | `notOneOf` | `{ name: "notOneOf", value: unknown[], error }` | Value must NOT be in the list |
222
213
 
223
214
  **Example — cross-field password match:**
224
215
 
225
216
  ```ts
226
217
  const schema = createSchema({
227
- password: { type: "string", rules: [{ name: "required", error: { message: "Required" } }] },
228
- confirmPassword: { type: "string", rules: [{ name: "matchField", value: "password", error: { message: "Passwords do not match" } }] },
218
+ password: { type: "string", rules: [{ name: "required", error: { message: "Required" } }] },
219
+ confirmPassword: {
220
+ type: "string",
221
+ rules: [
222
+ { name: "matchField", value: "password", error: { message: "Passwords do not match" } },
223
+ ],
224
+ },
229
225
  });
230
226
  ```
231
227
 
@@ -259,23 +255,23 @@ const schema = createSchema({
259
255
  import { t } from "@valfuse-node/form";
260
256
 
261
257
  // ─── String transformers ──────────────────────────────────────────────────────
262
- t.trim // " hi " → "hi"
263
- t.trimStart // " hi " → "hi "
264
- t.trimEnd // " hi " → " hi"
265
- t.toLowerCase // "Hi" → "hi"
266
- t.toUpperCase // "hi" → "HI"
267
- t.toTitleCase // "hello world" → "Hello World"
268
- t.toSentenceCase // "HELLO" → "Hello"
269
- t.collapseSpaces // "a b" → "a b"
258
+ t.trim; // " hi " → "hi"
259
+ t.trimStart; // " hi " → "hi "
260
+ t.trimEnd; // " hi " → " hi"
261
+ t.toLowerCase; // "Hi" → "hi"
262
+ t.toUpperCase; // "hi" → "HI"
263
+ t.toTitleCase; // "hello world" → "Hello World"
264
+ t.toSentenceCase; // "HELLO" → "Hello"
265
+ t.collapseSpaces; // "a b" → "a b"
270
266
 
271
267
  // ─── Coercion transformers ───────────────────────────────────────────────────
272
- t.toNumber // "42" → 42 (returns original if NaN)
273
- t.toInteger // "42.7" → 42
274
- t.toFloat // "3.14" → 3.14
275
- t.toBoolean // "true"/"1"/1/true → true; everything else → false
268
+ t.toNumber; // "42" → 42 (returns original if NaN)
269
+ t.toInteger; // "42.7" → 42
270
+ t.toFloat; // "3.14" → 3.14
271
+ t.toBoolean; // "true"/"1"/1/true → true; everything else → false
276
272
 
277
273
  // ─── Composition ──────────────────────────────────────────────────────────────
278
- t.pipe(t.trim, t.toLowerCase) // compose left-to-right
274
+ t.pipe(t.trim, t.toLowerCase); // compose left-to-right
279
275
  ```
280
276
 
281
277
  ### Custom transformers
@@ -283,8 +279,7 @@ t.pipe(t.trim, t.toLowerCase) // compose left-to-right
283
279
  Any function `(value: unknown) => unknown` is a valid transformer:
284
280
 
285
281
  ```ts
286
- const slugify = (v: unknown) =>
287
- typeof v === "string" ? v.toLowerCase().replace(/\s+/g, "-") : v;
282
+ const slugify = (v: unknown) => (typeof v === "string" ? v.toLowerCase().replace(/\s+/g, "-") : v);
288
283
 
289
284
  const schema = createSchema({
290
285
  slug: {
@@ -331,10 +326,10 @@ const errors = validateSchema(loginSchema, {
331
326
 
332
327
  ```ts
333
328
  interface ValfuseError {
334
- message: string; // user-facing message
329
+ message: string; // user-facing message
335
330
  type?: "validation" | "server" | "manual" | "custom";
336
- code?: string; // semantic code (e.g. "email.required")
337
- metadata?: Record<string, unknown>; // extra context
331
+ code?: string; // semantic code (e.g. "email.required")
332
+ metadata?: Record<string, unknown>; // extra context
338
333
  }
339
334
  ```
340
335
 
@@ -363,20 +358,20 @@ Useful when you need to merge API errors (which may be strings or objects) into
363
358
  ```ts
364
359
  interface ValfuseFieldError {
365
360
  message: string;
366
- type?: string; // "validation" | "server" | "manual" | "custom"
367
- code?: string; // e.g. "email.required", "auth.not_found"
361
+ type?: string; // "validation" | "server" | "manual" | "custom"
362
+ code?: string; // e.g. "email.required", "auth.not_found"
368
363
  metadata?: Record<string, unknown>;
369
364
  }
370
365
  ```
371
366
 
372
367
  ### Error types by origin
373
368
 
374
- | `type` | Origin | Typical use |
375
- |---|---|---|
376
- | `"validation"` | A schema rule failed | Automatic — emitted by `validateSchema` |
377
- | `"server"` | Injected via `form.setErrors` after a failed API call | Manual |
378
- | `"manual"` | Injected via `form.setErrors` for client-only logic | Manual |
379
- | `"custom"` | Returned by a `custom` / `refine` rule | Automatic — but tagged "custom" so consumers can distinguish |
369
+ | `type` | Origin | Typical use |
370
+ | -------------- | ----------------------------------------------------- | ------------------------------------------------------------ |
371
+ | `"validation"` | A schema rule failed | Automatic — emitted by `validateSchema` |
372
+ | `"server"` | Injected via `form.setErrors` after a failed API call | Manual |
373
+ | `"manual"` | Injected via `form.setErrors` for client-only logic | Manual |
374
+ | `"custom"` | Returned by a `custom` / `refine` rule | Automatic — but tagged "custom" so consumers can distinguish |
380
375
 
381
376
  ---
382
377
 
@@ -387,7 +382,13 @@ If you want the same form-state primitives the React/Vue adapters use internally
387
382
  ### Values
388
383
 
389
384
  ```ts
390
- import { createValuesState, updateValue, resetValues, computeIsDirty, computeDirtyFields } from "@valfuse-node/form";
385
+ import {
386
+ createValuesState,
387
+ updateValue,
388
+ resetValues,
389
+ computeIsDirty,
390
+ computeDirtyFields,
391
+ } from "@valfuse-node/form";
391
392
 
392
393
  const state = createValuesState({ email: "", age: 0 });
393
394
  updateValue(state, "email", "alice@example.com");
@@ -401,31 +402,49 @@ resetValues(state, { email: "", age: 0 });
401
402
  ### Touched
402
403
 
403
404
  ```ts
404
- import { createTouchedState, markTouched, isTouched, toTouchedFieldsRecord } from "@valfuse-node/form";
405
+ import {
406
+ createTouchedState,
407
+ markTouched,
408
+ isTouched,
409
+ toTouchedFieldsRecord,
410
+ } from "@valfuse-node/form";
405
411
 
406
412
  const touched = createTouchedState();
407
413
  markTouched(touched, "email");
408
- isTouched(touched, "email"); // true
409
- toTouchedFieldsRecord(touched); // { email: true }
414
+ isTouched(touched, "email"); // true
415
+ toTouchedFieldsRecord(touched); // { email: true }
410
416
  ```
411
417
 
412
418
  ### Errors
413
419
 
414
420
  ```ts
415
- import { createErrorsState, setFieldError, clearFieldErrors, hasErrors, getFieldError, toFormErrors } from "@valfuse-node/form";
421
+ import {
422
+ createErrorsState,
423
+ setFieldError,
424
+ clearFieldErrors,
425
+ hasErrors,
426
+ getFieldError,
427
+ toFormErrors,
428
+ } from "@valfuse-node/form";
416
429
 
417
430
  const errors = createErrorsState();
418
431
  setFieldError(errors, "email", { message: "Taken", code: "auth.duplicate" });
419
- hasErrors(errors); // true
420
- getFieldError(errors, "email"); // { message: "Taken", code: "auth.duplicate" }
432
+ hasErrors(errors); // true
433
+ getFieldError(errors, "email"); // { message: "Taken", code: "auth.duplicate" }
421
434
  clearFieldErrors(errors);
422
- toFormErrors(errors); // {} (object form)
435
+ toFormErrors(errors); // {} (object form)
423
436
  ```
424
437
 
425
438
  ### Submission
426
439
 
427
440
  ```ts
428
- import { createSubmissionState, startSubmit, endSubmitSuccess, endSubmitFailure, resetSubmission } from "@valfuse-node/form";
441
+ import {
442
+ createSubmissionState,
443
+ startSubmit,
444
+ endSubmitSuccess,
445
+ endSubmitFailure,
446
+ resetSubmission,
447
+ } from "@valfuse-node/form";
429
448
 
430
449
  const sub = createSubmissionState();
431
450
  startSubmit(sub);
@@ -470,11 +489,11 @@ Every rule is a discriminated union member with a discriminator field. The TypeS
470
489
  ```ts
471
490
  // Generic (work on any field type)
472
491
  type ValfuseGenericRule =
473
- | { name: "custom"; validate: (v, all) => boolean; error: ValfuseRuleError }
474
- | { name: "refine"; validate: (v, all) => boolean; error: ValfuseRuleError }
475
- | { name: "matchField"; value: string; error: ValfuseRuleError }
476
- | { name: "oneOf"; value: unknown[]; error: ValfuseRuleError }
477
- | { name: "notOneOf"; value: unknown[]; error: ValfuseRuleError };
492
+ | { name: "custom"; validate: (v, all) => boolean; error: ValfuseRuleError }
493
+ | { name: "refine"; validate: (v, all) => boolean; error: ValfuseRuleError }
494
+ | { name: "matchField"; value: string; error: ValfuseRuleError }
495
+ | { name: "oneOf"; value: unknown[]; error: ValfuseRuleError }
496
+ | { name: "notOneOf"; value: unknown[]; error: ValfuseRuleError };
478
497
  ```
479
498
 
480
499
  ### Error types
@@ -505,7 +524,11 @@ type ValfuseFieldErrors<TFieldName extends string = string> = Partial<
505
524
  import { createSchema, validateSchema, transformValues } from "@valfuse-node/form";
506
525
 
507
526
  const schema = createSchema({
508
- email: { type: "string", transform: (v) => String(v).toLowerCase(), rules: [{ name: "required", error: { message: "Required" } }] },
527
+ email: {
528
+ type: "string",
529
+ transform: (v) => String(v).toLowerCase(),
530
+ rules: [{ name: "required", error: { message: "Required" } }],
531
+ },
509
532
  });
510
533
 
511
534
  const input = process.argv[2] ?? "";
@@ -530,13 +553,25 @@ npx tsx scripts/validate-signup.ts "alice@example.com"
530
553
  import { createSchema, validateSchema, transformValues, normalizeError } from "@valfuse-node/form";
531
554
 
532
555
  const schema = createSchema({
533
- email: { type: "string", rules: [{ name: "required", error: { message: "Email required" } }, { name: "email", error: { message: "Invalid" } }] },
534
- password: { type: "string", rules: [{ name: "required", error: { message: "Password required" } }, { name: "minLength", value: 8, error: { message: "Min 8" } }] },
556
+ email: {
557
+ type: "string",
558
+ rules: [
559
+ { name: "required", error: { message: "Email required" } },
560
+ { name: "email", error: { message: "Invalid" } },
561
+ ],
562
+ },
563
+ password: {
564
+ type: "string",
565
+ rules: [
566
+ { name: "required", error: { message: "Password required" } },
567
+ { name: "minLength", value: 8, error: { message: "Min 8" } },
568
+ ],
569
+ },
535
570
  });
536
571
 
537
572
  export async function signupAction(formData: FormData) {
538
573
  const typed = transformValues(schema, {
539
- email: String(formData.get("email") ?? ""),
574
+ email: String(formData.get("email") ?? ""),
540
575
  password: String(formData.get("password") ?? ""),
541
576
  });
542
577
  const errors = validateSchema(schema, typed);
@@ -554,7 +589,7 @@ export async function signupAction(formData: FormData) {
554
589
  ```ts
555
590
  // schemas/user.ts (shared by web + mobile)
556
591
  import { createSchema } from "@valfuse-node/form";
557
- export const userSchema = createSchema({ /* … */ });
592
+ export const userSchema = createSchema({/* … */});
558
593
  ```
559
594
 
560
595
  ```tsx
package/dist/index.d.mts CHANGED
@@ -257,8 +257,10 @@ type SetErrorsInput<TFieldName extends string = string> = ValfuseFieldErrors<TFi
257
257
  * | `onSubmit` | Validate only when the form is submitted (default) |
258
258
  * | `onBlur` | Validate when a field loses focus |
259
259
  * | `onChange` | Validate on every keystroke / value change |
260
+ * | `onTouched` | Validate on the first blur; after that validate on every change |
261
+ * | `all` | Validate on both `onChange` and `onBlur` |
260
262
  */
261
- type ValfuseFormMode = "onSubmit" | "onBlur" | "onChange";
263
+ type ValfuseFormMode = "onSubmit" | "onBlur" | "onChange" | "onTouched" | "all";
262
264
  /** A single field error */
263
265
  interface ValfuseFieldError {
264
266
  message: string;
@@ -274,11 +276,11 @@ type ValfuseFormErrors<TSchema extends Record<string, unknown>> = {
274
276
  };
275
277
  /** Map of dirty fields (value changed from default) */
276
278
  type ValfuseDirtyFields<TSchema extends Record<string, unknown>> = {
277
- [K in keyof TSchema]?: true;
279
+ [K in keyof TSchema]?: boolean;
278
280
  };
279
281
  /** Map of touched fields (user interacted with) */
280
282
  type ValfuseTouchedFields<TSchema extends Record<string, unknown>> = {
281
- [K in keyof TSchema]?: true;
283
+ [K in keyof TSchema]?: boolean;
282
284
  };
283
285
  /** Reactive form state exposed to consumers */
284
286
  interface ValfuseFormState<TSchema extends Record<string, unknown>> {
@@ -402,7 +404,7 @@ interface UseValfuseFormReturn<TSchema extends Record<string, unknown>> {
402
404
  *
403
405
  * Returns true if all triggered fields are valid, false otherwise.
404
406
  */
405
- trigger: (name?: keyof TSchema & string | Array<keyof TSchema & string>) => boolean;
407
+ trigger: (name?: (keyof TSchema & string) | Array<keyof TSchema & string>) => boolean;
406
408
  /**
407
409
  * Watch field values:
408
410
  *
package/dist/index.d.ts CHANGED
@@ -257,8 +257,10 @@ type SetErrorsInput<TFieldName extends string = string> = ValfuseFieldErrors<TFi
257
257
  * | `onSubmit` | Validate only when the form is submitted (default) |
258
258
  * | `onBlur` | Validate when a field loses focus |
259
259
  * | `onChange` | Validate on every keystroke / value change |
260
+ * | `onTouched` | Validate on the first blur; after that validate on every change |
261
+ * | `all` | Validate on both `onChange` and `onBlur` |
260
262
  */
261
- type ValfuseFormMode = "onSubmit" | "onBlur" | "onChange";
263
+ type ValfuseFormMode = "onSubmit" | "onBlur" | "onChange" | "onTouched" | "all";
262
264
  /** A single field error */
263
265
  interface ValfuseFieldError {
264
266
  message: string;
@@ -274,11 +276,11 @@ type ValfuseFormErrors<TSchema extends Record<string, unknown>> = {
274
276
  };
275
277
  /** Map of dirty fields (value changed from default) */
276
278
  type ValfuseDirtyFields<TSchema extends Record<string, unknown>> = {
277
- [K in keyof TSchema]?: true;
279
+ [K in keyof TSchema]?: boolean;
278
280
  };
279
281
  /** Map of touched fields (user interacted with) */
280
282
  type ValfuseTouchedFields<TSchema extends Record<string, unknown>> = {
281
- [K in keyof TSchema]?: true;
283
+ [K in keyof TSchema]?: boolean;
282
284
  };
283
285
  /** Reactive form state exposed to consumers */
284
286
  interface ValfuseFormState<TSchema extends Record<string, unknown>> {
@@ -402,7 +404,7 @@ interface UseValfuseFormReturn<TSchema extends Record<string, unknown>> {
402
404
  *
403
405
  * Returns true if all triggered fields are valid, false otherwise.
404
406
  */
405
- trigger: (name?: keyof TSchema & string | Array<keyof TSchema & string>) => boolean;
407
+ trigger: (name?: (keyof TSchema & string) | Array<keyof TSchema & string>) => boolean;
406
408
  /**
407
409
  * Watch field values:
408
410
  *
package/dist/index.js CHANGED
@@ -291,13 +291,7 @@ function validateObjectRule(value, rule) {
291
291
  }
292
292
 
293
293
  // src/rules/generic.rule.ts
294
- var GENERIC_RULE_NAMES = /* @__PURE__ */ new Set([
295
- "custom",
296
- "refine",
297
- "matchField",
298
- "oneOf",
299
- "notOneOf"
300
- ]);
294
+ var GENERIC_RULE_NAMES = /* @__PURE__ */ new Set(["custom", "refine", "matchField", "oneOf", "notOneOf"]);
301
295
  function isGenericRule(rule) {
302
296
  return GENERIC_RULE_NAMES.has(rule.name);
303
297
  }
package/dist/index.mjs CHANGED
@@ -234,13 +234,7 @@ function validateObjectRule(value, rule) {
234
234
  }
235
235
 
236
236
  // src/rules/generic.rule.ts
237
- var GENERIC_RULE_NAMES = /* @__PURE__ */ new Set([
238
- "custom",
239
- "refine",
240
- "matchField",
241
- "oneOf",
242
- "notOneOf"
243
- ]);
237
+ var GENERIC_RULE_NAMES = /* @__PURE__ */ new Set(["custom", "refine", "matchField", "oneOf", "notOneOf"]);
244
238
  function isGenericRule(rule) {
245
239
  return GENERIC_RULE_NAMES.has(rule.name);
246
240
  }
package/package.json CHANGED
@@ -1,10 +1,16 @@
1
1
  {
2
2
  "name": "@valfuse-node/form",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Form domain package for valfuse-node — schema, validation, transformation, rules, state, and methods",
5
5
  "license": "MIT",
6
6
  "sideEffects": false,
7
- "keywords": ["validation", "schema", "form", "typescript", "valfuse"],
7
+ "keywords": [
8
+ "validation",
9
+ "schema",
10
+ "form",
11
+ "typescript",
12
+ "valfuse"
13
+ ],
8
14
  "main": "./dist/index.js",
9
15
  "module": "./dist/index.mjs",
10
16
  "types": "./dist/index.d.ts",
@@ -15,7 +21,11 @@
15
21
  "require": "./dist/index.js"
16
22
  }
17
23
  },
18
- "files": ["dist", "README.md", "LICENSE"],
24
+ "files": [
25
+ "dist",
26
+ "README.md",
27
+ "LICENSE"
28
+ ],
19
29
  "scripts": {
20
30
  "build": "tsup src/index.ts --format cjs,esm --dts --clean",
21
31
  "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
@@ -24,9 +34,23 @@
24
34
  "typecheck": "tsc --noEmit",
25
35
  "clean": "rm -rf dist coverage"
26
36
  },
27
- "publishConfig": { "access": "public" },
37
+ "publishConfig": {
38
+ "access": "public"
39
+ },
28
40
  "devDependencies": {
29
- "@vitest/coverage-v8": "latest",
30
- "vitest": "latest"
41
+ "@vitest/coverage-v8": "4.1.6",
42
+ "vitest": "4.1.6"
43
+ },
44
+ "engines": {
45
+ "node": ">=20.0.0"
46
+ },
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "git+https://github.com/alpinnz/valfuse-node.git",
50
+ "directory": "packages/form"
51
+ },
52
+ "homepage": "https://github.com/alpinnz/valfuse-node#readme",
53
+ "bugs": {
54
+ "url": "https://github.com/alpinnz/valfuse-node/issues"
31
55
  }
32
- }
56
+ }