@vectoriox/iox-ui 4.28.0 → 4.29.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.
@@ -3123,78 +3123,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
3123
3123
  type: Input
3124
3124
  }] } });
3125
3125
 
3126
- /** Which of the form's four states the canvas / runtime is showing. */
3127
- var FormRenderState;
3128
- (function (FormRenderState) {
3129
- FormRenderState["Idle"] = "idle";
3130
- FormRenderState["Submitting"] = "submitting";
3131
- FormRenderState["Success"] = "success";
3132
- FormRenderState["Error"] = "error";
3133
- })(FormRenderState || (FormRenderState = {}));
3134
- /**
3135
- * Directive-host Form: the real `<form>` IS the host — no wrapper, no inner/outer split. Children
3136
- * (fields, layout, the submit button, and the two state blocks) render into it via `childrenHost`.
3137
- *
3138
- * Scope is ANCESTRY, not naming (decision D1): a Field belongs to its nearest ancestor Form, so
3139
- * there is no form-id to pick and nothing to mis-wire. The runtime finds its fields by walking the
3140
- * DOM it owns.
3141
- *
3142
- * `renderState` drives which children are visible. In the builder it is set by the canvas state
3143
- * switcher so an author can DESIGN the success and error blocks; at runtime the same input is
3144
- * driven by the submit lifecycle. One mechanism, two drivers — the author designs exactly what a
3145
- * visitor will see.
3146
- *
3147
- * ⚠️ `novalidate` is deliberate: the browser's native bubbles would pre-empt our own messages,
3148
- * which are styleable, translated and announced. Validation still runs — ours does it.
3149
- */
3150
- class IoxFormComponent {
3151
- constructor() {
3152
- this.nodeId = '';
3153
- /** Stable id the submit endpoint resolves the saved spec by. */
3154
- this.formId = '';
3155
- /** Author-facing name — what the submissions inbox groups by. */
3156
- this.name = '';
3157
- this.renderState = FormRenderState.Idle;
3158
- /**
3159
- * Emitted instead of navigating. The host (builder canvas or engine runtime) owns the transport;
3160
- * this component owns only the DOM contract, so the same component works in both renderers.
3161
- */
3162
- this.formSubmit = new EventEmitter();
3163
- }
3164
- onSubmit(event) {
3165
- // Never let the browser navigate: a full page POST would lose the designed Success state
3166
- // and every value the visitor typed.
3167
- event.preventDefault();
3168
- this.formSubmit.emit(event);
3169
- }
3170
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: IoxFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
3171
- 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" }, outputs: { formSubmit: "formSubmit" }, 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" } }, viewQueries: [{ propertyName: "childrenHost", first: true, predicate: ["childrenHost"], descendants: true, read: ViewContainerRef, static: true }], ngImport: i0, template: `<ng-container #childrenHost></ng-container>`, isInline: true, styles: [":host{box-sizing:border-box}:host ::ng-deep .iox-form-success,:host ::ng-deep .iox-form-error{display:none}:host([data-iox-state=\"success\"]) ::ng-deep .iox-form-success{display:block}:host([data-iox-state=\"error\"]) ::ng-deep .iox-form-error{display:block}:host([data-iox-state=\"success\"]) ::ng-deep .iox-field,:host([data-iox-state=\"success\"]) ::ng-deep .iox-field-group,:host([data-iox-state=\"success\"]) ::ng-deep .iox-submit{display:none}\n"] }); }
3172
- }
3173
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: IoxFormComponent, decorators: [{
3174
- type: Component,
3175
- args: [{ selector: 'form[iox-form]', template: `<ng-container #childrenHost></ng-container>`, host: {
3176
- '[class]': "'iox-form iox-node-' + nodeId",
3177
- '[attr.novalidate]': "''",
3178
- '[attr.data-iox-form]': 'formId || null',
3179
- '[attr.data-iox-state]': 'renderState',
3180
- '[attr.aria-busy]': "renderState === 'submitting' ? 'true' : null",
3181
- '(submit)': 'onSubmit($event)',
3182
- }, standalone: false, styles: [":host{box-sizing:border-box}:host ::ng-deep .iox-form-success,:host ::ng-deep .iox-form-error{display:none}:host([data-iox-state=\"success\"]) ::ng-deep .iox-form-success{display:block}:host([data-iox-state=\"error\"]) ::ng-deep .iox-form-error{display:block}:host([data-iox-state=\"success\"]) ::ng-deep .iox-field,:host([data-iox-state=\"success\"]) ::ng-deep .iox-field-group,:host([data-iox-state=\"success\"]) ::ng-deep .iox-submit{display:none}\n"] }]
3183
- }], propDecorators: { nodeId: [{
3184
- type: Input
3185
- }], formId: [{
3186
- type: Input
3187
- }], name: [{
3188
- type: Input
3189
- }], renderState: [{
3190
- type: Input
3191
- }], formSubmit: [{
3192
- type: Output
3193
- }], childrenHost: [{
3194
- type: ViewChild,
3195
- args: ['childrenHost', { read: ViewContainerRef, static: true }]
3196
- }] } });
3197
-
3198
3126
  // The persisted shape of a form. Pure types + the one derived constant each renderer needs.
