@vectoriox/iox-ui 4.31.1 → 4.33.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.
@@ -242,5 +242,135 @@ declare function applyServerErrors(state: FormRuntimeState, errors: Record<strin
242
242
  /** A clean slate after a successful submit, for a form configured to clear itself. */
243
243
  declare function resetFormState(): FormRuntimeState;
244
244
 
245
- export { ControlType, DEFAULT_EN_MESSAGES, FIELD_NODE_TYPE, FORM_ERROR_KEYS, FORM_NODE_TYPE, FeedbackTiming, IMPLIED_PATTERN, INITIAL_FORM_STATE, MESSAGE_NAMESPACE, MULTI_VALUE_CONTROLS, PATTERN_MESSAGE_KEYS, PATTERN_PRESETS, PatternPreset, RULE_MESSAGE_KEYS, RuleName, STORE_ACTION, applyServerErrors, collectFieldNodes, collectFormSpec, deriveFieldName, firstInvalidField, onFieldBlur, onFieldChange, onSubmitAttempt, resetFormState, shouldShowError, uniqueFieldName, validateField, validateForm };
246
- export type { FieldError, FieldOption, FieldOptionSource, FieldResult, FieldSpec, FormNodeLike, FormResult, FormRuntimeState, FormSpec, SubmitAction, ValidationRule };
245
+ /**
246
+ * Which rules exist, and which controls they make sense on.
247
+ *
248
+ * The panel must not offer "must be ticked" on a text field, or a character limit on a date. That
249
+ * knowledge belongs with the validator, not duplicated into the editor — otherwise the two drift and
250
+ * an author configures a rule that silently never runs.
251
+ *
252
+ * Pure data, in forms-core, so the builder panel, the engine and the backend all read one catalog.
253
+ */
254
+
255
+ /** How a rule's parameter is edited. The panel maps these to its own controls. */
256
+ declare enum RuleParamKind {
257
+ /** No parameter — the rule is its own statement (`required`, `accepted`). */
258
+ None = "none",
259
+ /** A single number (`{ n }`) — lengths and bounds. */
260
+ Number = "number",
261
+ /** A preset regex, or a custom one (`{ preset }` / `{ regex }`). */
262
+ Pattern = "pattern",
263
+ /** Another field in the same form (`{ field }`). */
264
+ Field = "field"
265
+ }
266
+ interface RuleDescriptor {
267
+ name: RuleName;
268
+ /** i18n key for the rule's name in the Add-rule menu. */
269
+ labelKey: string;
270
+ param: RuleParamKind;
271
+ /** Controls this rule can apply to. Empty means every control. */
272
+ controls: readonly ControlType[];
273
+ /** Suggested starting value for the parameter, so a freshly added rule is already valid. */
274
+ defaultParams?: Record<string, any>;
275
+ }
276
+ declare const RULE_DESCRIPTORS: readonly RuleDescriptor[];
277
+ /** The rules that can be added to a field of this control type. */
278
+ declare function rulesFor(control: ControlType | string): RuleDescriptor[];
279
+ /**
280
+ * The rules an author may still ADD — the catalog minus what is already on the field.
281
+ *
282
+ * Each rule appears once: two `maxLength` rules on one field is not a configuration anyone means,
283
+ * and the validator would apply whichever came first. `pattern` is the exception a caller might
284
+ * expect to repeat, and it deliberately does not: a second pattern can only contradict the first.
285
+ */
286
+ declare function addableRules(control: ControlType | string, existing?: readonly {
287
+ name: RuleName;
288
+ }[]): RuleDescriptor[];
289
+ /** The preset options a Pattern rule offers, in the order the panel should list them. */
290
+ declare const PATTERN_PRESET_OPTIONS: readonly {
291
+ value: PatternPreset;
292
+ labelKey: string;
293
+ }[];
294
+ /** Destinations an author can add to a form, for the After-submit list. */
295
+ interface ActionDescriptor {
296
+ type: string;
297
+ labelKey: string;
298
+ /** Always present and not removable. */
299
+ locked?: boolean;
300
+ /** Not offered yet — shown greyed with a "coming soon" note rather than hidden, so the roadmap
301
+ * is visible rather than the feature looking absent. */
302
+ planned?: boolean;
303
+ }
304
+ declare const ACTION_DESCRIPTORS: readonly ActionDescriptor[];
305
+ /** Destinations that can still be added — each at most once, and never a planned one. */
306
+ declare function addableActions(existing?: readonly {
307
+ type: string;
308
+ }[]): ActionDescriptor[];
309
+
310
+ /**
311
+ * The submit transport CONTRACT — what a host must provide to make a form actually send.
312
+ *
313
+ * The Form component deliberately owns no HTTP: the builder canvas must never post (an author
314
+ * clicking Submit while designing would file a real submission), and the SSR engine posts through
315
+ * Angular's HttpClient. What IS shared is everything around the call — the endpoint's shape, the
316
+ * anti-spam fields, and how the server's answer maps onto the form's state. Those live here so the
317
+ * two hosts cannot drift, and so a third host (a hand-built integration) has one thing to read.
318
+ *
319
+ * ⚠️ Framework-free, like the rest of forms-core. No Angular, no RxJS, no fetch.
320
+ */
321
+
322
+ /** The field a honeypot input uses. Must match `iox-cms` `anti-spam.ts` — hence the comment there. */
323
+ declare const HONEYPOT_FIELD = "_iox_hp";
324
+ /** When the form was rendered, so the server can measure fill time. */
325
+ declare const RENDERED_AT_FIELD = "_iox_t";
326
+ /**
327
+ * What the form hands its host. There is deliberately no `orgId`: the tenant is the HOST's
328
+ * knowledge, not the form's, and a form that had to be told its org would be one more trait an
329
+ * author could get wrong.
330
+ */
331
+ interface SubmitRequest {
332
+ formId: string;
333
+ values: Record<string, any>;
334
+ }
335
+ /**
336
+ * The server's answer, normalised. Mirrors `SubmitResult` in `iox-cms`
337
+ * `form-submissions.service.ts` — the two are one contract, so change them together.
338
+ */
339
+ interface SubmitResponse {
340
+ ok: boolean;
341
+ /** Per-field verdicts from the server's own validation pass, keyed by field name. */
342
+ errors?: Record<string, FieldError>;
343
+ /** Where to send the visitor after a successful submit, when the form has a redirect action. */
344
+ redirect?: string;
345
+ }
346
+ /**
347
+ * The one function a host implements. Rejecting (or throwing) means the call never reached the
348
+ * server — the form shows its Error state, which is the honest thing to show for a dropped network.
349
+ */
350
+ type FormTransport = (request: SubmitRequest) => Promise<SubmitResponse>;
351
+ /**
352
+ * The public submit path. Built here rather than interpolated at each call site, so the engine and
353
+ * any future host cannot disagree about it — and so changing it is one edit.
354
+ *
355
+ * Deliberately relative and WITHOUT a leading slash: the engine mounts under a per-org base href.
356
+ */
357
+ declare function submitEndpointPath(orgId: string, formId: string): string;
358
+ /**
359
+ * Add the anti-spam fields to the collected values.
360
+ *
361
+ * Both are invisible to a real visitor and cost them nothing (decision D12). `renderedAt` is the
362
+ * time the form was rendered, NOT the time of submit — the server measures the difference to catch
363
+ * a script that fills and posts instantly.
364
+ */
365
+ declare function withAntiSpamFields(values: Record<string, any>, renderedAt: number, honeypotValue?: string): Record<string, any>;
366
+ /**
367
+ * Normalise whatever came back into a `SubmitResponse`.
368
+ *
369
+ * Fails CLOSED: anything that is not a recognisable success is treated as a failure. A malformed
370
+ * body must never show the visitor a Success state for a submission that was not stored — that is
371
+ * the one lie this feature must not tell.
372
+ */
373
+ declare function interpretSubmitResponse(body: any): SubmitResponse;
374
+
375
+ export { ACTION_DESCRIPTORS, ControlType, DEFAULT_EN_MESSAGES, FIELD_NODE_TYPE, FORM_ERROR_KEYS, FORM_NODE_TYPE, FeedbackTiming, HONEYPOT_FIELD, IMPLIED_PATTERN, INITIAL_FORM_STATE, MESSAGE_NAMESPACE, MULTI_VALUE_CONTROLS, PATTERN_MESSAGE_KEYS, PATTERN_PRESETS, PATTERN_PRESET_OPTIONS, PatternPreset, RENDERED_AT_FIELD, RULE_DESCRIPTORS, RULE_MESSAGE_KEYS, RuleName, RuleParamKind, STORE_ACTION, addableActions, addableRules, applyServerErrors, collectFieldNodes, collectFormSpec, deriveFieldName, firstInvalidField, interpretSubmitResponse, onFieldBlur, onFieldChange, onSubmitAttempt, resetFormState, rulesFor, shouldShowError, submitEndpointPath, uniqueFieldName, validateField, validateForm, withAntiSpamFields };
376
+ export type { ActionDescriptor, FieldError, FieldOption, FieldOptionSource, FieldResult, FieldSpec, FormNodeLike, FormResult, FormRuntimeState, FormSpec, FormTransport, RuleDescriptor, SubmitAction, SubmitRequest, SubmitResponse, ValidationRule };
package/index.d.ts CHANGED
@@ -988,6 +988,44 @@ interface FormRuntimeState {
988
988
  submitting: boolean;
989
989
  }
990
990
 
991
+ /**
992
+ * The submit transport CONTRACT — what a host must provide to make a form actually send.
993
+ *
994
+ * The Form component deliberately owns no HTTP: the builder canvas must never post (an author
995
+ * clicking Submit while designing would file a real submission), and the SSR engine posts through
996
+ * Angular's HttpClient. What IS shared is everything around the call — the endpoint's shape, the
997
+ * anti-spam fields, and how the server's answer maps onto the form's state. Those live here so the
998
+ * two hosts cannot drift, and so a third host (a hand-built integration) has one thing to read.
999
+ *
1000
+ * ⚠️ Framework-free, like the rest of forms-core. No Angular, no RxJS, no fetch.
1001
+ */
1002
+
1003
+ /**
1004
+ * What the form hands its host. There is deliberately no `orgId`: the tenant is the HOST's
1005
+ * knowledge, not the form's, and a form that had to be told its org would be one more trait an
1006
+ * author could get wrong.
1007
+ */
1008
+ interface SubmitRequest {
1009
+ formId: string;
1010
+ values: Record<string, any>;
1011
+ }
1012
+ /**
1013
+ * The server's answer, normalised. Mirrors `SubmitResult` in `iox-cms`
1014
+ * `form-submissions.service.ts` — the two are one contract, so change them together.
1015
+ */
1016
+ interface SubmitResponse {
1017
+ ok: boolean;
1018
+ /** Per-field verdicts from the server's own validation pass, keyed by field name. */
1019
+ errors?: Record<string, FieldError>;
1020
+ /** Where to send the visitor after a successful submit, when the form has a redirect action. */
1021
+ redirect?: string;
1022
+ }
1023
+ /**
1024
+ * The one function a host implements. Rejecting (or throwing) means the call never reached the
1025
+ * server — the form shows its Error state, which is the honest thing to show for a dropped network.
1026
+ */
1027
+ type FormTransport = (request: SubmitRequest) => Promise<SubmitResponse>;
1028
+
991
1029
  /**
992
1030
  * How a message KEY becomes text a visitor reads.
993
1031
  *
@@ -1049,6 +1087,15 @@ declare class FormRuntimeService {
1049
1087
  static ɵprov: i0.ɵɵInjectableDeclaration<FormRuntimeService>;
1050
1088
  }
1051
1089
 
1090
+ /**
1091
+ * How a host makes the form actually send. The SSR engine provides one (an HttpClient POST); the
1092
+ * builder canvas deliberately does NOT — an author pressing Submit while designing must never file
1093
+ * a real submission. Without it the component keeps its original behaviour and only emits
1094
+ * `formSubmit`, which is also the escape hatch for a host that wants the transport itself.
1095
+ */
1096
+ declare const IOX_FORM_TRANSPORT: InjectionToken<FormTransport>;
1097
+ /** Where a successful submit sends the visitor. Provided by the host; absent on the canvas. */
1098
+ declare const IOX_FORM_NAVIGATOR: InjectionToken<(url: string) => void>;
1052
1099
  /** Which of the form's four states the canvas / runtime is showing. */
1053
1100
  declare enum FormRenderState {
1054
1101
  Idle = "idle",
@@ -1072,9 +1119,11 @@ declare enum FormRenderState {
1072
1119
  * ⚠️ `novalidate` is deliberate: the browser's native bubbles would pre-empt our own messages,
1073
1120
  * which are styleable, translated and announced. Validation still runs — ours does it.
1074
1121
  */
1075
- declare class IoxFormComponent implements OnChanges {
1122
+ declare class IoxFormComponent implements OnChanges, OnInit {
1076
1123
  private readonly runtime;
1077
1124
  private readonly host;
1125
+ private readonly transport;
1126
+ private readonly navigate;
1078
1127
  nodeId: string;
1079
1128
  /** Stable id the submit endpoint resolves the saved spec by. */
1080
1129
  formId: string;
@@ -1094,16 +1143,35 @@ declare class IoxFormComponent implements OnChanges {
1094
1143
  /** Emitted when a submit attempt was refused locally, so a host can react (analytics, focus). */
1095
1144
  validationFailed: EventEmitter<Record<string, any>>;
1096
1145
  childrenHost: ViewContainerRef;
1146
+ /** Exposed to the template so the input's name and the server's check cannot drift. */
1147
+ readonly honeypotField = "_iox_hp";
1148
+ /** When this form was rendered — the fill-time signal the server checks. */
1149
+ private renderedAt;
1150
+ /** Guards against a double submit while a POST is in flight. */
1151
+ private inFlight;
1097
1152
  /**
1098
1153
  * Constructor injection rather than `inject()`, for the same reason as Field: this library's
1099
1154
  * specs construct components directly. `runtime` is provided by THIS component (see
1100
1155
  * `providers`), so every Field created into `childrenHost` resolves this instance by ancestry.
1101
1156
  */
1102
- constructor(runtime: FormRuntimeService, host: ElementRef<HTMLElement>);
1157
+ constructor(runtime: FormRuntimeService, host: ElementRef<HTMLElement>, transport?: FormTransport | null, navigate?: ((url: string) => void) | null);
1158
+ ngOnInit(): void;
1103
1159
  ngOnChanges(): void;
1104
1160
  onSubmit(event: Event): void;
1161
+ /**
1162
+ * The submit lifecycle, owned HERE so both hosts get the same one: submitting → the server's
1163
+ * verdict → success (and an optional redirect) or error. Only the HTTP call itself is the
1164
+ * host's, which is the one part that genuinely differs between the engine and anything else.
1165
+ */
1166
+ private send;
1167
+ /** Turn the server's answer into what the visitor sees. Public so a host-owned transport can reuse it. */
1168
+ applyResponse(response: SubmitResponse): void;
1169
+ /** The honeypot input's value, if the host rendered one. Absent reads as empty — never as spam. */
1170
+ private honeypotValue;
1171
+ /** Blank the real DOM controls after a success. The runtime state alone would leave them filled. */
1172
+ private resetControls;
1105
1173
  private focusFirstInvalid;
1106
- static ɵfac: i0.ɵɵFactoryDeclaration<IoxFormComponent, never>;
1174
+ static ɵfac: i0.ɵɵFactoryDeclaration<IoxFormComponent, [null, null, { optional: true; }, { optional: true; }]>;
1107
1175
  static ɵcmp: i0.ɵɵComponentDeclaration<IoxFormComponent, "form[iox-form]", never, { "nodeId": { "alias": "nodeId"; "required": false; }; "formId": { "alias": "formId"; "required": false; }; "name": { "alias": "name"; "required": false; }; "renderState": { "alias": "renderState"; "required": false; }; "feedback": { "alias": "feedback"; "required": false; }; }, { "formSubmit": "formSubmit"; "validationFailed": "validationFailed"; }, never, never, false, never>;
1108
1176
  }
1109
1177
 
@@ -2373,5 +2441,5 @@ declare class GalleryLoop {
2373
2441
  private emit;
2374
2442
  }
2375
2443
 
2376
- export { AOSService, AnalyticsService, BREAKPOINTS, Breakpoint, BreakpointQueryKind, BuilderButtonComponent, BuilderDividerComponent, BuilderHeadingComponent, BuilderIconComponent, BuilderImageComponent, BuilderLinkComponent, BuilderSpacerComponent, ButtonBlockComponent, CMSClientInfraModule, CONTEXT_ACTION_TYPES, CardComponent, ClientGalleryComponent, ClientListComponent, ClientListItemComponent, ClientRepeaterComponent, ClientSliderContainerComponent, ClientSliderSlideComponent, ComponentInstanceRegistryService, ConsentService, ContainerComponent, ContentService, ContextStore, DEFAULT_GALLERY_SCHEDULER, DEFAULT_PAGE_TRANSITION, DEFAULT_PAGE_TRANSITION_DURATION, DEFAULT_PAGE_TRANSITION_EASING, DEFAULT_PAGE_TRANSITION_PERSPECTIVE, DEFAULT_WIDTH_BY_TYPE, DeclarationContext, ENVIRONMENT, FormRenderState, FormRuntimeService, GALLERY_EFFECTS, GALLERY_EFFECT_DESCRIPTORS, GalleryLoop, IOX_BUILDER_EVENTS, IOX_FORM_MESSAGES, IS_PREVIEW, IoxAnimateContainer, IoxAosDirective, IoxAosModule, IoxBuilderComponentsModule, IoxComponentRegistryService, IoxFieldComponent, IoxFormComponent, IoxFormErrorComponent, IoxFormSuccessComponent, IoxPageComponent, IoxPageModule, IoxSubmitButtonComponent, IoxUiModule, LinkedContainerComponent, PAGE_TRANSITION_CATEGORIES, PAGE_TRANSITION_PRESETS, PIPE_DESCRIPTORS, PIPE_REGISTRY, PageScrollProvider, PrivacyModalComponent, PrivacyModalService, PrivacyModelEvent, RESERVED_ALIAS_PREFIX, SUPPRESSED_STYLE_PROPS_BY_TYPE, SWIPER_RESPONSIVE_TRAITS, SectionComponent, TRANSITION_FEELS, TextBlockComponent, VIRTUAL_TRAIT_GROUPS, VIRTUAL_TRAIT_KEYS, ViewPositionDirective, ViewPositionModule, WebSettingsService, activeActions, applyBindingPipes, applyFeel, breakpointDef, breakpointForWidth, breakpointQueryFor, buildDataSourceFilter, buildRelatedFilter, buildSwiperBreakpoints, buildTargetedStateSelector, buildTransitionTimeline, cascadeChain, clearInputOverride, clearOverride, collectContextSlotKeys, collectReferencedAliases, compileDeclarations, composeVirtualTraits, computeRelative, contextActionEffect, contextSlotKeys, countOverrides, dataSourcePlanToRequest, declaredInputNames, defaultMessageResolver, effectiveTransitionId, excludeSelf, feelFor, getByPath, initialSlotValues, isActionEnabled, isContextAction, isInputOverriddenAt, isOverriddenAt, isReservedAlias, isValidSlotKey, legacyEnterToTransition, matchRouteParams, mediaQueryFor, mergeSwiperBreakpoints, nearestCommonAncestorId, nodeCssId, nodeUsesContext, normalizeCollectionResult, normalizeGalleryImage, parentBreakpoint, parseUrlList, planDataSource, planResponsiveRules, referencedDataSources, referencedDataSourcesDeep, renderDefaultDeclarations, resolveContextValue, resolveDerivedInto, resolveGalleryEffect, resolveGalleryUrls, resolveInputChain, resolveInputs, resolvePageTransition, resolvePath, resolveRelativeInto, resolveRepeaterItems, resolveSiblings, resolveSingleItemId, resolveStateStyles, resolveStyleProps, resolveValueChain, rewriteViewportUnits, safeSetInput, setInputOverride, setOverride, stateNamesIn, stripSuppressedProps, styleKeyToKebab, variableAliases, variableItemsFrom, withContextSlots, wrapInBreakpointQuery, wrapInMediaQuery };
2444
+ export { AOSService, AnalyticsService, BREAKPOINTS, Breakpoint, BreakpointQueryKind, BuilderButtonComponent, BuilderDividerComponent, BuilderHeadingComponent, BuilderIconComponent, BuilderImageComponent, BuilderLinkComponent, BuilderSpacerComponent, ButtonBlockComponent, CMSClientInfraModule, CONTEXT_ACTION_TYPES, CardComponent, ClientGalleryComponent, ClientListComponent, ClientListItemComponent, ClientRepeaterComponent, ClientSliderContainerComponent, ClientSliderSlideComponent, ComponentInstanceRegistryService, ConsentService, ContainerComponent, ContentService, ContextStore, DEFAULT_GALLERY_SCHEDULER, DEFAULT_PAGE_TRANSITION, DEFAULT_PAGE_TRANSITION_DURATION, DEFAULT_PAGE_TRANSITION_EASING, DEFAULT_PAGE_TRANSITION_PERSPECTIVE, DEFAULT_WIDTH_BY_TYPE, DeclarationContext, ENVIRONMENT, FormRenderState, FormRuntimeService, GALLERY_EFFECTS, GALLERY_EFFECT_DESCRIPTORS, GalleryLoop, IOX_BUILDER_EVENTS, IOX_FORM_MESSAGES, IOX_FORM_NAVIGATOR, IOX_FORM_TRANSPORT, IS_PREVIEW, IoxAnimateContainer, IoxAosDirective, IoxAosModule, IoxBuilderComponentsModule, IoxComponentRegistryService, IoxFieldComponent, IoxFormComponent, IoxFormErrorComponent, IoxFormSuccessComponent, IoxPageComponent, IoxPageModule, IoxSubmitButtonComponent, IoxUiModule, LinkedContainerComponent, PAGE_TRANSITION_CATEGORIES, PAGE_TRANSITION_PRESETS, PIPE_DESCRIPTORS, PIPE_REGISTRY, PageScrollProvider, PrivacyModalComponent, PrivacyModalService, PrivacyModelEvent, RESERVED_ALIAS_PREFIX, SUPPRESSED_STYLE_PROPS_BY_TYPE, SWIPER_RESPONSIVE_TRAITS, SectionComponent, TRANSITION_FEELS, TextBlockComponent, VIRTUAL_TRAIT_GROUPS, VIRTUAL_TRAIT_KEYS, ViewPositionDirective, ViewPositionModule, WebSettingsService, activeActions, applyBindingPipes, applyFeel, breakpointDef, breakpointForWidth, breakpointQueryFor, buildDataSourceFilter, buildRelatedFilter, buildSwiperBreakpoints, buildTargetedStateSelector, buildTransitionTimeline, cascadeChain, clearInputOverride, clearOverride, collectContextSlotKeys, collectReferencedAliases, compileDeclarations, composeVirtualTraits, computeRelative, contextActionEffect, contextSlotKeys, countOverrides, dataSourcePlanToRequest, declaredInputNames, defaultMessageResolver, effectiveTransitionId, excludeSelf, feelFor, getByPath, initialSlotValues, isActionEnabled, isContextAction, isInputOverriddenAt, isOverriddenAt, isReservedAlias, isValidSlotKey, legacyEnterToTransition, matchRouteParams, mediaQueryFor, mergeSwiperBreakpoints, nearestCommonAncestorId, nodeCssId, nodeUsesContext, normalizeCollectionResult, normalizeGalleryImage, parentBreakpoint, parseUrlList, planDataSource, planResponsiveRules, referencedDataSources, referencedDataSourcesDeep, renderDefaultDeclarations, resolveContextValue, resolveDerivedInto, resolveGalleryEffect, resolveGalleryUrls, resolveInputChain, resolveInputs, resolvePageTransition, resolvePath, resolveRelativeInto, resolveRepeaterItems, resolveSiblings, resolveSingleItemId, resolveStateStyles, resolveStyleProps, resolveValueChain, rewriteViewportUnits, safeSetInput, setInputOverride, setOverride, stateNamesIn, stripSuppressedProps, styleKeyToKebab, variableAliases, variableItemsFrom, withContextSlots, wrapInBreakpointQuery, wrapInMediaQuery };
2377
2445
  export type { BreakpointDef, BuildRelatedFilterOptions, ContextActionEffect, ContextActionLike, ContextItemRef, ContextSlotDefault, ContextValueSpec, ContextWriteScope, DataSourceDomain, DataSourceEndpoints, DataSourceMode, DataSourcePlan, DataSourceQueryFilter, DataSourceRequest, DataSourceRequestSpec, DataSourceRouteFilter, DataSourceSpec, FormMessageResolver, GalleryBoundSource, GalleryDurations, GalleryEffect, GalleryEffectDescriptor, GalleryImage, GalleryLoopOptions, GalleryLoopScheduler, GalleryLoopState, GallerySlideState, IBuilderEventEmitter, InputMap, IoxContextSlot, IoxPipe, OverrideBreakpoint, PageTransitionCategory, PageTransitionCategoryMeta, PageTransitionOptions, PageTransitionPreset, PipeArgSpec, PipeContext, PipeDescriptor, RelativeInputs, RelativeSourceKind, RelativeSourceSpec, ResolveSiblingsOptions, ResolvedPageTransition, ResponsiveLayer, ResponsiveOverrides, ResponsiveRuleSet, RouteContext, SetContextParams, SiblingsResult, StateStyleMap, StyleMap, StyleTreeNode, SwiperBreakpointMap, ToggleableAction, TransitionFeel, TransitionPhase, TransitionTimelineInput, ValueSource, VirtualTraitGroup };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vectoriox/iox-ui",
3
- "version": "4.31.1",
3
+ "version": "4.33.0",
4
4
  "peerDependencies": {
5
5
  "@angular/common": ">=20.0.0",
6
6
  "@angular/core": ">=20.0.0",
@@ -15,3 +15,5 @@ export * from './messages';
15
15
  export * from './validate';
16
16
  export * from './collect';
17
17
  export * from './runtime';
18
+ export * from './rule-catalog';
19
+ export * from './transport';
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Which rules exist, and which controls they make sense on.
3
+ *
4
+ * The panel must not offer "must be ticked" on a text field, or a character limit on a date. That
5
+ * knowledge belongs with the validator, not duplicated into the editor — otherwise the two drift and
6
+ * an author configures a rule that silently never runs.
7
+ *
8
+ * Pure data, in forms-core, so the builder panel, the engine and the backend all read one catalog.
9
+ */
10
+
11
+ import { ControlType, PatternPreset, RuleName } from './form.model';
12
+
13
+ /** How a rule's parameter is edited. The panel maps these to its own controls. */
14
+ export enum RuleParamKind {
15
+ /** No parameter — the rule is its own statement (`required`, `accepted`). */
16
+ None = 'none',
17
+ /** A single number (`{ n }`) — lengths and bounds. */
18
+ Number = 'number',
19
+ /** A preset regex, or a custom one (`{ preset }` / `{ regex }`). */
20
+ Pattern = 'pattern',
21
+ /** Another field in the same form (`{ field }`). */
22
+ Field = 'field',
23
+ }
24
+
25
+ export interface RuleDescriptor {
26
+ name: RuleName;
27
+ /** i18n key for the rule's name in the Add-rule menu. */
28
+ labelKey: string;
29
+ param: RuleParamKind;
30
+ /** Controls this rule can apply to. Empty means every control. */
31
+ controls: readonly ControlType[];
32
+ /** Suggested starting value for the parameter, so a freshly added rule is already valid. */
33
+ defaultParams?: Record<string, any>;
34
+ }
35
+
36
+ /** Text-ish controls: the ones whose value is a string a person typed. */
37
+ const TEXTUAL: readonly ControlType[] = [
38
+ ControlType.Text, ControlType.Email, ControlType.Tel, ControlType.Url,
39
+ ControlType.Password, ControlType.Textarea,
40
+ ];
41
+
42
+ /** Controls with a comparable magnitude. */
43
+ const NUMERIC: readonly ControlType[] = [ControlType.Number, ControlType.Date, ControlType.Time];
44
+
45
+ export const RULE_DESCRIPTORS: readonly RuleDescriptor[] = Object.freeze([
46
+ {
47
+ name: RuleName.MinLength, labelKey: 'forms.rules.minLength',
48
+ param: RuleParamKind.Number, controls: TEXTUAL, defaultParams: { n: 2 },
49
+ },
50
+ {
51
+ name: RuleName.MaxLength, labelKey: 'forms.rules.maxLength',
52
+ param: RuleParamKind.Number, controls: TEXTUAL, defaultParams: { n: 200 },
53
+ },
54
+ {
55
+ name: RuleName.Min, labelKey: 'forms.rules.min',
56
+ param: RuleParamKind.Number, controls: NUMERIC, defaultParams: { n: 0 },
57
+ },
58
+ {
59
+ name: RuleName.Max, labelKey: 'forms.rules.max',
60
+ param: RuleParamKind.Number, controls: NUMERIC, defaultParams: { n: 100 },
61
+ },
62
+ {
63
+ name: RuleName.Pattern, labelKey: 'forms.rules.pattern',
64
+ param: RuleParamKind.Pattern, controls: TEXTUAL,
65
+ defaultParams: { preset: PatternPreset.Email },
66
+ },
67
+ {
68
+ // Cross-field: confirm-email, confirm-password.
69
+ name: RuleName.Matches, labelKey: 'forms.rules.matches',
70
+ param: RuleParamKind.Field, controls: [],
71
+ },
72
+ {
73
+ // A checkbox's `required` already becomes `accepted`, so offering it again would be a
74
+ // duplicate — it is here for a checkbox the author wants ticked WITHOUT marking it required.
75
+ name: RuleName.Accepted, labelKey: 'forms.rules.accepted',
76
+ param: RuleParamKind.None, controls: [ControlType.Checkbox],
77
+ },
78
+ ]);
79
+
80
+ /** The rules that can be added to a field of this control type. */
81
+ export function rulesFor(control: ControlType | string): RuleDescriptor[] {
82
+ return RULE_DESCRIPTORS.filter(rule =>
83
+ rule.controls.length === 0 || rule.controls.includes(control as ControlType));
84
+ }
85
+
86
+ /**
87
+ * The rules an author may still ADD — the catalog minus what is already on the field.
88
+ *
89
+ * Each rule appears once: two `maxLength` rules on one field is not a configuration anyone means,
90
+ * and the validator would apply whichever came first. `pattern` is the exception a caller might
91
+ * expect to repeat, and it deliberately does not: a second pattern can only contradict the first.
92
+ */
93
+ export function addableRules(
94
+ control: ControlType | string,
95
+ existing: readonly { name: RuleName }[] = [],
96
+ ): RuleDescriptor[] {
97
+ const used = new Set(existing.map(rule => rule.name));
98
+ return rulesFor(control).filter(rule => !used.has(rule.name));
99
+ }
100
+
101
+ /** The preset options a Pattern rule offers, in the order the panel should list them. */
102
+ export const PATTERN_PRESET_OPTIONS: readonly { value: PatternPreset; labelKey: string }[] = Object.freeze([
103
+ { value: PatternPreset.Email, labelKey: 'forms.rules.preset.email' },
104
+ { value: PatternPreset.Phone, labelKey: 'forms.rules.preset.phone' },
105
+ { value: PatternPreset.Url, labelKey: 'forms.rules.preset.url' },
106
+ { value: PatternPreset.Digits, labelKey: 'forms.rules.preset.digits' },
107
+ { value: PatternPreset.Letters, labelKey: 'forms.rules.preset.letters' },
108
+ { value: PatternPreset.Slug, labelKey: 'forms.rules.preset.slug' },
109
+ ]);
110
+
111
+ /** Destinations an author can add to a form, for the After-submit list. */
112
+ export interface ActionDescriptor {
113
+ type: string;
114
+ labelKey: string;
115
+ /** Always present and not removable. */
116
+ locked?: boolean;
117
+ /** Not offered yet — shown greyed with a "coming soon" note rather than hidden, so the roadmap
118
+ * is visible rather than the feature looking absent. */
119
+ planned?: boolean;
120
+ }
121
+
122
+ export const ACTION_DESCRIPTORS: readonly ActionDescriptor[] = Object.freeze([
123
+ { type: 'store', labelKey: 'forms.actions.store', locked: true },
124
+ { type: 'email', labelKey: 'forms.actions.email' },
125
+ { type: 'redirect', labelKey: 'forms.actions.redirect' },
126
+ { type: 'createItem', labelKey: 'forms.actions.createItem', planned: true },
127
+ { type: 'webhook', labelKey: 'forms.actions.webhook', planned: true },
128
+ { type: 'autoReply', labelKey: 'forms.actions.autoReply', planned: true },
129
+ ]);
130
+
131
+ /** Destinations that can still be added — each at most once, and never a planned one. */
132
+ export function addableActions(existing: readonly { type: string }[] = []): ActionDescriptor[] {
133
+ const used = new Set(existing.map(action => action.type));
134
+ return ACTION_DESCRIPTORS.filter(action => !action.locked && !action.planned && !used.has(action.type));
135
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * The submit transport CONTRACT — what a host must provide to make a form actually send.
3
+ *
4
+ * The Form component deliberately owns no HTTP: the builder canvas must never post (an author
5
+ * clicking Submit while designing would file a real submission), and the SSR engine posts through
6
+ * Angular's HttpClient. What IS shared is everything around the call — the endpoint's shape, the
7
+ * anti-spam fields, and how the server's answer maps onto the form's state. Those live here so the
8
+ * two hosts cannot drift, and so a third host (a hand-built integration) has one thing to read.
9
+ *
10
+ * ⚠️ Framework-free, like the rest of forms-core. No Angular, no RxJS, no fetch.
11
+ */
12
+
13
+ import { FieldError } from './validate';
14
+
15
+ /** The field a honeypot input uses. Must match `iox-cms` `anti-spam.ts` — hence the comment there. */
16
+ export const HONEYPOT_FIELD = '_iox_hp';
17
+ /** When the form was rendered, so the server can measure fill time. */
18
+ export const RENDERED_AT_FIELD = '_iox_t';
19
+
20
+ /**
21
+ * What the form hands its host. There is deliberately no `orgId`: the tenant is the HOST's
22
+ * knowledge, not the form's, and a form that had to be told its org would be one more trait an
23
+ * author could get wrong.
24
+ */
25
+ export interface SubmitRequest {
26
+ formId: string;
27
+ values: Record<string, any>;
28
+ }
29
+
30
+ /**
31
+ * The server's answer, normalised. Mirrors `SubmitResult` in `iox-cms`
32
+ * `form-submissions.service.ts` — the two are one contract, so change them together.
33
+ */
34
+ export interface SubmitResponse {
35
+ ok: boolean;
36
+ /** Per-field verdicts from the server's own validation pass, keyed by field name. */
37
+ errors?: Record<string, FieldError>;
38
+ /** Where to send the visitor after a successful submit, when the form has a redirect action. */
39
+ redirect?: string;
40
+ }
41
+
42
+ /**
43
+ * The one function a host implements. Rejecting (or throwing) means the call never reached the
44
+ * server — the form shows its Error state, which is the honest thing to show for a dropped network.
45
+ */
46
+ export type FormTransport = (request: SubmitRequest) => Promise<SubmitResponse>;
47
+
48
+ /**
49
+ * The public submit path. Built here rather than interpolated at each call site, so the engine and
50
+ * any future host cannot disagree about it — and so changing it is one edit.
51
+ *
52
+ * Deliberately relative and WITHOUT a leading slash: the engine mounts under a per-org base href.
53
+ */
54
+ export function submitEndpointPath(orgId: string, formId: string): string {
55
+ return `api/site/${encodeURIComponent(orgId)}/forms/${encodeURIComponent(formId)}/submit`;
56
+ }
57
+
58
+ /**
59
+ * Add the anti-spam fields to the collected values.
60
+ *
61
+ * Both are invisible to a real visitor and cost them nothing (decision D12). `renderedAt` is the
62
+ * time the form was rendered, NOT the time of submit — the server measures the difference to catch
63
+ * a script that fills and posts instantly.
64
+ */
65
+ export function withAntiSpamFields(
66
+ values: Record<string, any>,
67
+ renderedAt: number,
68
+ honeypotValue = '',
69
+ ): Record<string, any> {
70
+ return { ...values, [HONEYPOT_FIELD]: honeypotValue, [RENDERED_AT_FIELD]: renderedAt };
71
+ }
72
+
73
+ /**
74
+ * Normalise whatever came back into a `SubmitResponse`.
75
+ *
76
+ * Fails CLOSED: anything that is not a recognisable success is treated as a failure. A malformed
77
+ * body must never show the visitor a Success state for a submission that was not stored — that is
78
+ * the one lie this feature must not tell.
79
+ */
80
+ export function interpretSubmitResponse(body: any): SubmitResponse {
81
+ if (!body || typeof body !== 'object' || body.ok !== true) {
82
+ return {
83
+ ok: false,
84
+ errors: normaliseErrors(body?.errors),
85
+ };
86
+ }
87
+ const redirect = typeof body.redirect === 'string' && body.redirect.trim()
88
+ ? body.redirect.trim()
89
+ : undefined;
90
+ return { ok: true, redirect };
91
+ }
92
+
93
+ /** Keep only entries that actually carry a message, so a junk key cannot blank a field's error. */
94
+ function normaliseErrors(raw: any): Record<string, FieldError> | undefined {
95
+ if (!raw || typeof raw !== 'object') return undefined;
96
+ const errors: Record<string, FieldError> = {};
97
+ for (const [name, value] of Object.entries(raw as Record<string, any>)) {
98
+ const messageKey = typeof value?.messageKey === 'string' ? value.messageKey : '';
99
+ if (!messageKey) continue;
100
+ errors[name] = value?.params ? { messageKey, params: value.params } : { messageKey };
101
+ }
102
+ return Object.keys(errors).length ? errors : undefined;
103
+ }