@embeddables/forms 0.2.1 → 0.3.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.
package/README.md CHANGED
@@ -87,7 +87,7 @@ export function App() {
87
87
 
88
88
  ### File inputs
89
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">`.
90
+ For `value_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
91
 
92
92
  Example:
93
93
 
@@ -127,7 +127,8 @@ const schema = {
127
127
  {
128
128
  key: 'email',
129
129
  label: 'Email',
130
- type: 'email',
130
+ value_type: 'email',
131
+ field_type: 'user_input',
131
132
  validations: { required: true },
132
133
  },
133
134
  ],
@@ -155,9 +156,20 @@ from `embeddables/_dist` are already typed.
155
156
 
156
157
  ## Schema
157
158
 
158
- Each form is one object: `id`, optional `name`, and `fields`. Supported field
159
- types: `text`, `email`, `number`, `boolean`, `select`, `multiselect`, `json`,
160
- `file`.
159
+ Each form is one object: `id`, optional `name`, and `fields`. Each field has a
160
+ `value_type` (`text`, `email`, `number`, `boolean`, `select`, `multiselect`,
161
+ `json`, `file`).
162
+
163
+ Use `getFieldByKey(fieldKey)` for presentation metadata (`display_text`,
164
+ `display_description`, option `display_text`, `field_type`) instead of
165
+ duplicating copy in markup. `local_only` values still persist but are omitted
166
+ from `data:updated` / `field:updated` analytics patches.
167
+
168
+ ```ts
169
+ const plan = form.getFieldByKey('plan')
170
+ plan?.display_text
171
+ plan?.options?.[0]?.display_text
172
+ ```
161
173
 
162
174
  Declarative rules: `required`, `minLength`, `maxLength`, `min`, `max`,
163
175
  `pattern`, `oneOf`, and optional synchronous `validations.custom`. File fields
@@ -167,8 +179,9 @@ protocol question. `json` fields cannot. `FormFileRef` is exported for typed
167
179
  file values.
168
180
 
169
181
  A `select` or `multiselect` field declares its choices in a field-level `options`
170
- array of `{ value, label?, exclusive? }`, and the field's `type` — not the name of
171
- a rule — decides whether one or many of them may be selected. An option marked
182
+ array of `{ key, display_text, display_description?, exclusive? }`. The field's
183
+ `value_type` — not the name of a rule — decides whether one or many of them may
184
+ be selected. An option marked
172
185
  `exclusive` on a `multiselect` cannot coexist with any other value: selecting it
173
186
  clears the rest, and selecting a regular option afterwards clears it. Those two
174
187
  types no longer accept `validations.oneOf`, which stays available on every other
@@ -190,10 +203,11 @@ on `initForms` / `forms()`.
190
203
  | ------ | ------- |
191
204
  | `set({ … })` | Validate and persist a patch atomically; optional analytics; best-effort durable R2 write when configured |
192
205
  | `get(key)` / `getAll()` | Read declared fields from storage |
206
+ | `getFieldByKey(key)` | Read resolved schema metadata for one declared field |
193
207
  | `getValueByProtocolFieldId(protocolFieldId)` | Read by schema `protocolFieldId` (typed like `get()` for the backing field; `undefined` if unknown or unset) |
194
208
  | `validate({ … })` | Check values without writing or tracking |
195
209
  | `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()`) |
210
+ | `uploadFile({ key, file, fileName? })` | Upload bytes for a `value_type: file` field; returns `FormFileRef` (caller commits with `.set()`) |
197
211
  | `errors()` | Current validation messages |
198
212
  | `clear()` | Remove this form's stored values |
199
213
 
@@ -72,9 +72,9 @@ function validateValue({ field, value, values, pattern, validator }) {
72
72
  const isBlank = isAbsent || value === "" || Array.isArray(value) && value.length === 0;
73
73
  if (rules?.required === true && isBlank) messages.push(`${field.label} is required`);
74
74
  if (isAbsent) return messages;
75
- if (!FIELD_TYPE_PREDICATES[field.type](value)) messages.push(`${field.label} expects ${/^[aeiou]/.test(field.type) ? "an" : "a"} ${field.type} value`);
75
+ if (!FIELD_TYPE_PREDICATES[field.value_type](value)) messages.push(`${field.label} expects ${/^[aeiou]/.test(field.value_type) ? "an" : "a"} ${field.value_type} value`);
76
76
  if (typeof value === "string") {
77
- if (field.type === "email" && value !== "" && !EMAIL_PATTERN.test(value)) messages.push(`${field.label} must be a valid email address`);
77
+ if (field.value_type === "email" && value !== "" && !EMAIL_PATTERN.test(value)) messages.push(`${field.label} must be a valid email address`);
78
78
  if (rules?.minLength !== void 0 && value.length < rules.minLength) messages.push(`${field.label} must be at least ${rules.minLength} characters`);
79
79
  if (rules?.maxLength !== void 0 && value.length > rules.maxLength) messages.push(`${field.label} must be at most ${rules.maxLength} characters`);
80
80
  if (pattern && !pattern.test(value)) messages.push(`${field.label} is not in the expected format`);
@@ -87,12 +87,12 @@ function validateValue({ field, value, values, pattern, validator }) {
87
87
  if (rules?.min !== void 0 && value < rules.min) messages.push(`${field.label} must be at least ${rules.min}`);
88
88
  if (rules?.max !== void 0 && value > rules.max) messages.push(`${field.label} must be at most ${rules.max}`);
89
89
  }
90
- if (field.type === "select" || field.type === "multiselect") {
90
+ if (field.value_type === "select" || field.value_type === "multiselect") {
91
91
  const options = field.options;
92
92
  if (options) {
93
- const allowed = new Set(options.map((option) => option.value));
94
- if (field.type === "select" && typeof value === "string" && !allowed.has(value)) messages.push(`${field.label} must be one of the allowed options`);
95
- if (field.type === "multiselect" && Array.isArray(value)) {
93
+ const allowed = new Set(options.map((option) => option.key));
94
+ if (field.value_type === "select" && typeof value === "string" && !allowed.has(value)) messages.push(`${field.label} must be one of the allowed options`);
95
+ if (field.value_type === "multiselect" && Array.isArray(value)) {
96
96
  if (value.some((entry) => typeof entry !== "string" || !allowed.has(entry))) messages.push(`${field.label} has values that are not allowed options`);
97
97
  }
98
98
  }
@@ -101,7 +101,7 @@ function validateValue({ field, value, values, pattern, validator }) {
101
101
  const encoded = canonicalize(value);
102
102
  if (!rules.oneOf.some((option) => canonicalize(option) === encoded)) messages.push(`${field.label} must be one of the allowed options`);
103
103
  }
104
- if (field.type === "file" && isFormFileRef(value)) {
104
+ if (field.value_type === "file" && isFormFileRef(value)) {
105
105
  if (value.status !== "done") messages.push(`${field.label} upload is not complete`);
106
106
  if (rules?.maxSize !== void 0 && value.size > rules.maxSize) messages.push(`${field.label} must be at most ${rules.maxSize} bytes`);
107
107
  if (rules?.accept !== void 0 && !contentTypeMatchesAccept({
@@ -223,7 +223,7 @@ function writeFields({ storage, formKey, fields, fieldDefinitions }) {
223
223
  if (value === void 0) continue;
224
224
  nextForm[field.key] = {
225
225
  value,
226
- type: field.type,
226
+ type: field.value_type,
227
227
  label: field.label,
228
228
  ...field.registryId === void 0 ? {} : { registryId: field.registryId },
229
229
  ...field.protocolFieldId === void 0 ? {} : { protocolFieldId: field.protocolFieldId }
@@ -305,7 +305,7 @@ function buildCookiePayloadFromBag({ bag, fieldDefinitions }) {
305
305
  if (value === void 0) continue;
306
306
  payload[field.key] = {
307
307
  value,
308
- type: field.type,
308
+ type: field.value_type,
309
309
  label: field.label,
310
310
  ...field.registryId === void 0 ? {} : { registryId: field.registryId },
311
311
  ...field.protocolFieldId === void 0 ? {} : { protocolFieldId: field.protocolFieldId }
@@ -325,10 +325,10 @@ function isStoredField(value) {
325
325
  return true;
326
326
  }
327
327
  function entryMatchesFieldDefinition(entry, field) {
328
- if (entry.type !== field.type || entry.label !== field.label) return false;
328
+ if (entry.type !== field.value_type || entry.label !== field.label) return false;
329
329
  if ((entry.registryId ?? void 0) !== (field.registryId ?? void 0)) return false;
330
330
  if ((entry.protocolFieldId ?? void 0) !== (field.protocolFieldId ?? void 0)) return false;
331
- return FIELD_TYPE_PREDICATES[field.type](entry.value);
331
+ return FIELD_TYPE_PREDICATES[field.value_type](entry.value);
332
332
  }
333
333
  function serializeBrowserCookie(key, value) {
334
334
  const locationRef = globalThis.location;
@@ -589,9 +589,22 @@ async function uploadFormFile(config, input) {
589
589
  const MAX_VALUE_LENGTH = 1024;
590
590
  /** The ingest bound on a `data:updated` entry's `label`. */
591
591
  const MAX_LABEL_LENGTH = 256;
592
- /** Maps a form field's declared type to the analytics `field:updated` class. */
593
- function mapFieldUpdatedType(type) {
594
- return type;
592
+ /** Maps a form field's declared value type to the analytics `field:updated` class. */
593
+ function mapFieldUpdatedType(valueType) {
594
+ return valueType;
595
+ }
596
+ function resolvedFieldClassification(field) {
597
+ return field.field_type ?? "user_input";
598
+ }
599
+ function isAnalyticsExcludedField(field) {
600
+ return resolvedFieldClassification(field) === "local_only";
601
+ }
602
+ function analyticsPatchEntries({ fields, patch }) {
603
+ const byKey = new Map(fields.map((field) => [field.key, field]));
604
+ return Object.entries(patch).filter(([key]) => {
605
+ const field = byKey.get(key);
606
+ return field === void 0 || !isAnalyticsExcludedField(field);
607
+ });
595
608
  }
596
609
  /**
597
610
  * Stringifies values for `data:updated` entries. `field:updated` carries the
@@ -604,9 +617,13 @@ function formatFieldValue({ value }) {
604
617
  /** One event carrying every key in one `.set()` call. */
605
618
  function buildDataUpdatedEvent({ fields, patch, formId }) {
606
619
  const byKey = new Map(fields.map((field) => [field.key, field]));
620
+ const entries = analyticsPatchEntries({
621
+ fields,
622
+ patch
623
+ });
607
624
  return {
608
625
  event_name: "data:updated",
609
- data: Object.fromEntries(Object.entries(patch).map(([key, value]) => {
626
+ data: Object.fromEntries(entries.map(([key, value]) => {
610
627
  const field = byKey.get(key);
611
628
  return [key, {
612
629
  value: formatFieldValue({
@@ -622,12 +639,15 @@ function buildDataUpdatedEvent({ fields, patch, formId }) {
622
639
  /** One `field:updated` per changed key, emitted alongside `data:updated`. */
623
640
  function buildFieldUpdatedEvents({ fields, patch, formId }) {
624
641
  const byKey = new Map(fields.map((field) => [field.key, field]));
625
- return Object.entries(patch).map(([key, value]) => {
642
+ return analyticsPatchEntries({
643
+ fields,
644
+ patch
645
+ }).map(([key, value]) => {
626
646
  const field = byKey.get(key);
627
647
  const event = {
628
648
  event_name: "field:updated",
629
649
  field_key: key,
630
- field_type: mapFieldUpdatedType(field?.type ?? "text"),
650
+ field_type: mapFieldUpdatedType(field?.value_type ?? "text"),
631
651
  form_id: formId
632
652
  };
633
653
  if (value !== void 0) event.field_value = value;
@@ -645,7 +665,7 @@ function buildFieldUpdatedEvents({ fields, patch, formId }) {
645
665
  * values passes through.
646
666
  */
647
667
  function normalizeExclusiveSelection({ next, previous, options }) {
648
- const exclusive = new Set(options.filter((option) => option.exclusive === true).map((option) => option.value));
668
+ const exclusive = new Set(options.filter((option) => option.exclusive === true).map((option) => option.key));
649
669
  if (exclusive.size === 0) return next;
650
670
  const previousValues = new Set(previous);
651
671
  const added = next.filter((entry) => !previousValues.has(entry));
@@ -686,9 +706,17 @@ const NUMERIC_RULES = [
686
706
  "maxSize"
687
707
  ];
688
708
  const FILE_ONLY_RULES = ["accept", "maxSize"];
689
- const OPTION_KEYS = [
690
- "value",
691
- "label",
709
+ const FIELD_CLASSIFICATIONS = [
710
+ "user_input",
711
+ "computed",
712
+ "local_only"
713
+ ];
714
+ const SNAKE_CASE_FIELD_KEY = /^[a-z][a-z0-9_]*$/;
715
+ /** Keys allowed on a raw option object before normalization. */
716
+ const OPTION_INPUT_KEYS = [
717
+ "key",
718
+ "display_text",
719
+ "display_description",
692
720
  "exclusive"
693
721
  ];
694
722
  /** The two field types whose choices are declared through a field-level `options`. */
@@ -724,78 +752,141 @@ function validateAndCompile({ schema }) {
724
752
  if (fields.length === 0) throw new SchemaError("schema.fields: must declare at least one field");
725
753
  const patterns = /* @__PURE__ */ new Map();
726
754
  const seenKeys = /* @__PURE__ */ new Set();
755
+ const compiledFields = [];
727
756
  fields.forEach((field, index) => {
728
- validateField({
757
+ compiledFields.push(compileField({
729
758
  field,
730
759
  path: `schema.fields[${index}]`,
731
760
  seenKeys,
732
761
  patterns
733
- });
762
+ }));
734
763
  });
735
764
  return {
736
765
  formKey,
737
- fields: [...fields],
766
+ fields: compiledFields,
738
767
  patterns
739
768
  };
740
769
  }
741
- function validateField({ field, path, seenKeys, patterns }) {
770
+ function compileField({ field, path, seenKeys, patterns }) {
742
771
  if (!isObjectLike(field)) throw new SchemaError(`${path}: must be an object`);
743
772
  const key = field["key"];
744
773
  if (typeof key !== "string" || key.trim() === "") throw new SchemaError(`${path}.key: must be a non-empty string`);
774
+ if (!SNAKE_CASE_FIELD_KEY.test(key)) throw new SchemaError(`${path}.key: must be lowercase snake_case`);
745
775
  if (key.length > MAX_FIELD_KEY_LENGTH) throw new SchemaError(`${path}.key: must be at most ${MAX_FIELD_KEY_LENGTH} characters (received ${key.length})`);
746
776
  if (seenKeys.has(key)) throw new SchemaError(`${path}.key: duplicate field key "${key}" in this form`);
747
777
  seenKeys.add(key);
748
778
  const label = field["label"];
749
779
  if (typeof label !== "string" || label.trim() === "") throw new SchemaError(`${path}.label: must be a non-empty string`);
750
- const type = field["type"];
751
- if (typeof type !== "string" || !FIELD_TYPES.includes(type)) throw new SchemaError(`${path}.type: must be one of ${FIELD_TYPES.join(", ")}`);
780
+ if (field["type"] !== void 0) throw new SchemaError(`${path}.type: is not supported; use value_type`);
781
+ const valueTypeRaw = field["value_type"];
782
+ if (typeof valueTypeRaw !== "string" || valueTypeRaw.trim() === "") throw new SchemaError(`${path}.value_type: must be a non-empty string`);
783
+ const valueType = valueTypeRaw;
784
+ if (!FIELD_TYPES.includes(valueType)) throw new SchemaError(`${path}.value_type: must be one of ${FIELD_TYPES.join(", ")}`);
785
+ const displayText = field["display_text"];
786
+ if (displayText !== void 0) {
787
+ if (typeof displayText !== "string" || displayText.trim() === "") throw new SchemaError(`${path}.display_text: must be a non-empty string`);
788
+ }
789
+ const displayDescription = field["display_description"];
790
+ if (displayDescription !== void 0) {
791
+ if (typeof displayDescription !== "string" || displayDescription.trim() === "") throw new SchemaError(`${path}.display_description: must be a non-empty string`);
792
+ }
793
+ const fieldTypeRaw = field["field_type"];
794
+ let fieldType = "user_input";
795
+ if (fieldTypeRaw !== void 0) {
796
+ if (typeof fieldTypeRaw !== "string" || !FIELD_CLASSIFICATIONS.includes(fieldTypeRaw)) throw new SchemaError(`${path}.field_type: must be one of ${FIELD_CLASSIFICATIONS.join(", ")}`);
797
+ fieldType = fieldTypeRaw;
798
+ }
752
799
  const registryId = field["registryId"];
753
800
  if (registryId !== void 0 && typeof registryId !== "string") throw new SchemaError(`${path}.registryId: must be a string`);
754
801
  const protocolFieldId = field["protocolFieldId"];
755
802
  if (protocolFieldId !== void 0 && typeof protocolFieldId !== "string") throw new SchemaError(`${path}.protocolFieldId: must be a string`);
756
803
  const includeInCookies = field["includeInCookies"];
757
804
  if (includeInCookies !== void 0 && typeof includeInCookies !== "boolean") throw new SchemaError(`${path}.includeInCookies: must be a boolean`);
758
- validateOptions({
805
+ const normalizedOptions = normalizeOptions({
759
806
  field,
760
- type,
807
+ valueType,
761
808
  path
762
809
  });
763
810
  const validations = field["validations"];
764
- if (validations === void 0) return;
765
- validateValidations({
766
- validations,
767
- type,
768
- path: `${path}.validations`
769
- });
770
- if (!isObjectLike(validations)) return;
771
- const pattern = validations["pattern"];
772
- if (typeof pattern !== "string") return;
773
- patterns.set(key, compilePattern({
774
- pattern,
775
- path: `${path}.validations.pattern`
776
- }));
811
+ if (validations !== void 0) {
812
+ validateValidations({
813
+ validations,
814
+ type: valueType,
815
+ path: `${path}.validations`
816
+ });
817
+ if (isObjectLike(validations)) {
818
+ const pattern = validations["pattern"];
819
+ if (typeof pattern === "string") patterns.set(key, compilePattern({
820
+ pattern,
821
+ path: `${path}.validations.pattern`
822
+ }));
823
+ }
824
+ }
825
+ const compiled = {
826
+ ...field,
827
+ key,
828
+ label,
829
+ value_type: valueType
830
+ };
831
+ delete compiled["type"];
832
+ compiled["field_type"] = fieldType;
833
+ if (displayText !== void 0) compiled["display_text"] = displayText;
834
+ if (displayDescription !== void 0) compiled["display_description"] = displayDescription;
835
+ if (normalizedOptions !== void 0) compiled["options"] = normalizedOptions;
836
+ if (validations !== void 0) compiled["validations"] = cloneValidations(validations);
837
+ return compiled;
838
+ }
839
+ function cloneValidations(validations) {
840
+ if (!isObjectLike(validations)) return validations;
841
+ const cloned = {};
842
+ for (const [ruleKey, ruleValue] of Object.entries(validations)) {
843
+ if (ruleKey === "custom") {
844
+ cloned[ruleKey] = ruleValue;
845
+ continue;
846
+ }
847
+ if (ruleKey === "oneOf" || ruleKey === "accept") {
848
+ cloned[ruleKey] = Array.isArray(ruleValue) ? [...ruleValue] : ruleValue;
849
+ continue;
850
+ }
851
+ cloned[ruleKey] = ruleValue;
852
+ }
853
+ return cloned;
777
854
  }
778
- function validateOptions({ field, type, path }) {
855
+ function normalizeOptions({ field, valueType, path }) {
779
856
  const options = field["options"];
780
- if (options === void 0) return;
781
- if (!OPTION_FIELD_TYPES.includes(type)) throw new SchemaError(`${path}.options: only a select or multiselect field may declare options`);
857
+ if (options === void 0) return void 0;
858
+ if (!OPTION_FIELD_TYPES.includes(valueType)) throw new SchemaError(`${path}.options: only a select or multiselect field may declare options`);
782
859
  if (!(Array.isArray(options) && options.length > 0)) throw new SchemaError(`${path}.options: must be a non-empty array`);
783
860
  const entries = options;
784
- const seenValues = /* @__PURE__ */ new Set();
861
+ const seenKeys = /* @__PURE__ */ new Set();
862
+ const normalized = [];
785
863
  entries.forEach((option, index) => {
786
864
  const at = `${path}.options[${index}]`;
787
865
  if (!isObjectLike(option)) throw new SchemaError(`${at}: must be an object`);
788
- for (const optionKey of Object.keys(option)) if (!OPTION_KEYS.includes(optionKey)) throw new SchemaError(`${at}.${optionKey}: unknown option key; expected one of ${OPTION_KEYS.join(", ")}`);
789
- const value = option["value"];
790
- if (typeof value !== "string" || value.trim() === "") throw new SchemaError(`${at}.value: must be a non-empty string`);
791
- const label = option["label"];
792
- if (label !== void 0 && typeof label !== "string") throw new SchemaError(`${at}.label: must be a string`);
866
+ for (const optionKey of Object.keys(option)) if (!OPTION_INPUT_KEYS.includes(optionKey)) throw new SchemaError(`${at}.${optionKey}: unknown option key; expected one of ${OPTION_INPUT_KEYS.join(", ")}`);
867
+ const optionKey = option["key"];
868
+ const displayTextRaw = option["display_text"];
869
+ const displayDescription = option["display_description"];
870
+ if (typeof optionKey !== "string" || optionKey.trim() === "") throw new SchemaError(`${at}.key: must be a non-empty string`);
871
+ if (typeof displayTextRaw !== "string" || displayTextRaw.trim() === "") throw new SchemaError(`${at}.display_text: must be a non-empty string`);
872
+ const key = optionKey;
873
+ const displayText = displayTextRaw;
874
+ if (displayDescription !== void 0) {
875
+ if (typeof displayDescription !== "string" || displayDescription.trim() === "") throw new SchemaError(`${at}.display_description: must be a non-empty string`);
876
+ }
793
877
  const exclusive = option["exclusive"];
794
878
  if (exclusive !== void 0 && typeof exclusive !== "boolean") throw new SchemaError(`${at}.exclusive: must be a boolean`);
795
- if (exclusive === true && type === "select") throw new SchemaError(`${at}.exclusive: only a multiselect field may declare an exclusive option`);
796
- if (seenValues.has(value)) throw new SchemaError(`${at}.value: duplicate option value "${value}" on this field`);
797
- seenValues.add(value);
879
+ if (exclusive !== void 0 && valueType === "select") throw new SchemaError(`${at}.exclusive: only a multiselect field may declare an exclusive option`);
880
+ if (seenKeys.has(key)) throw new SchemaError(`${at}.key: duplicate option key "${key}" on this field`);
881
+ seenKeys.add(key);
882
+ normalized.push({
883
+ key,
884
+ display_text: displayText,
885
+ ...displayDescription === void 0 ? {} : { display_description: displayDescription },
886
+ ...exclusive === void 0 ? {} : { exclusive }
887
+ });
798
888
  });
889
+ return normalized;
799
890
  }
800
891
  function validateValidations({ validations, type, path }) {
801
892
  if (!isObjectLike(validations)) throw new SchemaError(`${path}: must be an object`);
@@ -1205,7 +1296,7 @@ function createFormInstance({ analyticsInstance, storage, persistence, uploadCon
1205
1296
  const previousBag = readBag();
1206
1297
  for (const [key, value] of Object.entries(changes)) {
1207
1298
  const field = declared.get(key);
1208
- if (!field || field.type !== "multiselect" || !field.options) continue;
1299
+ if (!field || field.value_type !== "multiselect" || !field.options) continue;
1209
1300
  if (!Array.isArray(value) || !value.every((entry) => typeof entry === "string")) continue;
1210
1301
  const stored = previousBag[key];
1211
1302
  changes[key] = [...normalizeExclusiveSelection({
@@ -1291,15 +1382,22 @@ function createFormInstance({ analyticsInstance, storage, persistence, uploadCon
1291
1382
  ok: true,
1292
1383
  errors: noErrors()
1293
1384
  });
1294
- return analyticsInstance.trackEvent([buildDataUpdatedEvent({
1385
+ const dataUpdated = buildDataUpdatedEvent({
1295
1386
  fields: resolved.fields,
1296
1387
  patch: changes,
1297
1388
  formId: schema.id
1298
- }), ...buildFieldUpdatedEvents({
1389
+ });
1390
+ const fieldUpdated = buildFieldUpdatedEvents({
1299
1391
  fields: resolved.fields,
1300
1392
  patch: changes,
1301
1393
  formId: schema.id
1302
- })]).then(() => ({
1394
+ });
1395
+ const analyticsEvents = [...Object.keys(dataUpdated.data).length > 0 ? [dataUpdated] : [], ...fieldUpdated];
1396
+ if (analyticsEvents.length === 0) return Promise.resolve({
1397
+ ok: true,
1398
+ errors: noErrors()
1399
+ });
1400
+ return analyticsInstance.trackEvent(analyticsEvents).then(() => ({
1303
1401
  ok: true,
1304
1402
  errors: noErrors()
1305
1403
  })).catch((error) => ({
@@ -1333,6 +1431,11 @@ function createFormInstance({ analyticsInstance, storage, persistence, uploadCon
1333
1431
  return get(fieldKey);
1334
1432
  });
1335
1433
  const getAll = () => narrow(state.bag);
1434
+ const getFieldByKey = (key) => {
1435
+ const fieldKey = key;
1436
+ if (!declared.has(fieldKey)) return void 0;
1437
+ return declared.get(fieldKey);
1438
+ };
1336
1439
  const submit = () => {
1337
1440
  const snapshot = narrow(readBag());
1338
1441
  const values = snapshot;
@@ -1478,7 +1581,7 @@ function createFormInstance({ analyticsInstance, storage, persistence, uploadCon
1478
1581
  const uploadFile = async ({ key, file, fileName }) => {
1479
1582
  const field = declared.get(key);
1480
1583
  if (!field) throw new FormsError(`Unknown field: ${key}`);
1481
- if (field.type !== "file") throw new FormsError(`Field "${key}" is not a file field`);
1584
+ if (field.value_type !== "file") throw new FormsError(`Field "${key}" is not a file field`);
1482
1585
  if (!uploadConfig) throw new FormsError("File uploads require a valid publishable key and initialized Embeddables core instance.");
1483
1586
  const rules = field.validations;
1484
1587
  const contentType = file.type || "application/octet-stream";
@@ -1500,6 +1603,7 @@ function createFormInstance({ analyticsInstance, storage, persistence, uploadCon
1500
1603
  key: schema.id,
1501
1604
  set,
1502
1605
  get,
1606
+ getFieldByKey,
1503
1607
  getValueByProtocolFieldId,
1504
1608
  getAll,
1505
1609
  submit,
@@ -1513,4 +1617,4 @@ function createFormInstance({ analyticsInstance, storage, persistence, uploadCon
1513
1617
  //#endregion
1514
1618
  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 };
1515
1619
 
1516
- //# sourceMappingURL=form-7wmC3G_q.js.map
1620
+ //# sourceMappingURL=form-CR5xJ_nQ.js.map