@verbb/formie-browser 1.0.2 → 1.0.4

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.
Files changed (94) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/chunks/{address-finder-CQW81kY0.js → address-finder-BU4roxT6.js} +2 -2
  3. package/dist/chunks/{bpoint-D8OjLmsn.js → bpoint-uNg5j6rt.js} +1 -1
  4. package/dist/chunks/{calculations-BqZsENCq.js → calculations-C9t6Zop9.js} +2 -2
  5. package/dist/chunks/{captcha-eu-DIa6aUWz.js → captcha-eu-BAPzHNts.js} +2 -2
  6. package/dist/chunks/{checkbox-radio-BI-UjgMF.js → checkbox-radio-CHeLNVIK.js} +39 -34
  7. package/dist/chunks/{conditions-BVl6jTYo.js → conditions-DKxVCsjD.js} +189 -163
  8. package/dist/chunks/{date-picker-69TgvErA.js → date-picker-B3iQ797Z.js} +2 -2
  9. package/dist/chunks/{eway-1GPxH6DU.js → eway-DCielIML.js} +2 -2
  10. package/dist/chunks/file-upload-DP2TOUYV.js +316 -0
  11. package/dist/chunks/{friendly-captcha-v1-DA2QPKA3.js → friendly-captcha-v1-DdlcGiFY.js} +1 -1
  12. package/dist/chunks/{friendly-captcha-v2-BorOWiz_.js → friendly-captcha-v2-76w-YnFm.js} +1 -1
  13. package/dist/chunks/{go-cardless-DwBgERcA.js → go-cardless-rwCkzbNK.js} +1 -1
  14. package/dist/chunks/{google-address-BcTBncyS.js → google-address-BD8IZ86V.js} +77 -58
  15. package/dist/chunks/{hcaptcha-BchdVCqq.js → hcaptcha-BNNTL6uf.js} +2 -2
  16. package/dist/chunks/{hidden-D8RqSPt7.js → hidden-CW6Rcf1N.js} +1 -1
  17. package/dist/chunks/{index-C1ZOKiAi.js → index-D6npYxvr.js} +1372 -1178
  18. package/dist/chunks/{loqate-nXuTX7In.js → loqate-B-ZNS65B.js} +2 -2
  19. package/dist/chunks/{mollie-qdCynQCd.js → mollie-BDAZ-1X4.js} +1 -1
  20. package/dist/chunks/{moneris-BNEA-laO.js → moneris-B1b0uEtY.js} +1 -1
  21. package/dist/chunks/{opayo-BJtXOkuK.js → opayo-CC5hraHo.js} +2 -2
  22. package/dist/chunks/{paddle-D72Pa8kr.js → paddle-B4GmrDsq.js} +2 -2
  23. package/dist/chunks/{paypal-Cqt2bned.js → paypal-BFS6XsIS.js} +2 -2
  24. package/dist/chunks/{payway-BjCqsc0H.js → payway-DekoJead.js} +2 -2
  25. package/dist/chunks/{phone-country-K9pYAlzM.js → phone-country-Cm5YUeXI.js} +2 -2
  26. package/dist/chunks/{place-kit-Dg6f_UoM.js → place-kit-DizZWV_e.js} +1 -1
  27. package/dist/chunks/{recaptcha-enterprise--VYAeeqz.js → recaptcha-enterprise-BbOvO7JZ.js} +2 -2
  28. package/dist/chunks/{recaptcha-shared-PSIEEEe1.js → recaptcha-shared-Dwg1eVFS.js} +2 -2
  29. package/dist/chunks/{recaptcha-v2-checkbox-atBNe5ml.js → recaptcha-v2-checkbox-SY3ti5iQ.js} +2 -2
  30. package/dist/chunks/{recaptcha-v2-invisible-DnBufNPT.js → recaptcha-v2-invisible-DSmQnn6c.js} +2 -2
  31. package/dist/chunks/{recaptcha-v3-CF6O8rDj.js → recaptcha-v3-DNh7YCnR.js} +2 -2
  32. package/dist/chunks/{repeater-CB7Yojvx.js → repeater-DM0vuFNQ.js} +2 -2
  33. package/dist/chunks/{rich-text-nAV_SQZo.js → rich-text-C8aGYp70.js} +1 -1
  34. package/dist/chunks/{scripts-BkAAux0i.js → scripts-CKTFtxPO.js} +1 -1
  35. package/dist/chunks/{shared-DGqeMvcJ.js → shared--BU5CFoL.js} +4 -4
  36. package/dist/chunks/{signature-BT6CsQ6n.js → signature-pFhZfH7d.js} +1 -1
  37. package/dist/chunks/{snaptcha-C7qQo02f.js → snaptcha-VuOUOaLj.js} +1 -1
  38. package/dist/chunks/{square-C5AH3trb.js → square-BR9pvToy.js} +4 -4
  39. package/dist/chunks/{stripe-EJK0RyUv.js → stripe-OHUwoV9g.js} +2 -2
  40. package/dist/chunks/{summary-DC_ipXCU.js → summary-Da7Ktg3N.js} +2 -2
  41. package/dist/chunks/{table-BEAI99qo.js → table-C0PmR9bI.js} +2 -2
  42. package/dist/chunks/text-limit-mLEwZDIa.js +144 -0
  43. package/dist/chunks/{turnstile-_uCRwnU-.js → turnstile-9tM7q_LK.js} +2 -2
  44. package/dist/css/theme/forms/_form.css +28 -0
  45. package/dist/index.js +9 -9
  46. package/dist/js/core/create-formie-client.d.ts.map +1 -1
  47. package/dist/js/core/field-error-container.d.ts +4 -0
  48. package/dist/js/core/field-error-container.d.ts.map +1 -0
  49. package/dist/js/core/submit-result-state.d.ts.map +1 -1
  50. package/dist/js/core/submit-result-ui.d.ts.map +1 -1
  51. package/dist/js/modules/address/google-address.d.ts +7 -0
  52. package/dist/js/modules/address/google-address.d.ts.map +1 -1
  53. package/dist/js/modules/fields/checkbox-radio.d.ts.map +1 -1
  54. package/dist/js/modules/fields/conditions/effects.d.ts +3 -1
  55. package/dist/js/modules/fields/conditions/effects.d.ts.map +1 -1
  56. package/dist/js/modules/fields/conditions.d.ts.map +1 -1
  57. package/dist/js/modules/fields/file-upload.d.ts.map +1 -1
  58. package/dist/js/modules/fields/text-limit.d.ts.map +1 -1
  59. package/dist/js/submit/pipeline.d.ts.map +1 -1
  60. package/dist/js/utils/i18n.d.ts.map +1 -1
  61. package/dist/js/validation/rules/email.d.ts.map +1 -1
  62. package/dist/js/validation/rules/number.d.ts.map +1 -1
  63. package/dist/js/validation/scope.d.ts +6 -0
  64. package/dist/js/validation/scope.d.ts.map +1 -0
  65. package/dist/js/validation/submit-readiness.d.ts +4 -0
  66. package/dist/js/validation/submit-readiness.d.ts.map +1 -0
  67. package/dist/js/validation/validator.d.ts +1 -0
  68. package/dist/js/validation/validator.d.ts.map +1 -1
  69. package/package.json +2 -2
  70. package/src/css/theme/forms/_form.css +28 -0
  71. package/src/js/core/create-formie-client.ts +6 -0
  72. package/src/js/core/field-error-container.ts +39 -0
  73. package/src/js/core/submit-result-state.ts +29 -2
  74. package/src/js/core/submit-result-ui.ts +3 -9
  75. package/src/js/modules/address/google-address.ts +33 -1
  76. package/src/js/modules/fields/checkbox-radio.ts +21 -15
  77. package/src/js/modules/fields/conditions/effects.ts +79 -2
  78. package/src/js/modules/fields/conditions.ts +53 -11
  79. package/src/js/modules/fields/file-upload.ts +200 -105
  80. package/src/js/modules/fields/text-limit.ts +87 -22
  81. package/src/js/submit/pipeline.ts +3 -28
  82. package/src/js/utils/i18n.ts +184 -5
  83. package/src/js/validation/rules/email.ts +3 -2
  84. package/src/js/validation/rules/match.ts +2 -2
  85. package/src/js/validation/rules/minmax.ts +4 -4
  86. package/src/js/validation/rules/number.ts +9 -5
  87. package/src/js/validation/rules/pattern.ts +4 -4
  88. package/src/js/validation/rules/required.ts +1 -1
  89. package/src/js/validation/rules/url.ts +1 -1
  90. package/src/js/validation/scope.ts +25 -0
  91. package/src/js/validation/submit-readiness.ts +157 -0
  92. package/src/js/validation/validator.ts +11 -7
  93. package/dist/chunks/file-upload-BDZZftgL.js +0 -281
  94. package/dist/chunks/text-limit-Blgq5G-G.js +0 -135
