@vectoriox/iox-ui 4.26.0 → 4.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
// The persisted shape of a form. Pure types + the one derived constant each renderer needs.
|
|
2
|
+
//
|
|
3
|
+
// A FormSpec is DERIVED from the node tree (see `collect.ts`), never hand-maintained beside it —
|
|
4
|
+
// there is one source of truth for "what fields does this form have", and it is the tree the author
|
|
5
|
+
// edited. The builder writes the derived spec into the saved page so the SERVER can re-validate
|
|
6
|
+
// against it without re-implementing the builder's tree walk.
|
|
7
|
+
/** What the visitor actually types into. Chosen by a trait; it also implies a validation rule. */
|
|
8
|
+
var ControlType;
|
|
9
|
+
(function (ControlType) {
|
|
10
|
+
ControlType["Text"] = "text";
|
|
11
|
+
ControlType["Email"] = "email";
|
|
12
|
+
ControlType["Tel"] = "tel";
|
|
13
|
+
ControlType["Url"] = "url";
|
|
14
|
+
ControlType["Number"] = "number";
|
|
15
|
+
ControlType["Password"] = "password";
|
|
16
|
+
ControlType["Textarea"] = "textarea";
|
|
17
|
+
ControlType["Select"] = "select";
|
|
18
|
+
ControlType["Checkbox"] = "checkbox";
|
|
19
|
+
ControlType["Radio"] = "radio";
|
|
20
|
+
ControlType["Date"] = "date";
|
|
21
|
+
ControlType["Time"] = "time";
|
|
22
|
+
ControlType["File"] = "file";
|
|
23
|
+
/** Not shown; carries a constant or a bound value (campaign, source page, …). */
|
|
24
|
+
ControlType["Hidden"] = "hidden";
|
|
25
|
+
})(ControlType || (ControlType = {}));
|
|
26
|
+
/** The v1 rule grammar. Phase 3's conditional logic reuses these same names for its conditions. */
|
|
27
|
+
var RuleName;
|
|
28
|
+
(function (RuleName) {
|
|
29
|
+
RuleName["Required"] = "required";
|
|
30
|
+
RuleName["MinLength"] = "minLength";
|
|
31
|
+
RuleName["MaxLength"] = "maxLength";
|
|
32
|
+
RuleName["Min"] = "min";
|
|
33
|
+
RuleName["Max"] = "max";
|
|
34
|
+
RuleName["Pattern"] = "pattern";
|
|
35
|
+
/** Cross-field: this value must equal another field's (confirm email / password). */
|
|
36
|
+
RuleName["Matches"] = "matches";
|
|
37
|
+
/** A checkbox that must be ticked — terms, consent. */
|
|
38
|
+
RuleName["Accepted"] = "accepted";
|
|
39
|
+
RuleName["FileType"] = "fileType";
|
|
40
|
+
RuleName["FileSize"] = "fileSize";
|
|
41
|
+
})(RuleName || (RuleName = {}));
|
|
42
|
+
/** Named regexes so an author picks "Email" rather than writing one. */
|
|
43
|
+
var PatternPreset;
|
|
44
|
+
(function (PatternPreset) {
|
|
45
|
+
PatternPreset["Email"] = "email";
|
|
46
|
+
PatternPreset["Phone"] = "phone";
|
|
47
|
+
PatternPreset["Url"] = "url";
|
|
48
|
+
PatternPreset["Digits"] = "digits";
|
|
49
|
+
PatternPreset["Letters"] = "letters";
|
|
50
|
+
PatternPreset["Slug"] = "slug";
|
|
51
|
+
})(PatternPreset || (PatternPreset = {}));
|
|
52
|
+
/**
|
|
53
|
+
* Deliberately permissive. A validator that rejects `user+tag@sub.domain.co.uk` or a real-world
|
|
54
|
+
* phone number is worse than one that lets a typo through: the address gets verified by sending to
|
|
55
|
+
* it, and a false rejection is a lost lead with no recourse.
|
|
56
|
+
*/
|
|
57
|
+
const PATTERN_PRESETS = Object.freeze({
|
|
58
|
+
[PatternPreset.Email]: /^[^\s@]+@[^\s@.]+(\.[^\s@.]+)+$/,
|
|
59
|
+
[PatternPreset.Phone]: /^\+?[\d\s().-]{6,20}$/,
|
|
60
|
+
[PatternPreset.Url]: /^(https?:\/\/)?[^\s.]+\.[^\s]{2,}$/i,
|
|
61
|
+
[PatternPreset.Digits]: /^\d+$/,
|
|
62
|
+
[PatternPreset.Letters]: /^[\p{L}\s'-]+$/u,
|
|
63
|
+
[PatternPreset.Slug]: /^[a-z0-9]+(-[a-z0-9]+)*$/,
|
|
64
|
+
});
|
|
65
|
+
/** When a field is validated, once the author has stopped deciding it per field. */
|
|
66
|
+
var FeedbackTiming;
|
|
67
|
+
(function (FeedbackTiming) {
|
|
68
|
+
/** Validate on submit, then live per-field once that field has been touched or has failed. */
|
|
69
|
+
FeedbackTiming["Smart"] = "smart";
|
|
70
|
+
FeedbackTiming["Submit"] = "submit";
|
|
71
|
+
FeedbackTiming["Live"] = "live";
|
|
72
|
+
})(FeedbackTiming || (FeedbackTiming = {}));
|
|
73
|
+
/**
|
|
74
|
+
* The rule a control type implies with no configuration. Choosing "Email" IS choosing email
|
|
75
|
+
* validation — the single biggest reason the panel can stay empty and still validate well.
|
|
76
|
+
*/
|
|
77
|
+
const IMPLIED_PATTERN = Object.freeze({
|
|
78
|
+
[ControlType.Email]: PatternPreset.Email,
|
|
79
|
+
[ControlType.Tel]: PatternPreset.Phone,
|
|
80
|
+
[ControlType.Url]: PatternPreset.Url,
|
|
81
|
+
});
|
|
82
|
+
/** Controls whose value is a list rather than a scalar. */
|
|
83
|
+
const MULTI_VALUE_CONTROLS = new Set([ControlType.File]);
|
|
84
|
+
|
|
85
|
+
// The default message for every rule, as an i18n KEY — never a rendered string.
|
|
86
|
+
//
|
|
87
|
+
// This is what lets the Validation panel stay empty and still produce good errors: an author who
|
|
88
|
+
// configures nothing gets "Enter a valid email address", not "Invalid". The author's own `message`
|
|
89
|
+
// on a rule always wins; this is the fallback, and it is the only place a default lives.
|
|
90
|
+
//
|
|
91
|
+
// Consumers translate the key with their own i18n (the CMS client via ngx-translate, the engine via
|
|
92
|
+
// the page's locale bundle). The backend returns the key too, so a server-side rejection renders in
|
|
93
|
+
// the visitor's language rather than in the server's.
|
|
94
|
+
/** Namespace for every forms validation key. Keep in sync with `en.json` / `he.json`. */
|
|
95
|
+
const MESSAGE_NAMESPACE = 'forms.validation';
|
|
96
|
+
/**
|
|
97
|
+
* Rule → i18n key. Params travel beside the key (`{ n: 8 }`, `{ field: 'Email' }`) so a translation
|
|
98
|
+
* can place the number wherever its grammar needs it, rather than having it baked into the string.
|
|
99
|
+
*/
|
|
100
|
+
const RULE_MESSAGE_KEYS = Object.freeze({
|
|
101
|
+
[RuleName.Required]: `${MESSAGE_NAMESPACE}.required`,
|
|
102
|
+
[RuleName.MinLength]: `${MESSAGE_NAMESPACE}.minLength`,
|
|
103
|
+
[RuleName.MaxLength]: `${MESSAGE_NAMESPACE}.maxLength`,
|
|
104
|
+
[RuleName.Min]: `${MESSAGE_NAMESPACE}.min`,
|
|
105
|
+
[RuleName.Max]: `${MESSAGE_NAMESPACE}.max`,
|
|
106
|
+
[RuleName.Pattern]: `${MESSAGE_NAMESPACE}.pattern`,
|
|
107
|
+
[RuleName.Matches]: `${MESSAGE_NAMESPACE}.matches`,
|
|
108
|
+
[RuleName.Accepted]: `${MESSAGE_NAMESPACE}.accepted`,
|
|
109
|
+
[RuleName.FileType]: `${MESSAGE_NAMESPACE}.fileType`,
|
|
110
|
+
[RuleName.FileSize]: `${MESSAGE_NAMESPACE}.fileSize`,
|
|
111
|
+
});
|
|
112
|
+
/**
|
|
113
|
+
* A pattern failure deserves better than "Invalid format". Each preset gets its own key so the
|
|
114
|
+
* message can say what a valid value looks like, which is the difference between a visitor fixing
|
|
115
|
+
* a typo and a visitor giving up.
|
|
116
|
+
*/
|
|
117
|
+
const PATTERN_MESSAGE_KEYS = Object.freeze({
|
|
118
|
+
email: `${MESSAGE_NAMESPACE}.pattern.email`,
|
|
119
|
+
phone: `${MESSAGE_NAMESPACE}.pattern.phone`,
|
|
120
|
+
url: `${MESSAGE_NAMESPACE}.pattern.url`,
|
|
121
|
+
digits: `${MESSAGE_NAMESPACE}.pattern.digits`,
|
|
122
|
+
letters: `${MESSAGE_NAMESPACE}.pattern.letters`,
|
|
123
|
+
slug: `${MESSAGE_NAMESPACE}.pattern.slug`,
|
|
124
|
+
});
|
|
125
|
+
/** Form-level outcomes the visitor sees in the Error state. One designed block, five reasons. */
|
|
126
|
+
const FORM_ERROR_KEYS = Object.freeze({
|
|
127
|
+
Network: `${MESSAGE_NAMESPACE}.form.network`,
|
|
128
|
+
Rejected: `${MESSAGE_NAMESPACE}.form.rejected`,
|
|
129
|
+
Spam: `${MESSAGE_NAMESPACE}.form.spam`,
|
|
130
|
+
RateLimited: `${MESSAGE_NAMESPACE}.form.rateLimited`,
|
|
131
|
+
Server: `${MESSAGE_NAMESPACE}.form.server`,
|
|
132
|
+
});
|
|
133
|
+
/** The English defaults, so a consumer that has not added the keys yet still shows real text. */
|
|
134
|
+
const DEFAULT_EN_MESSAGES = Object.freeze({
|
|
135
|
+
[RULE_MESSAGE_KEYS[RuleName.Required]]: 'This field is required',
|
|
136
|
+
[RULE_MESSAGE_KEYS[RuleName.MinLength]]: 'Use at least {{n}} characters',
|
|
137
|
+
[RULE_MESSAGE_KEYS[RuleName.MaxLength]]: 'Use no more than {{n}} characters',
|
|
138
|
+
[RULE_MESSAGE_KEYS[RuleName.Min]]: 'Enter {{n}} or more',
|
|
139
|
+
[RULE_MESSAGE_KEYS[RuleName.Max]]: 'Enter {{n}} or less',
|
|
140
|
+
[RULE_MESSAGE_KEYS[RuleName.Pattern]]: 'Check the format of this value',
|
|
141
|
+
[RULE_MESSAGE_KEYS[RuleName.Matches]]: 'This must match {{field}}',
|
|
142
|
+
[RULE_MESSAGE_KEYS[RuleName.Accepted]]: 'Please tick this to continue',
|
|
143
|
+
[RULE_MESSAGE_KEYS[RuleName.FileType]]: 'That file type isn’t accepted',
|
|
144
|
+
[RULE_MESSAGE_KEYS[RuleName.FileSize]]: 'That file is larger than {{n}}MB',
|
|
145
|
+
[PATTERN_MESSAGE_KEYS['email']]: 'Enter a valid email address',
|
|
146
|
+
[PATTERN_MESSAGE_KEYS['phone']]: 'Enter a valid phone number',
|
|
147
|
+
[PATTERN_MESSAGE_KEYS['url']]: 'Enter a valid web address',
|
|
148
|
+
[PATTERN_MESSAGE_KEYS['digits']]: 'Use digits only',
|
|
149
|
+
[PATTERN_MESSAGE_KEYS['letters']]: 'Use letters only',
|
|
150
|
+
[PATTERN_MESSAGE_KEYS['slug']]: 'Use lowercase letters, numbers and hyphens',
|
|
151
|
+
[FORM_ERROR_KEYS.Network]: 'We couldn’t reach the server — check your connection and try again',
|
|
152
|
+
[FORM_ERROR_KEYS.Rejected]: 'Some answers need fixing',
|
|
153
|
+
[FORM_ERROR_KEYS.Spam]: 'We couldn’t accept this submission',
|
|
154
|
+
[FORM_ERROR_KEYS.RateLimited]: 'Too many attempts — please wait a moment and try again',
|
|
155
|
+
[FORM_ERROR_KEYS.Server]: 'Something went wrong on our side — your answers are still here',
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// The validator. ONE implementation, run by the builder canvas, the SSR engine and `iox-cms`.
|
|
159
|
+
//
|
|
160
|
+
// Client-side validation is UX; server-side validation is truth. Because both call this function,
|
|
161
|
+
// a visitor can never be told a value is fine and then have the server reject it, and a hand-crafted
|
|
162
|
+
// POST can never bypass a rule the author configured.
|
|
163
|
+
/**
|
|
164
|
+
* Validate one field's value.
|
|
165
|
+
*
|
|
166
|
+
* Order is deliberate and is part of the contract:
|
|
167
|
+
* 1. `required` (or `accepted` for a checkbox) — the only rules an EMPTY value can fail.
|
|
168
|
+
* 2. Everything else, in the order the author arranged them, but ONLY for a non-empty value.
|
|
169
|
+
*
|
|
170
|
+
* That second point is what stops an optional "Phone" field from shouting "enter a valid phone
|
|
171
|
+
* number" at someone who simply left it blank — the single most common validation-UX bug.
|
|
172
|
+
* The FIRST failure wins: a visitor fixes one thing at a time.
|
|
173
|
+
*/
|
|
174
|
+
function validateField(value, spec, form = {}) {
|
|
175
|
+
const rules = effectiveRules(spec);
|
|
176
|
+
const empty = isEmpty(value, spec.control);
|
|
177
|
+
for (const rule of rules) {
|
|
178
|
+
const appliesToEmpty = rule.name === RuleName.Required || rule.name === RuleName.Accepted;
|
|
179
|
+
if (empty && !appliesToEmpty)
|
|
180
|
+
continue;
|
|
181
|
+
const failure = check(rule, value, spec, form);
|
|
182
|
+
if (failure) {
|
|
183
|
+
return {
|
|
184
|
+
valid: false,
|
|
185
|
+
messageKey: rule.message ?? failure.messageKey,
|
|
186
|
+
params: failure.params,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return { valid: true };
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Validate a whole submission. Every field is checked (not short-circuited at the first bad one) so
|
|
194
|
+
* the visitor sees every problem at once instead of fixing them one reload at a time.
|
|
195
|
+
*/
|
|
196
|
+
function validateForm(values, spec) {
|
|
197
|
+
const errors = {};
|
|
198
|
+
for (const field of spec.fields ?? []) {
|
|
199
|
+
const result = validateField(values?.[field.name], field, values ?? {});
|
|
200
|
+
if (!result.valid) {
|
|
201
|
+
errors[field.name] = { messageKey: result.messageKey, params: result.params };
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return { valid: Object.keys(errors).length === 0, errors };
|
|
205
|
+
}
|
|
206
|
+
/** The name of the first field with an error, in spec order — what the browser should focus. */
|
|
207
|
+
function firstInvalidField(spec, errors) {
|
|
208
|
+
for (const field of spec.fields ?? [])
|
|
209
|
+
if (errors[field.name])
|
|
210
|
+
return field.name;
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
// ─── internals ───────────────────────────────────────────────────────────────
|
|
214
|
+
/**
|
|
215
|
+
* The author's rules, plus the ones the field's own configuration implies: `required` from the
|
|
216
|
+
* switch (rather than making the author add a rule for the commonest case) and the pattern implied
|
|
217
|
+
* by an Email/Tel/Url control. An explicitly declared rule of the same name always wins, so an
|
|
218
|
+
* author who sets their own `pattern` on an Email field is not fighting an invisible second one.
|
|
219
|
+
*/
|
|
220
|
+
function effectiveRules(spec) {
|
|
221
|
+
const declared = spec.rules ?? [];
|
|
222
|
+
const has = (name) => declared.some(r => r.name === name);
|
|
223
|
+
const implied = [];
|
|
224
|
+
if (spec.required && !has(RuleName.Required)) {
|
|
225
|
+
// A checkbox's "required" means TICKED, which is a different message and a different check.
|
|
226
|
+
implied.push(spec.control === ControlType.Checkbox
|
|
227
|
+
? { name: RuleName.Accepted }
|
|
228
|
+
: { name: RuleName.Required });
|
|
229
|
+
}
|
|
230
|
+
const preset = IMPLIED_PATTERN[spec.control];
|
|
231
|
+
if (preset && !has(RuleName.Pattern)) {
|
|
232
|
+
implied.push({ name: RuleName.Pattern, params: { preset } });
|
|
233
|
+
}
|
|
234
|
+
return [...implied, ...declared];
|
|
235
|
+
}
|
|
236
|
+
/** Empty means "the visitor gave us nothing" — per control type, not a blanket falsy check. */
|
|
237
|
+
function isEmpty(value, control) {
|
|
238
|
+
if (value === null || value === undefined)
|
|
239
|
+
return true;
|
|
240
|
+
if (control === ControlType.Checkbox)
|
|
241
|
+
return value === false;
|
|
242
|
+
if (Array.isArray(value))
|
|
243
|
+
return value.length === 0;
|
|
244
|
+
if (typeof value === 'string')
|
|
245
|
+
return value.trim() === '';
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
function check(rule, value, spec, form) {
|
|
249
|
+
const fail = (messageKey, params) => ({ messageKey, params });
|
|
250
|
+
const defaultKey = RULE_MESSAGE_KEYS[rule.name];
|
|
251
|
+
const n = Number(rule.params?.['n']);
|
|
252
|
+
switch (rule.name) {
|
|
253
|
+
case RuleName.Required:
|
|
254
|
+
return isEmpty(value, spec.control) ? fail(defaultKey) : null;
|
|
255
|
+
case RuleName.Accepted:
|
|
256
|
+
return value === true ? null : fail(defaultKey);
|
|
257
|
+
case RuleName.MinLength:
|
|
258
|
+
return text(value).length < n ? fail(defaultKey, { n }) : null;
|
|
259
|
+
case RuleName.MaxLength:
|
|
260
|
+
return text(value).length > n ? fail(defaultKey, { n }) : null;
|
|
261
|
+
case RuleName.Min: {
|
|
262
|
+
const num = numeric(value);
|
|
263
|
+
return num === null || num < n ? fail(defaultKey, { n }) : null;
|
|
264
|
+
}
|
|
265
|
+
case RuleName.Max: {
|
|
266
|
+
const num = numeric(value);
|
|
267
|
+
return num === null || num > n ? fail(defaultKey, { n }) : null;
|
|
268
|
+
}
|
|
269
|
+
case RuleName.Pattern: {
|
|
270
|
+
const regex = patternFor(rule);
|
|
271
|
+
if (!regex)
|
|
272
|
+
return null; // an unparsable custom regex must not block a submission
|
|
273
|
+
if (regex.test(text(value)))
|
|
274
|
+
return null;
|
|
275
|
+
const preset = rule.params?.['preset'];
|
|
276
|
+
return fail((preset && PATTERN_MESSAGE_KEYS[preset]) || defaultKey);
|
|
277
|
+
}
|
|
278
|
+
case RuleName.Matches: {
|
|
279
|
+
const other = String(rule.params?.['field'] ?? '');
|
|
280
|
+
return text(value) === text(form?.[other]) ? null : fail(defaultKey, { field: other });
|
|
281
|
+
}
|
|
282
|
+
case RuleName.FileType: {
|
|
283
|
+
const accepted = list(rule.params?.['types']).map(t => t.toLowerCase().replace(/^\./, ''));
|
|
284
|
+
if (!accepted.length)
|
|
285
|
+
return null;
|
|
286
|
+
const bad = files(value).some(f => {
|
|
287
|
+
const ext = (f.name ?? '').split('.').pop()?.toLowerCase() ?? '';
|
|
288
|
+
const mime = (f.type ?? '').toLowerCase();
|
|
289
|
+
return !accepted.some(t => t === ext || mime === t || mime.startsWith(t + '/'));
|
|
290
|
+
});
|
|
291
|
+
return bad ? fail(defaultKey, { types: accepted.join(', ') }) : null;
|
|
292
|
+
}
|
|
293
|
+
case RuleName.FileSize: {
|
|
294
|
+
const maxBytes = n * 1024 * 1024;
|
|
295
|
+
return files(value).some(f => (f.size ?? 0) > maxBytes) ? fail(defaultKey, { n }) : null;
|
|
296
|
+
}
|
|
297
|
+
default:
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
function patternFor(rule) {
|
|
302
|
+
const preset = rule.params?.['preset'];
|
|
303
|
+
if (preset && PATTERN_PRESETS[preset])
|
|
304
|
+
return PATTERN_PRESETS[preset];
|
|
305
|
+
const custom = rule.params?.['regex'];
|
|
306
|
+
if (typeof custom !== 'string' || !custom)
|
|
307
|
+
return null;
|
|
308
|
+
// A malformed regex is the AUTHOR's mistake; failing open is the only safe reading, because
|
|
309
|
+
// failing closed would make a live form permanently unsubmittable for every visitor.
|
|
310
|
+
try {
|
|
311
|
+
return new RegExp(custom);
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
const text = (value) => value === null || value === undefined ? '' : String(value).trim();
|
|
318
|
+
function numeric(value) {
|
|
319
|
+
const parsed = typeof value === 'number' ? value : parseFloat(text(value));
|
|
320
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
321
|
+
}
|
|
322
|
+
const list = (value) => Array.isArray(value) ? value.map(String)
|
|
323
|
+
: typeof value === 'string' ? value.split(',').map(s => s.trim()).filter(Boolean)
|
|
324
|
+
: [];
|
|
325
|
+
const files = (value) => Array.isArray(value) ? value
|
|
326
|
+
: value && typeof value === 'object' ? [value]
|
|
327
|
+
: [];
|
|
328
|
+
|
|
329
|
+
// Derive a FormSpec from a node tree.
|
|
330
|
+
//
|
|
331
|
+
// The tree the author edited is the SINGLE source of truth for "what fields does this form have".
|
|
332
|
+
// The spec is derived from it — never maintained beside it — so a field cannot exist in one and not
|
|
333
|
+
// the other. The builder runs this on save and stores the result in the page document; the backend
|
|
334
|
+
// reads THAT stored spec to re-validate a submission (it must never trust a spec sent by the
|
|
335
|
+
// browser). See iox-ai-guidance `architecture/builder/forms-and-inputs-plan.md` §5.1 and D11.
|
|
336
|
+
const FORM_NODE_TYPE = 'Form';
|
|
337
|
+
const FIELD_NODE_TYPE = 'Field';
|
|
338
|
+
/** Always present, never removable — the safety net that makes "store first, deliver after" true. */
|
|
339
|
+
const STORE_ACTION = Object.freeze({ type: 'store' });
|
|
340
|
+
/**
|
|
341
|
+
* Turn a label into the key that arrives in the submitted data — `Full name` → `fullName`.
|
|
342
|
+
* Authors never have to invent keys (decision: auto-derive, with an editable override).
|
|
343
|
+
*
|
|
344
|
+
* Diacritics are stripped rather than dropped, so a Hebrew or accented label still yields a usable
|
|
345
|
+
* ASCII key instead of an empty one; a label with no usable characters falls back to `field`.
|
|
346
|
+
*/
|
|
347
|
+
function deriveFieldName(label) {
|
|
348
|
+
const words = (label ?? '')
|
|
349
|
+
.normalize('NFD').replace(/[̀-ͯ]/g, '')
|
|
350
|
+
.replace(/[^a-zA-Z0-9]+/g, ' ')
|
|
351
|
+
.trim()
|
|
352
|
+
.split(/\s+/)
|
|
353
|
+
.filter(Boolean);
|
|
354
|
+
if (!words.length)
|
|
355
|
+
return 'field';
|
|
356
|
+
const [first, ...rest] = words;
|
|
357
|
+
const name = first.toLowerCase() + rest.map(w => w[0].toUpperCase() + w.slice(1).toLowerCase()).join('');
|
|
358
|
+
// A key must not start with a digit — it ends up as an object key and a form control id.
|
|
359
|
+
return /^\d/.test(name) ? `field${name}` : name;
|
|
360
|
+
}
|
|
361
|
+
/** Make `name` unique within `taken`, appending 2, 3, … — two "Email" fields must not collide. */
|
|
362
|
+
function uniqueFieldName(name, taken) {
|
|
363
|
+
if (!taken.has(name))
|
|
364
|
+
return name;
|
|
365
|
+
let n = 2;
|
|
366
|
+
while (taken.has(`${name}${n}`))
|
|
367
|
+
n++;
|
|
368
|
+
return `${name}${n}`;
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Walk a Form subtree and produce its spec.
|
|
372
|
+
*
|
|
373
|
+
* Nested forms are not collected: a Field belongs to its NEAREST ancestor Form (decision D1), so the
|
|
374
|
+
* walk stops at any inner Form node rather than stealing its fields.
|
|
375
|
+
*/
|
|
376
|
+
function collectFormSpec(node) {
|
|
377
|
+
if (!node || node.type !== FORM_NODE_TYPE)
|
|
378
|
+
return null;
|
|
379
|
+
const props = node.props ?? {};
|
|
380
|
+
const fields = [];
|
|
381
|
+
const taken = new Set();
|
|
382
|
+
for (const fieldNode of collectFieldNodes(node.children ?? [])) {
|
|
383
|
+
const spec = toFieldSpec(fieldNode, taken);
|
|
384
|
+
taken.add(spec.name);
|
|
385
|
+
fields.push(spec);
|
|
386
|
+
}
|
|
387
|
+
return {
|
|
388
|
+
formId: props['formId'] ?? '',
|
|
389
|
+
name: props['name'] ?? 'Form',
|
|
390
|
+
feedback: props['feedback'] ?? FeedbackTiming.Smart,
|
|
391
|
+
fields,
|
|
392
|
+
actions: normaliseActions(props['actions']),
|
|
393
|
+
antiSpam: {
|
|
394
|
+
honeypot: props['honeypot'] !== false,
|
|
395
|
+
minFillMs: Number(props['minFillMs'] ?? 1200),
|
|
396
|
+
...(props['turnstile'] ? { turnstile: true } : {}),
|
|
397
|
+
},
|
|
398
|
+
...(props['retentionDays'] ? { retention: { days: Number(props['retentionDays']) } } : {}),
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
/** Every Field in this subtree, in document order, not descending into a nested Form. */
|
|
402
|
+
function collectFieldNodes(nodes) {
|
|
403
|
+
const out = [];
|
|
404
|
+
for (const node of nodes ?? []) {
|
|
405
|
+
if (!node?.type)
|
|
406
|
+
continue;
|
|
407
|
+
if (node.type === FORM_NODE_TYPE)
|
|
408
|
+
continue; // a nested form owns its own fields
|
|
409
|
+
if (node.type === FIELD_NODE_TYPE) {
|
|
410
|
+
out.push(node);
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
out.push(...collectFieldNodes(node.children ?? []));
|
|
414
|
+
}
|
|
415
|
+
return out;
|
|
416
|
+
}
|
|
417
|
+
function toFieldSpec(node, taken) {
|
|
418
|
+
const props = node.props ?? {};
|
|
419
|
+
const label = props['label'] ?? '';
|
|
420
|
+
// The author's explicit override wins; otherwise derive from the label.
|
|
421
|
+
const name = uniqueFieldName(props['name'] || deriveFieldName(label), taken);
|
|
422
|
+
const spec = {
|
|
423
|
+
name,
|
|
424
|
+
control: props['control'] ?? ControlType.Text,
|
|
425
|
+
label,
|
|
426
|
+
};
|
|
427
|
+
if (props['required'])
|
|
428
|
+
spec.required = true;
|
|
429
|
+
const rules = normaliseRules(props['rules']);
|
|
430
|
+
if (rules.length)
|
|
431
|
+
spec.rules = rules;
|
|
432
|
+
if (Array.isArray(props['options']) && props['options'].length)
|
|
433
|
+
spec.options = props['options'];
|
|
434
|
+
if (props['optionsSource'])
|
|
435
|
+
spec.optionsSource = props['optionsSource'];
|
|
436
|
+
if (props['defaultValue'] !== undefined && props['defaultValue'] !== '') {
|
|
437
|
+
spec.defaultValue = props['defaultValue'];
|
|
438
|
+
}
|
|
439
|
+
return spec;
|
|
440
|
+
}
|
|
441
|
+
/** Drop anything that isn't a rule we know — a hand-edited page must not break validation. */
|
|
442
|
+
function normaliseRules(raw) {
|
|
443
|
+
if (!Array.isArray(raw))
|
|
444
|
+
return [];
|
|
445
|
+
const known = new Set(Object.values(RuleName));
|
|
446
|
+
return raw
|
|
447
|
+
.filter(r => r && known.has(r.name))
|
|
448
|
+
.map(r => ({
|
|
449
|
+
name: r.name,
|
|
450
|
+
...(r.params ? { params: r.params } : {}),
|
|
451
|
+
...(r.message ? { message: r.message } : {}),
|
|
452
|
+
}));
|
|
453
|
+
}
|
|
454
|
+
/** `store` is always first and always present, however the page was edited. */
|
|
455
|
+
function normaliseActions(raw) {
|
|
456
|
+
const declared = Array.isArray(raw)
|
|
457
|
+
? raw.filter(a => a && typeof a.type === 'string')
|
|
458
|
+
: [];
|
|
459
|
+
return [STORE_ACTION, ...declared.filter(a => a.type !== STORE_ACTION.type)];
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/*
|
|
463
|
+
* @vectoriox/iox-ui/forms-core — the shared, FRAMEWORK-FREE core of the forms feature.
|
|
464
|
+
*
|
|
465
|
+
* ⚠️ Nothing in this entry point may import Angular, RxJS, the DOM, or Node. It is consumed by
|
|
466
|
+
* THREE runtimes: the builder canvas (Angular), the SSR engine (Angular), and `iox-cms` (NestJS,
|
|
467
|
+
* plain Node). The main `@vectoriox/iox-ui` entry pulls in `@angular/core`, which Nest must never
|
|
468
|
+
* load — that is the entire reason this secondary entry point exists.
|
|
469
|
+
*
|
|
470
|
+
* Client-side validation is UX; server-side validation is truth. Both run THIS code, so they cannot
|
|
471
|
+
* disagree. See iox-ai-guidance `architecture/builder/forms-and-inputs-plan.md` §5.
|
|
472
|
+
*/
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Generated bundle index. Do not edit.
|
|
476
|
+
*/
|
|
477
|
+
|
|
478
|
+
export { ControlType, DEFAULT_EN_MESSAGES, FIELD_NODE_TYPE, FORM_ERROR_KEYS, FORM_NODE_TYPE, FeedbackTiming, IMPLIED_PATTERN, MESSAGE_NAMESPACE, MULTI_VALUE_CONTROLS, PATTERN_MESSAGE_KEYS, PATTERN_PRESETS, PatternPreset, RULE_MESSAGE_KEYS, RuleName, STORE_ACTION, collectFieldNodes, collectFormSpec, deriveFieldName, firstInvalidField, uniqueFieldName, validateField, validateForm };
|
|
479
|
+
//# sourceMappingURL=vectoriox-iox-ui-forms-core.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vectoriox-iox-ui-forms-core.mjs","sources":["../../../projects/iox-ui/src/forms-core/form.model.ts","../../../projects/iox-ui/src/forms-core/messages.ts","../../../projects/iox-ui/src/forms-core/validate.ts","../../../projects/iox-ui/src/forms-core/collect.ts","../../../projects/iox-ui/src/forms-core/public-api.ts","../../../projects/iox-ui/src/forms-core/vectoriox-iox-ui-forms-core.ts"],"sourcesContent":["// The persisted shape of a form. Pure types + the one derived constant each renderer needs.\n//\n// A FormSpec is DERIVED from the node tree (see `collect.ts`), never hand-maintained beside it —\n// there is one source of truth for \"what fields does this form have\", and it is the tree the author\n// edited. The builder writes the derived spec into the saved page so the SERVER can re-validate\n// against it without re-implementing the builder's tree walk.\n\n/** What the visitor actually types into. Chosen by a trait; it also implies a validation rule. */\nexport enum ControlType {\n Text = 'text',\n Email = 'email',\n Tel = 'tel',\n Url = 'url',\n Number = 'number',\n Password = 'password',\n Textarea = 'textarea',\n Select = 'select',\n Checkbox = 'checkbox',\n Radio = 'radio',\n Date = 'date',\n Time = 'time',\n File = 'file',\n /** Not shown; carries a constant or a bound value (campaign, source page, …). */\n Hidden = 'hidden',\n}\n\n/** The v1 rule grammar. Phase 3's conditional logic reuses these same names for its conditions. */\nexport enum RuleName {\n Required = 'required',\n MinLength = 'minLength',\n MaxLength = 'maxLength',\n Min = 'min',\n Max = 'max',\n Pattern = 'pattern',\n /** Cross-field: this value must equal another field's (confirm email / password). */\n Matches = 'matches',\n /** A checkbox that must be ticked — terms, consent. */\n Accepted = 'accepted',\n FileType = 'fileType',\n FileSize = 'fileSize',\n}\n\n/** Named regexes so an author picks \"Email\" rather than writing one. */\nexport enum PatternPreset {\n Email = 'email',\n Phone = 'phone',\n Url = 'url',\n Digits = 'digits',\n Letters = 'letters',\n Slug = 'slug',\n}\n\n/**\n * Deliberately permissive. A validator that rejects `user+tag@sub.domain.co.uk` or a real-world\n * phone number is worse than one that lets a typo through: the address gets verified by sending to\n * it, and a false rejection is a lost lead with no recourse.\n */\nexport const PATTERN_PRESETS: Readonly<Record<PatternPreset, RegExp>> = Object.freeze({\n [PatternPreset.Email]: /^[^\\s@]+@[^\\s@.]+(\\.[^\\s@.]+)+$/,\n [PatternPreset.Phone]: /^\\+?[\\d\\s().-]{6,20}$/,\n [PatternPreset.Url]: /^(https?:\\/\\/)?[^\\s.]+\\.[^\\s]{2,}$/i,\n [PatternPreset.Digits]: /^\\d+$/,\n [PatternPreset.Letters]: /^[\\p{L}\\s'-]+$/u,\n [PatternPreset.Slug]: /^[a-z0-9]+(-[a-z0-9]+)*$/,\n});\n\nexport interface ValidationRule {\n name: RuleName;\n /** `{ n }` for lengths/bounds · `{ preset } | { regex }` for pattern · `{ field }` for matches. */\n params?: Record<string, any>;\n /** Author's override. Absent → the shared message catalog supplies a translated key. */\n message?: string;\n}\n\nexport interface FieldOption {\n label: string;\n value: string;\n}\n\n/** Options pulled from a data source instead of typed by hand (a Select of CMS categories). */\nexport interface FieldOptionSource {\n source: string;\n sourcePath?: string;\n valuePath: string;\n labelPath: string;\n}\n\nexport interface FieldSpec {\n /** The submitted key. Derived from the label, unique within the form, editable. */\n name: string;\n control: ControlType;\n label: string;\n /** Surfaced as its own switch in the panel rather than buried in the rule list. */\n required?: boolean;\n rules?: ValidationRule[];\n options?: FieldOption[];\n optionsSource?: FieldOptionSource;\n defaultValue?: any;\n}\n\n/** When a field is validated, once the author has stopped deciding it per field. */\nexport enum FeedbackTiming {\n /** Validate on submit, then live per-field once that field has been touched or has failed. */\n Smart = 'smart',\n Submit = 'submit',\n Live = 'live',\n}\n\n/** What happens to a submission, in order. Card #1 (`store`) is always present. */\nexport interface SubmitAction {\n type: string;\n config?: Record<string, any>;\n /** `retry` (default) · `fail` · `ignore`. */\n onFailure?: 'retry' | 'fail' | 'ignore';\n}\n\nexport interface FormSpec {\n formId: string;\n /** Author-facing name — the submissions inbox groups by it. */\n name: string;\n feedback: FeedbackTiming;\n fields: FieldSpec[];\n actions: SubmitAction[];\n antiSpam: { honeypot: boolean; minFillMs: number; turnstile?: boolean };\n retention?: { days: number };\n}\n\n/**\n * The rule a control type implies with no configuration. Choosing \"Email\" IS choosing email\n * validation — the single biggest reason the panel can stay empty and still validate well.\n */\nexport const IMPLIED_PATTERN: Readonly<Partial<Record<ControlType, PatternPreset>>> = Object.freeze({\n [ControlType.Email]: PatternPreset.Email,\n [ControlType.Tel]: PatternPreset.Phone,\n [ControlType.Url]: PatternPreset.Url,\n});\n\n/** Controls whose value is a list rather than a scalar. */\nexport const MULTI_VALUE_CONTROLS: ReadonlySet<ControlType> = new Set([ControlType.File]);\n","// The default message for every rule, as an i18n KEY — never a rendered string.\n//\n// This is what lets the Validation panel stay empty and still produce good errors: an author who\n// configures nothing gets \"Enter a valid email address\", not \"Invalid\". The author's own `message`\n// on a rule always wins; this is the fallback, and it is the only place a default lives.\n//\n// Consumers translate the key with their own i18n (the CMS client via ngx-translate, the engine via\n// the page's locale bundle). The backend returns the key too, so a server-side rejection renders in\n// the visitor's language rather than in the server's.\n\nimport { RuleName } from './form.model';\n\n/** Namespace for every forms validation key. Keep in sync with `en.json` / `he.json`. */\nexport const MESSAGE_NAMESPACE = 'forms.validation';\n\n/**\n * Rule → i18n key. Params travel beside the key (`{ n: 8 }`, `{ field: 'Email' }`) so a translation\n * can place the number wherever its grammar needs it, rather than having it baked into the string.\n */\nexport const RULE_MESSAGE_KEYS: Readonly<Record<RuleName, string>> = Object.freeze({\n [RuleName.Required]: `${MESSAGE_NAMESPACE}.required`,\n [RuleName.MinLength]: `${MESSAGE_NAMESPACE}.minLength`,\n [RuleName.MaxLength]: `${MESSAGE_NAMESPACE}.maxLength`,\n [RuleName.Min]: `${MESSAGE_NAMESPACE}.min`,\n [RuleName.Max]: `${MESSAGE_NAMESPACE}.max`,\n [RuleName.Pattern]: `${MESSAGE_NAMESPACE}.pattern`,\n [RuleName.Matches]: `${MESSAGE_NAMESPACE}.matches`,\n [RuleName.Accepted]: `${MESSAGE_NAMESPACE}.accepted`,\n [RuleName.FileType]: `${MESSAGE_NAMESPACE}.fileType`,\n [RuleName.FileSize]: `${MESSAGE_NAMESPACE}.fileSize`,\n});\n\n/**\n * A pattern failure deserves better than \"Invalid format\". Each preset gets its own key so the\n * message can say what a valid value looks like, which is the difference between a visitor fixing\n * a typo and a visitor giving up.\n */\nexport const PATTERN_MESSAGE_KEYS: Readonly<Record<string, string>> = Object.freeze({\n email: `${MESSAGE_NAMESPACE}.pattern.email`,\n phone: `${MESSAGE_NAMESPACE}.pattern.phone`,\n url: `${MESSAGE_NAMESPACE}.pattern.url`,\n digits: `${MESSAGE_NAMESPACE}.pattern.digits`,\n letters: `${MESSAGE_NAMESPACE}.pattern.letters`,\n slug: `${MESSAGE_NAMESPACE}.pattern.slug`,\n});\n\n/** Form-level outcomes the visitor sees in the Error state. One designed block, five reasons. */\nexport const FORM_ERROR_KEYS = Object.freeze({\n Network: `${MESSAGE_NAMESPACE}.form.network`,\n Rejected: `${MESSAGE_NAMESPACE}.form.rejected`,\n Spam: `${MESSAGE_NAMESPACE}.form.spam`,\n RateLimited: `${MESSAGE_NAMESPACE}.form.rateLimited`,\n Server: `${MESSAGE_NAMESPACE}.form.server`,\n});\n\n/** The English defaults, so a consumer that has not added the keys yet still shows real text. */\nexport const DEFAULT_EN_MESSAGES: Readonly<Record<string, string>> = Object.freeze({\n [RULE_MESSAGE_KEYS[RuleName.Required]]: 'This field is required',\n [RULE_MESSAGE_KEYS[RuleName.MinLength]]: 'Use at least {{n}} characters',\n [RULE_MESSAGE_KEYS[RuleName.MaxLength]]: 'Use no more than {{n}} characters',\n [RULE_MESSAGE_KEYS[RuleName.Min]]: 'Enter {{n}} or more',\n [RULE_MESSAGE_KEYS[RuleName.Max]]: 'Enter {{n}} or less',\n [RULE_MESSAGE_KEYS[RuleName.Pattern]]: 'Check the format of this value',\n [RULE_MESSAGE_KEYS[RuleName.Matches]]: 'This must match {{field}}',\n [RULE_MESSAGE_KEYS[RuleName.Accepted]]: 'Please tick this to continue',\n [RULE_MESSAGE_KEYS[RuleName.FileType]]: 'That file type isn’t accepted',\n [RULE_MESSAGE_KEYS[RuleName.FileSize]]: 'That file is larger than {{n}}MB',\n [PATTERN_MESSAGE_KEYS['email']]: 'Enter a valid email address',\n [PATTERN_MESSAGE_KEYS['phone']]: 'Enter a valid phone number',\n [PATTERN_MESSAGE_KEYS['url']]: 'Enter a valid web address',\n [PATTERN_MESSAGE_KEYS['digits']]: 'Use digits only',\n [PATTERN_MESSAGE_KEYS['letters']]: 'Use letters only',\n [PATTERN_MESSAGE_KEYS['slug']]: 'Use lowercase letters, numbers and hyphens',\n [FORM_ERROR_KEYS.Network]: 'We couldn’t reach the server — check your connection and try again',\n [FORM_ERROR_KEYS.Rejected]: 'Some answers need fixing',\n [FORM_ERROR_KEYS.Spam]: 'We couldn’t accept this submission',\n [FORM_ERROR_KEYS.RateLimited]: 'Too many attempts — please wait a moment and try again',\n [FORM_ERROR_KEYS.Server]: 'Something went wrong on our side — your answers are still here',\n});\n","// The validator. ONE implementation, run by the builder canvas, the SSR engine and `iox-cms`.\n//\n// Client-side validation is UX; server-side validation is truth. Because both call this function,\n// a visitor can never be told a value is fine and then have the server reject it, and a hand-crafted\n// POST can never bypass a rule the author configured.\n\nimport {\n ControlType,\n FieldSpec,\n FormSpec,\n IMPLIED_PATTERN,\n PATTERN_PRESETS,\n PatternPreset,\n RuleName,\n ValidationRule,\n} from './form.model';\nimport { PATTERN_MESSAGE_KEYS, RULE_MESSAGE_KEYS } from './messages';\n\nexport interface FieldError {\n /** i18n key, or the author's own override string when they set one. */\n messageKey: string;\n /** Interpolation values (`{ n: 8 }`, `{ field: 'Email' }`). */\n params?: Record<string, any>;\n}\n\nexport interface FieldResult extends Partial<FieldError> {\n valid: boolean;\n}\n\nexport interface FormResult {\n valid: boolean;\n /** Keyed by field name. Empty when valid. */\n errors: Record<string, FieldError>;\n}\n\n/** A file, as either runtime hands it over. Kept structural so this file imports no DOM types. */\ninterface FileLike {\n name?: string;\n size?: number;\n type?: string;\n}\n\n/**\n * Validate one field's value.\n *\n * Order is deliberate and is part of the contract:\n * 1. `required` (or `accepted` for a checkbox) — the only rules an EMPTY value can fail.\n * 2. Everything else, in the order the author arranged them, but ONLY for a non-empty value.\n *\n * That second point is what stops an optional \"Phone\" field from shouting \"enter a valid phone\n * number\" at someone who simply left it blank — the single most common validation-UX bug.\n * The FIRST failure wins: a visitor fixes one thing at a time.\n */\nexport function validateField(\n value: unknown,\n spec: FieldSpec,\n form: Record<string, unknown> = {},\n): FieldResult {\n const rules = effectiveRules(spec);\n const empty = isEmpty(value, spec.control);\n\n for (const rule of rules) {\n const appliesToEmpty = rule.name === RuleName.Required || rule.name === RuleName.Accepted;\n if (empty && !appliesToEmpty) continue;\n\n const failure = check(rule, value, spec, form);\n if (failure) {\n return {\n valid: false,\n messageKey: rule.message ?? failure.messageKey,\n params: failure.params,\n };\n }\n }\n return { valid: true };\n}\n\n/**\n * Validate a whole submission. Every field is checked (not short-circuited at the first bad one) so\n * the visitor sees every problem at once instead of fixing them one reload at a time.\n */\nexport function validateForm(\n values: Record<string, unknown>,\n spec: FormSpec,\n): FormResult {\n const errors: Record<string, FieldError> = {};\n for (const field of spec.fields ?? []) {\n const result = validateField(values?.[field.name], field, values ?? {});\n if (!result.valid) {\n errors[field.name] = { messageKey: result.messageKey!, params: result.params };\n }\n }\n return { valid: Object.keys(errors).length === 0, errors };\n}\n\n/** The name of the first field with an error, in spec order — what the browser should focus. */\nexport function firstInvalidField(spec: FormSpec, errors: Record<string, FieldError>): string | null {\n for (const field of spec.fields ?? []) if (errors[field.name]) return field.name;\n return null;\n}\n\n// ─── internals ───────────────────────────────────────────────────────────────\n\n/**\n * The author's rules, plus the ones the field's own configuration implies: `required` from the\n * switch (rather than making the author add a rule for the commonest case) and the pattern implied\n * by an Email/Tel/Url control. An explicitly declared rule of the same name always wins, so an\n * author who sets their own `pattern` on an Email field is not fighting an invisible second one.\n */\nfunction effectiveRules(spec: FieldSpec): ValidationRule[] {\n const declared = spec.rules ?? [];\n const has = (name: RuleName) => declared.some(r => r.name === name);\n const implied: ValidationRule[] = [];\n\n if (spec.required && !has(RuleName.Required)) {\n // A checkbox's \"required\" means TICKED, which is a different message and a different check.\n implied.push(spec.control === ControlType.Checkbox\n ? { name: RuleName.Accepted }\n : { name: RuleName.Required });\n }\n\n const preset = IMPLIED_PATTERN[spec.control];\n if (preset && !has(RuleName.Pattern)) {\n implied.push({ name: RuleName.Pattern, params: { preset } });\n }\n\n return [...implied, ...declared];\n}\n\n/** Empty means \"the visitor gave us nothing\" — per control type, not a blanket falsy check. */\nfunction isEmpty(value: unknown, control: ControlType): boolean {\n if (value === null || value === undefined) return true;\n if (control === ControlType.Checkbox) return value === false;\n if (Array.isArray(value)) return value.length === 0;\n if (typeof value === 'string') return value.trim() === '';\n return false;\n}\n\nfunction check(\n rule: ValidationRule,\n value: unknown,\n spec: FieldSpec,\n form: Record<string, unknown>,\n): FieldError | null {\n const fail = (messageKey: string, params?: Record<string, any>): FieldError => ({ messageKey, params });\n const defaultKey = RULE_MESSAGE_KEYS[rule.name];\n const n = Number(rule.params?.['n']);\n\n switch (rule.name) {\n case RuleName.Required:\n return isEmpty(value, spec.control) ? fail(defaultKey) : null;\n\n case RuleName.Accepted:\n return value === true ? null : fail(defaultKey);\n\n case RuleName.MinLength:\n return text(value).length < n ? fail(defaultKey, { n }) : null;\n\n case RuleName.MaxLength:\n return text(value).length > n ? fail(defaultKey, { n }) : null;\n\n case RuleName.Min: {\n const num = numeric(value);\n return num === null || num < n ? fail(defaultKey, { n }) : null;\n }\n\n case RuleName.Max: {\n const num = numeric(value);\n return num === null || num > n ? fail(defaultKey, { n }) : null;\n }\n\n case RuleName.Pattern: {\n const regex = patternFor(rule);\n if (!regex) return null; // an unparsable custom regex must not block a submission\n if (regex.test(text(value))) return null;\n const preset = rule.params?.['preset'] as string | undefined;\n return fail((preset && PATTERN_MESSAGE_KEYS[preset]) || defaultKey);\n }\n\n case RuleName.Matches: {\n const other = String(rule.params?.['field'] ?? '');\n return text(value) === text(form?.[other]) ? null : fail(defaultKey, { field: other });\n }\n\n case RuleName.FileType: {\n const accepted = list(rule.params?.['types']).map(t => t.toLowerCase().replace(/^\\./, ''));\n if (!accepted.length) return null;\n const bad = files(value).some(f => {\n const ext = (f.name ?? '').split('.').pop()?.toLowerCase() ?? '';\n const mime = (f.type ?? '').toLowerCase();\n return !accepted.some(t => t === ext || mime === t || mime.startsWith(t + '/'));\n });\n return bad ? fail(defaultKey, { types: accepted.join(', ') }) : null;\n }\n\n case RuleName.FileSize: {\n const maxBytes = n * 1024 * 1024;\n return files(value).some(f => (f.size ?? 0) > maxBytes) ? fail(defaultKey, { n }) : null;\n }\n\n default:\n return null;\n }\n}\n\nfunction patternFor(rule: ValidationRule): RegExp | null {\n const preset = rule.params?.['preset'] as PatternPreset | undefined;\n if (preset && PATTERN_PRESETS[preset]) return PATTERN_PRESETS[preset];\n const custom = rule.params?.['regex'];\n if (typeof custom !== 'string' || !custom) return null;\n // A malformed regex is the AUTHOR's mistake; failing open is the only safe reading, because\n // failing closed would make a live form permanently unsubmittable for every visitor.\n try { return new RegExp(custom); } catch { return null; }\n}\n\nconst text = (value: unknown): string =>\n value === null || value === undefined ? '' : String(value).trim();\n\nfunction numeric(value: unknown): number | null {\n const parsed = typeof value === 'number' ? value : parseFloat(text(value));\n return Number.isFinite(parsed) ? parsed : null;\n}\n\nconst list = (value: unknown): string[] =>\n Array.isArray(value) ? value.map(String)\n : typeof value === 'string' ? value.split(',').map(s => s.trim()).filter(Boolean)\n : [];\n\nconst files = (value: unknown): FileLike[] =>\n Array.isArray(value) ? value as FileLike[]\n : value && typeof value === 'object' ? [value as FileLike]\n : [];\n","// Derive a FormSpec from a node tree.\n//\n// The tree the author edited is the SINGLE source of truth for \"what fields does this form have\".\n// The spec is derived from it — never maintained beside it — so a field cannot exist in one and not\n// the other. The builder runs this on save and stores the result in the page document; the backend\n// reads THAT stored spec to re-validate a submission (it must never trust a spec sent by the\n// browser). See iox-ai-guidance `architecture/builder/forms-and-inputs-plan.md` §5.1 and D11.\n\nimport {\n ControlType,\n FeedbackTiming,\n FieldSpec,\n FormSpec,\n RuleName,\n SubmitAction,\n ValidationRule,\n} from './form.model';\n\n/** The minimum a node must look like. Structural on purpose: this file knows no renderer. */\nexport interface FormNodeLike {\n type: string;\n props?: Record<string, any>;\n children?: FormNodeLike[];\n}\n\nexport const FORM_NODE_TYPE = 'Form';\nexport const FIELD_NODE_TYPE = 'Field';\n\n/** Always present, never removable — the safety net that makes \"store first, deliver after\" true. */\nexport const STORE_ACTION: SubmitAction = Object.freeze({ type: 'store' });\n\n/**\n * Turn a label into the key that arrives in the submitted data — `Full name` → `fullName`.\n * Authors never have to invent keys (decision: auto-derive, with an editable override).\n *\n * Diacritics are stripped rather than dropped, so a Hebrew or accented label still yields a usable\n * ASCII key instead of an empty one; a label with no usable characters falls back to `field`.\n */\nexport function deriveFieldName(label: string): string {\n const words = (label ?? '')\n .normalize('NFD').replace(/[̀-ͯ]/g, '')\n .replace(/[^a-zA-Z0-9]+/g, ' ')\n .trim()\n .split(/\\s+/)\n .filter(Boolean);\n if (!words.length) return 'field';\n const [first, ...rest] = words;\n const name = first.toLowerCase() + rest.map(w => w[0].toUpperCase() + w.slice(1).toLowerCase()).join('');\n // A key must not start with a digit — it ends up as an object key and a form control id.\n return /^\\d/.test(name) ? `field${name}` : name;\n}\n\n/** Make `name` unique within `taken`, appending 2, 3, … — two \"Email\" fields must not collide. */\nexport function uniqueFieldName(name: string, taken: ReadonlySet<string>): string {\n if (!taken.has(name)) return name;\n let n = 2;\n while (taken.has(`${name}${n}`)) n++;\n return `${name}${n}`;\n}\n\n/**\n * Walk a Form subtree and produce its spec.\n *\n * Nested forms are not collected: a Field belongs to its NEAREST ancestor Form (decision D1), so the\n * walk stops at any inner Form node rather than stealing its fields.\n */\nexport function collectFormSpec(node: FormNodeLike | null | undefined): FormSpec | null {\n if (!node || node.type !== FORM_NODE_TYPE) return null;\n const props = node.props ?? {};\n\n const fields: FieldSpec[] = [];\n const taken = new Set<string>();\n for (const fieldNode of collectFieldNodes(node.children ?? [])) {\n const spec = toFieldSpec(fieldNode, taken);\n taken.add(spec.name);\n fields.push(spec);\n }\n\n return {\n formId: props['formId'] ?? '',\n name: props['name'] ?? 'Form',\n feedback: (props['feedback'] as FeedbackTiming) ?? FeedbackTiming.Smart,\n fields,\n actions: normaliseActions(props['actions']),\n antiSpam: {\n honeypot: props['honeypot'] !== false,\n minFillMs: Number(props['minFillMs'] ?? 1200),\n ...(props['turnstile'] ? { turnstile: true } : {}),\n },\n ...(props['retentionDays'] ? { retention: { days: Number(props['retentionDays']) } } : {}),\n };\n}\n\n/** Every Field in this subtree, in document order, not descending into a nested Form. */\nexport function collectFieldNodes(nodes: readonly FormNodeLike[]): FormNodeLike[] {\n const out: FormNodeLike[] = [];\n for (const node of nodes ?? []) {\n if (!node?.type) continue;\n if (node.type === FORM_NODE_TYPE) continue; // a nested form owns its own fields\n if (node.type === FIELD_NODE_TYPE) { out.push(node); continue; }\n out.push(...collectFieldNodes(node.children ?? []));\n }\n return out;\n}\n\nfunction toFieldSpec(node: FormNodeLike, taken: ReadonlySet<string>): FieldSpec {\n const props = node.props ?? {};\n const label = props['label'] ?? '';\n // The author's explicit override wins; otherwise derive from the label.\n const name = uniqueFieldName(props['name'] || deriveFieldName(label), taken);\n\n const spec: FieldSpec = {\n name,\n control: (props['control'] as ControlType) ?? ControlType.Text,\n label,\n };\n if (props['required']) spec.required = true;\n const rules = normaliseRules(props['rules']);\n if (rules.length) spec.rules = rules;\n if (Array.isArray(props['options']) && props['options'].length) spec.options = props['options'];\n if (props['optionsSource']) spec.optionsSource = props['optionsSource'];\n if (props['defaultValue'] !== undefined && props['defaultValue'] !== '') {\n spec.defaultValue = props['defaultValue'];\n }\n return spec;\n}\n\n/** Drop anything that isn't a rule we know — a hand-edited page must not break validation. */\nfunction normaliseRules(raw: unknown): ValidationRule[] {\n if (!Array.isArray(raw)) return [];\n const known = new Set<string>(Object.values(RuleName));\n return raw\n .filter(r => r && known.has(r.name))\n .map(r => ({\n name: r.name as RuleName,\n ...(r.params ? { params: r.params } : {}),\n ...(r.message ? { message: r.message } : {}),\n }));\n}\n\n/** `store` is always first and always present, however the page was edited. */\nfunction normaliseActions(raw: unknown): SubmitAction[] {\n const declared: SubmitAction[] = Array.isArray(raw)\n ? raw.filter(a => a && typeof a.type === 'string')\n : [];\n return [STORE_ACTION, ...declared.filter(a => a.type !== STORE_ACTION.type)];\n}\n","/*\n * @vectoriox/iox-ui/forms-core — the shared, FRAMEWORK-FREE core of the forms feature.\n *\n * ⚠️ Nothing in this entry point may import Angular, RxJS, the DOM, or Node. It is consumed by\n * THREE runtimes: the builder canvas (Angular), the SSR engine (Angular), and `iox-cms` (NestJS,\n * plain Node). The main `@vectoriox/iox-ui` entry pulls in `@angular/core`, which Nest must never\n * load — that is the entire reason this secondary entry point exists.\n *\n * Client-side validation is UX; server-side validation is truth. Both run THIS code, so they cannot\n * disagree. See iox-ai-guidance `architecture/builder/forms-and-inputs-plan.md` §5.\n */\n\nexport * from './form.model';\nexport * from './messages';\nexport * from './validate';\nexport * from './collect';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/** What the visitor actually types into. Chosen by a trait; it also implies a validation rule. */
|
|
2
|
+
declare enum ControlType {
|
|
3
|
+
Text = "text",
|
|
4
|
+
Email = "email",
|
|
5
|
+
Tel = "tel",
|
|
6
|
+
Url = "url",
|
|
7
|
+
Number = "number",
|
|
8
|
+
Password = "password",
|
|
9
|
+
Textarea = "textarea",
|
|
10
|
+
Select = "select",
|
|
11
|
+
Checkbox = "checkbox",
|
|
12
|
+
Radio = "radio",
|
|
13
|
+
Date = "date",
|
|
14
|
+
Time = "time",
|
|
15
|
+
File = "file",
|
|
16
|
+
/** Not shown; carries a constant or a bound value (campaign, source page, …). */
|
|
17
|
+
Hidden = "hidden"
|
|
18
|
+
}
|
|
19
|
+
/** The v1 rule grammar. Phase 3's conditional logic reuses these same names for its conditions. */
|
|
20
|
+
declare enum RuleName {
|
|
21
|
+
Required = "required",
|
|
22
|
+
MinLength = "minLength",
|
|
23
|
+
MaxLength = "maxLength",
|
|
24
|
+
Min = "min",
|
|
25
|
+
Max = "max",
|
|
26
|
+
Pattern = "pattern",
|
|
27
|
+
/** Cross-field: this value must equal another field's (confirm email / password). */
|
|
28
|
+
Matches = "matches",
|
|
29
|
+
/** A checkbox that must be ticked — terms, consent. */
|
|
30
|
+
Accepted = "accepted",
|
|
31
|
+
FileType = "fileType",
|
|
32
|
+
FileSize = "fileSize"
|
|
33
|
+
}
|
|
34
|
+
/** Named regexes so an author picks "Email" rather than writing one. */
|
|
35
|
+
declare enum PatternPreset {
|
|
36
|
+
Email = "email",
|
|
37
|
+
Phone = "phone",
|
|
38
|
+
Url = "url",
|
|
39
|
+
Digits = "digits",
|
|
40
|
+
Letters = "letters",
|
|
41
|
+
Slug = "slug"
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Deliberately permissive. A validator that rejects `user+tag@sub.domain.co.uk` or a real-world
|
|
45
|
+
* phone number is worse than one that lets a typo through: the address gets verified by sending to
|
|
46
|
+
* it, and a false rejection is a lost lead with no recourse.
|
|
47
|
+
*/
|
|
48
|
+
declare const PATTERN_PRESETS: Readonly<Record<PatternPreset, RegExp>>;
|
|
49
|
+
interface ValidationRule {
|
|
50
|
+
name: RuleName;
|
|
51
|
+
/** `{ n }` for lengths/bounds · `{ preset } | { regex }` for pattern · `{ field }` for matches. */
|
|
52
|
+
params?: Record<string, any>;
|
|
53
|
+
/** Author's override. Absent → the shared message catalog supplies a translated key. */
|
|
54
|
+
message?: string;
|
|
55
|
+
}
|
|
56
|
+
interface FieldOption {
|
|
57
|
+
label: string;
|
|
58
|
+
value: string;
|
|
59
|
+
}
|
|
60
|
+
/** Options pulled from a data source instead of typed by hand (a Select of CMS categories). */
|
|
61
|
+
interface FieldOptionSource {
|
|
62
|
+
source: string;
|
|
63
|
+
sourcePath?: string;
|
|
64
|
+
valuePath: string;
|
|
65
|
+
labelPath: string;
|
|
66
|
+
}
|
|
67
|
+
interface FieldSpec {
|
|
68
|
+
/** The submitted key. Derived from the label, unique within the form, editable. */
|
|
69
|
+
name: string;
|
|
70
|
+
control: ControlType;
|
|
71
|
+
label: string;
|
|
72
|
+
/** Surfaced as its own switch in the panel rather than buried in the rule list. */
|
|
73
|
+
required?: boolean;
|
|
74
|
+
rules?: ValidationRule[];
|
|
75
|
+
options?: FieldOption[];
|
|
76
|
+
optionsSource?: FieldOptionSource;
|
|
77
|
+
defaultValue?: any;
|
|
78
|
+
}
|
|
79
|
+
/** When a field is validated, once the author has stopped deciding it per field. */
|
|
80
|
+
declare enum FeedbackTiming {
|
|
81
|
+
/** Validate on submit, then live per-field once that field has been touched or has failed. */
|
|
82
|
+
Smart = "smart",
|
|
83
|
+
Submit = "submit",
|
|
84
|
+
Live = "live"
|
|
85
|
+
}
|
|
86
|
+
/** What happens to a submission, in order. Card #1 (`store`) is always present. */
|
|
87
|
+
interface SubmitAction {
|
|
88
|
+
type: string;
|
|
89
|
+
config?: Record<string, any>;
|
|
90
|
+
/** `retry` (default) · `fail` · `ignore`. */
|
|
91
|
+
onFailure?: 'retry' | 'fail' | 'ignore';
|
|
92
|
+
}
|
|
93
|
+
interface FormSpec {
|
|
94
|
+
formId: string;
|
|
95
|
+
/** Author-facing name — the submissions inbox groups by it. */
|
|
96
|
+
name: string;
|
|
97
|
+
feedback: FeedbackTiming;
|
|
98
|
+
fields: FieldSpec[];
|
|
99
|
+
actions: SubmitAction[];
|
|
100
|
+
antiSpam: {
|
|
101
|
+
honeypot: boolean;
|
|
102
|
+
minFillMs: number;
|
|
103
|
+
turnstile?: boolean;
|
|
104
|
+
};
|
|
105
|
+
retention?: {
|
|
106
|
+
days: number;
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* The rule a control type implies with no configuration. Choosing "Email" IS choosing email
|
|
111
|
+
* validation — the single biggest reason the panel can stay empty and still validate well.
|
|
112
|
+
*/
|
|
113
|
+
declare const IMPLIED_PATTERN: Readonly<Partial<Record<ControlType, PatternPreset>>>;
|
|
114
|
+
/** Controls whose value is a list rather than a scalar. */
|
|
115
|
+
declare const MULTI_VALUE_CONTROLS: ReadonlySet<ControlType>;
|
|
116
|
+
|
|
117
|
+
/** Namespace for every forms validation key. Keep in sync with `en.json` / `he.json`. */
|
|
118
|
+
declare const MESSAGE_NAMESPACE = "forms.validation";
|
|
119
|
+
/**
|
|
120
|
+
* Rule → i18n key. Params travel beside the key (`{ n: 8 }`, `{ field: 'Email' }`) so a translation
|
|
121
|
+
* can place the number wherever its grammar needs it, rather than having it baked into the string.
|
|
122
|
+
*/
|
|
123
|
+
declare const RULE_MESSAGE_KEYS: Readonly<Record<RuleName, string>>;
|
|
124
|
+
/**
|
|
125
|
+
* A pattern failure deserves better than "Invalid format". Each preset gets its own key so the
|
|
126
|
+
* message can say what a valid value looks like, which is the difference between a visitor fixing
|
|
127
|
+
* a typo and a visitor giving up.
|
|
128
|
+
*/
|
|
129
|
+
declare const PATTERN_MESSAGE_KEYS: Readonly<Record<string, string>>;
|
|
130
|
+
/** Form-level outcomes the visitor sees in the Error state. One designed block, five reasons. */
|
|
131
|
+
declare const FORM_ERROR_KEYS: Readonly<{
|
|
132
|
+
Network: "forms.validation.form.network";
|
|
133
|
+
Rejected: "forms.validation.form.rejected";
|
|
134
|
+
Spam: "forms.validation.form.spam";
|
|
135
|
+
RateLimited: "forms.validation.form.rateLimited";
|
|
136
|
+
Server: "forms.validation.form.server";
|
|
137
|
+
}>;
|
|
138
|
+
/** The English defaults, so a consumer that has not added the keys yet still shows real text. */
|
|
139
|
+
declare const DEFAULT_EN_MESSAGES: Readonly<Record<string, string>>;
|
|
140
|
+
|
|
141
|
+
interface FieldError {
|
|
142
|
+
/** i18n key, or the author's own override string when they set one. */
|
|
143
|
+
messageKey: string;
|
|
144
|
+
/** Interpolation values (`{ n: 8 }`, `{ field: 'Email' }`). */
|
|
145
|
+
params?: Record<string, any>;
|
|
146
|
+
}
|
|
147
|
+
interface FieldResult extends Partial<FieldError> {
|
|
148
|
+
valid: boolean;
|
|
149
|
+
}
|
|
150
|
+
interface FormResult {
|
|
151
|
+
valid: boolean;
|
|
152
|
+
/** Keyed by field name. Empty when valid. */
|
|
153
|
+
errors: Record<string, FieldError>;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Validate one field's value.
|
|
157
|
+
*
|
|
158
|
+
* Order is deliberate and is part of the contract:
|
|
159
|
+
* 1. `required` (or `accepted` for a checkbox) — the only rules an EMPTY value can fail.
|
|
160
|
+
* 2. Everything else, in the order the author arranged them, but ONLY for a non-empty value.
|
|
161
|
+
*
|
|
162
|
+
* That second point is what stops an optional "Phone" field from shouting "enter a valid phone
|
|
163
|
+
* number" at someone who simply left it blank — the single most common validation-UX bug.
|
|
164
|
+
* The FIRST failure wins: a visitor fixes one thing at a time.
|
|
165
|
+
*/
|
|
166
|
+
declare function validateField(value: unknown, spec: FieldSpec, form?: Record<string, unknown>): FieldResult;
|
|
167
|
+
/**
|
|
168
|
+
* Validate a whole submission. Every field is checked (not short-circuited at the first bad one) so
|
|
169
|
+
* the visitor sees every problem at once instead of fixing them one reload at a time.
|
|
170
|
+
*/
|
|
171
|
+
declare function validateForm(values: Record<string, unknown>, spec: FormSpec): FormResult;
|
|
172
|
+
/** The name of the first field with an error, in spec order — what the browser should focus. */
|
|
173
|
+
declare function firstInvalidField(spec: FormSpec, errors: Record<string, FieldError>): string | null;
|
|
174
|
+
|
|
175
|
+
/** The minimum a node must look like. Structural on purpose: this file knows no renderer. */
|
|
176
|
+
interface FormNodeLike {
|
|
177
|
+
type: string;
|
|
178
|
+
props?: Record<string, any>;
|
|
179
|
+
children?: FormNodeLike[];
|
|
180
|
+
}
|
|
181
|
+
declare const FORM_NODE_TYPE = "Form";
|
|
182
|
+
declare const FIELD_NODE_TYPE = "Field";
|
|
183
|
+
/** Always present, never removable — the safety net that makes "store first, deliver after" true. */
|
|
184
|
+
declare const STORE_ACTION: SubmitAction;
|
|
185
|
+
/**
|
|
186
|
+
* Turn a label into the key that arrives in the submitted data — `Full name` → `fullName`.
|
|
187
|
+
* Authors never have to invent keys (decision: auto-derive, with an editable override).
|
|
188
|
+
*
|
|
189
|
+
* Diacritics are stripped rather than dropped, so a Hebrew or accented label still yields a usable
|
|
190
|
+
* ASCII key instead of an empty one; a label with no usable characters falls back to `field`.
|
|
191
|
+
*/
|
|
192
|
+
declare function deriveFieldName(label: string): string;
|
|
193
|
+
/** Make `name` unique within `taken`, appending 2, 3, … — two "Email" fields must not collide. */
|
|
194
|
+
declare function uniqueFieldName(name: string, taken: ReadonlySet<string>): string;
|
|
195
|
+
/**
|
|
196
|
+
* Walk a Form subtree and produce its spec.
|
|
197
|
+
*
|
|
198
|
+
* Nested forms are not collected: a Field belongs to its NEAREST ancestor Form (decision D1), so the
|
|
199
|
+
* walk stops at any inner Form node rather than stealing its fields.
|
|
200
|
+
*/
|
|
201
|
+
declare function collectFormSpec(node: FormNodeLike | null | undefined): FormSpec | null;
|
|
202
|
+
/** Every Field in this subtree, in document order, not descending into a nested Form. */
|
|
203
|
+
declare function collectFieldNodes(nodes: readonly FormNodeLike[]): FormNodeLike[];
|
|
204
|
+
|
|
205
|
+
export { ControlType, DEFAULT_EN_MESSAGES, FIELD_NODE_TYPE, FORM_ERROR_KEYS, FORM_NODE_TYPE, FeedbackTiming, IMPLIED_PATTERN, MESSAGE_NAMESPACE, MULTI_VALUE_CONTROLS, PATTERN_MESSAGE_KEYS, PATTERN_PRESETS, PatternPreset, RULE_MESSAGE_KEYS, RuleName, STORE_ACTION, collectFieldNodes, collectFormSpec, deriveFieldName, firstInvalidField, uniqueFieldName, validateField, validateForm };
|
|
206
|
+
export type { FieldError, FieldOption, FieldOptionSource, FieldResult, FieldSpec, FormNodeLike, FormResult, FormSpec, SubmitAction, ValidationRule };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vectoriox/iox-ui",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.27.0",
|
|
4
4
|
"peerDependencies": {
|
|
5
5
|
"@angular/common": ">=20.0.0",
|
|
6
6
|
"@angular/core": ">=20.0.0",
|
|
@@ -18,6 +18,10 @@
|
|
|
18
18
|
".": {
|
|
19
19
|
"types": "./index.d.ts",
|
|
20
20
|
"default": "./fesm2022/vectoriox-iox-ui.mjs"
|
|
21
|
+
},
|
|
22
|
+
"./forms-core": {
|
|
23
|
+
"types": "./forms-core/index.d.ts",
|
|
24
|
+
"default": "./fesm2022/vectoriox-iox-ui-forms-core.mjs"
|
|
21
25
|
}
|
|
22
26
|
},
|
|
23
27
|
"sideEffects": false
|