3199
3127
  //
3200
3128
  // A FormSpec is DERIVED from the node tree (see `collect.ts`), never hand-maintained beside it —
@@ -3656,6 +3584,87 @@ function normaliseActions(raw) {
3656
3584
  return [STORE_ACTION, ...declared.filter(a => a.type !== STORE_ACTION.type)];
3657
3585
  }
3658
3586
 
3587
+ // When a field's error is allowed to be VISIBLE — the pure half of the form runtime.
3588
+ //
3589
+ // Validity and visibility are different questions. A field can be invalid from the moment the page
3590
+ // loads (an empty required field always is), but showing that immediately means greeting a visitor
3591
+ // with a wall of red before they have typed anything. This module owns the second question, and it
3592
+ // owns it in one place so the builder canvas, the SSR engine and any future runtime agree.
3593
+ //
3594
+ // Framework-free, like the rest of forms-core.
3595
+ const INITIAL_FORM_STATE = Object.freeze({
3596
+ touched: new Set(),
3597
+ submitted: false,
3598
+ errors: {},
3599
+ submitting: false,
3600
+ });
3601
+ /**
3602
+ * Should this field's error be on screen right now?
3603
+ *
3604
+ * - `Submit` — only after a submit attempt. Quietest; nothing moves while typing.
3605
+ * - `Live` — as soon as it is invalid. Loudest; shouts at a half-typed email address.
3606
+ * - `Smart` — the default, and the reason the author gets one dropdown instead of a decision per
3607
+ * field: stay quiet until the visitor has either left the field or tried to submit.
3608
+ * After that, correct live, so fixing an error clears it as they type.
3609
+ */
3610
+ function shouldShowError(fieldName, state, timing = FeedbackTiming.Smart) {
3611
+ if (!state.errors[fieldName])
3612
+ return false;
3613
+ switch (timing) {
3614
+ case FeedbackTiming.Live: return true;
3615
+ case FeedbackTiming.Submit: return state.submitted;
3616
+ default: return state.submitted || state.touched.has(fieldName);
3617
+ }
3618
+ }
3619
+ /**
3620
+ * Re-validate one field after a change. Only that field is re-checked, EXCEPT that a `matches` rule
3621
+ * makes another field's validity depend on this one — so a confirm-email field stops being wrong
3622
+ * the moment you fix the address it points at, rather than waiting for its own blur.
3623
+ */
3624
+ function onFieldChange(state, fieldName, values, spec) {
3625
+ const errors = { ...state.errors };
3626
+ const affected = [fieldName, ...dependentsOf(fieldName, spec)];
3627
+ for (const name of affected) {
3628
+ const field = (spec.fields ?? []).find(f => f.name === name);
3629
+ if (!field)
3630
+ continue;
3631
+ const result = validateField(values?.[name], field, values ?? {});
3632
+ if (result.valid)
3633
+ delete errors[name];
3634
+ else
3635
+ errors[name] = { messageKey: result.messageKey, params: result.params };
3636
+ }
3637
+ return { ...state, errors };
3638
+ }
3639
+ /** Mark a field touched (on blur) and re-check it, so Smart timing can start showing its error. */
3640
+ function onFieldBlur(state, fieldName, values, spec) {
3641
+ const touched = new Set(state.touched);
3642
+ touched.add(fieldName);
3643
+ return { ...onFieldChange(state, fieldName, values, spec), touched };
3644
+ }
3645
+ /**
3646
+ * A submit attempt. Validates everything and flips `submitted`, so from here on every error shows
3647
+ * whatever the timing setting is — a visitor who pressed the button is owed the full picture.
3648
+ */
3649
+ function onSubmitAttempt(state, values, spec) {
3650
+ const { errors } = validateForm(values ?? {}, spec);
3651
+ return { ...state, submitted: true, errors };
3652
+ }
3653
+ /** Server-side rejection: adopt the server's errors verbatim. It is the authority, not the client. */
3654
+ function applyServerErrors(state, errors) {
3655
+ return { ...state, submitted: true, submitting: false, errors: { ...errors } };
3656
+ }
3657
+ /** A clean slate after a successful submit, for a form configured to clear itself. */
3658
+ function resetFormState() {
3659
+ return { touched: new Set(), submitted: false, errors: {}, submitting: false };
3660
+ }
3661
+ /** Field names whose validity depends on `fieldName` through a `matches` rule. */
3662
+ function dependentsOf(fieldName, spec) {
3663
+ return (spec.fields ?? [])
3664
+ .filter(f => (f.rules ?? []).some(r => r.params?.['field'] === fieldName))
3665
+ .map(f => f.name);
3666
+ }
3667
+
3659
3668
  /*
3660
3669
  * @vectoriox/iox-ui/forms-core — the shared, FRAMEWORK-FREE core of the forms feature.
3661
3670
  *
@@ -3668,6 +3677,229 @@ function normaliseActions(raw) {
3668
3677
  * disagree. See iox-ai-guidance `architecture/builder/forms-and-inputs-plan.md` §5.
3669
3678
  */
