@holo-js/forms 0.1.0

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.
@@ -0,0 +1,124 @@
1
+ // src/contracts.ts
2
+ import {
3
+ createErrorBag,
4
+ defineSchema,
5
+ isValidationSchema,
6
+ validate as validateInput
7
+ } from "@holo-js/validation";
8
+ var FormContractError = class extends Error {
9
+ constructor(message) {
10
+ super(message);
11
+ this.name = "FormContractError";
12
+ }
13
+ };
14
+ function schema(shapeOrSchema) {
15
+ const base = isValidationSchema(shapeOrSchema) ? shapeOrSchema : defineSchema(shapeOrSchema);
16
+ return Object.freeze({
17
+ ...base,
18
+ mode: "form"
19
+ });
20
+ }
21
+ function isFormSchema(value) {
22
+ return isValidationSchema(value) && value.mode === "form";
23
+ }
24
+ function normalizeStatus(value, fallback) {
25
+ if (typeof value === "undefined") {
26
+ return fallback;
27
+ }
28
+ if (!Number.isInteger(value) || value < 100) {
29
+ throw new FormContractError("HTTP status codes must be integers greater than or equal to 100.");
30
+ }
31
+ return value;
32
+ }
33
+ function serializeSubmissionState(valid, values, errors) {
34
+ return Object.freeze({
35
+ valid,
36
+ submitted: true,
37
+ values,
38
+ errors: errors.flatten()
39
+ });
40
+ }
41
+ function createSubmission(valid, values, errors) {
42
+ const serialize = () => serializeSubmissionState(valid, values, errors);
43
+ const failure = () => ({
44
+ ok: false,
45
+ status: 422,
46
+ valid: false,
47
+ values,
48
+ errors: errors.flatten()
49
+ });
50
+ const success = (payload, status) => ({
51
+ ok: true,
52
+ status: normalizeStatus(status, 200),
53
+ data: payload
54
+ });
55
+ if (valid) {
56
+ const data = values;
57
+ return Object.freeze({
58
+ valid: true,
59
+ submitted: true,
60
+ data,
61
+ values: data,
62
+ errors,
63
+ serialize,
64
+ success,
65
+ fail(status) {
66
+ const payload = failure();
67
+ return {
68
+ ...payload,
69
+ status: normalizeStatus(status, payload.status)
70
+ };
71
+ }
72
+ });
73
+ }
74
+ return Object.freeze({
75
+ valid: false,
76
+ submitted: true,
77
+ data: void 0,
78
+ values,
79
+ errors,
80
+ serialize,
81
+ success,
82
+ fail(status) {
83
+ const payload = failure();
84
+ return {
85
+ ...payload,
86
+ status: normalizeStatus(status, payload.status)
87
+ };
88
+ }
89
+ });
90
+ }
91
+ function createSuccessfulSubmission(schemaDefinition, data) {
92
+ void schemaDefinition;
93
+ return createSubmission(true, data, createErrorBag());
94
+ }
95
+ function createFailedSubmission(schemaDefinition, values, flattenedErrors) {
96
+ void schemaDefinition;
97
+ return createSubmission(
98
+ false,
99
+ values,
100
+ createErrorBag(flattenedErrors)
101
+ );
102
+ }
103
+ async function validate(input, schemaDefinition) {
104
+ const result = await validateInput(input, schemaDefinition);
105
+ if (result.valid) {
106
+ return createSuccessfulSubmission(schemaDefinition, result.data);
107
+ }
108
+ return createFailedSubmission(schemaDefinition, result.values, result.errors.flatten());
109
+ }
110
+ var formsInternals = {
111
+ createSubmission,
112
+ normalizeStatus,
113
+ serializeSubmissionState
114
+ };
115
+
116
+ export {
117
+ FormContractError,
118
+ schema,
119
+ isFormSchema,
120
+ createSuccessfulSubmission,
121
+ createFailedSubmission,
122
+ validate,
123
+ formsInternals
124
+ };
@@ -0,0 +1,51 @@
1
+ import { FormSubmissionResult, SerializedFormSubmission, FormFailurePayload, FormSuccessPayload, FormSchema } from './index.js';
2
+ import { WebFileLike, ValidationErrorBag, SchemaInputShape, InferSchemaData } from '@holo-js/validation';
3
+
4
+ type PrimitiveLike = string | number | boolean | bigint | symbol | null | undefined | Date | Blob | WebFileLike;
5
+ type ValidateOnMode = 'submit' | 'blur' | 'change';
6
+ interface ClientSubmitContext<TData> {
7
+ readonly action?: string;
8
+ readonly method: string;
9
+ readonly values: TData;
10
+ readonly formData: FormData;
11
+ }
12
+ type ClientSubmitResult<TData> = FormSubmissionResult<TData> | SerializedFormSubmission<TData> | FormFailurePayload<TData> | FormSuccessPayload<unknown>;
13
+ interface UseFormOptions<TData> {
14
+ readonly action?: string;
15
+ readonly method?: string;
16
+ readonly validateOn?: ValidateOnMode;
17
+ readonly initialValues?: Partial<TData>;
18
+ readonly initialState?: SerializedFormSubmission<TData>;
19
+ readonly submitter?: (context: ClientSubmitContext<TData>) => Promise<ClientSubmitResult<TData>> | ClientSubmitResult<TData>;
20
+ }
21
+ interface FormFieldState<TValue> {
22
+ value: TValue;
23
+ readonly errors: readonly string[];
24
+ readonly touched: boolean;
25
+ readonly dirty: boolean;
26
+ set(value: TValue): Promise<void>;
27
+ onInput(value: TValue): Promise<void>;
28
+ onBlur(): Promise<void>;
29
+ validate(): Promise<readonly string[]>;
30
+ }
31
+ type FormFieldTree<TData> = TData extends readonly unknown[] ? FormFieldState<TData> : TData extends PrimitiveLike ? FormFieldState<TData> : TData extends Record<string, unknown> ? {
32
+ readonly [K in keyof TData]: FormFieldTree<TData[K]>;
33
+ } : FormFieldState<TData>;
34
+ interface UseFormResult<TData> {
35
+ readonly fields: FormFieldTree<TData>;
36
+ readonly values: TData;
37
+ readonly errors: ValidationErrorBag<TData>;
38
+ readonly submitting: boolean;
39
+ readonly valid: boolean;
40
+ readonly lastSubmission?: SerializedFormSubmission<TData> | FormSuccessPayload<unknown>;
41
+ subscribe(listener: () => void): () => void;
42
+ validate(): Promise<FormSubmissionResult<TData>>;
43
+ validateField(path: string): Promise<readonly string[]>;
44
+ submit(): Promise<ClientSubmitResult<TData>>;
45
+ reset(values?: Partial<TData>): void;
46
+ setValue(path: string, value: unknown): Promise<void>;
47
+ applyServerState(result: ClientSubmitResult<TData>): ClientSubmitResult<TData>;
48
+ }
49
+ declare function useForm<TSchema extends FormSchema<SchemaInputShape>>(schemaDefinition: TSchema, options?: UseFormOptions<InferSchemaData<TSchema['fields']>>): UseFormResult<InferSchemaData<TSchema['fields']>>;
50
+
51
+ export { type ClientSubmitContext, type ClientSubmitResult, type FormFieldState, type FormFieldTree, type UseFormOptions, type UseFormResult, type ValidateOnMode, useForm };
@@ -0,0 +1,467 @@
1
+ import {
2
+ createFailedSubmission,
3
+ createSuccessfulSubmission,
4
+ validate
5
+ } from "./chunk-Z6TEFT7S.mjs";
6
+
7
+ // src/client.ts
8
+ import { createErrorBag } from "@holo-js/validation";
9
+ function isPlainObject(value) {
10
+ return !!value && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date) && !(value instanceof Blob);
11
+ }
12
+ function isSchemaFieldLike(value) {
13
+ return !!value && typeof value === "object" && value.kind === "field" && !!value.definition && typeof value.definition === "object";
14
+ }
15
+ function areEqual(left, right) {
16
+ if (Object.is(left, right)) {
17
+ return true;
18
+ }
19
+ if (left instanceof Date && right instanceof Date) {
20
+ return left.getTime() === right.getTime();
21
+ }
22
+ if (left instanceof Blob || right instanceof Blob) {
23
+ return false;
24
+ }
25
+ if (Array.isArray(left) && Array.isArray(right)) {
26
+ return left.length === right.length && left.every((value, index) => areEqual(value, right[index]));
27
+ }
28
+ if (isPlainObject(left) && isPlainObject(right)) {
29
+ const leftKeys = Object.keys(left);
30
+ const rightKeys = Object.keys(right);
31
+ return leftKeys.length === rightKeys.length && leftKeys.every((key) => key in right && areEqual(left[key], right[key]));
32
+ }
33
+ return false;
34
+ }
35
+ function cloneValue(value) {
36
+ if (Array.isArray(value)) {
37
+ return value.map((item) => cloneValue(item));
38
+ }
39
+ if (value instanceof Date) {
40
+ return new Date(value.getTime());
41
+ }
42
+ if (isPlainObject(value)) {
43
+ return Object.fromEntries(
44
+ Object.entries(value).map(([key, entry]) => [key, cloneValue(entry)])
45
+ );
46
+ }
47
+ return value;
48
+ }
49
+ function isLeafValue(value) {
50
+ return Array.isArray(value) || value instanceof Date || value instanceof Blob || isSchemaFieldLike(value) || !isPlainObject(value);
51
+ }
52
+ function normalizeObject(value) {
53
+ return isPlainObject(value) ? cloneValue(value) : {};
54
+ }
55
+ function mergeValues(base, override) {
56
+ if (!override || !isPlainObject(override)) {
57
+ return cloneValue(base);
58
+ }
59
+ const output = cloneValue(base);
60
+ for (const [key, value] of Object.entries(override)) {
61
+ if (isPlainObject(value) && isPlainObject(output[key])) {
62
+ output[key] = mergeValues(output[key], value);
63
+ continue;
64
+ }
65
+ output[key] = cloneValue(value);
66
+ }
67
+ return output;
68
+ }
69
+ function splitPath(path) {
70
+ return path.split(".").map((part) => part.trim()).filter(Boolean);
71
+ }
72
+ function getValueAtPath(root, path) {
73
+ let cursor = root;
74
+ for (const part of splitPath(path)) {
75
+ if (!isPlainObject(cursor) && !Array.isArray(cursor)) {
76
+ return void 0;
77
+ }
78
+ cursor = cursor[part];
79
+ }
80
+ return cursor;
81
+ }
82
+ function setValueAtPath(root, path, value) {
83
+ const parts = splitPath(path);
84
+ let cursor = root;
85
+ for (const [index, part] of parts.entries()) {
86
+ const last = index === parts.length - 1;
87
+ const nextPart = parts[index + 1];
88
+ if (Array.isArray(cursor)) {
89
+ const offset = Number(part);
90
+ if (!Number.isInteger(offset) || offset < 0) {
91
+ return;
92
+ }
93
+ if (last) {
94
+ cursor[offset] = value;
95
+ return;
96
+ }
97
+ const existing2 = cursor[offset];
98
+ if (!isPlainObject(existing2) && !Array.isArray(existing2)) {
99
+ cursor[offset] = /^\d+$/.test(nextPart) ? [] : {};
100
+ }
101
+ cursor = cursor[offset];
102
+ continue;
103
+ }
104
+ if (last) {
105
+ cursor[part] = value;
106
+ return;
107
+ }
108
+ const existing = cursor[part];
109
+ if (!isPlainObject(existing) && !Array.isArray(existing)) {
110
+ cursor[part] = /^\d+$/.test(nextPart) ? [] : {};
111
+ }
112
+ cursor = cursor[part];
113
+ }
114
+ }
115
+ function flattenLeafPaths(value, prefix = "") {
116
+ if (isLeafValue(value)) {
117
+ return [prefix].filter(Boolean);
118
+ }
119
+ return Object.entries(value).flatMap(([key, nested]) => {
120
+ const next = prefix ? `${prefix}.${key}` : key;
121
+ return flattenLeafPaths(nested, next);
122
+ });
123
+ }
124
+ function buildFormData(value, path = "", formData = new FormData()) {
125
+ if (typeof value === "undefined") {
126
+ return formData;
127
+ }
128
+ if (value instanceof Date) {
129
+ formData.append(path, value.toISOString());
130
+ return formData;
131
+ }
132
+ if (value instanceof Blob) {
133
+ formData.append(path, value);
134
+ return formData;
135
+ }
136
+ if (Array.isArray(value)) {
137
+ for (const item of value) {
138
+ buildFormData(item, `${path}[]`, formData);
139
+ }
140
+ return formData;
141
+ }
142
+ if (isPlainObject(value)) {
143
+ for (const [key, nested] of Object.entries(value)) {
144
+ const next = path ? `${path}.${key}` : key;
145
+ buildFormData(nested, next, formData);
146
+ }
147
+ return formData;
148
+ }
149
+ formData.append(path, String(value));
150
+ return formData;
151
+ }
152
+ function createTypedErrorBag(flattenedErrors) {
153
+ return createErrorBag(flattenedErrors);
154
+ }
155
+ function notifyListeners(state) {
156
+ for (const listener of state.listeners) {
157
+ listener();
158
+ }
159
+ }
160
+ function collectErrorsForPath(flattenedErrors, path) {
161
+ const direct = flattenedErrors[path] ?? [];
162
+ const prefix = `${path}.`;
163
+ const nested = Object.entries(flattenedErrors).filter(([key]) => key.startsWith(prefix)).flatMap(([, messages]) => messages);
164
+ return direct.length > 0 || nested.length > 0 ? [...direct, ...nested] : [];
165
+ }
166
+ function buildFieldsTree(state, schemaDefinition, source, validateOn, prefix = "") {
167
+ if (isLeafValue(source)) {
168
+ const path = prefix;
169
+ return Object.freeze({
170
+ get value() {
171
+ return getValueAtPath(state.values, path);
172
+ },
173
+ set value(next) {
174
+ void this.set(next);
175
+ },
176
+ get errors() {
177
+ return collectErrorsForPath(state.flattenedErrors, path);
178
+ },
179
+ get touched() {
180
+ return state.touched.has(path);
181
+ },
182
+ get dirty() {
183
+ return state.dirty.has(path);
184
+ },
185
+ async set(value) {
186
+ setValueAtPath(state.values, path, value);
187
+ state.touched.add(path);
188
+ if (!areEqual(getValueAtPath(state.initialValues, path), value)) {
189
+ state.dirty.add(path);
190
+ } else {
191
+ state.dirty.delete(path);
192
+ }
193
+ if (validateOn === "change") {
194
+ const submission = await validate(state.values, schemaDefinition);
195
+ state.flattenedErrors = submission.errors.flatten();
196
+ state.lastSubmission = submission.serialize();
197
+ }
198
+ notifyListeners(state);
199
+ },
200
+ async onInput(value) {
201
+ await this.set(value);
202
+ },
203
+ async onBlur() {
204
+ state.touched.add(path);
205
+ if (validateOn === "blur") {
206
+ const submission = await validate(state.values, schemaDefinition);
207
+ state.flattenedErrors = submission.errors.flatten();
208
+ state.lastSubmission = submission.serialize();
209
+ }
210
+ notifyListeners(state);
211
+ },
212
+ async validate() {
213
+ const submission = await validate(state.values, schemaDefinition);
214
+ state.flattenedErrors = submission.errors.flatten();
215
+ notifyListeners(state);
216
+ return state.flattenedErrors[path] ?? [];
217
+ }
218
+ });
219
+ }
220
+ const entries = Object.entries(source).map(([key, value]) => {
221
+ const next = prefix ? `${prefix}.${key}` : key;
222
+ return [key, buildFieldsTree(state, schemaDefinition, value, validateOn, next)];
223
+ });
224
+ return Object.freeze(Object.fromEntries(entries));
225
+ }
226
+ function isSubmissionResult(value) {
227
+ return "valid" in value && "errors" in value && "values" in value && !!value.errors && typeof value.errors === "object" && "flatten" in value.errors && typeof value.errors.flatten === "function";
228
+ }
229
+ function isSerializedSubmission(value) {
230
+ return "submitted" in value && "errors" in value && "values" in value && !("ok" in value);
231
+ }
232
+ function normalizeSubmissionLike(schemaDefinition, values, result) {
233
+ if (isSubmissionResult(result)) {
234
+ return result;
235
+ }
236
+ if ("ok" in result && result.ok === false && "errors" in result && "values" in result) {
237
+ return {
238
+ ...result,
239
+ submitted: true
240
+ };
241
+ }
242
+ if (isSerializedSubmission(result)) {
243
+ if (result.valid) {
244
+ return createSuccessfulSubmission(schemaDefinition, result.values);
245
+ }
246
+ return createFailedSubmission(schemaDefinition, result.values, result.errors);
247
+ }
248
+ if ("ok" in result && result.ok === true) {
249
+ return result;
250
+ }
251
+ return createSuccessfulSubmission(schemaDefinition, values);
252
+ }
253
+ function appendQueryString(action, formData) {
254
+ const [rawPath = "", hash = ""] = action.split("#", 2);
255
+ const [pathname = "", search = ""] = rawPath.split("?", 2);
256
+ const params = new URLSearchParams(search);
257
+ for (const [key, value] of formData.entries()) {
258
+ params.append(key, typeof value === "string" ? value : value.name);
259
+ }
260
+ const query = params.toString();
261
+ const suffix = hash ? `#${hash}` : "";
262
+ return query ? `${pathname}?${query}${suffix}` : `${pathname}${suffix}`;
263
+ }
264
+ function isJsonResponse(response) {
265
+ const contentType = response.headers?.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase();
266
+ return contentType === "application/json" || contentType?.endsWith("+json") === true;
267
+ }
268
+ async function normalizeFetchResponse(response, fallbackValues) {
269
+ if (response.status === 204 || response.status === 205) {
270
+ return {
271
+ ok: true,
272
+ status: response.status,
273
+ data: void 0
274
+ };
275
+ }
276
+ if (isJsonResponse(response)) {
277
+ return await response.json();
278
+ }
279
+ if (response.ok) {
280
+ return {
281
+ ok: true,
282
+ status: response.status,
283
+ data: void 0
284
+ };
285
+ }
286
+ try {
287
+ return await response.json();
288
+ } catch {
289
+ return {
290
+ ok: false,
291
+ status: response.status,
292
+ valid: false,
293
+ values: fallbackValues,
294
+ errors: {}
295
+ };
296
+ }
297
+ }
298
+ async function defaultSubmitter(context) {
299
+ if (typeof fetch !== "function" || !context.action) {
300
+ return {
301
+ ok: true,
302
+ status: 200,
303
+ data: context.values
304
+ };
305
+ }
306
+ const method = context.method.toUpperCase();
307
+ if (method === "GET" || method === "HEAD") {
308
+ const response2 = await fetch(appendQueryString(context.action, context.formData), {
309
+ method
310
+ });
311
+ if (method === "HEAD" || response2.status === 204 || response2.status === 205) {
312
+ return {
313
+ ok: true,
314
+ status: response2.status,
315
+ data: void 0
316
+ };
317
+ }
318
+ return await normalizeFetchResponse(response2, context.values);
319
+ }
320
+ const response = await fetch(context.action, {
321
+ method,
322
+ body: context.formData
323
+ });
324
+ return await normalizeFetchResponse(response, context.values);
325
+ }
326
+ function useForm(schemaDefinition, options = {}) {
327
+ const initialValues = mergeValues(
328
+ normalizeObject(options.initialState?.values),
329
+ options.initialValues
330
+ );
331
+ const state = {
332
+ values: cloneValue(initialValues),
333
+ initialValues: cloneValue(initialValues),
334
+ flattenedErrors: { ...options.initialState?.errors ?? {} },
335
+ touched: /* @__PURE__ */ new Set(),
336
+ dirty: /* @__PURE__ */ new Set(),
337
+ submitting: false,
338
+ lastSubmission: options.initialState,
339
+ listeners: /* @__PURE__ */ new Set()
340
+ };
341
+ const validateOn = options.validateOn ?? "submit";
342
+ const fieldPaths = flattenLeafPaths(schemaDefinition.fields);
343
+ const fields = buildFieldsTree(
344
+ state,
345
+ schemaDefinition,
346
+ schemaDefinition.fields,
347
+ validateOn
348
+ );
349
+ async function runValidation() {
350
+ const submission = await validate(state.values, schemaDefinition);
351
+ state.flattenedErrors = submission.errors.flatten();
352
+ state.lastSubmission = submission.serialize();
353
+ notifyListeners(state);
354
+ return submission;
355
+ }
356
+ return Object.freeze({
357
+ fields,
358
+ get values() {
359
+ return state.values;
360
+ },
361
+ get errors() {
362
+ return createTypedErrorBag(state.flattenedErrors);
363
+ },
364
+ get submitting() {
365
+ return state.submitting;
366
+ },
367
+ get valid() {
368
+ return Object.keys(state.flattenedErrors).length === 0;
369
+ },
370
+ get lastSubmission() {
371
+ return state.lastSubmission;
372
+ },
373
+ subscribe(listener) {
374
+ state.listeners.add(listener);
375
+ return () => {
376
+ state.listeners.delete(listener);
377
+ };
378
+ },
379
+ async validate() {
380
+ return runValidation();
381
+ },
382
+ async validateField(path) {
383
+ const submission = await runValidation();
384
+ return submission.errors.get(path);
385
+ },
386
+ async submit() {
387
+ state.submitting = true;
388
+ notifyListeners(state);
389
+ try {
390
+ const local = await runValidation();
391
+ if (!local.valid) {
392
+ return local;
393
+ }
394
+ const submitter = options.submitter ?? defaultSubmitter;
395
+ const response = await submitter({
396
+ action: options.action,
397
+ method: options.method ?? "POST",
398
+ values: state.values,
399
+ formData: buildFormData(state.values)
400
+ });
401
+ return this.applyServerState(response);
402
+ } finally {
403
+ state.submitting = false;
404
+ notifyListeners(state);
405
+ }
406
+ },
407
+ reset(values) {
408
+ const next = mergeValues(state.initialValues, values);
409
+ state.values = cloneValue(next);
410
+ state.initialValues = cloneValue(next);
411
+ state.flattenedErrors = {};
412
+ state.touched.clear();
413
+ state.dirty.clear();
414
+ state.lastSubmission = void 0;
415
+ notifyListeners(state);
416
+ },
417
+ async setValue(path, value) {
418
+ setValueAtPath(state.values, path, value);
419
+ state.touched.add(path);
420
+ if (!areEqual(getValueAtPath(state.initialValues, path), value)) {
421
+ state.dirty.add(path);
422
+ } else {
423
+ state.dirty.delete(path);
424
+ }
425
+ if (validateOn === "change" && fieldPaths.includes(path)) {
426
+ const submission = await runValidation();
427
+ state.flattenedErrors = submission.errors.flatten();
428
+ return;
429
+ }
430
+ notifyListeners(state);
431
+ },
432
+ applyServerState(result) {
433
+ const normalized = normalizeSubmissionLike(
434
+ schemaDefinition,
435
+ state.values,
436
+ result
437
+ );
438
+ if ("ok" in normalized && normalized.ok === true) {
439
+ state.lastSubmission = normalized;
440
+ state.flattenedErrors = {};
441
+ notifyListeners(state);
442
+ return normalized;
443
+ }
444
+ if ("ok" in normalized && normalized.ok === false) {
445
+ state.values = mergeValues(state.values, normalized.values);
446
+ state.flattenedErrors = normalized.errors;
447
+ state.lastSubmission = {
448
+ valid: false,
449
+ submitted: true,
450
+ values: normalized.values,
451
+ errors: normalized.errors
452
+ };
453
+ notifyListeners(state);
454
+ return normalized;
455
+ }
456
+ const normalizedSubmission = normalized;
457
+ state.values = mergeValues(state.values, normalizedSubmission.values);
458
+ state.flattenedErrors = normalizedSubmission.errors.flatten();
459
+ state.lastSubmission = normalizedSubmission.serialize();
460
+ notifyListeners(state);
461
+ return normalizedSubmission;
462
+ }
463
+ });
464
+ }
465
+ export {
466
+ useForm
467
+ };
@@ -0,0 +1,66 @@
1
+ import { SchemaInputShape, ValidationSchema, InferSchemaData, ValidationErrorBag, FormLikeValidationInput } from '@holo-js/validation';
2
+ export { ErrorTree, ErrorTreeNode, FieldDefinition, FieldRule, InferSchemaData, StandardSchemaV1, StandardSchemaV1Issue, StandardSchemaV1Props, StandardSchemaV1Result, ValidationErrorBag, ValidationFailure, ValidationResult, ValidationSchema, ValidationSuccess, WebFileLike, createErrorBag, defineSchema, field, parse, safeParse } from '@holo-js/validation';
3
+
4
+ interface FormSchema<TShape extends SchemaInputShape = SchemaInputShape> extends ValidationSchema<TShape> {
5
+ readonly mode: 'form';
6
+ readonly $values?: Partial<InferSchemaData<TShape>>;
7
+ }
8
+ interface FormFailurePayload<TData> {
9
+ readonly ok: false;
10
+ readonly status: number;
11
+ readonly valid: false;
12
+ readonly values: Partial<TData>;
13
+ readonly errors: Record<string, readonly string[]>;
14
+ }
15
+ interface FormSuccessPayload<TPayload = undefined> {
16
+ readonly ok: true;
17
+ readonly status: number;
18
+ readonly data: TPayload;
19
+ }
20
+ interface SerializedFormSubmission<TData> {
21
+ readonly valid: boolean;
22
+ readonly submitted: true;
23
+ readonly values: Partial<TData> | TData;
24
+ readonly errors: Record<string, readonly string[]>;
25
+ }
26
+ interface FormSubmissionSuccess<TData> {
27
+ readonly valid: true;
28
+ readonly submitted: true;
29
+ readonly data: TData;
30
+ readonly values: TData;
31
+ readonly errors: ValidationErrorBag<TData>;
32
+ serialize(): SerializedFormSubmission<TData>;
33
+ success(): FormSuccessPayload<undefined>;
34
+ success<TPayload>(payload: TPayload, status?: number): FormSuccessPayload<TPayload>;
35
+ fail(status?: number): FormFailurePayload<TData>;
36
+ }
37
+ interface FormSubmissionFailure<TData> {
38
+ readonly valid: false;
39
+ readonly submitted: true;
40
+ readonly data?: undefined;
41
+ readonly values: Partial<TData>;
42
+ readonly errors: ValidationErrorBag<TData>;
43
+ serialize(): SerializedFormSubmission<TData>;
44
+ success(): FormSuccessPayload<undefined>;
45
+ success<TPayload>(payload: TPayload, status?: number): FormSuccessPayload<TPayload>;
46
+ fail(status?: number): FormFailurePayload<TData>;
47
+ }
48
+ type FormSubmissionResult<TData> = FormSubmissionSuccess<TData> | FormSubmissionFailure<TData>;
49
+ declare class FormContractError extends Error {
50
+ constructor(message: string);
51
+ }
52
+ declare function schema<TShape extends SchemaInputShape>(shapeOrSchema: TShape | ValidationSchema<TShape>): FormSchema<TShape>;
53
+ declare function isFormSchema(value: unknown): value is FormSchema;
54
+ declare function normalizeStatus(value: number | undefined, fallback: number): number;
55
+ declare function serializeSubmissionState<TData>(valid: boolean, values: Partial<TData> | TData, errors: ValidationErrorBag<TData>): SerializedFormSubmission<TData>;
56
+ declare function createSubmission<TData>(valid: boolean, values: Partial<TData> | TData, errors: ValidationErrorBag<TData>): FormSubmissionResult<TData>;
57
+ declare function createSuccessfulSubmission<TShape extends SchemaInputShape, TSchema extends FormSchema<TShape>>(schemaDefinition: TSchema, data: InferSchemaData<TShape>): FormSubmissionSuccess<InferSchemaData<TShape>>;
58
+ declare function createFailedSubmission<TShape extends SchemaInputShape, TSchema extends FormSchema<TShape>>(schemaDefinition: TSchema, values: Partial<InferSchemaData<TShape>>, flattenedErrors: Record<string, readonly string[]>): FormSubmissionFailure<InferSchemaData<TShape>>;
59
+ declare function validate<TShape extends SchemaInputShape, TSchema extends FormSchema<TShape>>(input: FormLikeValidationInput, schemaDefinition: TSchema): Promise<FormSubmissionResult<InferSchemaData<TShape>>>;
60
+ declare const formsInternals: {
61
+ createSubmission: typeof createSubmission;
62
+ normalizeStatus: typeof normalizeStatus;
63
+ serializeSubmissionState: typeof serializeSubmissionState;
64
+ };
65
+
66
+ export { FormContractError, type FormFailurePayload, type FormSchema, type FormSubmissionFailure, type FormSubmissionResult, type FormSubmissionSuccess, type FormSuccessPayload, type SerializedFormSubmission, createFailedSubmission, createSuccessfulSubmission, formsInternals, isFormSchema, schema, validate };
package/dist/index.mjs ADDED
@@ -0,0 +1,32 @@
1
+ import {
2
+ FormContractError,
3
+ createFailedSubmission,
4
+ createSuccessfulSubmission,
5
+ formsInternals,
6
+ isFormSchema,
7
+ schema,
8
+ validate
9
+ } from "./chunk-Z6TEFT7S.mjs";
10
+
11
+ // src/index.ts
12
+ import {
13
+ createErrorBag,
14
+ defineSchema,
15
+ field,
16
+ parse,
17
+ safeParse
18
+ } from "@holo-js/validation";
19
+ export {
20
+ FormContractError,
21
+ createErrorBag,
22
+ createFailedSubmission,
23
+ createSuccessfulSubmission,
24
+ defineSchema,
25
+ field,
26
+ formsInternals,
27
+ isFormSchema,
28
+ parse,
29
+ safeParse,
30
+ schema,
31
+ validate
32
+ };
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@holo-js/forms",
3
+ "version": "0.1.0",
4
+ "description": "Holo-JS Framework - forms-first validation and submission contracts",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.mjs",
11
+ "default": "./dist/index.mjs"
12
+ },
13
+ "./client": {
14
+ "types": "./dist/client.d.ts",
15
+ "import": "./dist/client.mjs",
16
+ "default": "./dist/client.mjs"
17
+ }
18
+ },
19
+ "main": "./dist/index.mjs",
20
+ "types": "./dist/index.d.ts",
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "scripts": {
25
+ "build": "tsup",
26
+ "stub": "tsup",
27
+ "typecheck": "tsc -p tsconfig.json --noEmit",
28
+ "test": "vitest --run"
29
+ },
30
+ "dependencies": {
31
+ "@holo-js/validation": "workspace:*"
32
+ },
33
+ "devDependencies": {
34
+ "@types/node": "catalog:",
35
+ "tsup": "catalog:",
36
+ "typescript": "catalog:",
37
+ "vitest": "catalog:"
38
+ }
39
+ }