@holo-js/forms 0.3.15 → 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.
@@ -1,5 +1,4 @@
1
- import { ValidationSchema, SchemaInputShape, ValidationErrorBag, InferSchemaData, FormLikeValidationInput } from '@holo-js/validation';
2
- import { FormSchema } from './schema.js';
1
+ import { ValidationSchema, SchemaInputShape, ValidationErrorBag, InferValidationSchemaData, InferSchemaData, FormLikeValidationInput } from '@holo-js/validation';
3
2
 
4
3
  type FormFailureErrors = Readonly<Partial<Record<string, readonly string[]>>>;
5
4
  interface FormFailureOptions {
@@ -17,6 +16,7 @@ declare const sensitiveInputInternals: {
17
16
  DEFAULT_DONT_FLASH_FIELDS: readonly string[];
18
17
  };
19
18
 
19
+ type InferFormData<TSchema extends ValidationSchema> = InferValidationSchemaData<TSchema>;
20
20
  interface FormFailurePayload<TData> {
21
21
  readonly ok: false;
22
22
  readonly status: number;
@@ -109,15 +109,15 @@ interface FormSubmissionFailure<TData> {
109
109
  fail(options: FormFailureOptions): FormFailurePayload<TData>;
110
110
  }
111
111
  type FormSubmissionResult<TData> = FormSubmissionSuccess<TData> | FormSubmissionFailure<TData>;
112
- declare function serializeSubmissionState<TData>(valid: boolean, values: Partial<TData> | TData, errors: ValidationErrorBag<TData>, schemaDefinition?: FormSchema): SerializedFormSubmission<TData>;
113
- declare function createSubmission<TData>(valid: boolean, values: Partial<TData> | TData, errors: ValidationErrorBag<TData>, failureStatus?: number, schemaDefinition?: FormSchema, failureMetadata?: FormFailureMetadata): FormSubmissionResult<TData>;
114
- declare function createSuccessfulSubmission<TShape extends SchemaInputShape>(schemaDefinition: FormSchema<TShape>, data: InferSchemaData<TShape>): FormSubmissionSuccess<InferSchemaData<TShape>>;
115
- declare function createFailedSubmission<TShape extends SchemaInputShape>(schemaDefinition: FormSchema<TShape>, values: Partial<InferSchemaData<TShape>>, flattenedErrors: Record<string, readonly string[]>, status?: number, failureMetadata?: FormFailureMetadata): FormSubmissionFailure<InferSchemaData<TShape>>;
112
+ declare function serializeSubmissionState<TData>(valid: boolean, values: Partial<TData> | TData, errors: ValidationErrorBag<TData>, schemaDefinition?: ValidationSchema): SerializedFormSubmission<TData>;
113
+ declare function createSubmission<TData>(valid: boolean, values: Partial<TData> | TData, errors: ValidationErrorBag<TData>, failureStatus?: number, schemaDefinition?: ValidationSchema, failureMetadata?: FormFailureMetadata): FormSubmissionResult<TData>;
114
+ declare function createSuccessfulSubmission<TShape extends SchemaInputShape>(schemaDefinition: ValidationSchema<TShape>, data: InferSchemaData<TShape>): FormSubmissionSuccess<InferSchemaData<TShape>>;
115
+ declare function createFailedSubmission<TShape extends SchemaInputShape>(schemaDefinition: ValidationSchema<TShape>, values: Partial<InferSchemaData<TShape>>, flattenedErrors: Record<string, readonly string[]>, status?: number, failureMetadata?: FormFailureMetadata): FormSubmissionFailure<InferSchemaData<TShape>>;
116
116
  declare function isRequestLikeHeaders(value: unknown): value is RequestLikeHeaders;
117
117
  declare function normalizeRequestHeaders(input: unknown): Headers;
118
118
  declare function normalizeRequestLikeInput(input: FormLikeValidationInput | FormRequestLikeInput | null | undefined): Request | undefined;
119
- declare function safeParse<TShape extends SchemaInputShape>(input: FormLikeValidationInput | FormRequestLikeInput, schemaDefinition: FormSchema<TShape>, options?: FormSecurityOptions): Promise<FormSubmissionResult<InferSchemaData<TShape>>>;
120
- declare function validate<TShape extends SchemaInputShape>(input: FormLikeValidationInput | FormRequestLikeInput, schemaDefinition: FormSchema<TShape>, options?: FormSecurityOptions): Promise<InferSchemaData<TShape>>;
119
+ declare function safeParse<TShape extends SchemaInputShape>(input: FormLikeValidationInput | FormRequestLikeInput, schemaDefinition: ValidationSchema<TShape>, options?: FormSecurityOptions): Promise<FormSubmissionResult<InferSchemaData<TShape>>>;
120
+ declare function validate<TShape extends SchemaInputShape>(input: FormLikeValidationInput | FormRequestLikeInput, schemaDefinition: ValidationSchema<TShape>, options?: FormSecurityOptions): Promise<InferSchemaData<TShape>>;
121
121
  declare const formsInternals: {
122
122
  createSubmission: typeof createSubmission;
123
123
  isRequestLikeHeaders: typeof isRequestLikeHeaders;
@@ -129,4 +129,4 @@ declare const formsInternals: {
129
129
  serializeSubmissionState: typeof serializeSubmissionState;
130
130
  };
131
131
 
132
- export { type FormFailureErrors as F, type SerializedFormSubmission as S, type FormFailureInput as a, type FormFailureOptions as b, type FormFailurePayload as c, type FormRequestLikeInput as d, type FormSecurityOptions as e, type FormSubmissionFailure as f, type FormSubmissionResult as g, type FormSubmissionSuccess as h, type FormSuccessPayload as i, createFailedSubmission as j, createSuccessfulSubmission as k, formsInternals as l, sanitizeFlashedInput as m, sensitiveInputInternals as n, safeParse as s, validate as v };
132
+ export { type FormFailureErrors as F, type InferFormData as I, type SerializedFormSubmission as S, type FormFailureInput as a, type FormFailureOptions as b, type FormFailurePayload as c, type FormRequestLikeInput as d, type FormSecurityOptions as e, type FormSubmissionFailure as f, type FormSubmissionResult as g, type FormSubmissionSuccess as h, type FormSuccessPayload as i, createFailedSubmission as j, createSuccessfulSubmission as k, formsInternals as l, sanitizeFlashedInput as m, sensitiveInputInternals as n, safeParse as s, validate as v };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,5 @@
1
- export { F as FormFailureErrors, a as FormFailureInput, b as FormFailureOptions, c as FormFailurePayload, d as FormRequestLikeInput, e as FormSecurityOptions, f as FormSubmissionFailure, g as FormSubmissionResult, h as FormSubmissionSuccess, i as FormSuccessPayload, S as SerializedFormSubmission, j as createFailedSubmission, k as createSuccessfulSubmission, l as formsInternals, s as safeParse, m as sanitizeFlashedInput, n as sensitiveInputInternals, v as validate } from './contracts-BOhBkI9c.js';
2
- export { FormSchema, InferFormData, isFormSchema, schema } from './schema.js';
3
- export { DEFAULT_VALIDATION_BAG, ErrorTree, ErrorTreeNode, FieldDefinition, FieldRule, InferSchemaData, SerializedValidationException, StandardSchemaV1, StandardSchemaV1Issue, StandardSchemaV1Props, StandardSchemaV1Result, ValidationErrorBag, ValidationException, ValidationExceptionOptions, ValidationFailure, ValidationResult, ValidationSchema, ValidationSuccess, WebFileLike, createErrorBag, defineSchema, field, isValidationException, parse } from '@holo-js/validation';
1
+ export { F as FormFailureErrors, a as FormFailureInput, b as FormFailureOptions, c as FormFailurePayload, d as FormRequestLikeInput, e as FormSecurityOptions, f as FormSubmissionFailure, g as FormSubmissionResult, h as FormSubmissionSuccess, i as FormSuccessPayload, I as InferFormData, S as SerializedFormSubmission, j as createFailedSubmission, k as createSuccessfulSubmission, l as formsInternals, s as safeParse, m as sanitizeFlashedInput, n as sensitiveInputInternals, v as validate } from './contracts-CJ9Aq8pS.js';
2
+ import '@holo-js/validation';
4
3
 
5
4
  declare class FormContractError extends Error {
6
5
  constructor(message: string);
package/dist/index.mjs CHANGED
@@ -1,14 +1,3 @@
1
- import {
2
- DEFAULT_VALIDATION_BAG,
3
- ValidationException,
4
- createErrorBag,
5
- defineSchema,
6
- field,
7
- isFormSchema,
8
- isValidationException,
9
- parse,
10
- schema
11
- } from "./chunk-GPWXGHZE.mjs";
12
1
  import {
13
2
  normalizeFailureErrors,
14
3
  normalizeFailureInput,
@@ -22,8 +11,8 @@ import {
22
11
 
23
12
  // src/contracts.ts
24
13
  import {
25
- ValidationException as ValidationException2,
26
- createErrorBag as createErrorBag2,
14
+ ValidationException,
15
+ createErrorBag,
27
16
  safeParse as safeParseInput,
28
17
  validationInternals
29
18
  } from "@holo-js/validation";
@@ -89,7 +78,7 @@ function createSuccessfulSubmission(schemaDefinition, data) {
89
78
  return createSubmission(
90
79
  true,
91
80
  data,
92
- createErrorBag2(),
81
+ createErrorBag(),
93
82
  422,
94
83
  schemaDefinition
95
84
  );
@@ -100,7 +89,7 @@ function createFailedSubmission(schemaDefinition, values, flattenedErrors, statu
100
89
  return createSubmission(
101
90
  false,
102
91
  values,
103
- createErrorBag2(flattenedErrors),
92
+ createErrorBag(flattenedErrors),
104
93
  normalizedStatus,
105
94
  schemaDefinition,
106
95
  failureMetadata
@@ -479,7 +468,7 @@ async function validate(input, schemaDefinition, options = {}) {
479
468
  }
480
469
  const failure = result.fail();
481
470
  const exception = validationInternals.setValidationExceptionValues(
482
- ValidationException2.withMessages(result.errors.flatten(), {
471
+ ValidationException.withMessages(result.errors.flatten(), {
483
472
  bag: options.bag
484
473
  }),
485
474
  result.values
@@ -502,21 +491,12 @@ var formsInternals = {
502
491
  serializeSubmissionState
503
492
  };
504
493
  export {
505
- DEFAULT_VALIDATION_BAG,
506
494
  FormContractError,
507
- ValidationException,
508
- createErrorBag,
509
495
  createFailedSubmission,
510
496
  createSuccessfulSubmission,
511
- defineSchema,
512
- field,
513
497
  formsInternals,
514
- isFormSchema,
515
- isValidationException,
516
- parse,
517
498
  safeParse,
518
499
  sanitizeFlashedInput,
519
- schema,
520
500
  sensitiveInputInternals,
521
501
  validate
522
502
  };
@@ -1,6 +1,5 @@
1
- import { S as SerializedFormSubmission, c as FormFailurePayload, i as FormSuccessPayload, g as FormSubmissionResult } from '../contracts-BOhBkI9c.js';
2
- import { ValidationErrorBag, WebFileLike, SchemaInputShape, FieldBuilderInput, InferFieldOutput } from '@holo-js/validation';
3
- import { FormSchema, InferFormData } from '../schema.js';
1
+ import { S as SerializedFormSubmission, c as FormFailurePayload, i as FormSuccessPayload, g as FormSubmissionResult, I as InferFormData } from '../contracts-CJ9Aq8pS.js';
2
+ import { ValidationErrorBag, WebFileLike, ValidationSchema, SchemaInputShape, FieldBuilderInput, InferFieldOutput } from '@holo-js/validation';
4
3
 
5
4
  declare function collectFormDirtyPaths(current: unknown, initial: unknown, prefix?: string): readonly string[];
6
5
  declare class FormClientState<TValues, TSuccess = unknown> {
@@ -8,21 +7,35 @@ declare class FormClientState<TValues, TSuccess = unknown> {
8
7
  values: TValues;
9
8
  initialValues: TValues;
10
9
  flattenedErrors: Record<string, readonly string[]>;
11
- touched: Set<string>;
12
- dirty: Set<string>;
10
+ readonly touched: Set<string>;
13
11
  lastSubmission?: SerializedFormSubmission<TValues> | FormFailurePayload<TValues> | FormSuccessPayload<TSuccess>;
14
12
  readonly listeners: Set<() => void>;
15
- validationSequence: number;
16
13
  constructor(values: TValues, initialState?: SerializedFormSubmission<TValues> | FormFailurePayload<TValues>);
17
14
  get submitting(): boolean;
18
15
  get errors(): ValidationErrorBag<TValues>;
19
16
  get dirtyPaths(): readonly string[];
20
- 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;
21
30
  startSubmission(signal?: AbortSignal): () => void;
22
31
  }
23
32
 
24
33
  type PrimitiveLike = string | number | boolean | bigint | symbol | null | undefined | Date | Blob | WebFileLike;
25
34
  type ValidateOnMode = 'submit' | 'blur' | 'change';
35
+ type FormRequestCredentials = 'omit' | 'same-origin' | 'include';
36
+ interface ClientCsrfOptions {
37
+ readonly endpoint: string;
38
+ }
26
39
  interface ClientSubmitContext<TData> {
27
40
  readonly action?: string;
28
41
  readonly method: string;
@@ -41,6 +54,8 @@ type BrowserFormElement = {
41
54
  interface UseFormOptions<TData, TSuccess = unknown> {
42
55
  readonly action?: string;
43
56
  readonly method?: string;
57
+ readonly credentials?: FormRequestCredentials;
58
+ readonly csrf?: ClientCsrfOptions;
44
59
  readonly validateOn?: ValidateOnMode;
45
60
  readonly initialValues?: Partial<TData>;
46
61
  readonly initialState?: SerializedFormSubmission<TData> | FormFailurePayload<TData> | null;
@@ -63,7 +78,7 @@ type FormFieldTree<TData> = [TData] extends [readonly unknown[]] ? FormFieldStat
63
78
  type FormFieldTreeFromShape<TShape extends SchemaInputShape> = {
64
79
  readonly [K in Extract<keyof TShape, string>]: TShape[K] extends FieldBuilderInput ? FormFieldState<InferFieldOutput<TShape[K]>> : TShape[K] extends SchemaInputShape ? FormFieldTreeFromShape<TShape[K]> : never;
65
80
  };
66
- type InferFormFieldTree<TSchema extends FormSchema> = TSchema extends FormSchema<infer TShape> ? FormFieldTreeFromShape<TShape> : never;
81
+ type InferFormFieldTree<TSchema extends ValidationSchema> = TSchema extends ValidationSchema<infer TShape> ? FormFieldTreeFromShape<TShape> : never;
67
82
  interface UseFormResult<TData, TSuccess = unknown, TFields = FormFieldTree<TData>> {
68
83
  readonly fields: TFields;
69
84
  readonly values: TData;
@@ -83,6 +98,6 @@ declare function runWithBrowserFormElement<TData, TSuccess>(formClient: Pick<Use
83
98
  /**
84
99
  * @internal Shared headless form runtime for framework adapters.
85
100
  */
86
- declare function createFormClient<TSchema extends FormSchema, TSuccess = unknown>(schemaDefinition: TSchema, options?: UseFormOptions<InferFormData<TSchema>, TSuccess>): UseFormResult<InferFormData<TSchema>, TSuccess, InferFormFieldTree<TSchema>>;
101
+ declare function createFormClient<TSchema extends ValidationSchema, TSuccess = unknown>(schemaDefinition: TSchema, options?: UseFormOptions<InferFormData<TSchema>, TSuccess>): UseFormResult<InferFormData<TSchema>, TSuccess, InferFormFieldTree<TSchema>>;
87
102
 
88
- export { type ClientSubmitContext, type ClientSubmitResult, FormClientState, type FormFieldState, type FormFieldTree, type InferFormFieldTree, type UseFormOptions, type UseFormResult, type ValidateOnMode, collectFormDirtyPaths, createFormClient, markClientSubmitControlFlowError, runWithBrowserFormElement };
103
+ export { type ClientCsrfOptions, type ClientSubmitContext, type ClientSubmitResult, FormClientState, type FormFieldState, type FormFieldTree, type FormRequestCredentials, type InferFormFieldTree, type UseFormOptions, type UseFormResult, type ValidateOnMode, collectFormDirtyPaths, createFormClient, markClientSubmitControlFlowError, runWithBrowserFormElement };
@@ -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
  });
@@ -601,7 +618,7 @@ function normalizeSubmissionLike(schemaDefinition, values, result) {
601
618
  valid: false,
602
619
  values: result.values,
603
620
  errors: result.errors,
604
- ...result.status === 429 && "retryAfter" in result ? { retryAfter: result.retryAfter } : {},
621
+ ...result.status === 429 && "retryAfterSeconds" in result ? { retryAfterSeconds: result.retryAfterSeconds } : {},
605
622
  ...result.status === 429 && "retryAt" in result ? { retryAt: result.retryAt } : {}
606
623
  };
607
624
  }
@@ -659,7 +676,7 @@ async function normalizeFetchResponse(response, fallbackValues) {
659
676
  return createTransportFailure(fallbackValues, response.status);
660
677
  }
661
678
  }
662
- async function defaultSubmitter(context) {
679
+ async function defaultSubmitter(context, options) {
663
680
  if (typeof fetch !== "function" || !context.action) {
664
681
  return {
665
682
  ok: true,
@@ -668,9 +685,11 @@ async function defaultSubmitter(context) {
668
685
  };
669
686
  }
670
687
  const method = context.method.toUpperCase();
688
+ const credentials = options.credentials ? { credentials: options.credentials } : {};
671
689
  if (method === "GET" || method === "HEAD") {
672
690
  const response2 = await fetch(appendQueryString(context.action, context.formData), {
673
- method
691
+ method,
692
+ ...credentials
674
693
  });
675
694
  if (method === "HEAD" || response2.status === 204 || response2.status === 205) {
676
695
  return {
@@ -681,8 +700,24 @@ async function defaultSubmitter(context) {
681
700
  }
682
701
  return await normalizeFetchResponse(response2, context.values);
683
702
  }
703
+ const headers = new Headers();
704
+ if (options.csrf) {
705
+ const csrfResponse = await fetch(options.csrf.endpoint, {
706
+ ...credentials
707
+ });
708
+ if (!csrfResponse.ok) {
709
+ return createTransportFailure(context.values, csrfResponse.status);
710
+ }
711
+ const csrfPayload = await csrfResponse.json();
712
+ if (!csrfPayload || typeof csrfPayload !== "object" || !("token" in csrfPayload) || typeof csrfPayload.token !== "string" || !csrfPayload.token) {
713
+ return createTransportFailure(context.values);
714
+ }
715
+ headers.set("X-CSRF-TOKEN", csrfPayload.token);
716
+ }
684
717
  const response = await fetch(context.action, {
685
718
  method,
719
+ ...credentials,
720
+ ...options.csrf ? { headers } : {},
686
721
  body: context.formData
687
722
  });
688
723
  return await normalizeFetchResponse(response, context.values);
@@ -710,28 +745,21 @@ function createFormClient(schemaDefinition, options = {}) {
710
745
  return await runValidationForInput(cloneFormValue(state.values));
711
746
  }
712
747
  async function runValidationForInput(input) {
713
- const sequence = nextValidationSequence(state);
748
+ const sequence = state.beginValidation();
714
749
  const submission = await validateClientValues(input, schemaDefinition);
715
- if (isLatestValidation(state, sequence)) {
716
- state.values = mergeFormValues(
717
- state.values,
718
- submission.valid ? submission.data : submission.values
719
- );
720
- state.flattenedErrors = submission.errors.flatten();
721
- notifyListeners(state);
722
- }
750
+ state.applyValidation(sequence, submission.errors.flatten(), submission.valid ? submission.data : submission.values);
723
751
  return submission;
724
752
  }
725
- function applyServerState(result) {
753
+ function applyServerState(result, submissionSequence) {
726
754
  const normalized = normalizeSubmissionLike(
727
755
  schemaDefinition,
728
756
  state.values,
729
757
  result
730
758
  );
759
+ const applies = submissionSequence === void 0 || state.ownsSubmissionEffects(submissionSequence);
760
+ if (submissionSequence === void 0) state.invalidateEffects();
731
761
  if ("ok" in normalized && normalized.ok === true) {
732
- state.lastSubmission = normalized;
733
- state.flattenedErrors = {};
734
- notifyListeners(state);
762
+ if (applies) state.applyServer(state.values, {}, normalized);
735
763
  return normalized;
736
764
  }
737
765
  if ("ok" in normalized && normalized.ok === false) {
@@ -739,26 +767,27 @@ function createFormClient(schemaDefinition, options = {}) {
739
767
  ...normalized,
740
768
  values: sanitizeFlashedInput(normalized.values, schemaDefinition)
741
769
  };
742
- state.values = mergeFormValues(state.values, sanitized.values);
743
- clearSensitiveInputValues(state.values, schemaDefinition);
744
- state.flattenedErrors = sanitized.errors;
745
- state.lastSubmission = sanitized;
746
- 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
+ }
747
775
  return sanitized;
748
776
  }
749
777
  const normalizedSubmission = normalized;
750
- state.values = mergeFormValues(state.values, normalizedSubmission.values);
751
- clearSensitiveInputValues(state.values, schemaDefinition);
752
- state.flattenedErrors = normalizedSubmission.errors.flatten();
753
- state.lastSubmission = normalizedSubmission.valid ? void 0 : normalizedSubmission.fail();
754
- 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
+ }
755
783
  return normalizedSubmission;
756
784
  }
757
785
  async function submit(browserForm) {
786
+ const submissionSequence = state.beginSubmissionEffects();
758
787
  const finishSubmission = state.startSubmission();
759
- notifyListeners(state);
788
+ state.notify();
760
789
  try {
761
- const submitter = options.submitter ?? defaultSubmitter;
790
+ const submitter = options.submitter ?? ((context) => defaultSubmitter(context, options));
762
791
  const liveForm = browserForm ?? getActiveElementBrowserForm();
763
792
  const method = options.method ?? liveForm?.method ?? "POST";
764
793
  const action = options.action ?? liveForm?.action;
@@ -787,17 +816,18 @@ function createFormClient(schemaDefinition, options = {}) {
787
816
  throw error;
788
817
  }
789
818
  return applyServerState(
790
- validationExceptionToFailure(error, state.values) ?? createTransportFailure(state.values)
819
+ validationExceptionToFailure(error, localSubmission.data) ?? createTransportFailure(localSubmission.data),
820
+ submissionSequence
791
821
  );
792
822
  }
793
823
  return applyServerState(response ?? {
794
824
  ok: true,
795
825
  status: 204,
796
826
  data: void 0
797
- });
827
+ }, submissionSequence);
798
828
  } finally {
799
829
  finishSubmission();
800
- notifyListeners(state);
830
+ state.notify();
801
831
  }
802
832
  }
803
833
  return Object.freeze({
@@ -837,28 +867,15 @@ function createFormClient(schemaDefinition, options = {}) {
837
867
  return submit(form);
838
868
  },
839
869
  reset(values) {
840
- const next = mergeFormValues(state.initialValues, values);
841
- state.values = cloneFormValue(next);
842
- state.initialValues = cloneFormValue(next);
843
- state.flattenedErrors = {};
844
- state.touched.clear();
845
- state.dirty.clear();
846
- state.lastSubmission = void 0;
847
- notifyListeners(state);
870
+ state.reset(values);
848
871
  },
849
872
  async setValue(path, value) {
850
- setFormValueAtPath(state.values, path, value);
851
- state.touched.add(path);
852
- if (!areFormValuesEqual(getFormValueAtPath(state.initialValues, path), value)) {
853
- state.dirty.add(path);
854
- } else {
855
- state.dirty.delete(path);
856
- }
873
+ state.edit(path, value);
857
874
  if (validateOn === "change" && fieldPaths.includes(path)) {
858
875
  await runValidation();
859
876
  return;
860
877
  }
861
- notifyListeners(state);
878
+ state.notify();
862
879
  },
863
880
  applyServerState(result) {
864
881
  return applyServerState(result);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@holo-js/forms",
3
- "version": "0.3.15",
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",
@@ -10,11 +10,6 @@
10
10
  "import": "./dist/index.mjs",
11
11
  "default": "./dist/index.mjs"
12
12
  },
13
- "./schema": {
14
- "types": "./dist/schema.d.ts",
15
- "import": "./dist/schema.mjs",
16
- "default": "./dist/schema.mjs"
17
- },
18
13
  "./internal/client": {
19
14
  "types": "./dist/internal/client.d.ts",
20
15
  "import": "./dist/internal/client.mjs",
@@ -33,10 +28,10 @@
33
28
  "test": "vitest --run"
34
29
  },
35
30
  "dependencies": {
36
- "@holo-js/validation": "^0.3.15"
31
+ "@holo-js/validation": "^0.3.17"
37
32
  },
38
33
  "peerDependencies": {
39
- "@holo-js/security": "^0.3.15"
34
+ "@holo-js/security": "^0.3.17"
40
35
  },
41
36
  "peerDependenciesMeta": {
42
37
  "@holo-js/security": {
@@ -50,6 +45,6 @@
50
45
  "vitest": "^4.1.5"
51
46
  },
52
47
  "engines": {
53
- "node": ">=22.12.0"
48
+ "node": "^22.12.0 || ^24.0.0 || ^26.0.0"
54
49
  }
55
50
  }
@@ -1,40 +0,0 @@
1
- // src/schema.ts
2
- import {
3
- defineSchema,
4
- isValidationSchema
5
- } from "@holo-js/validation";
6
- import {
7
- createErrorBag,
8
- DEFAULT_VALIDATION_BAG,
9
- defineSchema as defineSchema2,
10
- field,
11
- isValidationException,
12
- parse,
13
- safeParse,
14
- ValidationException,
15
- validationInternals
16
- } from "@holo-js/validation";
17
- function schema(shapeOrSchema) {
18
- const base = isValidationSchema(shapeOrSchema) ? shapeOrSchema : defineSchema(shapeOrSchema);
19
- return Object.freeze({
20
- ...base,
21
- mode: "form"
22
- });
23
- }
24
- function isFormSchema(value) {
25
- return isValidationSchema(value) && value.mode === "form";
26
- }
27
-
28
- export {
29
- schema,
30
- isFormSchema,
31
- createErrorBag,
32
- DEFAULT_VALIDATION_BAG,
33
- defineSchema2 as defineSchema,
34
- field,
35
- isValidationException,
36
- parse,
37
- safeParse,
38
- ValidationException,
39
- validationInternals
40
- };
package/dist/schema.d.ts DELETED
@@ -1,13 +0,0 @@
1
- import { SchemaInputShape, ValidationSchema, InferSchemaData } from '@holo-js/validation';
2
- export { DEFAULT_VALIDATION_BAG, ErrorTree, ErrorTreeNode, FieldDefinition, FieldRule, InferSchemaData, SerializedValidationException, StandardSchemaV1, StandardSchemaV1Issue, StandardSchemaV1Props, StandardSchemaV1Result, ValidationErrorBag, ValidationException, ValidationExceptionOptions, ValidationFailure, ValidationResult, ValidationSchema, ValidationSuccess, WebFileLike, createErrorBag, defineSchema, field, isValidationException, parse, safeParse, validationInternals } from '@holo-js/validation';
3
-
4
- interface FormSchema<TShape extends SchemaInputShape = SchemaInputShape> extends ValidationSchema<TShape> {
5
- readonly mode: 'form';
6
- readonly fields: ValidationSchema<TShape>['fields'];
7
- readonly $values?: Partial<InferSchemaData<TShape>>;
8
- }
9
- type InferFormData<TSchema extends FormSchema> = TSchema extends FormSchema<infer TShape> ? InferSchemaData<TShape> : never;
10
- declare function schema<TShape extends SchemaInputShape>(shapeOrSchema: TShape | ValidationSchema<TShape>): FormSchema<TShape>;
11
- declare function isFormSchema(value: unknown): value is FormSchema;
12
-
13
- export { type FormSchema, type InferFormData, isFormSchema, schema };
package/dist/schema.mjs DELETED
@@ -1,26 +0,0 @@
1
- import {
2
- DEFAULT_VALIDATION_BAG,
3
- ValidationException,
4
- createErrorBag,
5
- defineSchema,
6
- field,
7
- isFormSchema,
8
- isValidationException,
9
- parse,
10
- safeParse,
11
- schema,
12
- validationInternals
13
- } from "./chunk-GPWXGHZE.mjs";
14
- export {
15
- DEFAULT_VALIDATION_BAG,
16
- ValidationException,
17
- createErrorBag,
18
- defineSchema,
19
- field,
20
- isFormSchema,
21
- isValidationException,
22
- parse,
23
- safeParse,
24
- schema,
25
- validationInternals
26
- };