@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.
@@ -3768,6 +3768,73 @@ function addableActions(existing = []) {
3768
3768
  return ACTION_DESCRIPTORS.filter(action => !action.locked && !action.planned && !used.has(action.type));
3769
3769
  }
3770
3770
 
3771
+ /**
3772
+ * The submit transport CONTRACT — what a host must provide to make a form actually send.
3773
+ *
3774
+ * The Form component deliberately owns no HTTP: the builder canvas must never post (an author
3775
+ * clicking Submit while designing would file a real submission), and the SSR engine posts through
3776
+ * Angular's HttpClient. What IS shared is everything around the call — the endpoint's shape, the
3777
+ * anti-spam fields, and how the server's answer maps onto the form's state. Those live here so the
3778
+ * two hosts cannot drift, and so a third host (a hand-built integration) has one thing to read.
3779
+ *
3780
+ * ⚠️ Framework-free, like the rest of forms-core. No Angular, no RxJS, no fetch.
3781
+ */
3782
+ /** The field a honeypot input uses. Must match `iox-cms` `anti-spam.ts` — hence the comment there. */
3783
+ const HONEYPOT_FIELD = '_iox_hp';
3784
+ /** When the form was rendered, so the server can measure fill time. */
3785
+ const RENDERED_AT_FIELD = '_iox_t';
3786
+ /**
3787
+ * The public submit path. Built here rather than interpolated at each call site, so the engine and
3788
+ * any future host cannot disagree about it — and so changing it is one edit.
3789
+ *
3790
+ * Deliberately relative and WITHOUT a leading slash: the engine mounts under a per-org base href.
3791
+ */
3792
+ function submitEndpointPath(orgId, formId) {
3793
+ return `api/site/${encodeURIComponent(orgId)}/forms/${encodeURIComponent(formId)}/submit`;
3794
+ }
3795
+ /**
3796
+ * Add the anti-spam fields to the collected values.
3797
+ *
3798
+ * Both are invisible to a real visitor and cost them nothing (decision D12). `renderedAt` is the
3799
+ * time the form was rendered, NOT the time of submit — the server measures the difference to catch
3800
+ * a script that fills and posts instantly.
3801
+ */
3802
+ function withAntiSpamFields(values, renderedAt, honeypotValue = '') {
3803
+ return { ...values, [HONEYPOT_FIELD]: honeypotValue, [RENDERED_AT_FIELD]: renderedAt };
3804
+ }
3805
+ /**
3806
+ * Normalise whatever came back into a `SubmitResponse`.
3807
+ *
3808
+ * Fails CLOSED: anything that is not a recognisable success is treated as a failure. A malformed
3809
+ * body must never show the visitor a Success state for a submission that was not stored — that is
3810
+ * the one lie this feature must not tell.
3811
+ */
3812
+ function interpretSubmitResponse(body) {
3813
+ if (!body || typeof body !== 'object' || body.ok !== true) {
3814
+ return {
3815
+ ok: false,
3816
+ errors: normaliseErrors(body?.errors),
3817
+ };
3818
+ }
3819
+ const redirect = typeof body.redirect === 'string' && body.redirect.trim()
3820
+ ? body.redirect.trim()
3821
+ : undefined;
3822
+ return { ok: true, redirect };
3823
+ }
3824
+ /** Keep only entries that actually carry a message, so a junk key cannot blank a field's error. */
3825
+ function normaliseErrors(raw) {
3826
+ if (!raw || typeof raw !== 'object')
3827
+ return undefined;
3828
+ const errors = {};
3829
+ for (const [name, value] of Object.entries(raw)) {
3830
+ const messageKey = typeof value?.messageKey === 'string' ? value.messageKey : '';
3831
+ if (!messageKey)
3832
+ continue;
3833
+ errors[name] = value?.params ? { messageKey, params: value.params } : { messageKey };
3834
+ }
3835
+ return Object.keys(errors).length ? errors : undefined;
3836
+ }
3837
+
3771
3838
  /*
3772
3839
  * @vectoriox/iox-ui/forms-core — the shared, FRAMEWORK-FREE core of the forms feature.
3773
3840
  *
@@ -3887,6 +3954,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
3887
3954
  type: Injectable
3888
3955
  }] });
3889
3956
 
3957
+ /**
3958
+ * How a host makes the form actually send. The SSR engine provides one (an HttpClient POST); the
3959
+ * builder canvas deliberately does NOT — an author pressing Submit while designing must never file
3960
+ * a real submission. Without it the component keeps its original behaviour and only emits
3961
+ * `formSubmit`, which is also the escape hatch for a host that wants the transport itself.
3962
+ */
3963
+ const IOX_FORM_TRANSPORT = new InjectionToken('IOX_FORM_TRANSPORT');
3964
+ /** Where a successful submit sends the visitor. Provided by the host; absent on the canvas. */
3965
+ const IOX_FORM_NAVIGATOR = new InjectionToken('IOX_FORM_NAVIGATOR');
3890
3966
  /** Which of the form's four states the canvas / runtime is showing. */
