@embeddables/forms 0.2.0 → 0.2.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 +38 -3
- package/dist/{form-Bt5pwVP6.js → form-7wmC3G_q.js} +138 -11
- package/dist/form-7wmC3G_q.js.map +1 -0
- package/dist/{form-BOHdxeuO.cjs → form-CdIfHqNc.cjs} +138 -11
- package/dist/form-CdIfHqNc.cjs.map +1 -0
- package/dist/{form-Br23yPDS.d.ts → form-x4EJcR9A.d.ts} +63 -37
- package/dist/form-x4EJcR9A.d.ts.map +1 -0
- package/dist/{index-BB698udY.d.ts → index-CmcgIxTH.d.ts} +13 -13
- package/dist/index-CmcgIxTH.d.ts.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/react.cjs +95 -1
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.ts +31 -3
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +95 -2
- package/dist/react.js.map +1 -1
- package/dist/server.cjs +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.js +1 -1
- package/package.json +1 -1
- package/dist/form-BOHdxeuO.cjs.map +0 -1
- package/dist/form-Br23yPDS.d.ts.map +0 -1
- package/dist/form-Bt5pwVP6.js.map +0 -1
- package/dist/index-BB698udY.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@ Example:
|
|
|
27
27
|
```tsx
|
|
28
28
|
import { EmbeddablesProvider } from '@embeddables/core/react'
|
|
29
29
|
import { analytics } from '@embeddables/analytics/react'
|
|
30
|
-
import { forms, useForm, useFormField } from '@embeddables/forms/react'
|
|
30
|
+
import { forms, useForm, useFormField, useFormFileUpload } from '@embeddables/forms/react'
|
|
31
31
|
import { config } from './embeddables/_dist/config.ts'
|
|
32
32
|
|
|
33
33
|
function SignupField() {
|
|
@@ -85,6 +85,35 @@ export function App() {
|
|
|
85
85
|
- One live `FormInstance` per `schema.id` is shared across hooks in the same client.
|
|
86
86
|
- Imperative consumers can also call `form.subscribe(listener)` to observe value and error changes.
|
|
87
87
|
|
|
88
|
+
### File inputs
|
|
89
|
+
|
|
90
|
+
For `type: file` fields, use `useFormFileUpload`. It uploads through the publishable-key API, commits the returned `FormFileRef`, and exposes `inputProps` for a native `<input type="file">`.
|
|
91
|
+
|
|
92
|
+
Example:
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
function IdPhotoField() {
|
|
96
|
+
const { form } = useForm({ formId: 'intake' })
|
|
97
|
+
const { value, error, isLoading, uploadError, inputProps } = useFormFileUpload({
|
|
98
|
+
form,
|
|
99
|
+
key: 'id_photo',
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<label>
|
|
104
|
+
ID photo
|
|
105
|
+
<input {...inputProps} accept="image/jpeg,image/png" />
|
|
106
|
+
{isLoading && <span>Uploading…</span>}
|
|
107
|
+
{uploadError && <span>{uploadError}</span>}
|
|
108
|
+
{error?.[0]}
|
|
109
|
+
{value?.name && <span>{value.name}</span>}
|
|
110
|
+
</label>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Imperative equivalent: `const ref = await form.uploadFile({ key: 'id_photo', file })` then `await form.set({ id_photo: ref })`.
|
|
116
|
+
|
|
88
117
|
## Quick start
|
|
89
118
|
|
|
90
119
|
```typescript
|
|
@@ -127,10 +156,15 @@ from `embeddables/_dist` are already typed.
|
|
|
127
156
|
## Schema
|
|
128
157
|
|
|
129
158
|
Each form is one object: `id`, optional `name`, and `fields`. Supported field
|
|
130
|
-
types: `text`, `email`, `number`, `boolean`, `select`, `multiselect`, `json
|
|
159
|
+
types: `text`, `email`, `number`, `boolean`, `select`, `multiselect`, `json`,
|
|
160
|
+
`file`.
|
|
131
161
|
|
|
132
162
|
Declarative rules: `required`, `minLength`, `maxLength`, `min`, `max`,
|
|
133
|
-
`pattern`, `oneOf`, and optional synchronous `validations.custom`.
|
|
163
|
+
`pattern`, `oneOf`, and optional synchronous `validations.custom`. File fields
|
|
164
|
+
also accept `validations.accept` (MIME list) and `validations.maxSize` (bytes).
|
|
165
|
+
Compatible field types may declare `protocolFieldId` to link a form field to a
|
|
166
|
+
protocol question. `json` fields cannot. `FormFileRef` is exported for typed
|
|
167
|
+
file values.
|
|
134
168
|
|
|
135
169
|
A `select` or `multiselect` field declares its choices in a field-level `options`
|
|
136
170
|
array of `{ value, label?, exclusive? }`, and the field's `type` — not the name of
|
|
@@ -159,6 +193,7 @@ on `initForms` / `forms()`.
|
|
|
159
193
|
| `getValueByProtocolFieldId(protocolFieldId)` | Read by schema `protocolFieldId` (typed like `get()` for the backing field; `undefined` if unknown or unset) |
|
|
160
194
|
| `validate({ … })` | Check values without writing or tracking |
|
|
161
195
|
| `submit()` | Validate all fields, best-effort durable R2 write, and emit `form:submitted` when analytics is configured |
|
|
196
|
+
| `uploadFile({ key, file, fileName? })` | Upload bytes for a `type: file` field; returns `FormFileRef` (caller commits with `.set()`) |
|
|
162
197
|
| `errors()` | Current validation messages |
|
|
163
198
|
| `clear()` | Remove this form's stored values |
|
|
164
199
|
|
|
@@ -32,6 +32,21 @@ var ValidatorError = class extends FormsError {
|
|
|
32
32
|
}
|
|
33
33
|
};
|
|
34
34
|
//#endregion
|
|
35
|
+
//#region src/types/form-file.ts
|
|
36
|
+
function contentTypeMatchesAccept({ contentType, accept }) {
|
|
37
|
+
const normalized = contentType.split(";", 1)[0]?.trim().toLowerCase() || "";
|
|
38
|
+
return accept.some((entry) => {
|
|
39
|
+
const pattern = entry.trim().toLowerCase();
|
|
40
|
+
if (pattern.endsWith("/*")) return normalized.startsWith(pattern.slice(0, -1));
|
|
41
|
+
return normalized === pattern;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function isFormFileRef(value) {
|
|
45
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
46
|
+
const record = value;
|
|
47
|
+
return typeof record.file_id === "string" && typeof record.name === "string" && typeof record.content_type === "string" && typeof record.size === "number" && Number.isFinite(record.size) && (record.status === "uploading" || record.status === "done" || record.status === "error");
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
35
50
|
//#region src/core/validation.ts
|
|
36
51
|
/** Runtime counterpart to `FieldType`. Exhaustive by construction. */
|
|
37
52
|
const FIELD_TYPE_PREDICATES = {
|
|
@@ -41,7 +56,8 @@ const FIELD_TYPE_PREDICATES = {
|
|
|
41
56
|
boolean: (value) => typeof value === "boolean",
|
|
42
57
|
select: (value) => typeof value === "string",
|
|
43
58
|
multiselect: (value) => Array.isArray(value),
|
|
44
|
-
json: () => true
|
|
59
|
+
json: () => true,
|
|
60
|
+
file: (value) => value === null || isFormFileRef(value)
|
|
45
61
|
};
|
|
46
62
|
const EMAIL_PATTERN = /^[\w.!#$%&'*+/=?^`{|}~-]+@[a-zA-Z\d](?:[a-zA-Z\d-]{0,61}[a-zA-Z\d])?(?:\.[a-zA-Z\d](?:[a-zA-Z\d-]{0,61}[a-zA-Z\d])?)*$/;
|
|
47
63
|
/**
|
|
@@ -85,6 +101,14 @@ function validateValue({ field, value, values, pattern, validator }) {
|
|
|
85
101
|
const encoded = canonicalize(value);
|
|
86
102
|
if (!rules.oneOf.some((option) => canonicalize(option) === encoded)) messages.push(`${field.label} must be one of the allowed options`);
|
|
87
103
|
}
|
|
104
|
+
if (field.type === "file" && isFormFileRef(value)) {
|
|
105
|
+
if (value.status !== "done") messages.push(`${field.label} upload is not complete`);
|
|
106
|
+
if (rules?.maxSize !== void 0 && value.size > rules.maxSize) messages.push(`${field.label} must be at most ${rules.maxSize} bytes`);
|
|
107
|
+
if (rules?.accept !== void 0 && !contentTypeMatchesAccept({
|
|
108
|
+
contentType: value.content_type,
|
|
109
|
+
accept: rules.accept
|
|
110
|
+
})) messages.push(`${field.label} must be one of the allowed file types`);
|
|
111
|
+
}
|
|
88
112
|
if (!validator || messages.length > 0) return messages;
|
|
89
113
|
return normalizeValidatorResult({
|
|
90
114
|
result: validator({
|
|
@@ -127,7 +151,8 @@ const FIELD_TYPES$2 = [
|
|
|
127
151
|
"boolean",
|
|
128
152
|
"select",
|
|
129
153
|
"multiselect",
|
|
130
|
-
"json"
|
|
154
|
+
"json",
|
|
155
|
+
"file"
|
|
131
156
|
];
|
|
132
157
|
const LIVE_DOCUMENTS = /* @__PURE__ */ new WeakMap();
|
|
133
158
|
function readDocumentFromStorage({ storage }) {
|
|
@@ -436,7 +461,7 @@ function createNoopPersistence() {
|
|
|
436
461
|
}
|
|
437
462
|
//#endregion
|
|
438
463
|
//#region src/storage/persistence-client.ts
|
|
439
|
-
const PUBLISHABLE_KEY_HEADER = "x-publishable-key";
|
|
464
|
+
const PUBLISHABLE_KEY_HEADER$1 = "x-publishable-key";
|
|
440
465
|
const hcWithType = (...args) => hc(...args);
|
|
441
466
|
function withTimeout(fetchImpl, timeoutMs) {
|
|
442
467
|
return async (input, init) => {
|
|
@@ -469,7 +494,7 @@ function createApiPersistence(config) {
|
|
|
469
494
|
const root = config.baseUrl.replace(/\/+$/, "");
|
|
470
495
|
const rpc = hcWithType(`${root}/forms`, {
|
|
471
496
|
fetch: withTimeout(config.fetch, config.timeoutMs),
|
|
472
|
-
headers: { [PUBLISHABLE_KEY_HEADER]: config.publishableKey }
|
|
497
|
+
headers: { [PUBLISHABLE_KEY_HEADER$1]: config.publishableKey }
|
|
473
498
|
});
|
|
474
499
|
return {
|
|
475
500
|
savePartial({ formKey, values }) {
|
|
@@ -498,6 +523,67 @@ function resolveDefaultPersistence(config) {
|
|
|
498
523
|
return createApiPersistence(resolved);
|
|
499
524
|
}
|
|
500
525
|
//#endregion
|
|
526
|
+
//#region src/storage/upload-client.ts
|
|
527
|
+
const PUBLISHABLE_KEY_HEADER = "x-publishable-key";
|
|
528
|
+
function mapUploadErrorMessage(problem, status) {
|
|
529
|
+
const code = problem?.code;
|
|
530
|
+
if (code === "validation.file_too_large") return problem?.detail ?? "Maximum upload size is 25 MiB.";
|
|
531
|
+
if (code === "validation.unsupported_content_type") return problem?.detail ?? "Unsupported file type.";
|
|
532
|
+
if (code === "validation.upload_failed") return problem?.detail ?? "Upload could not be completed.";
|
|
533
|
+
if (code === "service.form_uploads_not_configured") return problem?.detail ?? "File uploads are not configured for this project.";
|
|
534
|
+
if (code === "validation.failed") return problem?.detail ?? "Upload metadata is invalid.";
|
|
535
|
+
return problem?.detail ?? problem?.title ?? `File upload failed (${status}).`;
|
|
536
|
+
}
|
|
537
|
+
function uploadResultToFormFileRef(result) {
|
|
538
|
+
return {
|
|
539
|
+
file_id: result.file_id,
|
|
540
|
+
name: result.name,
|
|
541
|
+
content_type: result.content_type,
|
|
542
|
+
size: result.size,
|
|
543
|
+
status: "done",
|
|
544
|
+
uploaded_at: result.uploaded_at
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
function toUploadFetchError(error, path, timeoutMs) {
|
|
548
|
+
if (error instanceof FormsError) return error;
|
|
549
|
+
if (error instanceof Error && error.name === "AbortError") return new FormsError(`Request to ${path} timed out after ${timeoutMs}ms`, { cause: error });
|
|
550
|
+
return new FormsError(`Request to ${path} failed to reach the API`, { cause: error });
|
|
551
|
+
}
|
|
552
|
+
function toUploadDecodeError(error, path) {
|
|
553
|
+
return new FormsError(`Upload response from ${path} could not be decoded`, { cause: error });
|
|
554
|
+
}
|
|
555
|
+
async function uploadFormFile(config, input) {
|
|
556
|
+
const url = `${config.baseUrl.replace(/\/+$/, "")}/forms/v1/public/uploads`;
|
|
557
|
+
const formData = new FormData();
|
|
558
|
+
formData.append("project_id", config.projectId);
|
|
559
|
+
formData.append("app_user_id", config.appUserId);
|
|
560
|
+
formData.append("form_id", input.formId);
|
|
561
|
+
formData.append("field_key", input.fieldKey);
|
|
562
|
+
const fileName = input.fileName ?? (typeof File !== "undefined" && input.file instanceof File ? input.file.name : "upload");
|
|
563
|
+
formData.append("file", input.file, fileName);
|
|
564
|
+
const controller = new AbortController();
|
|
565
|
+
const timer = setTimeout(() => controller.abort(), config.timeoutMs);
|
|
566
|
+
try {
|
|
567
|
+
const res = await config.fetch(url, {
|
|
568
|
+
method: "POST",
|
|
569
|
+
headers: { [PUBLISHABLE_KEY_HEADER]: config.publishableKey },
|
|
570
|
+
body: formData,
|
|
571
|
+
signal: controller.signal
|
|
572
|
+
});
|
|
573
|
+
clearTimeout(timer);
|
|
574
|
+
if (!res.ok) throw new FormsError(mapUploadErrorMessage(await res.json().catch(() => null), res.status));
|
|
575
|
+
try {
|
|
576
|
+
return uploadResultToFormFileRef(await res.json());
|
|
577
|
+
} catch (error) {
|
|
578
|
+
throw toUploadDecodeError(error, url);
|
|
579
|
+
}
|
|
580
|
+
} catch (error) {
|
|
581
|
+
throw toUploadFetchError(error, url, config.timeoutMs);
|
|
582
|
+
} finally {
|
|
583
|
+
clearTimeout(timer);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
//#endregion
|
|
501
587
|
//#region src/core/analytics.ts
|
|
502
588
|
/** The ingest bound on a `data:updated` entry's `value`. */
|
|
503
589
|
const MAX_VALUE_LENGTH = 1024;
|
|
@@ -577,7 +663,8 @@ const FIELD_TYPES = [
|
|
|
577
663
|
"boolean",
|
|
578
664
|
"select",
|
|
579
665
|
"multiselect",
|
|
580
|
-
"json"
|
|
666
|
+
"json",
|
|
667
|
+
"file"
|
|
581
668
|
];
|
|
582
669
|
const VALIDATION_RULES = [
|
|
583
670
|
"required",
|
|
@@ -587,14 +674,18 @@ const VALIDATION_RULES = [
|
|
|
587
674
|
"max",
|
|
588
675
|
"pattern",
|
|
589
676
|
"oneOf",
|
|
677
|
+
"accept",
|
|
678
|
+
"maxSize",
|
|
590
679
|
"custom"
|
|
591
680
|
];
|
|
592
681
|
const NUMERIC_RULES = [
|
|
593
682
|
"minLength",
|
|
594
683
|
"maxLength",
|
|
595
684
|
"min",
|
|
596
|
-
"max"
|
|
685
|
+
"max",
|
|
686
|
+
"maxSize"
|
|
597
687
|
];
|
|
688
|
+
const FILE_ONLY_RULES = ["accept", "maxSize"];
|
|
598
689
|
const OPTION_KEYS = [
|
|
599
690
|
"value",
|
|
600
691
|
"label",
|
|
@@ -708,7 +799,10 @@ function validateOptions({ field, type, path }) {
|
|
|
708
799
|
}
|
|
709
800
|
function validateValidations({ validations, type, path }) {
|
|
710
801
|
if (!isObjectLike(validations)) throw new SchemaError(`${path}: must be an object`);
|
|
711
|
-
for (const rule of Object.keys(validations))
|
|
802
|
+
for (const rule of Object.keys(validations)) {
|
|
803
|
+
if (!VALIDATION_RULES.includes(rule)) throw new SchemaError(`${path}.${rule}: unknown validation rule; expected one of ${VALIDATION_RULES.join(", ")}`);
|
|
804
|
+
if (type !== "file" && FILE_ONLY_RULES.includes(rule)) throw new SchemaError(`${path}.${rule}: is only valid for file fields`);
|
|
805
|
+
}
|
|
712
806
|
const required = validations["required"];
|
|
713
807
|
if (required !== void 0 && typeof required !== "boolean") throw new SchemaError(`${path}.required: must be a boolean`);
|
|
714
808
|
for (const rule of NUMERIC_RULES) {
|
|
@@ -724,6 +818,10 @@ function validateValidations({ validations, type, path }) {
|
|
|
724
818
|
const oneOf = validations["oneOf"];
|
|
725
819
|
if (oneOf !== void 0 && OPTION_FIELD_TYPES.includes(type)) throw new SchemaError(`${path}.oneOf: not allowed on a ${type} field; declare choices through the field's options instead`);
|
|
726
820
|
if (oneOf !== void 0 && !(Array.isArray(oneOf) && oneOf.length > 0)) throw new SchemaError(`${path}.oneOf: must be a non-empty array`);
|
|
821
|
+
const accept = validations["accept"];
|
|
822
|
+
if (accept !== void 0 && !(Array.isArray(accept) && accept.length > 0 && accept.every((entry) => typeof entry === "string" && entry.trim() !== ""))) throw new SchemaError(`${path}.accept: must be a non-empty array of strings`);
|
|
823
|
+
const maxSize = validations["maxSize"];
|
|
824
|
+
if (maxSize !== void 0 && !(typeof maxSize === "number" && Number.isFinite(maxSize) && maxSize > 0)) throw new SchemaError(`${path}.maxSize: must be a positive finite number`);
|
|
727
825
|
const pattern = validations["pattern"];
|
|
728
826
|
if (pattern !== void 0 && typeof pattern !== "string") throw new SchemaError(`${path}.pattern: must be a string`);
|
|
729
827
|
const custom = validations["custom"];
|
|
@@ -957,16 +1055,22 @@ function resolveInitConfig({ core, customValidations, serverFormData }) {
|
|
|
957
1055
|
}
|
|
958
1056
|
return registry;
|
|
959
1057
|
}
|
|
960
|
-
function createFormsClient({ core, customValidations, serverFormData, analyticsInstance, baseUrl, storage, persistence }) {
|
|
1058
|
+
function createFormsClient({ core, customValidations, serverFormData, analyticsInstance, baseUrl, fetch: fetchImpl, storage, persistence }) {
|
|
961
1059
|
if (!hasRequiredCoreMethods(core)) throw new FormsError("initForms requires an initialized Embeddables core instance.");
|
|
962
1060
|
const registry = resolveInitConfig({
|
|
963
1061
|
core,
|
|
964
1062
|
customValidations,
|
|
965
1063
|
serverFormData
|
|
966
1064
|
});
|
|
1065
|
+
const uploadConfig = resolvePersistenceConfig({
|
|
1066
|
+
core,
|
|
1067
|
+
baseUrl,
|
|
1068
|
+
fetch: fetchImpl
|
|
1069
|
+
});
|
|
967
1070
|
const resolvedPersistence = persistence ?? resolveDefaultPersistence({
|
|
968
1071
|
core,
|
|
969
|
-
baseUrl
|
|
1072
|
+
baseUrl,
|
|
1073
|
+
fetch: fetchImpl
|
|
970
1074
|
});
|
|
971
1075
|
const instances = /* @__PURE__ */ new Map();
|
|
972
1076
|
return { getForm({ formId }) {
|
|
@@ -978,6 +1082,7 @@ function createFormsClient({ core, customValidations, serverFormData, analyticsI
|
|
|
978
1082
|
analyticsInstance,
|
|
979
1083
|
storage,
|
|
980
1084
|
persistence: resolvedPersistence,
|
|
1085
|
+
uploadConfig,
|
|
981
1086
|
schema: entry.schema,
|
|
982
1087
|
customValidations: entry.customValidations,
|
|
983
1088
|
serverFormData: entry.serverFormData,
|
|
@@ -994,7 +1099,7 @@ function mergeInitialBag({ fromStorage, serverFormData }) {
|
|
|
994
1099
|
...serverFormData
|
|
995
1100
|
};
|
|
996
1101
|
}
|
|
997
|
-
function createFormInstance({ analyticsInstance, storage, persistence, schema, customValidations, serverFormData, projectId }) {
|
|
1102
|
+
function createFormInstance({ analyticsInstance, storage, persistence, uploadConfig, schema, customValidations, serverFormData, projectId }) {
|
|
998
1103
|
const resolved = resolveForm({ schema: mergeCustomValidations({
|
|
999
1104
|
schema,
|
|
1000
1105
|
customValidations
|
|
@@ -1370,6 +1475,27 @@ function createFormInstance({ analyticsInstance, storage, persistence, schema, c
|
|
|
1370
1475
|
listeners.delete(listener);
|
|
1371
1476
|
};
|
|
1372
1477
|
};
|
|
1478
|
+
const uploadFile = async ({ key, file, fileName }) => {
|
|
1479
|
+
const field = declared.get(key);
|
|
1480
|
+
if (!field) throw new FormsError(`Unknown field: ${key}`);
|
|
1481
|
+
if (field.type !== "file") throw new FormsError(`Field "${key}" is not a file field`);
|
|
1482
|
+
if (!uploadConfig) throw new FormsError("File uploads require a valid publishable key and initialized Embeddables core instance.");
|
|
1483
|
+
const rules = field.validations;
|
|
1484
|
+
const contentType = file.type || "application/octet-stream";
|
|
1485
|
+
const byteLength = file.size;
|
|
1486
|
+
if (byteLength > 26214400) throw new FormsError("Maximum upload size is 25 MiB.");
|
|
1487
|
+
if (rules?.maxSize !== void 0 && byteLength > rules.maxSize) throw new FormsError(`${field.label} must be at most ${rules.maxSize} bytes`);
|
|
1488
|
+
if (rules?.accept !== void 0 && !contentTypeMatchesAccept({
|
|
1489
|
+
contentType,
|
|
1490
|
+
accept: rules.accept
|
|
1491
|
+
})) throw new FormsError(`${field.label} must be one of the allowed file types`);
|
|
1492
|
+
return uploadFormFile(uploadConfig, {
|
|
1493
|
+
formId: resolved.formKey,
|
|
1494
|
+
fieldKey: key,
|
|
1495
|
+
file,
|
|
1496
|
+
fileName
|
|
1497
|
+
});
|
|
1498
|
+
};
|
|
1373
1499
|
return {
|
|
1374
1500
|
key: schema.id,
|
|
1375
1501
|
set,
|
|
@@ -1380,10 +1506,11 @@ function createFormInstance({ analyticsInstance, storage, persistence, schema, c
|
|
|
1380
1506
|
validate,
|
|
1381
1507
|
errors: () => freeze(state.errors),
|
|
1382
1508
|
clear,
|
|
1509
|
+
uploadFile,
|
|
1383
1510
|
subscribe
|
|
1384
1511
|
};
|
|
1385
1512
|
}
|
|
1386
1513
|
//#endregion
|
|
1387
1514
|
export { FORM_DATA_KEY as a, SchemaError as c, seedServerFormsStorageFromCookies as i, ValidatorError as l, initForms as n, createMemoryFormsStorage as o, createNoopPersistence as r, FormsError as s, createFormsClient as t };
|
|
1388
1515
|
|
|
1389
|
-
//# sourceMappingURL=form-
|
|
1516
|
+
//# sourceMappingURL=form-7wmC3G_q.js.map
|