@@ -79,18 +79,197 @@ export function mergeFormieTranslations(translations: Record<string, string>): R
79
79
  return getFormieTranslations();
80
80
  }
81
81
 
82
- export function t(message: string, replacements: TranslationReplacements = {}): string {
83
- let output = getTranslationStore()[message] || message;
82
+ function parsePluralBranches(body: string): Record<string, string> {
83
+ const branches: Record<string, string> = {};
84
+ let index = 0;
85
+
86
+ while (index < body.length) {
87
+ while (index < body.length && /\s/.test(body[index])) {
88
+ index++;
89
+ }
90
+
91
+ if (index >= body.length) {
92
+ break;
93
+ }
94
+
95
+ const branchMatch = body.slice(index).match(/^(\w+|=\d+)\{/);
96
+
97
+ if (!branchMatch) {
98
+ break;
99
+ }
100
+
101
+ const key = branchMatch[1];
102
+ index += branchMatch[0].length;
103
+
104
+ let depth = 1;
105
+ const contentStart = index;
106
+
107
+ while (index < body.length && depth > 0) {
108
+ if (body[index] === '{') {
109
+ depth++;
110
+ } else if (body[index] === '}') {
111
+ depth--;
112
+ }
113
+
114
+ if (depth > 0) {
115
+ index++;
116
+ }
117
+ }
118
+
119
+ branches[key] = body.slice(contentStart, index);
120
+ index++;
121
+ }
122
+
123
+ return branches;
124
+ }
125
+
126
+ function selectPluralBranch(count: number, branches: Record<string, string>): string {
127
+ const exactKey = `=${count}`;
128
+
129
+ if (Object.prototype.hasOwnProperty.call(branches, exactKey)) {
130
+ return branches[exactKey];
131
+ }
132
+
133
+ if (typeof Intl !== 'undefined' && typeof Intl.PluralRules === 'function') {
134
+ const category = new Intl.PluralRules().select(count);
135
+
136
+ if (Object.prototype.hasOwnProperty.call(branches, category)) {
137
+ return branches[category];
138
+ }
139
+ }
140
+
141
+ if (count === 1 && Object.prototype.hasOwnProperty.call(branches, 'one')) {
142
+ return branches.one;
143
+ }
144
+
145
+ if (Object.prototype.hasOwnProperty.call(branches, 'other')) {
146
+ return branches.other;
147
+ }
148
+
149
+ const firstKey = Object.keys(branches)[0];
150
+
151
+ return firstKey ? branches[firstKey] : '';
152
+ }
153
+
154
+ function parsePluralExpression(message: string, startIndex: number): { param: string; body: string; endIndex: number } | null {
155
+ const headerMatch = message.slice(startIndex).match(/^\{(\w+),\s*plural,\s*/);
156
+
157
+ if (!headerMatch) {
158
+ return null;
159
+ }
160
+
161
+ const param = headerMatch[1];
162
+ const bodyStart = startIndex + headerMatch[0].length;
163
+ let index = bodyStart;
164
+
165
+ while (index < message.length) {
166
+ while (index < message.length && /\s/.test(message[index])) {
167
+ index++;
168
+ }
169
+
170
+ if (index >= message.length || message[index] === '}') {
171
+ break;
172
+ }
173
+
174
+ const branchMatch = message.slice(index).match(/^(\w+|=\d+)\{/);
84
175
 
85
- // Keep replacement syntax intentionally small and predictable because these
86
- // messages are often shared across PHP-rendered and JS-rendered validation UI.
87
- output = output.replace(/{([a-zA-Z0-9]+)}/g, (match, key) => {
176
+ if (!branchMatch) {
177
+ return null;
178
+ }
179
+
180
+ index += branchMatch[0].length;
181
+
182
+ let depth = 1;
183
+
184
+ while (index < message.length && depth > 0) {
185
+ if (message[index] === '{') {
186
+ depth++;
187
+ } else if (message[index] === '}') {
188
+ depth--;
189
+ }
190
+
191
+ if (depth > 0) {
192
+ index++;
193
+ }
194
+ }
195
+
196
+ index++;
197
+ }
198
+
199
+ if (index >= message.length || message[index] !== '}') {
200
+ return null;
201
+ }
202
+
203
+ return {
204
+ param,
205
+ body: message.slice(bodyStart, index),
206
+ endIndex: index,
207
+ };
208
+ }
209
+
210
+ function formatPluralBlocks(message: string, replacements: TranslationReplacements): string {
211
+ let output = '';
212
+ let index = 0;
213
+
214
+ while (index < message.length) {
215
+ if (message[index] !== '{') {
216
+ output += message[index];
217
+ index++;
218
+ continue;
219
+ }
220
+
221
+ const parsed = parsePluralExpression(message, index);
222
+
223
+ if (!parsed) {
224
+ output += message[index];
225
+ index++;
226
+ continue;
227
+ }
228
+
229
+ const rawValue = replacements[parsed.param];
230
+ const count = typeof rawValue === 'number'
231
+ ? rawValue
232
+ : Number.parseInt(String(rawValue ?? ''), 10) || 0;
233
+ const branches = parsePluralBranches(parsed.body);
234
+ let selected = selectPluralBranch(count, branches);
235
+
236
+ selected = selected.replace(/#/g, String(count));
237
+
238
+ output += selected;
239
+ index = parsed.endIndex + 1;
240
+ }
241
+
242
+ return output;
243
+ }
244
+
245
+ function formatNumberBlocks(message: string, replacements: TranslationReplacements): string {
246
+ return message.replace(/\{(\w+),\s*number\}/g, (match, key) => {
247
+ if (!Object.prototype.hasOwnProperty.call(replacements, key)) {
248
+ return match;
249
+ }
250
+
251
+ const value = replacements[key];
252
+
253
+ return typeof value === 'number' ? value.toLocaleString() : String(value);
254
+ });
255
+ }
256
+
257
+ function replaceSimpleTokens(message: string, replacements: TranslationReplacements): string {
258
+ return message.replace(/\{(\w+)\}/g, (match, key) => {
88
259
  if (Object.prototype.hasOwnProperty.call(replacements, key)) {
89
260
  return String(replacements[key]);
90
261
  }
91
262
 
92
263
  return match;
93
264
  });
265
+ }
266
+
267
+ export function t(message: string, replacements: TranslationReplacements = {}): string {
268
+ let output = getTranslationStore()[message] || message;
269
+
270
+ output = formatPluralBlocks(output, replacements);
271
+ output = formatNumberBlocks(output, replacements);
272
+ output = replaceSimpleTokens(output, replacements);
94
273
 
95
274
  return output;
96
275
  }
@@ -9,9 +9,10 @@ const email: ValidationRuleDefinition = {
9
9
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input.value);
10
10
  },
11
11
  message: ({ input, label, t }) => {
12
- return input.getAttribute('data-formie-pattern-email-message')
12
+ return input.getAttribute('data-formie-validation-email-message')
13
+ ?? input.getAttribute('data-formie-pattern-email-message')
13
14
  ?? input.getAttribute('data-pattern-email-message')
14
- ?? t('{attribute} is not a valid email address.', { attribute: label });
15
+ ?? t('{label} is not a valid email address.', { label });
15
16
  },
16
17
  };
17
18
 
@@ -16,8 +16,8 @@ const match: ValidationRuleDefinition = {
16
16
  const sourceField = sourceInput?.closest('[data-formie-field-handle]') as HTMLElement | null;
17
17
  const sourceLabel = getLabelText(sourceField);
18
18
 
19
- return ctx.t('{name} must match {value}.', {
20
- name: ctx.label,
19
+ return ctx.t('{label} must match {value}.', {
20
+ label: ctx.label,
21
21
  value: sourceLabel,
22
22
  });
23
23
  },
@@ -29,18 +29,18 @@ const minmax: ValidationRuleDefinition = {
29
29
  const max = input.hasAttribute('max') ? parseFloat(input.getAttribute('max') || '') : null;
30
30
 
31
31
  if (min !== null && max !== null) {
32
- return t('{attribute} must be between {min} and {max}.', { attribute: label, min, max });
32
+ return t('{label} must be between {min} and {max}.', { label, min, max });
33
33
  }
34
34
 
35
35
  if (min !== null) {
36
- return t('{attribute} must be no less than {min}.', { attribute: label, min });
36
+ return t('{label} must be no less than {min}.', { label, min });
37
37
  }
38
38
 
39
39
  if (max !== null) {
40
- return t('{attribute} must be no greater than {max}.', { attribute: label, max });
40
+ return t('{label} must be no greater than {max}.', { label, max });
41
41
  }
42
42
 
43
- return t('{attribute} has an invalid value.', { attribute: label });
43
+ return t('{label} has an invalid value.', { label });
44
44
  },
45
45
  };
46
46
 
@@ -35,20 +35,24 @@ const number: ValidationRuleDefinition = {
35
35
  const max = rule !== true && rule && typeof rule === 'object' && typeof rule.max === 'number' ? rule.max : null;
36
36
 
37
37
  if (min !== null && max !== null) {
38
- return t('{attribute} must be between {min} and {max}.', { attribute: label, min, max });
38
+ return input.getAttribute('data-formie-validation-number-min-message')
39
+ ?? t('{label} must be between {min} and {max}.', { label, min, max });
39
40
  }
40
41
 
41
42
  if (min !== null) {
42
- return t('{attribute} must be no less than {min}.', { attribute: label, min });
43
+ return input.getAttribute('data-formie-validation-number-min-message')
44
+ ?? t('{label} must be no less than {min}.', { label, min });
43
45
  }
44
46
 
45
47
  if (max !== null) {
46
- return t('{attribute} must be no greater than {max}.', { attribute: label, max });
48
+ return input.getAttribute('data-formie-validation-number-max-message')
49
+ ?? t('{label} must be no greater than {max}.', { label, max });
47
50
  }
48
51
 
49
- return input.getAttribute('data-formie-pattern-number-message')
52
+ return input.getAttribute('data-formie-validation-number-message')
53
+ ?? input.getAttribute('data-formie-pattern-number-message')
50
54
  ?? input.getAttribute('data-pattern-number-message')
51
- ?? t('{attribute} is not a valid number.', { attribute: label });
55
+ ?? t('{label} is not a valid number.', { label });
52
56
  },
53
57
  };
54
58
 
@@ -13,10 +13,10 @@ const pattern: ValidationRuleDefinition = {
13
13
  },
14
14
  message: ({ input, label, t }) => {
15
15
  const messages = {
16
- email: t('{attribute} is not a valid email address.', { attribute: label }),
17
- url: t('{attribute} is not a valid URL.', { attribute: label }),
18
- number: t('{attribute} is not a valid number.', { attribute: label }),
19
- default: t('{attribute} is not a valid format.', { attribute: label }),
16
+ email: t('{label} is not a valid email address.', { label }),
17
+ url: t('{label} is not a valid URL.', { label }),
18
+ number: t('{label} is not a valid number.', { label }),
19
+ default: t('{label} is not a valid format.', { label }),
20
20
  };
21
21
 
22
22
  return input.getAttribute(`data-formie-pattern-${input.type}-message`)
@@ -23,7 +23,7 @@ const required: ValidationRuleDefinition = {
23
23
  message: ({ input, label, t }) => {
24
24
  return input.getAttribute('data-formie-required-message')
25
25
  ?? input.getAttribute('data-required-message')
26
- ?? t('{attribute} cannot be blank.', { attribute: label });
26
+ ?? t('{label} cannot be blank.', { label });
27
27
  },
28
28
  };
29
29
 
@@ -16,7 +16,7 @@ const url: ValidationRuleDefinition = {
16
16
  message: ({ input, label, t }) => {
17
17
  return input.getAttribute('data-formie-pattern-url-message')
18
18
  ?? input.getAttribute('data-pattern-url-message')
19
- ?? t('{attribute} is not a valid URL.', { attribute: label });
19
+ ?? t('{label} is not a valid URL.', { label });
20
20
  },
21
21
  };
22
22
 
@@ -0,0 +1,25 @@
1
+ export function getFormPages(form: HTMLFormElement): HTMLElement[] {
2
+ return Array.from(form.querySelectorAll('[data-formie-page]')) as HTMLElement[];
3
+ }
4
+
5
+ export function getValidationScope(form: HTMLFormElement): { scope: Element; final: boolean } {
6
+ const pages = getFormPages(form);
7
+
8
+ if (!pages.length) {
9
+ return {
10
+ scope: form,
11
+ final: true,
12
+ };
13
+ }
14
+
15
+ // Multi-page submits validate the visible page until the user reaches the
16
+ // final step, where hidden earlier pages are included again.
17
+ const currentPage = pages.find((page) => {
18
+ return !page.hasAttribute('data-formie-page-hidden');
19
+ }) || pages[pages.length - 1];
20
+
21
+ return {
22
+ scope: currentPage,
23
+ final: currentPage === pages[pages.length - 1],
24
+ };
25
+ }
@@ -0,0 +1,157 @@
1
+ import { getValidationScope } from '#validation/scope';
2
+ import type { FormieValidator } from '#validation/validator';
3
+
4
+ const SUBMIT_VALIDATION_DISABLED_ATTR = 'data-formie-submit-validation-disabled';
5
+ const PRESERVED_DISABLED_ATTR = 'data-formie-preserve-disabled';
6
+ const SUBMIT_READY_ATTR = 'data-formie-submit-ready';
7
+
8
+ function isSubmitReadinessEnabled(form: HTMLFormElement): boolean {
9
+ return form.dataset.formieDisableSubmitUntilValid === 'true';
10
+ }
11
+
12
+ function getPrimarySubmitButtons(form: HTMLFormElement): HTMLButtonElement[] {
13
+ return Array.from(form.querySelectorAll('button[data-formie-action="submit"]')).filter((button): button is HTMLButtonElement => {
14
+ return button instanceof HTMLButtonElement;
15
+ });
16
+ }
17
+
18
+ function isSubmitButtonVisible(button: HTMLButtonElement): boolean {
19
+ return !button.hasAttribute('data-formie-conditionally-hidden')
20
+ && !button.closest('[data-formie-conditionally-hidden]');
21
+ }
22
+
23
+ export function syncSubmitReadiness(form: HTMLFormElement, validator: FormieValidator): void {
24
+ if (!isSubmitReadinessEnabled(form)) {
25
+ return;
26
+ }
27
+
28
+ if (form.getAttribute('data-formie-loading') === 'true') {
29
+ return;
30
+ }
31
+
32
+ const { scope, final } = getValidationScope(form);
33
+ const isReady = validator.isValid(scope, {
34
+ includeHiddenPages: final,
35
+ });
36
+
37
+ form.setAttribute(SUBMIT_READY_ATTR, isReady ? 'true' : 'false');
38
+
39
+ getPrimarySubmitButtons(form).forEach((button) => {
40
+ if (!isSubmitButtonVisible(button)) {
41
+ return;
42
+ }
43
+
44
+ if (isReady) {
45
+ if (!button.hasAttribute(SUBMIT_VALIDATION_DISABLED_ATTR)) {
46
+ return;
47
+ }
48
+
49
+ if (button.hasAttribute(PRESERVED_DISABLED_ATTR)) {
50
+ button.disabled = true;
51
+ button.removeAttribute(PRESERVED_DISABLED_ATTR);
52
+ } else {
53
+ button.disabled = false;
54
+ }
55
+
56
+ button.removeAttribute(SUBMIT_VALIDATION_DISABLED_ATTR);
57
+ return;
58
+ }
59
+
60
+ if (!button.hasAttribute(SUBMIT_VALIDATION_DISABLED_ATTR)) {
61
+ if (button.disabled) {
62
+ button.setAttribute(PRESERVED_DISABLED_ATTR, 'true');
63
+ }
64
+
65
+ button.setAttribute(SUBMIT_VALIDATION_DISABLED_ATTR, 'true');
66
+ }
67
+
68
+ button.disabled = true;
69
+ });
70
+ }
71
+
72
+ export function bindSubmitReadiness(
73
+ form: HTMLFormElement,
74
+ validator: FormieValidator,
75
+ target: Element,
76
+ ): () => void {
77
+ if (!isSubmitReadinessEnabled(form)) {
78
+ return () => {};
79
+ }
80
+
81
+ let syncQueued = false;
82
+
83
+ const scheduleSync = (): void => {
84
+ if (syncQueued) {
85
+ return;
86
+ }
87
+
88
+ syncQueued = true;
89
+
90
+ queueMicrotask(() => {
91
+ syncQueued = false;
92
+ syncSubmitReadiness(form, validator);
93
+ });
94
+ };
95
+
96
+ scheduleSync();
97
+
98
+ const inputHandler = (): void => {
99
+ scheduleSync();
100
+ };
101
+
102
+ form.addEventListener('input', inputHandler, true);
103
+ form.addEventListener('change', inputHandler, true);
104
+
105
+ const resetHandler = (): void => {
106
+ window.setTimeout(() => {
107
+ scheduleSync();
108
+ }, 0);
109
+ };
110
+
111
+ form.addEventListener('reset', resetHandler);
112
+
113
+ const conditionsHandler = (): void => {
114
+ scheduleSync();
115
+ };
116
+
117
+ target.addEventListener('formie:conditions:evaluated', conditionsHandler as EventListener);
118
+
119
+ const observer = new MutationObserver((mutations) => {
120
+ const shouldSync = mutations.some((mutation) => {
121
+ if (mutation.type === 'attributes') {
122
+ const attributeName = mutation.attributeName || '';
123
+
124
+ return attributeName === 'data-formie-page-hidden'
125
+ || attributeName === 'data-formie-conditionally-hidden'
126
+ || attributeName === 'data-formie-loading'
127
+ || attributeName === 'disabled';
128
+ }
129
+
130
+ return mutation.type === 'childList';
131
+ });
132
+
133
+ if (shouldSync) {
134
+ scheduleSync();
135
+ }
136
+ });
137
+
138
+ observer.observe(form, {
139
+ childList: true,
140
+ subtree: true,
141
+ attributes: true,
142
+ attributeFilter: [
143
+ 'data-formie-page-hidden',
144
+ 'data-formie-conditionally-hidden',
145
+ 'data-formie-loading',
146
+ 'disabled',
147
+ ],
148
+ });
149
+
150
+ return () => {
151
+ form.removeEventListener('input', inputHandler, true);
152
+ form.removeEventListener('change', inputHandler, true);
153
+ form.removeEventListener('reset', resetHandler);
154
+ target.removeEventListener('formie:conditions:evaluated', conditionsHandler as EventListener);
155
+ observer.disconnect();
156
+ };
157
+ }
@@ -1,3 +1,4 @@
1
+ import { ensureFieldErrorContainer } from '#core/field-error-container';
1
2
  import { syncPageTabErrors } from '#core/page-tab-errors';
2
3
  import rules from '#validation/rules';
3
4
  import type {
@@ -176,6 +177,10 @@ export class FormieValidator {
176
177
  return this.submitted || this.activated.has(input);
177
178
  }
178
179
 
180
+ isValid(inputOrSelector: Element | null = null, options: ValidateOptions = {}): boolean {
181
+ return this.validate(inputOrSelector, options).length === 0;
182
+ }
183
+
179
184
  validate(inputOrSelector: Element | null = null, options: ValidateOptions = {}): ValidationError[] {
180
185
  this.errors = [];
181
186
 
@@ -316,12 +321,11 @@ export class FormieValidator {
316
321
  let errorMessages = fieldContainer.querySelector('[data-formie-field-errors]') as HTMLElement | null;
317
322
 
318
323
  if (!errorMessages) {
319
- errorMessages = document.createElement('div');
320
- errorMessages.setAttribute('data-formie-field-errors', 'true');
321
- if (this.config.messagesClass.length) {
322
- errorMessages.classList.add(...this.config.messagesClass);
323
- }
324
- fieldContainer.appendChild(errorMessages);
324
+ errorMessages = ensureFieldErrorContainer(fieldContainer, (container) => {
325
+ if (this.config.messagesClass.length) {
326
+ container.classList.add(...this.config.messagesClass);
327
+ }
328
+ });
325
329
  }
326
330
 
327
331
  if (this.config.messagesClass.length) {
@@ -398,7 +402,7 @@ export class FormieValidator {
398
402
 
399
403
  getErrorMessage(input: ValidationInput, validatorName: string, validator: ValidatorDefinition, opts: ValidationContext): string {
400
404
  const errorMessage = typeof validator.errorMessage === 'function' ? validator.errorMessage(opts) : validator.errorMessage;
401
- return errorMessage ?? t('{attribute} is invalid.', { attribute: opts.label });
405
+ return errorMessage ?? t('{label} is invalid.', { label: opts.label });
402
406
  }
403
407
 
404
408
  getErrors(): ValidationError[] {