3891
3967
  var FormRenderState;
3892
3968
  (function (FormRenderState) {
@@ -3917,9 +3993,11 @@ class IoxFormComponent {
3917
3993
  * specs construct components directly. `runtime` is provided by THIS component (see
3918
3994
  * `providers`), so every Field created into `childrenHost` resolves this instance by ancestry.
3919
3995
  */
3920
- constructor(runtime, host) {
3996
+ constructor(runtime, host, transport = null, navigate = null) {
3921
3997
  this.runtime = runtime;
3922
3998
  this.host = host;
3999
+ this.transport = transport;
4000
+ this.navigate = navigate;
3923
4001
  this.nodeId = '';
3924
4002
  /** Stable id the submit endpoint resolves the saved spec by. */
3925
4003
  this.formId = '';
@@ -3935,6 +4013,17 @@ class IoxFormComponent {
3935
4013
  this.formSubmit = new EventEmitter();
3936
4014
  /** Emitted when a submit attempt was refused locally, so a host can react (analytics, focus). */
3937
4015
  this.validationFailed = new EventEmitter();
4016
+ /** Exposed to the template so the input's name and the server's check cannot drift. */
4017
+ this.honeypotField = HONEYPOT_FIELD;
4018
+ /** When this form was rendered — the fill-time signal the server checks. */
4019
+ this.renderedAt = 0;
4020
+ /** Guards against a double submit while a POST is in flight. */
4021
+ this.inFlight = false;
4022
+ }
4023
+ ngOnInit() {
4024
+ // Stamp render time for the fill-time check. Taken ONCE, when the form appears — the whole
4025
+ // point is the gap between seeing the form and sending it.
4026
+ this.renderedAt = Date.now();
3938
4027
  }
3939
4028
  ngOnChanges() {
3940
4029
  // Traits are edited by DIRECT ASSIGNMENT on the canvas, which does not call ngOnChanges —
@@ -3954,7 +4043,71 @@ class IoxFormComponent {
3954
4043
  this.validationFailed.emit(this.runtime.state.errors);
3955
4044
  return;
3956
4045
  }
3957
- this.formSubmit.emit({ values: this.runtime.values(), event });
4046
+ const values = this.runtime.values();
4047
+ this.formSubmit.emit({ values, event });
4048
+ // No transport means the host owns sending (the builder canvas, which must never post).
4049
+ if (!this.transport || this.inFlight)
4050
+ return;
4051
+ void this.send(values);
4052
+ }
4053
+ /**
4054
+ * The submit lifecycle, owned HERE so both hosts get the same one: submitting → the server's
4055
+ * verdict → success (and an optional redirect) or error. Only the HTTP call itself is the
4056
+ * host's, which is the one part that genuinely differs between the engine and anything else.
4057
+ */
4058
+ async send(values) {
4059
+ this.inFlight = true;
4060
+ this.renderState = FormRenderState.Submitting;
4061
+ this.runtime.setSubmitting(true);
4062
+ let response;
4063
+ try {
4064
+ response = await this.transport({
4065
+ formId: this.formId,
4066
+ values: withAntiSpamFields(values, this.renderedAt, this.honeypotValue()),
4067
+ });
4068
+ }
4069
+ catch {
4070
+ // The call never reached the server. Showing Success here would tell the visitor their
4071
+ // message was sent when nothing was stored — the one lie this feature must not tell.
4072
+ response = { ok: false };
4073
+ }
4074
+ this.inFlight = false;
4075
+ this.runtime.setSubmitting(false);
4076
+ this.applyResponse(response);
4077
+ }
4078
+ /** Turn the server's answer into what the visitor sees. Public so a host-owned transport can reuse it. */
4079
+ applyResponse(response) {
4080
+ if (!response?.ok) {
4081
+ // Per-field verdicts go back onto the fields; anything else is a whole-form failure,
4082
+ // which is what the designed Error block is for.
4083
+ if (response?.errors) {
4084
+ this.runtime.applyErrors(response.errors);
4085
+ this.renderState = FormRenderState.Idle;
4086
+ this.focusFirstInvalid();
4087
+ return;
4088
+ }
4089
+ this.renderState = FormRenderState.Error;
4090
+ return;
4091
+ }
4092
+ this.renderState = FormRenderState.Success;
4093
+ // Clear the values so a visitor who stays on the page does not see their old answers behind
4094
+ // the success message, and cannot resubmit them by pressing enter.
4095
+ this.runtime.reset();
4096
+ this.resetControls();
4097
+ if (response.redirect && this.navigate)
4098
+ this.navigate(response.redirect);
4099
+ }
4100
+ /** The honeypot input's value, if the host rendered one. Absent reads as empty — never as spam. */
4101
+ honeypotValue() {
4102
+ const input = this.host.nativeElement
4103
+ .querySelector?.(`input[name="${HONEYPOT_FIELD}"]`);
4104
+ return input?.value ?? '';
4105
+ }
4106
+ /** Blank the real DOM controls after a success. The runtime state alone would leave them filled. */
4107
+ resetControls() {
4108
+ const form = this.host.nativeElement;
4109
+ if (typeof form.reset === 'function')
4110
+ form.reset();
3958
4111
  }
3959
4112
  focusFirstInvalid() {
3960
4113
  const name = this.runtime.firstInvalidName();
@@ -3971,20 +4124,40 @@ class IoxFormComponent {
3971
4124
  return;
3972
4125
  }
3973
4126
  }
3974
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: IoxFormComponent, deps: [{ token: FormRuntimeService }, { token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component }); }
3975
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.31", type: IoxFormComponent, isStandalone: false, selector: "form[iox-form]", inputs: { nodeId: "nodeId", formId: "formId", name: "name", renderState: "renderState", feedback: "feedback" }, outputs: { formSubmit: "formSubmit", validationFailed: "validationFailed" }, host: { listeners: { "submit": "onSubmit($event)" }, properties: { "class": "'iox-form iox-node-' + nodeId", "attr.novalidate": "''", "attr.data-iox-form": "formId || null", "attr.data-iox-state": "renderState", "attr.aria-busy": "renderState === 'submitting' ? 'true' : null" } }, providers: [FormRuntimeService], viewQueries: [{ propertyName: "childrenHost", first: true, predicate: ["childrenHost"], descendants: true, read: ViewContainerRef, static: true }], usesOnChanges: true, ngImport: i0, template: `<ng-container #childrenHost></ng-container>`, isInline: true, styles: [":host{box-sizing:border-box}\n"] }); }
4127
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: IoxFormComponent, deps: [{ token: FormRuntimeService }, { token: i0.ElementRef }, { token: IOX_FORM_TRANSPORT, optional: true }, { token: IOX_FORM_NAVIGATOR, optional: true }], target: i0.ɵɵFactoryTarget.Component }); }
4128
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.31", type: IoxFormComponent, isStandalone: false, selector: "form[iox-form]", inputs: { nodeId: "nodeId", formId: "formId", name: "name", renderState: "renderState", feedback: "feedback" }, outputs: { formSubmit: "formSubmit", validationFailed: "validationFailed" }, host: { listeners: { "submit": "onSubmit($event)" }, properties: { "class": "'iox-form iox-node-' + nodeId", "attr.novalidate": "''", "attr.data-iox-form": "formId || null", "attr.data-iox-state": "renderState", "attr.aria-busy": "renderState === 'submitting' ? 'true' : null" } }, providers: [FormRuntimeService], viewQueries: [{ propertyName: "childrenHost", first: true, predicate: ["childrenHost"], descendants: true, read: ViewContainerRef, static: true }], usesOnChanges: true, ngImport: i0, template: `
4129
+ <div class="iox-form__hp" aria-hidden="true">
4130
+ <input type="text" [attr.name]="honeypotField" tabindex="-1" autocomplete="off" />
4131
+ </div>
4132
+ <ng-container #childrenHost></ng-container>
4133
+ `, isInline: true, styles: [":host{box-sizing:border-box}.iox-form__hp{position:absolute;inset-inline-start:-9999px;width:1px;height:1px;overflow:hidden}\n"] }); }
3976
4134
  }
3977
4135
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: IoxFormComponent, decorators: [{
3978
4136
  type: Component,
3979
- args: [{ selector: 'form[iox-form]', template: `<ng-container #childrenHost></ng-container>`, host: {
4137
+ args: [{ selector: 'form[iox-form]', template: `
4138
+ <div class="iox-form__hp" aria-hidden="true">
4139
+ <input type="text" [attr.name]="honeypotField" tabindex="-1" autocomplete="off" />
4140
+ </div>
4141
+ <ng-container #childrenHost></ng-container>
4142
+ `, host: {
3980
4143
  '[class]': "'iox-form iox-node-' + nodeId",
3981
4144
  '[attr.novalidate]': "''",
3982
4145
  '[attr.data-iox-form]': 'formId || null',
3983
4146
  '[attr.data-iox-state]': 'renderState',
3984
4147
  '[attr.aria-busy]': "renderState === 'submitting' ? 'true' : null",
3985
4148
  '(submit)': 'onSubmit($event)',
3986
- }, providers: [FormRuntimeService], standalone: false, styles: [":host{box-sizing:border-box}\n"] }]
3987
- }], ctorParameters: () => [{ type: FormRuntimeService }, { type: i0.ElementRef }], propDecorators: { nodeId: [{
4149
+ }, providers: [FormRuntimeService], standalone: false, styles: [":host{box-sizing:border-box}.iox-form__hp{position:absolute;inset-inline-start:-9999px;width:1px;height:1px;overflow:hidden}\n"] }]
4150
+ }], ctorParameters: () => [{ type: FormRuntimeService }, { type: i0.ElementRef }, { type: undefined, decorators: [{
4151
+ type: Optional
4152
+ }, {
4153
+ type: Inject,
4154
+ args: [IOX_FORM_TRANSPORT]
4155
+ }] }, { type: undefined, decorators: [{
4156
+ type: Optional
4157
+ }, {
4158
+ type: Inject,
4159
+ args: [IOX_FORM_NAVIGATOR]
4160
+ }] }], propDecorators: { nodeId: [{
3988
4161
  type: Input
3989
4162
  }], formId: [{
3990
4163
  type: Input
@@ -5980,5 +6153,5 @@ function effectiveTransitionId(anim) {
5980
6153
  * Generated bundle index. Do not edit.
5981
6154
  */
5982
6155
 
5983
- 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 };
6156
+ 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 };
5984
6157
  //# sourceMappingURL=vectoriox-iox-ui.mjs.map