3670
3679
 
3680
+ const IOX_FORM_MESSAGES = new InjectionToken('IOX_FORM_MESSAGES', {
3681
+ providedIn: 'root',
3682
+ factory: () => defaultMessageResolver,
3683
+ });
3684
+ /** `{{n}}` / `{{field}}` interpolation over the built-in English catalog. */
3685
+ const defaultMessageResolver = (key, params) => {
3686
+ const template = DEFAULT_EN_MESSAGES[key] ?? key;
3687
+ if (!params)
3688
+ return template;
3689
+ return template.replace(/\{\{(\w+)\}\}/g, (_, name) => String(params[name] ?? ''));
3690
+ };
3691
+ /**
3692
+ * Per-form runtime. **Provided by `IoxFormComponent`**, so every Field created into that form's
3693
+ * `childrenHost` resolves THIS instance through its element injector — which is exactly how a field
3694
+ * finds its form without a form-id, matching the ancestry rule (decision D1). A Field outside any
3695
+ * form injects nothing and simply renders.
3696
+ *
3697
+ * All the decisions live in the pure `forms-core` runtime; this class only holds the mutable state,
3698
+ * the registered fields and the Angular-shaped API. Nothing here decides *when* an error shows.
3699
+ */
3700
+ class FormRuntimeService {
3701
+ constructor() {
3702
+ /** Registered fields, in DOM order — the order the spec and focus-first-invalid depend on. */
3703
+ this.fields = [];
3704
+ this._state = INITIAL_FORM_STATE;
3705
+ this._spec = null;
3706
+ this._timing = FeedbackTiming.Smart;
3707
+ }
3708
+ get state() { return this._state; }
3709
+ get submitting() { return this._state.submitting; }
3710
+ /** Set by the Form from its own traits. */
3711
+ configure(spec, timing) {
3712
+ this._spec = spec;
3713
+ this._timing = timing;
3714
+ }
3715
+ /**
3716
+ * A Field announces itself here on init. It passes FUNCTIONS rather than values so the runtime
3717
+ * always reads the field's current spec and value — a trait edited on the canvas takes effect
3718
+ * without re-registering anything.
3719
+ */
3720
+ register(field) {
3721
+ this.fields.push(field);
3722
+ return () => {
3723
+ const index = this.fields.indexOf(field);
3724
+ if (index >= 0)
3725
+ this.fields.splice(index, 1);
3726
+ };
3727
+ }
3728
+ /** The live spec, derived from what is registered right now. */
3729
+ specNow() {
3730
+ const base = this._spec ?? {
3731
+ formId: '', name: '', feedback: FeedbackTiming.Smart,
3732
+ fields: [], actions: [], antiSpam: { honeypot: true, minFillMs: 1200 },
3733
+ };
3734
+ return { ...base, fields: this.fields.map(f => f.spec()) };
3735
+ }
3736
+ /** Current values, keyed by submitted field name. */
3737
+ values() {
3738
+ const out = {};
3739
+ for (const field of this.fields)
3740
+ out[field.spec().name] = field.getValue();
3741
+ return out;
3742
+ }
3743
+ onChange(name) {
3744
+ this._state = onFieldChange(this._state, name, this.values(), this.specNow());
3745
+ }
3746
+ onBlur(name) {
3747
+ this._state = onFieldBlur(this._state, name, this.values(), this.specNow());
3748
+ }
3749
+ /** Validate everything. Returns whether the submission may proceed. */
3750
+ validateAll() {
3751
+ this._state = onSubmitAttempt(this._state, this.values(), this.specNow());
3752
+ return Object.keys(this._state.errors).length === 0;
3753
+ }
3754
+ setSubmitting(submitting) {
3755
+ this._state = { ...this._state, submitting };
3756
+ }
3757
+ /** The server rejected it — its verdict replaces ours. */
3758
+ applyErrors(errors) {
3759
+ this._state = { ...this._state, submitted: true, submitting: false, errors: { ...errors } };
3760
+ }
3761
+ reset() {
3762
+ this._state = resetFormState();
3763
+ }
3764
+ /** Whether this field's error is allowed on screen right now. */
3765
+ showsError(name) {
3766
+ return shouldShowError(name, this._state, this._timing);
3767
+ }
3768
+ errorFor(name) {
3769
+ return this._state.errors[name];
3770
+ }
3771
+ /** The first invalid field in registration (DOM) order — what submit should focus. */
3772
+ firstInvalidName() {
3773
+ for (const field of this.fields) {
3774
+ const name = field.spec().name;
3775
+ if (this._state.errors[name])
3776
+ return name;
3777
+ }
3778
+ return null;
3779
+ }
3780
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: FormRuntimeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
3781
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: FormRuntimeService }); }
3782
+ }
3783
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: FormRuntimeService, decorators: [{
3784
+ type: Injectable
3785
+ }] });
3786
+
3787
+ /** Which of the form's four states the canvas / runtime is showing. */
3788
+ var FormRenderState;
3789
+ (function (FormRenderState) {
3790
+ FormRenderState["Idle"] = "idle";
3791
+ FormRenderState["Submitting"] = "submitting";
3792
+ FormRenderState["Success"] = "success";
3793
+ FormRenderState["Error"] = "error";
3794
+ })(FormRenderState || (FormRenderState = {}));
3795
+ /**
3796
+ * Directive-host Form: the real `<form>` IS the host — no wrapper, no inner/outer split. Children
3797
+ * (fields, layout, the submit button, and the two state blocks) render into it via `childrenHost`.
3798
+ *
3799
+ * Scope is ANCESTRY, not naming (decision D1): a Field belongs to its nearest ancestor Form, so
3800
+ * there is no form-id to pick and nothing to mis-wire. The runtime finds its fields by walking the
3801
+ * DOM it owns.
3802
+ *
3803
+ * `renderState` drives which children are visible. In the builder it is set by the canvas state
3804
+ * switcher so an author can DESIGN the success and error blocks; at runtime the same input is
3805
+ * driven by the submit lifecycle. One mechanism, two drivers — the author designs exactly what a
3806
+ * visitor will see.
3807
+ *
3808
+ * ⚠️ `novalidate` is deliberate: the browser's native bubbles would pre-empt our own messages,
3809
+ * which are styleable, translated and announced. Validation still runs — ours does it.
3810
+ */
3811
+ class IoxFormComponent {
3812
+ /**
3813
+ * Constructor injection rather than `inject()`, for the same reason as Field: this library's
3814
+ * specs construct components directly. `runtime` is provided by THIS component (see
3815
+ * `providers`), so every Field created into `childrenHost` resolves this instance by ancestry.
3816
+ */
3817
+ constructor(runtime, host) {
3818
+ this.runtime = runtime;
3819
+ this.host = host;
3820
+ this.nodeId = '';
3821
+ /** Stable id the submit endpoint resolves the saved spec by. */
3822
+ this.formId = '';
3823
+ /** Author-facing name — what the submissions inbox groups by. */
3824
+ this.name = '';
3825
+ this.renderState = FormRenderState.Idle;
3826
+ this.feedback = FeedbackTiming.Smart;
3827
+ /**
3828
+ * Emitted ONLY when client-side validation passes, carrying the collected values. The host
3829
+ * (builder canvas or engine runtime) owns the transport — posting, retries and the server's
3830
+ * verdict — so the same component serves both renderers.
3831
+ */
3832
+ this.formSubmit = new EventEmitter();
3833
+ /** Emitted when a submit attempt was refused locally, so a host can react (analytics, focus). */
3834
+ this.validationFailed = new EventEmitter();
3835
+ }
3836
+ ngOnChanges() {
3837
+ // Traits are edited by DIRECT ASSIGNMENT on the canvas, which does not call ngOnChanges —
3838
+ // but `feedback` is also read live by `runtime.showsError`, so re-configuring here covers
3839
+ // the setInput path and the getter covers the rest.
3840
+ this.runtime.configure(this.runtime.specNow(), this.feedback);
3841
+ }
3842
+ onSubmit(event) {
3843
+ // Never let the browser navigate: a full page POST would lose the designed Success state
3844
+ // and every value the visitor typed.
3845
+ event.preventDefault();
3846
+ this.runtime.configure(this.runtime.specNow(), this.feedback);
3847
+ if (!this.runtime.validateAll()) {
3848
+ // Move the visitor TO the problem rather than leaving them to hunt for red text —
3849
+ // the single most valuable thing a failed submit can do.
3850
+ this.focusFirstInvalid();
3851
+ this.validationFailed.emit(this.runtime.state.errors);
3852
+ return;
3853
+ }
3854
+ this.formSubmit.emit({ values: this.runtime.values(), event });
3855
+ }
3856
+ focusFirstInvalid() {
3857
+ const name = this.runtime.firstInvalidName();
3858
+ if (!name)
3859
+ return;
3860
+ // Compare the attribute rather than interpolating the name into a selector: `CSS.escape` is
3861
+ // a browser-only global (undefined on the server and under the unit-test runner), and an
3862
+ // author-overridden field key can contain characters a raw selector would choke on.
3863
+ const fields = this.host.nativeElement.querySelectorAll?.('[data-iox-field]') ?? [];
3864
+ for (const element of Array.from(fields)) {
3865
+ if (element.getAttribute('data-iox-field') !== name)
3866
+ continue;
3867
+ element.querySelector('.iox-field__control')?.focus();
3868
+ return;
3869
+ }
3870
+ }
3871
+ 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 }); }
3872
+ 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}:host ::ng-deep .iox-form-success,:host ::ng-deep .iox-form-error{display:none}:host([data-iox-state=\"success\"]) ::ng-deep .iox-form-success{display:block}:host([data-iox-state=\"error\"]) ::ng-deep .iox-form-error{display:block}:host([data-iox-state=\"success\"]) ::ng-deep .iox-field,:host([data-iox-state=\"success\"]) ::ng-deep .iox-field-group,:host([data-iox-state=\"success\"]) ::ng-deep .iox-submit{display:none}\n"] }); }
3873
+ }
3874
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: IoxFormComponent, decorators: [{
3875
+ type: Component,
3876
+ args: [{ selector: 'form[iox-form]', template: `<ng-container #childrenHost></ng-container>`, host: {
3877
+ '[class]': "'iox-form iox-node-' + nodeId",
3878
+ '[attr.novalidate]': "''",
3879
+ '[attr.data-iox-form]': 'formId || null',
3880
+ '[attr.data-iox-state]': 'renderState',
3881
+ '[attr.aria-busy]': "renderState === 'submitting' ? 'true' : null",
3882
+ '(submit)': 'onSubmit($event)',
3883
+ }, providers: [FormRuntimeService], standalone: false, styles: [":host{box-sizing:border-box}:host ::ng-deep .iox-form-success,:host ::ng-deep .iox-form-error{display:none}:host([data-iox-state=\"success\"]) ::ng-deep .iox-form-success{display:block}:host([data-iox-state=\"error\"]) ::ng-deep .iox-form-error{display:block}:host([data-iox-state=\"success\"]) ::ng-deep .iox-field,:host([data-iox-state=\"success\"]) ::ng-deep .iox-field-group,:host([data-iox-state=\"success\"]) ::ng-deep .iox-submit{display:none}\n"] }]
3884
+ }], ctorParameters: () => [{ type: FormRuntimeService }, { type: i0.ElementRef }], propDecorators: { nodeId: [{
3885
+ type: Input
3886
+ }], formId: [{
3887
+ type: Input
3888
+ }], name: [{
3889
+ type: Input
3890
+ }], renderState: [{
3891
+ type: Input
3892
+ }], feedback: [{
3893
+ type: Input
3894
+ }], formSubmit: [{
3895
+ type: Output
3896
+ }], validationFailed: [{
3897
+ type: Output
3898
+ }], childrenHost: [{
3899
+ type: ViewChild,
3900
+ args: ['childrenHost', { read: ViewContainerRef, static: true }]
3901
+ }] } });
3902
+
3671
3903
  let fieldSeq = 0;
