@vectoriox/iox-ui 4.32.0 → 4.33.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/vectoriox-iox-ui-forms-core.mjs +68 -1
- package/fesm2022/vectoriox-iox-ui-forms-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui.mjs +181 -8
- package/fesm2022/vectoriox-iox-ui.mjs.map +1 -1
- package/forms-core/index.d.ts +67 -2
- package/index.d.ts +72 -4
- package/package.json +1 -1
- package/src/forms-core/public-api.ts +1 -0
- package/src/forms-core/transport.ts +103 -0
package/forms-core/index.d.ts
CHANGED
|
@@ -307,5 +307,70 @@ declare function addableActions(existing?: readonly {
|
|
|
307
307
|
type: string;
|
|
308
308
|
}[]): ActionDescriptor[];
|
|
309
309
|
|
|
310
|
-
|
|
311
|
-
|
|
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,
|
|
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
|
@@ -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
|
+
}
|