@axenstudio/axen-form 0.1.18 → 0.1.19

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.
@@ -23,6 +23,9 @@ function createFormStore(options) {
23
23
  values: structuredClone(initialValues),
24
24
  errors: {},
25
25
  touched: {},
26
+ validationStatus: "unvalidated",
27
+ fieldValidationStatus: {},
28
+ isValidating: false,
26
29
  dirty: false,
27
30
  isSubmitting: false,
28
31
  submitCount: 0,
@@ -73,19 +76,42 @@ function createFormStore(options) {
73
76
  }
74
77
  function setFieldValue(name, value) {
75
78
  const nextValues = pathUtils.set(state.values, name, value);
79
+ const statuses = { ...state.fieldValidationStatus, [name]: "unvalidated" };
80
+ const dependents = (options.fields ?? []).filter((field) => {
81
+ var _a;
82
+ return field.name !== name && ((_a = field.rules) == null ? void 0 : _a.countryField) === name;
83
+ });
84
+ const errors = { ...state.errors };
85
+ for (const field of dependents) {
86
+ statuses[field.name] = "unvalidated";
87
+ delete errors[field.name];
88
+ }
76
89
  valuesRevision += 1;
77
- state = { ...state, values: nextValues, dirty: !valuesEqual(nextValues, state.initialValues) };
90
+ state = {
91
+ ...state,
92
+ values: nextValues,
93
+ errors,
94
+ dirty: !valuesEqual(nextValues, state.initialValues),
95
+ validationStatus: "unvalidated",
96
+ isValidating: false,
97
+ fieldValidationStatus: statuses
98
+ };
78
99
  for (const field of options.fields ?? []) {
79
100
  if (field.hidden && state.errors[field.name] && (typeof field.hidden === "function" ? field.hidden(nextValues) : field.hidden)) {
80
101
  setFieldError(field.name, void 0);
81
102
  }
82
103
  }
83
104
  notifyValuePath(name);
105
+ for (const field of dependents) notifyField(field.name);
84
106
  if (validateOnChange && validationAdapter && validationSchema) {
85
107
  const revision = valuesRevision;
86
- void runFieldValidation(name).catch((error) => {
87
- if (revision === valuesRevision) setFieldError(name, error instanceof Error ? error.message : "Validation failed");
88
- });
108
+ for (const fieldName of [name, ...dependents.map((field) => field.name)]) {
109
+ void runFieldValidation(fieldName).catch((error) => {
110
+ if (revision === valuesRevision) {
111
+ setFieldError(fieldName, error instanceof Error ? error.message : "Validation failed");
112
+ }
113
+ });
114
+ }
89
115
  }
90
116
  }
91
117
  function setFieldTouched(name, touched = true) {
@@ -103,23 +129,39 @@ function createFormStore(options) {
103
129
  }
104
130
  }
105
131
  function setFieldError(name, error) {
106
- const next = { ...state.errors };
107
- if (error) {
108
- next[name] = error;
109
- } else {
110
- delete next[name];
111
- }
112
- state = { ...state, errors: next };
132
+ const errors = { ...state.errors };
133
+ if (error) errors[name] = error;
134
+ else delete errors[name];
135
+ state = {
136
+ ...state,
137
+ errors,
138
+ validationStatus: Object.keys(errors).length ? "invalid" : "unvalidated",
139
+ isValidating: false,
140
+ fieldValidationStatus: { ...state.fieldValidationStatus, [name]: error ? "invalid" : "unvalidated" }
141
+ };
113
142
  notifyField(name);
114
143
  notify();
115
144
  }
116
145
  function setValues(values) {
117
146
  valuesRevision += 1;
118
- state = { ...state, values, dirty: !valuesEqual(values, state.initialValues) };
147
+ state = {
148
+ ...state,
149
+ values,
150
+ dirty: !valuesEqual(values, state.initialValues),
151
+ validationStatus: "unvalidated",
152
+ isValidating: false,
153
+ fieldValidationStatus: {}
154
+ };
119
155
  notifyAllFields();
120
156
  }
121
157
  function setErrors(errors) {
122
- state = { ...state, errors };
158
+ state = {
159
+ ...state,
160
+ errors,
161
+ validationStatus: Object.keys(errors).length ? "invalid" : "unvalidated",
162
+ isValidating: false,
163
+ fieldValidationStatus: {}
164
+ };
123
165
  notifyAllFields();
124
166
  }
125
167
  function setFieldArrayValue(name, items, origins) {
@@ -152,7 +194,10 @@ function createFormStore(options) {
152
194
  values,
153
195
  dirty: !valuesEqual(values, state.initialValues),
154
196
  errors: remap(state.errors),
155
- touched: remap(state.touched)
197
+ touched: remap(state.touched),
198
+ validationStatus: "unvalidated",
199
+ isValidating: false,
200
+ fieldValidationStatus: remap(state.fieldValidationStatus)
156
201
  };
157
202
  notifyValuePath(name);
158
203
  }
@@ -162,6 +207,9 @@ function createFormStore(options) {
162
207
  values: newValues,
163
208
  errors: {},
164
209
  touched: {},
210
+ validationStatus: "unvalidated",
211
+ fieldValidationStatus: {},
212
+ isValidating: false,
165
213
  dirty: false,
166
214
  isSubmitting: false,
167
215
  submitCount: state.submitCount,
@@ -174,12 +222,30 @@ function createFormStore(options) {
174
222
  if (!validationAdapter || !validationSchema) return {};
175
223
  const revision = valuesRevision;
176
224
  const request = ++formRequest;
177
- const errors = await validationAdapter.validate(validationSchema, state.values);
178
- if (revision !== valuesRevision || request !== formRequest) return state.errors;
179
- state = { ...state, errors };
225
+ state = { ...state, validationStatus: "validating", isValidating: true };
180
226
  notify();
181
- for (const name of fieldSubscribers.keys()) notifyField(name);
182
- return errors;
227
+ try {
228
+ const errors = await validationAdapter.validate(validationSchema, state.values);
229
+ if (revision !== valuesRevision || request !== formRequest) return state.errors;
230
+ const statuses = { ...state.fieldValidationStatus };
231
+ for (const field of options.fields ?? []) statuses[field.name] = errors[field.name] ? "invalid" : "valid";
232
+ state = {
233
+ ...state,
234
+ errors,
235
+ validationStatus: Object.keys(errors).length ? "invalid" : "valid",
236
+ isValidating: false,
237
+ fieldValidationStatus: statuses
238
+ };
239
+ notify();
240
+ for (const name of fieldSubscribers.keys()) notifyField(name);
241
+ return errors;
242
+ } catch (error) {
243
+ if (revision === valuesRevision && request === formRequest) {
244
+ state = { ...state, validationStatus: "unvalidated", isValidating: false };
245
+ notify();
246
+ }
247
+ throw error;
248
+ }
183
249
  }
184
250
  async function validateField(name) {
185
251
  var _a;
@@ -188,17 +254,47 @@ function createFormStore(options) {
188
254
  const request = (fieldRequests.get(name) ?? 0) + 1;
189
255
  fieldRequests.set(name, request);
190
256
  const config = (_a = options.fields) == null ? void 0 : _a.find((field) => field.name === name);
191
- if ((config == null ? void 0 : config.hidden) && (typeof config.hidden === "function" ? config.hidden(state.values) : config.hidden)) {
257
+ const hidden = (config == null ? void 0 : config.hidden) && (typeof config.hidden === "function" ? config.hidden(state.values) : config.hidden);
258
+ if (hidden) {
192
259
  setFieldError(name, void 0);
260
+ state = { ...state, fieldValidationStatus: { ...state.fieldValidationStatus, [name]: "valid" } };
261
+ notifyField(name);
262
+ notify();
193
263
  return void 0;
194
264
  }
265
+ state = {
266
+ ...state,
267
+ fieldValidationStatus: { ...state.fieldValidationStatus, [name]: "validating" },
268
+ validationStatus: state.validationStatus === "validating" ? "validating" : "unvalidated"
269
+ };
270
+ notifyField(name);
271
+ notify();
195
272
  const fieldValue = pathUtils.get(state.values, name);
196
- const error = await validationAdapter.validateField(validationSchema, name, fieldValue);
197
- if (revision === valuesRevision && request === fieldRequests.get(name)) {
198
- setFieldError(name, error);
199
- return error;
273
+ try {
274
+ const error = await validationAdapter.validateField(validationSchema, name, fieldValue, state.values);
275
+ if (revision === valuesRevision && request === fieldRequests.get(name)) {
276
+ const errors = { ...state.errors };
277
+ if (error) errors[name] = error;
278
+ else delete errors[name];
279
+ state = {
280
+ ...state,
281
+ errors,
282
+ validationStatus: state.validationStatus === "validating" ? "validating" : Object.keys(errors).length ? "invalid" : "unvalidated",
283
+ fieldValidationStatus: { ...state.fieldValidationStatus, [name]: error ? "invalid" : "valid" }
284
+ };
285
+ notifyField(name);
286
+ notify();
287
+ return error;
288
+ }
289
+ return state.errors[name];
290
+ } catch (error) {
291
+ if (revision === valuesRevision && request === fieldRequests.get(name)) {
292
+ state = { ...state, fieldValidationStatus: { ...state.fieldValidationStatus, [name]: "unvalidated" } };
293
+ notifyField(name);
294
+ notify();
295
+ }
296
+ throw error;
200
297
  }
201
- return state.errors[name];
202
298
  }
203
299
  async function runFieldValidation(name) {
204
300
  if (validationAdapter == null ? void 0 : validationAdapter.validateField) {
@@ -1,8 +1,11 @@
1
- import { FieldConfig, ValidationAdapter } from '../types';
1
+ import { FieldConfig, ValidationAdapter, ValidationStatus } from '../types';
2
2
  export interface FormState<TValues extends Record<string, unknown> = Record<string, unknown>> {
3
3
  values: TValues;
4
4
  errors: Record<string, string>;
5
5
  touched: Record<string, boolean>;
6
+ validationStatus: ValidationStatus;
7
+ fieldValidationStatus: Record<string, ValidationStatus>;
8
+ isValidating: boolean;
6
9
  dirty: boolean;
7
10
  isSubmitting: boolean;
8
11
  submitCount: number;
@@ -21,6 +21,9 @@ function createFormStore(options) {
21
21
  values: structuredClone(initialValues),
22
22
  errors: {},
23
23
  touched: {},
24
+ validationStatus: "unvalidated",
25
+ fieldValidationStatus: {},
26
+ isValidating: false,
24
27
  dirty: false,
25
28
  isSubmitting: false,
26
29
  submitCount: 0,
@@ -71,19 +74,42 @@ function createFormStore(options) {
71
74
  }
72
75
  function setFieldValue(name, value) {
73
76
  const nextValues = set(state.values, name, value);
77
+ const statuses = { ...state.fieldValidationStatus, [name]: "unvalidated" };
78
+ const dependents = (options.fields ?? []).filter((field) => {
79
+ var _a;
80
+ return field.name !== name && ((_a = field.rules) == null ? void 0 : _a.countryField) === name;
81
+ });
82
+ const errors = { ...state.errors };
83
+ for (const field of dependents) {
84
+ statuses[field.name] = "unvalidated";
85
+ delete errors[field.name];
86
+ }
74
87
  valuesRevision += 1;
75
- state = { ...state, values: nextValues, dirty: !valuesEqual(nextValues, state.initialValues) };
88
+ state = {
89
+ ...state,
90
+ values: nextValues,
91
+ errors,
92
+ dirty: !valuesEqual(nextValues, state.initialValues),
93
+ validationStatus: "unvalidated",
94
+ isValidating: false,
95
+ fieldValidationStatus: statuses
96
+ };
76
97
  for (const field of options.fields ?? []) {
77
98
  if (field.hidden && state.errors[field.name] && (typeof field.hidden === "function" ? field.hidden(nextValues) : field.hidden)) {
78
99
  setFieldError(field.name, void 0);
79
100
  }
80
101
  }
81
102
  notifyValuePath(name);
103
+ for (const field of dependents) notifyField(field.name);
82
104
  if (validateOnChange && validationAdapter && validationSchema) {
83
105
  const revision = valuesRevision;
84
- void runFieldValidation(name).catch((error) => {
85
- if (revision === valuesRevision) setFieldError(name, error instanceof Error ? error.message : "Validation failed");
86
- });
106
+ for (const fieldName of [name, ...dependents.map((field) => field.name)]) {
107
+ void runFieldValidation(fieldName).catch((error) => {
108
+ if (revision === valuesRevision) {
109
+ setFieldError(fieldName, error instanceof Error ? error.message : "Validation failed");
110
+ }
111
+ });
112
+ }
87
113
  }
88
114
  }
89
115
  function setFieldTouched(name, touched = true) {
@@ -101,23 +127,39 @@ function createFormStore(options) {
101
127
  }
102
128
  }
103
129
  function setFieldError(name, error) {
104
- const next = { ...state.errors };
105
- if (error) {
106
- next[name] = error;
107
- } else {
108
- delete next[name];
109
- }
110
- state = { ...state, errors: next };
130
+ const errors = { ...state.errors };
131
+ if (error) errors[name] = error;
132
+ else delete errors[name];
133
+ state = {
134
+ ...state,
135
+ errors,
136
+ validationStatus: Object.keys(errors).length ? "invalid" : "unvalidated",
137
+ isValidating: false,
138
+ fieldValidationStatus: { ...state.fieldValidationStatus, [name]: error ? "invalid" : "unvalidated" }
139
+ };
111
140
  notifyField(name);
112
141
  notify();
113
142
  }
114
143
  function setValues(values) {
115
144
  valuesRevision += 1;
116
- state = { ...state, values, dirty: !valuesEqual(values, state.initialValues) };
145
+ state = {
146
+ ...state,
147
+ values,
148
+ dirty: !valuesEqual(values, state.initialValues),
149
+ validationStatus: "unvalidated",
150
+ isValidating: false,
151
+ fieldValidationStatus: {}
152
+ };
117
153
  notifyAllFields();
118
154
  }
119
155
  function setErrors(errors) {
120
- state = { ...state, errors };
156
+ state = {
157
+ ...state,
158
+ errors,
159
+ validationStatus: Object.keys(errors).length ? "invalid" : "unvalidated",
160
+ isValidating: false,
161
+ fieldValidationStatus: {}
162
+ };
121
163
  notifyAllFields();
122
164
  }
123
165
  function setFieldArrayValue(name, items, origins) {
@@ -150,7 +192,10 @@ function createFormStore(options) {
150
192
  values,
151
193
  dirty: !valuesEqual(values, state.initialValues),
152
194
  errors: remap(state.errors),
153
- touched: remap(state.touched)
195
+ touched: remap(state.touched),
196
+ validationStatus: "unvalidated",
197
+ isValidating: false,
198
+ fieldValidationStatus: remap(state.fieldValidationStatus)
154
199
  };
155
200
  notifyValuePath(name);
156
201
  }
@@ -160,6 +205,9 @@ function createFormStore(options) {
160
205
  values: newValues,
161
206
  errors: {},
162
207
  touched: {},
208
+ validationStatus: "unvalidated",
209
+ fieldValidationStatus: {},
210
+ isValidating: false,
163
211
  dirty: false,
164
212
  isSubmitting: false,
165
213
  submitCount: state.submitCount,
@@ -172,12 +220,30 @@ function createFormStore(options) {
172
220
  if (!validationAdapter || !validationSchema) return {};
173
221
  const revision = valuesRevision;
174
222
  const request = ++formRequest;
175
- const errors = await validationAdapter.validate(validationSchema, state.values);
176
- if (revision !== valuesRevision || request !== formRequest) return state.errors;
177
- state = { ...state, errors };
223
+ state = { ...state, validationStatus: "validating", isValidating: true };
178
224
  notify();
179
- for (const name of fieldSubscribers.keys()) notifyField(name);
180
- return errors;
225
+ try {
226
+ const errors = await validationAdapter.validate(validationSchema, state.values);
227
+ if (revision !== valuesRevision || request !== formRequest) return state.errors;
228
+ const statuses = { ...state.fieldValidationStatus };
229
+ for (const field of options.fields ?? []) statuses[field.name] = errors[field.name] ? "invalid" : "valid";
230
+ state = {
231
+ ...state,
232
+ errors,
233
+ validationStatus: Object.keys(errors).length ? "invalid" : "valid",
234
+ isValidating: false,
235
+ fieldValidationStatus: statuses
236
+ };
237
+ notify();
238
+ for (const name of fieldSubscribers.keys()) notifyField(name);
239
+ return errors;
240
+ } catch (error) {
241
+ if (revision === valuesRevision && request === formRequest) {
242
+ state = { ...state, validationStatus: "unvalidated", isValidating: false };
243
+ notify();
244
+ }
245
+ throw error;
246
+ }
181
247
  }
182
248
  async function validateField(name) {
183
249
  var _a;
@@ -186,17 +252,47 @@ function createFormStore(options) {
186
252
  const request = (fieldRequests.get(name) ?? 0) + 1;
187
253
  fieldRequests.set(name, request);
188
254
  const config = (_a = options.fields) == null ? void 0 : _a.find((field) => field.name === name);
189
- if ((config == null ? void 0 : config.hidden) && (typeof config.hidden === "function" ? config.hidden(state.values) : config.hidden)) {
255
+ const hidden = (config == null ? void 0 : config.hidden) && (typeof config.hidden === "function" ? config.hidden(state.values) : config.hidden);
256
+ if (hidden) {
190
257
  setFieldError(name, void 0);
258
+ state = { ...state, fieldValidationStatus: { ...state.fieldValidationStatus, [name]: "valid" } };
259
+ notifyField(name);
260
+ notify();
191
261
  return void 0;
192
262
  }
263
+ state = {
264
+ ...state,
265
+ fieldValidationStatus: { ...state.fieldValidationStatus, [name]: "validating" },
266
+ validationStatus: state.validationStatus === "validating" ? "validating" : "unvalidated"
267
+ };
268
+ notifyField(name);
269
+ notify();
193
270
  const fieldValue = get(state.values, name);
194
- const error = await validationAdapter.validateField(validationSchema, name, fieldValue);
195
- if (revision === valuesRevision && request === fieldRequests.get(name)) {
196
- setFieldError(name, error);
197
- return error;
271
+ try {
272
+ const error = await validationAdapter.validateField(validationSchema, name, fieldValue, state.values);
273
+ if (revision === valuesRevision && request === fieldRequests.get(name)) {
274
+ const errors = { ...state.errors };
275
+ if (error) errors[name] = error;
276
+ else delete errors[name];
277
+ state = {
278
+ ...state,
279
+ errors,
280
+ validationStatus: state.validationStatus === "validating" ? "validating" : Object.keys(errors).length ? "invalid" : "unvalidated",
281
+ fieldValidationStatus: { ...state.fieldValidationStatus, [name]: error ? "invalid" : "valid" }
282
+ };
283
+ notifyField(name);
284
+ notify();
285
+ return error;
286
+ }
287
+ return state.errors[name];
288
+ } catch (error) {
289
+ if (revision === valuesRevision && request === fieldRequests.get(name)) {
290
+ state = { ...state, fieldValidationStatus: { ...state.fieldValidationStatus, [name]: "unvalidated" } };
291
+ notifyField(name);
292
+ notify();
293
+ }
294
+ throw error;
198
295
  }
199
- return state.errors[name];
200
296
  }
201
297
  async function runFieldValidation(name) {
202
298
  if (validationAdapter == null ? void 0 : validationAdapter.validateField) {
@@ -4,8 +4,8 @@ const pathUtils = require("./pathUtils.cjs");
4
4
  function isHidden(field, values) {
5
5
  return typeof field.hidden === "function" ? Boolean(field.hidden(values)) : Boolean(field.hidden);
6
6
  }
7
- function validateSemanticValue(field, value, policy) {
8
- var _a;
7
+ function validateSemanticValue(field, value, policy, values = {}) {
8
+ var _a, _b;
9
9
  if (field.validation) return void 0;
10
10
  const rules = field.rules;
11
11
  const inferred = field.type === "tel" ? "phone" : field.type;
@@ -29,13 +29,23 @@ function validateSemanticValue(field, value, policy) {
29
29
  const region = ((rules == null ? void 0 : rules.region) ?? "").toUpperCase();
30
30
  valid = region === "ID" ? /^(?:\+62|62|0)8\d{7,11}$/u.test(clean) : /^\+[1-9]\d{7,14}$/u.test(clean);
31
31
  } else if (format === "postalCode") {
32
- const country = ((rules == null ? void 0 : rules.country) ?? "").toUpperCase();
32
+ const resolved = ((_a = rules == null ? void 0 : rules.countryResolver) == null ? void 0 : _a.call(rules, values)) ?? ((rules == null ? void 0 : rules.countryField) ? pathUtils.get(values, rules.countryField) : void 0) ?? (rules == null ? void 0 : rules.country);
33
+ const country = typeof resolved === "string" ? resolved.trim().toUpperCase() : "";
34
+ if (!country && ((rules == null ? void 0 : rules.countryField) || (rules == null ? void 0 : rules.countryResolver))) return void 0;
33
35
  const patterns = {
34
36
  ID: /^\d{5}$/u,
35
37
  US: /^\d{5}(?:-\d{4})?$/u,
36
- SG: /^\d{6}$/u
38
+ SG: /^\d{6}$/u,
39
+ JP: /^\d{3}-?\d{4}$/u,
40
+ GB: /^(?:GIR\s?0AA|[A-Z]{1,2}\d[A-Z\d]?\s?\d[A-Z]{2})$/iu,
41
+ UK: /^(?:GIR\s?0AA|[A-Z]{1,2}\d[A-Z\d]?\s?\d[A-Z]{2})$/iu
37
42
  };
38
- valid = Boolean((_a = patterns[country]) == null ? void 0 : _a.test(input));
43
+ const validator = ((_b = policy == null ? void 0 : policy.postalCodePatterns) == null ? void 0 : _b[country]) ?? patterns[country];
44
+ if (!validator) valid = (policy == null ? void 0 : policy.unknownCountry) === "ignore";
45
+ else if (validator instanceof RegExp) {
46
+ validator.lastIndex = 0;
47
+ valid = validator.test(input);
48
+ } else valid = Boolean(validator(input, values));
39
49
  }
40
50
  return valid ? void 0 : (rules == null ? void 0 : rules.message) ?? `Invalid ${format}`;
41
51
  }
@@ -51,17 +61,20 @@ function withValidationPolicy(adapter, policy) {
51
61
  if (isHidden(field, values)) {
52
62
  delete errors[field.name];
53
63
  } else if (!errors[field.name]) {
54
- const error = validateSemanticValue(field, pathUtils.get(values, field.name), policy);
64
+ const error = validateSemanticValue(field, pathUtils.get(values, field.name), policy, values);
55
65
  if (error) errors[field.name] = error;
56
66
  }
57
67
  }
58
68
  return errors;
59
69
  },
60
- async validateField(schema, name, value) {
70
+ async validateField(schema, name, value, values) {
61
71
  var _a;
62
72
  const { base, fields } = schema;
63
- const error = await ((_a = adapter.validateField) == null ? void 0 : _a.call(adapter, base, name, value));
64
- return error ?? validateSemanticValue(fields.find((f) => f.name === name) ?? { type: "text" }, value, policy);
73
+ const context = values ?? { [name]: value };
74
+ const field = fields.find((f) => f.name === name) ?? { name, type: "text" };
75
+ if (isHidden(field, context)) return void 0;
76
+ const error = await ((_a = adapter.validateField) == null ? void 0 : _a.call(adapter, base, name, value, context));
77
+ return error ?? validateSemanticValue(field, value, policy, context);
65
78
  }
66
79
  };
67
80
  }
@@ -1,5 +1,5 @@
1
1
  import { FieldConfig, ValidationAdapter, ValidationPolicy } from '../types';
2
2
  /** Optional semantics: custom Yup/Zod field schemas retain precedence. */
3
- export declare function validateSemanticValue(field: FieldConfig, value: unknown, policy?: ValidationPolicy): string | undefined;
3
+ export declare function validateSemanticValue(field: FieldConfig, value: unknown, policy?: ValidationPolicy, values?: Record<string, unknown>): string | undefined;
4
4
  /** Adapter-independent policy wrapper. No mandatory Yup, Zod, or UI dependencies. */
5
5
  export declare function withValidationPolicy(adapter: ValidationAdapter, policy?: ValidationPolicy): ValidationAdapter;
@@ -2,8 +2,8 @@ import { get } from "./pathUtils.js";
2
2
  function isHidden(field, values) {
3
3
  return typeof field.hidden === "function" ? Boolean(field.hidden(values)) : Boolean(field.hidden);
4
4
  }
5
- function validateSemanticValue(field, value, policy) {
6
- var _a;
5
+ function validateSemanticValue(field, value, policy, values = {}) {
6
+ var _a, _b;
7
7
  if (field.validation) return void 0;
8
8
  const rules = field.rules;
9
9
  const inferred = field.type === "tel" ? "phone" : field.type;
@@ -27,13 +27,23 @@ function validateSemanticValue(field, value, policy) {
27
27
  const region = ((rules == null ? void 0 : rules.region) ?? "").toUpperCase();
28
28
  valid = region === "ID" ? /^(?:\+62|62|0)8\d{7,11}$/u.test(clean) : /^\+[1-9]\d{7,14}$/u.test(clean);
29
29
  } else if (format === "postalCode") {
30
- const country = ((rules == null ? void 0 : rules.country) ?? "").toUpperCase();
30
+ const resolved = ((_a = rules == null ? void 0 : rules.countryResolver) == null ? void 0 : _a.call(rules, values)) ?? ((rules == null ? void 0 : rules.countryField) ? get(values, rules.countryField) : void 0) ?? (rules == null ? void 0 : rules.country);
31
+ const country = typeof resolved === "string" ? resolved.trim().toUpperCase() : "";
32
+ if (!country && ((rules == null ? void 0 : rules.countryField) || (rules == null ? void 0 : rules.countryResolver))) return void 0;
31
33
  const patterns = {
32
34
  ID: /^\d{5}$/u,
33
35
  US: /^\d{5}(?:-\d{4})?$/u,
34
- SG: /^\d{6}$/u
36
+ SG: /^\d{6}$/u,
37
+ JP: /^\d{3}-?\d{4}$/u,
38
+ GB: /^(?:GIR\s?0AA|[A-Z]{1,2}\d[A-Z\d]?\s?\d[A-Z]{2})$/iu,
39
+ UK: /^(?:GIR\s?0AA|[A-Z]{1,2}\d[A-Z\d]?\s?\d[A-Z]{2})$/iu
35
40
  };
36
- valid = Boolean((_a = patterns[country]) == null ? void 0 : _a.test(input));
41
+ const validator = ((_b = policy == null ? void 0 : policy.postalCodePatterns) == null ? void 0 : _b[country]) ?? patterns[country];
42
+ if (!validator) valid = (policy == null ? void 0 : policy.unknownCountry) === "ignore";
43
+ else if (validator instanceof RegExp) {
44
+ validator.lastIndex = 0;
45
+ valid = validator.test(input);
46
+ } else valid = Boolean(validator(input, values));
37
47
  }
38
48
  return valid ? void 0 : (rules == null ? void 0 : rules.message) ?? `Invalid ${format}`;
39
49
  }
@@ -49,17 +59,20 @@ function withValidationPolicy(adapter, policy) {
49
59
  if (isHidden(field, values)) {
50
60
  delete errors[field.name];
51
61
  } else if (!errors[field.name]) {
52
- const error = validateSemanticValue(field, get(values, field.name), policy);
62
+ const error = validateSemanticValue(field, get(values, field.name), policy, values);
53
63
  if (error) errors[field.name] = error;
54
64
  }
55
65
  }
56
66
  return errors;
57
67
  },
58
- async validateField(schema, name, value) {
68
+ async validateField(schema, name, value, values) {
59
69
  var _a;
60
70
  const { base, fields } = schema;
61
- const error = await ((_a = adapter.validateField) == null ? void 0 : _a.call(adapter, base, name, value));
62
- return error ?? validateSemanticValue(fields.find((f) => f.name === name) ?? { type: "text" }, value, policy);
71
+ const context = values ?? { [name]: value };
72
+ const field = fields.find((f) => f.name === name) ?? { name, type: "text" };
73
+ if (isHidden(field, context)) return void 0;
74
+ const error = await ((_a = adapter.validateField) == null ? void 0 : _a.call(adapter, base, name, value, context));
75
+ return error ?? validateSemanticValue(field, value, policy, context);
63
76
  }
64
77
  };
65
78
  }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export type { ArrayFieldConfig, ArrayHelpers, AutocompleteOption, AxenFormProps, AxenFormRef, BoxProps, Breakpoint, BuiltInFieldType, DividerProps, FieldComponentProps, FieldConfig, FieldGroupConfig, FieldOption, FieldTypeComponentMap, FormApi, FormConfig, GridProps, GridSize, PayloadConfig, SpacerProps, StackProps, ValidationAdapter, ValidationPolicy, ValidationRules, } from './types';
1
+ export type { ArrayFieldConfig, ArrayHelpers, AutocompleteOption, AxenFormProps, AxenFormRef, BoxProps, Breakpoint, BuiltInFieldType, DividerProps, FieldComponentProps, FieldConfig, FieldGroupConfig, FieldOption, FieldTypeComponentMap, FormApi, FormConfig, GridProps, GridSize, PayloadConfig, SpacerProps, StackProps, ValidationAdapter, ValidationPolicy, ValidationRules, ValidationStatus, } from './types';
2
2
  export { isFieldGroup } from './types';
3
3
  export { formatDateLocale, formatDateTimeLocale, formatTimeLocale, getDefaultLocale, toISODateString, toISODateTimeLocalString, toISOTimeString, } from './core/dateUtils';
4
4
  export { createFormStore } from './core/formStore';
@@ -57,6 +57,8 @@ function ArrayItemField({
57
57
  touched: store.getState().touched,
58
58
  dirty: store.getState().dirty,
59
59
  isSubmitting: store.getState().isSubmitting,
60
+ validationStatus: store.getState().validationStatus,
61
+ fieldValidationStatus: store.getState().fieldValidationStatus,
60
62
  isValid: Object.keys(store.getState().errors).length === 0,
61
63
  submitCount: store.getState().submitCount,
62
64
  setFieldValue: store.setFieldValue,
@@ -55,6 +55,8 @@ function ArrayItemField({
55
55
  touched: store.getState().touched,
56
56
  dirty: store.getState().dirty,
57
57
  isSubmitting: store.getState().isSubmitting,
58
+ validationStatus: store.getState().validationStatus,
59
+ fieldValidationStatus: store.getState().fieldValidationStatus,
58
60
  isValid: Object.keys(store.getState().errors).length === 0,
59
61
  submitCount: store.getState().submitCount,
60
62
  setFieldValue: store.setFieldValue,
@@ -29,6 +29,8 @@ function AxenField({ name, config, component, children }) {
29
29
  touched: store.getState().touched,
30
30
  dirty: store.getState().dirty,
31
31
  isSubmitting: store.getState().isSubmitting,
32
+ validationStatus: store.getState().validationStatus,
33
+ fieldValidationStatus: store.getState().fieldValidationStatus,
32
34
  isValid: Object.keys(store.getState().errors).length === 0,
33
35
  submitCount: store.getState().submitCount,
34
36
  setFieldValue: store.setFieldValue,
@@ -27,6 +27,8 @@ function AxenField({ name, config, component, children }) {
27
27
  touched: store.getState().touched,
28
28
  dirty: store.getState().dirty,
29
29
  isSubmitting: store.getState().isSubmitting,
30
+ validationStatus: store.getState().validationStatus,
31
+ fieldValidationStatus: store.getState().fieldValidationStatus,
30
32
  isValid: Object.keys(store.getState().errors).length === 0,
31
33
  submitCount: store.getState().submitCount,
32
34
  setFieldValue: store.setFieldValue,
@@ -70,6 +70,12 @@ function useAxenForm(props) {
70
70
  get isSubmitting() {
71
71
  return store.getState().isSubmitting;
72
72
  },
73
+ get validationStatus() {
74
+ return store.getState().validationStatus;
75
+ },
76
+ get fieldValidationStatus() {
77
+ return store.getState().fieldValidationStatus;
78
+ },
73
79
  get isValid() {
74
80
  return Object.keys(store.getState().errors).length === 0;
75
81
  },
@@ -68,6 +68,12 @@ function useAxenForm(props) {
68
68
  get isSubmitting() {
69
69
  return store.getState().isSubmitting;
70
70
  },
71
+ get validationStatus() {
72
+ return store.getState().validationStatus;
73
+ },
74
+ get fieldValidationStatus() {
75
+ return store.getState().fieldValidationStatus;
76
+ },
71
77
  get isValid() {
72
78
  return Object.keys(store.getState().errors).length === 0;
73
79
  },
package/dist/types.d.ts CHANGED
@@ -17,13 +17,20 @@ export interface ValidationRules {
17
17
  format?: 'email' | 'url' | 'phone' | 'postalCode';
18
18
  region?: string;
19
19
  country?: string;
20
+ /** Resolve country from a sibling or nested form field. */
21
+ countryField?: string;
22
+ /** Optional custom country resolver, taking precedence over countryField. */
23
+ countryResolver?: (values: Record<string, unknown>) => string | undefined;
20
24
  message?: string;
21
25
  }
22
26
  /** Legacy required-only mode remains the default. */
23
27
  export interface ValidationPolicy {
24
28
  mode?: 'legacy-required-only' | 'standard';
25
29
  locale?: string;
30
+ postalCodePatterns?: Record<string, RegExp | ((code: string, values: Record<string, unknown>) => boolean)>;
31
+ unknownCountry?: 'reject' | 'ignore';
26
32
  }
33
+ export type ValidationStatus = 'unvalidated' | 'validating' | 'valid' | 'invalid';
27
34
  /** Base field configuration */
28
35
  export interface FieldConfig<TValues extends Record<string, unknown> = Record<string, unknown>> {
29
36
  name: string;
@@ -125,6 +132,8 @@ export interface FormApi<TValues extends Record<string, unknown> = Record<string
125
132
  touched: Record<string, boolean>;
126
133
  dirty: boolean;
127
134
  isSubmitting: boolean;
135
+ validationStatus: ValidationStatus;
136
+ fieldValidationStatus: Record<string, ValidationStatus>;
128
137
  isValid: boolean;
129
138
  submitCount: number;
130
139
  setFieldValue: (name: string, value: unknown) => void;
@@ -183,7 +192,7 @@ export interface AxenFormProps<TValues extends Record<string, unknown> = Record<
183
192
  export interface ValidationAdapter {
184
193
  buildSchema: (fields: FieldConfig[]) => unknown;
185
194
  validate: (schema: unknown, values: Record<string, unknown>) => Promise<Record<string, string>>;
186
- validateField?: (schema: unknown, name: string, value: unknown) => Promise<string | undefined>;
195
+ validateField?: (schema: unknown, name: string, value: unknown, values?: Record<string, unknown>) => Promise<string | undefined>;
187
196
  }
188
197
  export interface ArrayHelpers {
189
198
  push: (value: unknown) => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axenstudio/axen-form",
3
- "version": "0.1.18",
3
+ "version": "0.1.19",
4
4
  "description": "Lightweight, config-driven, enterprise-grade React form library. Zero UI dependency. CSS Modules. A11y-aware.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",