3672
3904
  /**
3673
3905
  * The compound Field: label + control + help + error + required mark, as ONE node (decision D2).
@@ -3685,8 +3917,19 @@ let fieldSeq = 0;
3685
3917
  * and choosing `email` also chooses email validation.
3686
3918
  */
3687
3919
  class IoxFieldComponent {
3688
- constructor() {
3920
+ /**
3921
+ * Classic constructor injection, not `inject()`: this library's specs construct components
3922
+ * directly (its vitest setup cannot run `TestBed.createComponent`), and a field initializer
3923
+ * calling `inject()` throws NG0203 outside an injection context.
3924
+ *
3925
+ * `runtime` is optional because a Field dropped OUTSIDE any Form must still render — it simply
3926
+ * has nothing to submit to. That is the same ancestry rule from the other side.
3927
+ */
3928
+ constructor(runtime, resolveMessage) {
3929
+ this.runtime = runtime;
3930
+ this.resolveMessage = resolveMessage;
3689
3931
  this.nodeId = '';
3932
+ /** The submitted key. Blank means "derive it from the label" — authors never invent keys. */
3690
3933
  this.name = '';
3691
3934
  this.label = '';
3692
3935
  this.control = ControlType.Text;
@@ -3700,10 +3943,15 @@ class IoxFieldComponent {
3700
3943
  this.accept = '';
3701
3944
  this.options = [];
3702
3945
  this.value = '';
3703
- /** The resolved, already-translated message. Resolving the key is the host's job, not ours. */
3704
- this.errorText = '';
3705
- /** Design-time or runtime failure. The style compiler targets `.iox-invalid`. */
3706
- this.invalid = false;
3946
+ /** Rules beyond `required` — carried on the node, interpreted by forms-core. */
3947
+ this.rules = [];
3948
+ /**
3949
+ * Forces the error appearance without a real failure — how the builder lets an author DESIGN
3950
+ * the error state (decision D5). At runtime the state comes from the runtime service instead.
3951
+ */
3952
+ this.forceInvalid = false;
3953
+ /** Placeholder shown while designing the error state, so the block has something to style. */
3954
+ this.designErrorText = 'Please check this field';
3707
3955
  /** Only after a successful check — never on an untouched field. */
3708
3956
  this.showValid = false;
3709
3957
  this.valueChange = new EventEmitter();
@@ -3711,6 +3959,47 @@ class IoxFieldComponent {
3711
3959
  /** Stable per instance so `for`/`id` and `aria-describedby` always pair up. */
3712
3960
  this.controlId = `iox-field-${++fieldSeq}`;
3713
3961
  }
3962
+ ngOnInit() {
3963
+ this.unregister = this.runtime?.register({
3964
+ // Functions, not values: a trait edited on the canvas takes effect with no re-register.
3965
+ spec: () => this.toSpec(),
3966
+ getValue: () => this.value,
3967
+ });
3968
+ }
3969
+ ngOnDestroy() {
3970
+ this.unregister?.();
3971
+ }
3972
+ /** The submitted key: the author's override, else derived from the label. */
3973
+ get fieldName() {
3974
+ return this.name || deriveFieldName(this.label);
3975
+ }
3976
+ /** Failing AND allowed to say so — or forced, while an author designs the state. */
3977
+ get invalid() {
3978
+ if (this.forceInvalid)
3979
+ return true;
3980
+ return !!this.runtime?.showsError(this.fieldName);
3981
+ }
3982
+ /** The message a visitor reads. Translation is the consumer's, via IOX_FORM_MESSAGES. */
3983
+ get errorText() {
3984
+ if (this.forceInvalid)
3985
+ return this.designErrorText;
3986
+ const error = this.runtime?.errorFor(this.fieldName);
3987
+ return error ? this.resolveMessage(error.messageKey, error.params) : '';
3988
+ }
3989
+ toSpec() {
3990
+ const spec = {
3991
+ name: this.fieldName,
3992
+ control: this.control ?? ControlType.Text,
3993
+ label: this.label,
3994
+ };
3995
+ if (this.required)
3996
+ spec.required = true;
3997
+ if (this.rules?.length)
3998
+ spec.rules = this.rules;
3999
+ if (this.options?.length)
4000
+ spec.options = this.options;
4001
+ return spec;
4002
+ }
3714
4003
  /** `file`, `date`, `number`… map straight through; everything else is a text input. */
3715
4004
  get inputType() {
3716
4005
  const passthrough = ['email', 'tel', 'url', 'number', 'password', 'date', 'time', 'file', 'hidden'];
@@ -3727,13 +4016,16 @@ class IoxFieldComponent {
3727
4016
  }
3728
4017
  emit(value) {
3729
4018
  this.value = value;
4019
+ this.runtime?.onChange(this.fieldName);
3730
4020
  this.valueChange.emit(value);
3731
4021
  }
3732
4022
  touch() {
4023
+ // Blur is what lets Smart timing start speaking for this field.
4024
+ this.runtime?.onBlur(this.fieldName);
3733
4025
  this.touched.emit();
3734
4026
  }
3735
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: IoxFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
3736
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.31", type: IoxFieldComponent, isStandalone: false, selector: "div[iox-field]", inputs: { nodeId: "nodeId", name: "name", label: "label", control: "control", placeholder: "placeholder", help: "help", required: "required", disabled: "disabled", labelHidden: "labelHidden", requiredMark: "requiredMark", rows: "rows", accept: "accept", options: "options", value: "value", errorText: "errorText", invalid: "invalid", showValid: "showValid" }, outputs: { valueChange: "valueChange", touched: "touched" }, host: { properties: { "class": "'iox-field iox-node-' + nodeId", "class.iox-invalid": "invalid", "class.iox-valid": "showValid", "attr.data-iox-field": "name || null", "hidden": "control === 'hidden' ? true : null" } }, ngImport: i0, template: `
4027
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: IoxFieldComponent, deps: [{ token: FormRuntimeService, optional: true }, { token: IOX_FORM_MESSAGES }], target: i0.ɵɵFactoryTarget.Component }); }
4028
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.31", type: IoxFieldComponent, isStandalone: false, selector: "div[iox-field]", inputs: { nodeId: "nodeId", name: "name", label: "label", control: "control", placeholder: "placeholder", help: "help", required: "required", disabled: "disabled", labelHidden: "labelHidden", requiredMark: "requiredMark", rows: "rows", accept: "accept", options: "options", value: "value", rules: "rules", forceInvalid: "forceInvalid", designErrorText: "designErrorText", showValid: "showValid" }, outputs: { valueChange: "valueChange", touched: "touched" }, host: { properties: { "class": "'iox-field iox-node-' + nodeId", "class.iox-invalid": "invalid", "class.iox-valid": "showValid", "attr.data-iox-field": "fieldName || null", "hidden": "control === 'hidden' ? true : null" } }, ngImport: i0, template: `
3737
4029
  <label class="iox-field__label"
3738
4030
  [class.iox-field__label--hidden]="labelHidden"
3739
4031
  [attr.for]="controlId">
@@ -3747,7 +4039,7 @@ class IoxFieldComponent {
3747
4039
  @case ('textarea') {
3748
4040
  <textarea class="iox-field__control"
3749
4041
  [id]="controlId"
3750
- [name]="name"
4042
+ [name]="fieldName"
3751
4043
  [rows]="rows"
3752
4044
  [attr.placeholder]="placeholder || null"
3753
4045
  [attr.required]="required ? '' : null"
@@ -3761,7 +4053,7 @@ class IoxFieldComponent {
3761
4053
  @case ('select') {
3762
4054
  <select class="iox-field__control"
3763
4055
  [id]="controlId"
3764
- [name]="name"
4056
+ [name]="fieldName"
3765
4057
  [attr.required]="required ? '' : null"
3766
4058
  [attr.aria-invalid]="invalid ? 'true' : null"
3767
4059
  [attr.aria-describedby]="describedBy"
@@ -3778,7 +4070,7 @@ class IoxFieldComponent {
3778
4070
  <input class="iox-field__control iox-field__control--checkbox"
3779
4071
  type="checkbox"
3780
4072
  [id]="controlId"
3781
- [name]="name"
4073
+ [name]="fieldName"
3782
4074
  [attr.required]="required ? '' : null"
3783
4075
  [attr.aria-invalid]="invalid ? 'true' : null"
3784
4076
  [attr.aria-describedby]="describedBy"
@@ -3792,7 +4084,7 @@ class IoxFieldComponent {
3792
4084
  @for (option of options; track option.value) {
3793
4085
  <label class="iox-field__option">
3794
4086
  <input type="radio"
3795
- [name]="name"
4087
+ [name]="fieldName"
3796
4088
  [value]="option.value"
3797
4089
  [checked]="option.value === value"
3798
4090
  [attr.disabled]="disabled ? '' : null"
@@ -3806,7 +4098,7 @@ class IoxFieldComponent {
3806
4098
  @default {
3807
4099
  <input class="iox-field__control"
3808
4100
  [id]="controlId"
3809
- [name]="name"
4101
+ [name]="fieldName"
3810
4102
  [type]="inputType"
3811
4103
  [attr.placeholder]="placeholder || null"
3812
4104
  [attr.required]="required ? '' : null"
@@ -3844,7 +4136,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
3844
4136
  @case ('textarea') {
3845
4137
  <textarea class="iox-field__control"
3846
4138
  [id]="controlId"
3847
- [name]="name"
4139
+ [name]="fieldName"
3848
4140
  [rows]="rows"
3849
4141
  [attr.placeholder]="placeholder || null"
3850
4142
  [attr.required]="required ? '' : null"
@@ -3858,7 +4150,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
3858
4150
  @case ('select') {
3859
4151
  <select class="iox-field__control"
3860
4152
  [id]="controlId"
3861
- [name]="name"
4153
+ [name]="fieldName"
3862
4154
  [attr.required]="required ? '' : null"
3863
4155
  [attr.aria-invalid]="invalid ? 'true' : null"
3864
4156
  [attr.aria-describedby]="describedBy"
@@ -3875,7 +4167,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
3875
4167
  <input class="iox-field__control iox-field__control--checkbox"
3876
4168
  type="checkbox"
3877
4169
  [id]="controlId"
3878
- [name]="name"
4170
+ [name]="fieldName"
3879
4171
  [attr.required]="required ? '' : null"
3880
4172
  [attr.aria-invalid]="invalid ? 'true' : null"
3881
4173
  [attr.aria-describedby]="describedBy"
@@ -3889,7 +4181,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
3889
4181
  @for (option of options; track option.value) {
3890
4182
  <label class="iox-field__option">
3891
4183
  <input type="radio"
3892
- [name]="name"
4184
+ [name]="fieldName"
3893
4185
  [value]="option.value"
3894
4186
  [checked]="option.value === value"
3895
4187
  [attr.disabled]="disabled ? '' : null"
@@ -3903,7 +4195,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
3903
4195
  @default {
3904
4196
  <input class="iox-field__control"
3905
4197
  [id]="controlId"
3906
- [name]="name"
4198
+ [name]="fieldName"
3907
4199
  [type]="inputType"
3908
4200
  [attr.placeholder]="placeholder || null"
3909
4201
  [attr.required]="required ? '' : null"
@@ -3927,10 +4219,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
3927
4219
  '[class]': "'iox-field iox-node-' + nodeId",
3928
4220
  '[class.iox-invalid]': 'invalid',
3929
4221
  '[class.iox-valid]': 'showValid',
3930
- '[attr.data-iox-field]': 'name || null',
4222
+ '[attr.data-iox-field]': 'fieldName || null',
3931
4223
  '[hidden]': "control === 'hidden' ? true : null",
3932
4224
  }, standalone: false, styles: [":host{box-sizing:border-box;display:block}.iox-field__label--hidden{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap}\n"] }]
3933
- }], propDecorators: { nodeId: [{
4225
+ }], ctorParameters: () => [{ type: FormRuntimeService, decorators: [{
4226
+ type: Optional
4227
+ }] }, { type: undefined, decorators: [{
4228
+ type: Inject,
4229
+ args: [IOX_FORM_MESSAGES]
4230
+ }] }], propDecorators: { nodeId: [{
3934
4231
  type: Input
3935
4232
  }], name: [{
3936
4233
  type: Input
@@ -3958,9 +4255,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
3958
4255
  type: Input
3959
4256
  }], value: [{
3960
4257
  type: Input
3961
- }], errorText: [{
4258
+ }], rules: [{
4259
+ type: Input
4260
+ }], forceInvalid: [{
3962
4261
  type: Input
3963
- }], invalid: [{
4262
+ }], designErrorText: [{
3964
4263
  type: Input
3965
4264
  }], showValid: [{
3966
4265
  type: Input
@@ -5578,5 +5877,5 @@ function effectiveTransitionId(anim) {
5578
5877
  * Generated bundle index. Do not edit.
5579
5878
  */
5580
5879
 
5581
- 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, GALLERY_EFFECTS, GALLERY_EFFECT_DESCRIPTORS, GalleryLoop, IOX_BUILDER_EVENTS, 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, 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 };
5880
+ 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 };
5582
5881
  //# sourceMappingURL=vectoriox-iox-ui.mjs.map