@egose/shadcn-theme-ng-tw 0.3.0 → 0.4.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.
Files changed (63) hide show
  1. package/form-autocomplete/README.md +20 -19
  2. package/form-autocomplete/fesm2022/form-autocomplete.mjs +41 -9
  3. package/form-autocomplete/types/form-autocomplete.d.ts +21 -1
  4. package/form-checkbox/README.md +18 -17
  5. package/form-checkbox/fesm2022/form-checkbox.mjs +42 -15
  6. package/form-checkbox/types/form-checkbox.d.ts +21 -1
  7. package/form-combobox/README.md +21 -20
  8. package/form-combobox/fesm2022/form-combobox.mjs +41 -9
  9. package/form-combobox/types/form-combobox.d.ts +21 -1
  10. package/form-date-picker/README.md +28 -27
  11. package/form-date-picker/fesm2022/form-date-picker.mjs +42 -15
  12. package/form-date-picker/types/form-date-picker.d.ts +21 -1
  13. package/form-date-picker-multi/README.md +28 -27
  14. package/form-date-picker-multi/fesm2022/form-date-picker-multi.mjs +42 -15
  15. package/form-date-picker-multi/types/form-date-picker-multi.d.ts +21 -1
  16. package/form-date-range-picker/README.md +28 -27
  17. package/form-date-range-picker/fesm2022/form-date-range-picker.mjs +42 -15
  18. package/form-date-range-picker/types/form-date-range-picker.d.ts +21 -1
  19. package/form-field/README.md +9 -0
  20. package/form-field/fesm2022/form-field.mjs +80 -5
  21. package/form-field/types/form-field.d.ts +50 -2
  22. package/form-field-simple/fesm2022/form-field-simple.mjs +15 -1
  23. package/form-field-simple/types/form-field-simple.d.ts +9 -2
  24. package/form-input-otp/README.md +17 -16
  25. package/form-input-otp/fesm2022/form-input-otp.mjs +41 -9
  26. package/form-input-otp/types/form-input-otp.d.ts +21 -1
  27. package/form-month-year-picker/README.md +26 -25
  28. package/form-month-year-picker/fesm2022/form-month-year-picker.mjs +42 -15
  29. package/form-month-year-picker/types/form-month-year-picker.d.ts +21 -1
  30. package/form-native-select/README.md +18 -17
  31. package/form-native-select/fesm2022/form-native-select.mjs +56 -17
  32. package/form-native-select/types/form-native-select.d.ts +21 -1
  33. package/form-phone-input/README.md +23 -22
  34. package/form-phone-input/fesm2022/form-phone-input.mjs +42 -15
  35. package/form-phone-input/types/form-phone-input.d.ts +21 -1
  36. package/form-radio-group/README.md +18 -17
  37. package/form-radio-group/fesm2022/form-radio-group.mjs +42 -15
  38. package/form-radio-group/types/form-radio-group.d.ts +21 -1
  39. package/form-searchable-multiselect/README.md +18 -17
  40. package/form-searchable-multiselect/fesm2022/form-searchable-multiselect.mjs +42 -15
  41. package/form-searchable-multiselect/types/form-searchable-multiselect.d.ts +21 -1
  42. package/form-select/README.md +20 -19
  43. package/form-select/fesm2022/form-select.mjs +42 -15
  44. package/form-select/types/form-select.d.ts +21 -1
  45. package/form-slider/README.md +19 -18
  46. package/form-slider/fesm2022/form-slider.mjs +41 -9
  47. package/form-slider/types/form-slider.d.ts +21 -1
  48. package/form-switch/README.md +16 -15
  49. package/form-switch/fesm2022/form-switch.mjs +42 -15
  50. package/form-switch/types/form-switch.d.ts +21 -1
  51. package/form-text-input/README.md +27 -26
  52. package/form-text-input/fesm2022/form-text-input.mjs +42 -15
  53. package/form-text-input/types/form-text-input.d.ts +21 -1
  54. package/form-textarea/README.md +23 -22
  55. package/form-textarea/fesm2022/form-textarea.mjs +42 -15
  56. package/form-textarea/types/form-textarea.d.ts +21 -1
  57. package/form-toggle/README.md +17 -16
  58. package/form-toggle/fesm2022/form-toggle.mjs +41 -7
  59. package/form-toggle/types/form-toggle.d.ts +21 -3
  60. package/form-toggle-group/README.md +20 -19
  61. package/form-toggle-group/fesm2022/form-toggle-group.mjs +42 -15
  62. package/form-toggle-group/types/form-toggle-group.d.ts +21 -1
  63. package/package.json +1 -1
@@ -1,7 +1,7 @@
1
1
  import * as i0 from '@angular/core';
2
- import { input, computed, Directive, contentChild, contentChildren, effect, ChangeDetectionStrategy, Component, inject, APP_ID, Injectable, NgModule } from '@angular/core';
2
+ import { input, computed, Directive, contentChild, contentChildren, inject, signal, effect, ChangeDetectionStrategy, Component, InjectionToken, APP_ID, Injectable, NgModule } from '@angular/core';
3
3
  import { hlm } from '@egose/shadcn-theme-ng-tw/utils';
4
- import { FormGroupDirective, ControlContainer } from '@angular/forms';
4
+ import { FormGroupDirective, NgForm, ControlContainer } from '@angular/forms';
5
5
  import * as i1 from '@spartan-ng/brain/field';
6
6
  import { BrnFieldControl, BrnField } from '@spartan-ng/brain/field';
7
7
 
@@ -30,10 +30,26 @@ class HlmFormField {
30
30
  ...(ngDevMode ? [{ debugName: "control" }] : /* istanbul ignore next */ []));
31
31
  errorChildren = contentChildren(HlmError, /* @ts-ignore */
32
32
  ...(ngDevMode ? [{ debugName: "errorChildren" }] : /* istanbul ignore next */ []));
33
+ _formGroupDirective = inject(FormGroupDirective, { optional: true });
34
+ _ngForm = inject(NgForm, { optional: true });
35
+ /**
36
+ * Tracks parent submit state. `FormGroupDirective.submitted` / `NgForm.submitted`
37
+ * are plain booleans (not signals), so mirror them into a signal via `ngDoCheck`
38
+ * to keep `_hasDisplayedMessage` reactive under OnPush.
39
+ */
40
+ _submitted = signal(false, /* @ts-ignore */
41
+ ...(ngDevMode ? [{ debugName: "_submitted" }] : /* istanbul ignore next */ []));
42
+ ngDoCheck() {
43
+ const submitted = !!this._formGroupDirective?.submitted || !!this._ngForm?.submitted;
44
+ if (this._submitted() !== submitted)
45
+ this._submitted.set(submitted);
46
+ }
33
47
  _hasDisplayedMessage = computed(() => {
34
- const errors = this.control()?.errors();
48
+ const ctrl = this.control();
49
+ const errors = ctrl?.errors();
35
50
  const hasErrors = !!errors && Object.keys(errors).length > 0;
36
- return this.errorChildren() && this.errorChildren().length > 0 && hasErrors ? 'error' : 'hint';
51
+ const interacted = !!ctrl?.touched() || !!ctrl?.dirty() || this._submitted();
52
+ return this.errorChildren() && this.errorChildren().length > 0 && hasErrors && interacted ? 'error' : 'hint';
37
53
  }, /* @ts-ignore */
38
54
  ...(ngDevMode ? [{ debugName: "_hasDisplayedMessage" }] : /* istanbul ignore next */ []));
39
55
  constructor() {
@@ -99,6 +115,65 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
99
115
  }]
100
116
  }], propDecorators: { userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }] } });
101
117
 
118
+ /** Fallback label used when the field has no `label()` set. */
119
+ const EG_FORM_DEFAULT_ERROR_LABEL = 'This field';
120
+ /** Canonical priority order for resolving the first displayed message. */
121
+ const ERROR_KEY_PRIORITY = ['required', 'requiredTrue', 'email', 'minlength', 'maxlength', 'min', 'max', 'pattern'];
122
+ /**
123
+ * Default English messages for Angular's built-in validator error keys.
124
+ * App-wide wording/i18n is customized via {@link provideEgFormErrorMessages}.
125
+ */
126
+ const DEFAULT_EG_FORM_ERROR_MESSAGES = {
127
+ required: (_, label) => `${label} is required`,
128
+ requiredTrue: (_, label) => `${label} is required`,
129
+ email: () => 'Please enter a valid email address',
130
+ minlength: (params, label) => `${label} must be at least ${params?.['requiredLength']} characters`,
131
+ maxlength: (params, label) => `${label} must be at most ${params?.['requiredLength']} characters`,
132
+ min: (params, label) => `${label} must be at least ${params?.['min']}`,
133
+ max: (params, label) => `${label} must be at most ${params?.['max']}`,
134
+ pattern: (_, label) => `${label} is invalid`,
135
+ };
136
+ const EgFormErrorMessagesToken = new InjectionToken('EgFormErrorMessages');
137
+ /**
138
+ * Overrides/adds error messages app-wide (e.g. custom validator keys or i18n).
139
+ * Merged over {@link DEFAULT_EG_FORM_ERROR_MESSAGES} at injection time.
140
+ *
141
+ * ```ts
142
+ * provideEgFormErrorMessages({ usernameTaken: 'This username is already taken' })
143
+ * ```
144
+ */
145
+ function provideEgFormErrorMessages(messages) {
146
+ return { provide: EgFormErrorMessagesToken, useValue: { ...messages } };
147
+ }
148
+ /** Merged custom + default messages (defaults win only when not overridden). */
149
+ function injectEgFormErrorMessages() {
150
+ return { ...DEFAULT_EG_FORM_ERROR_MESSAGES, ...(inject(EgFormErrorMessagesToken, { optional: true }) ?? {}) };
151
+ }
152
+ /**
153
+ * Resolves the first displayable message for a `ValidationErrors` object.
154
+ * Returns `undefined` when there are no errors. Unknown keys fall back to a
155
+ * generic `${label} is invalid` message (or a custom dictionary entry).
156
+ */
157
+ function resolveEgFormError(errors, label, messages = DEFAULT_EG_FORM_ERROR_MESSAGES) {
158
+ if (!errors)
159
+ return undefined;
160
+ const keys = Object.keys(errors);
161
+ if (keys.length === 0)
162
+ return undefined;
163
+ const resolvedLabel = label || EG_FORM_DEFAULT_ERROR_LABEL;
164
+ const ordered = [
165
+ ...ERROR_KEY_PRIORITY.filter((key) => key in errors),
166
+ ...keys.filter((key) => !ERROR_KEY_PRIORITY.includes(key)),
167
+ ];
168
+ const key = ordered[0];
169
+ const entry = messages[key];
170
+ if (typeof entry === 'function')
171
+ return entry((errors[key] ?? {}), resolvedLabel);
172
+ if (typeof entry === 'string')
173
+ return entry;
174
+ return `${resolvedLabel} is invalid`;
175
+ }
176
+
102
177
  class HlmFormIdGenerator {
103
178
  appId = inject(APP_ID);
104
179
  nextId = 0;
@@ -134,5 +209,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
134
209
  * Generated bundle index. Do not edit.
135
210
  */
136
211
 
137
- export { HlmError, HlmFormField, HlmFormFieldImports, HlmFormFieldModule, HlmFormIdGenerator, HlmHint };
212
+ export { DEFAULT_EG_FORM_ERROR_MESSAGES, EG_FORM_DEFAULT_ERROR_LABEL, HlmError, HlmFormField, HlmFormFieldImports, HlmFormFieldModule, HlmFormIdGenerator, HlmHint, injectEgFormErrorMessages, provideEgFormErrorMessages, resolveEgFormError };
138
213
 
@@ -1,7 +1,9 @@
1
1
  import * as i0 from '@angular/core';
2
+ import { DoCheck, ValueProvider } from '@angular/core';
2
3
  import { ClassValue } from 'clsx';
3
4
  import * as i1 from '@spartan-ng/brain/field';
4
5
  import { BrnFieldControl } from '@spartan-ng/brain/field';
6
+ import { ValidationErrors } from '@angular/forms';
5
7
 
6
8
  declare class HlmError {
7
9
  readonly userClass: i0.InputSignal<ClassValue>;
@@ -10,11 +12,20 @@ declare class HlmError {
10
12
  static ɵdir: i0.ɵɵDirectiveDeclaration<HlmError, "hlm-error", never, { "userClass": { "alias": "class"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
11
13
  }
12
14
 
13
- declare class HlmFormField {
15
+ declare class HlmFormField implements DoCheck {
14
16
  readonly userClass: i0.InputSignal<ClassValue>;
15
17
  protected readonly _computedClass: i0.Signal<string>;
16
18
  readonly control: i0.Signal<BrnFieldControl | undefined>;
17
19
  readonly errorChildren: i0.Signal<readonly HlmError[]>;
20
+ private readonly _formGroupDirective;
21
+ private readonly _ngForm;
22
+ /**
23
+ * Tracks parent submit state. `FormGroupDirective.submitted` / `NgForm.submitted`
24
+ * are plain booleans (not signals), so mirror them into a signal via `ngDoCheck`
25
+ * to keep `_hasDisplayedMessage` reactive under OnPush.
26
+ */
27
+ protected readonly _submitted: i0.WritableSignal<boolean>;
28
+ ngDoCheck(): void;
18
29
  protected readonly _hasDisplayedMessage: i0.Signal<"error" | "hint">;
19
30
  constructor();
20
31
  static ɵfac: i0.ɵɵFactoryDeclaration<HlmFormField, never>;
@@ -28,6 +39,42 @@ declare class HlmHint {
28
39
  static ɵdir: i0.ɵɵDirectiveDeclaration<HlmHint, "hlm-hint", never, { "userClass": { "alias": "class"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
29
40
  }
30
41
 
42
+ /**
43
+ * Factory for a single error message. Receives the validator's error params
44
+ * (e.g. `{ requiredLength, actualLength }` for `minlength`) and the field label.
45
+ */
46
+ type EgFormErrorMessageFn = (params: Record<string, unknown>, label: string) => string;
47
+ /**
48
+ * Dictionary of `ValidationErrors` key → message or message factory.
49
+ * Custom validator keys (e.g. `usernameTaken`) are covered by adding entries
50
+ * here, either globally via {@link provideEgFormErrorMessages} or per call.
51
+ */
52
+ type EgFormErrorMessages = Record<string, string | EgFormErrorMessageFn>;
53
+ /** Fallback label used when the field has no `label()` set. */
54
+ declare const EG_FORM_DEFAULT_ERROR_LABEL = "This field";
55
+ /**
56
+ * Default English messages for Angular's built-in validator error keys.
57
+ * App-wide wording/i18n is customized via {@link provideEgFormErrorMessages}.
58
+ */
59
+ declare const DEFAULT_EG_FORM_ERROR_MESSAGES: EgFormErrorMessages;
60
+ /**
61
+ * Overrides/adds error messages app-wide (e.g. custom validator keys or i18n).
62
+ * Merged over {@link DEFAULT_EG_FORM_ERROR_MESSAGES} at injection time.
63
+ *
64
+ * ```ts
65
+ * provideEgFormErrorMessages({ usernameTaken: 'This username is already taken' })
66
+ * ```
67
+ */
68
+ declare function provideEgFormErrorMessages(messages: EgFormErrorMessages): ValueProvider;
69
+ /** Merged custom + default messages (defaults win only when not overridden). */
70
+ declare function injectEgFormErrorMessages(): EgFormErrorMessages;
71
+ /**
72
+ * Resolves the first displayable message for a `ValidationErrors` object.
73
+ * Returns `undefined` when there are no errors. Unknown keys fall back to a
74
+ * generic `${label} is invalid` message (or a custom dictionary entry).
75
+ */
76
+ declare function resolveEgFormError(errors: ValidationErrors | null | undefined, label?: string | null, messages?: EgFormErrorMessages): string | undefined;
77
+
31
78
  declare class HlmFormIdGenerator {
32
79
  private readonly appId;
33
80
  private nextId;
@@ -43,4 +90,5 @@ declare class HlmFormFieldModule {
43
90
  static ɵinj: i0.ɵɵInjectorDeclaration<HlmFormFieldModule>;
44
91
  }
45
92
 
46
- export { HlmError, HlmFormField, HlmFormFieldImports, HlmFormFieldModule, HlmFormIdGenerator, HlmHint };
93
+ export { DEFAULT_EG_FORM_ERROR_MESSAGES, EG_FORM_DEFAULT_ERROR_LABEL, HlmError, HlmFormField, HlmFormFieldImports, HlmFormFieldModule, HlmFormIdGenerator, HlmHint, injectEgFormErrorMessages, provideEgFormErrorMessages, resolveEgFormError };
94
+ export type { EgFormErrorMessageFn, EgFormErrorMessages };
@@ -14,6 +14,13 @@ class EgFormField {
14
14
  form = this.formGroupDirective.form;
15
15
  statusSignal = signal(null, /* @ts-ignore */
16
16
  ...(ngDevMode ? [{ debugName: "statusSignal" }] : /* istanbul ignore next */ []));
17
+ /**
18
+ * Mirrors `FormGroupDirective.submitted` (plain boolean) into a signal so
19
+ * `hasError` stays reactive under OnPush and errors appear after submit
20
+ * even when the control was never touched/dirty.
21
+ */
22
+ submittedSignal = signal(false, /* @ts-ignore */
23
+ ...(ngDevMode ? [{ debugName: "submittedSignal" }] : /* istanbul ignore next */ []));
17
24
  sub = new Subscription();
18
25
  ngAfterContentInit() {
19
26
  const ctrlDir = this.control();
@@ -24,6 +31,11 @@ class EgFormField {
24
31
  this.statusSignal.set(ctrlDir.control.status);
25
32
  }
26
33
  }
34
+ ngDoCheck() {
35
+ const submitted = !!this.formGroupDirective?.submitted;
36
+ if (this.submittedSignal() !== submitted)
37
+ this.submittedSignal.set(submitted);
38
+ }
27
39
  ngOnDestroy() {
28
40
  this.sub.unsubscribe();
29
41
  }
@@ -32,7 +44,9 @@ class EgFormField {
32
44
  if (!ctrlDir)
33
45
  return false;
34
46
  this.statusSignal();
35
- return !!ctrlDir.control.errors && (ctrlDir.control.touched || ctrlDir.control.dirty);
47
+ this.submittedSignal();
48
+ return (!!ctrlDir.control.errors &&
49
+ (ctrlDir.control.touched || ctrlDir.control.dirty || !!this.formGroupDirective?.submitted));
36
50
  }, /* @ts-ignore */
37
51
  ...(ngDevMode ? [{ debugName: "hasError" }] : /* istanbul ignore next */ []));
38
52
  firstErrorKey = computed(() => {
@@ -1,17 +1,24 @@
1
1
  import * as _angular_core from '@angular/core';
2
- import { AfterContentInit, OnDestroy } from '@angular/core';
2
+ import { AfterContentInit, DoCheck, OnDestroy } from '@angular/core';
3
3
  import { FormControlName, FormGroup } from '@angular/forms';
4
4
  import { ClassValue } from 'clsx';
5
5
 
6
- declare class EgFormField implements AfterContentInit, OnDestroy {
6
+ declare class EgFormField implements AfterContentInit, DoCheck, OnDestroy {
7
7
  readonly userClass: _angular_core.InputSignal<ClassValue>;
8
8
  protected readonly _computedClass: _angular_core.Signal<string>;
9
9
  readonly control: _angular_core.Signal<FormControlName | undefined>;
10
10
  private readonly formGroupDirective;
11
11
  readonly form: FormGroup;
12
12
  private statusSignal;
13
+ /**
14
+ * Mirrors `FormGroupDirective.submitted` (plain boolean) into a signal so
15
+ * `hasError` stays reactive under OnPush and errors appear after submit
16
+ * even when the control was never touched/dirty.
17
+ */
18
+ private submittedSignal;
13
19
  private sub;
14
20
  ngAfterContentInit(): void;
21
+ ngDoCheck(): void;
15
22
  ngOnDestroy(): void;
16
23
  readonly hasError: _angular_core.Signal<boolean>;
17
24
  readonly firstErrorKey: _angular_core.Signal<string | null>;
@@ -76,22 +76,23 @@ Selector (from source): `eg-form-input-otp` (standalone component, host `tw:w-fu
76
76
 
77
77
  ### `EgFormInputOtp` — `eg-form-input-otp`
78
78
 
79
- | Input | Type | Default | Description |
80
- | ------------- | --------------------- | ------- | ------------------------------------------------------------------ |
81
- | `controlName` | `string` | `''` | `formControlName` key inside the parent `FormGroup`. **Required.** |
82
- | `label` | `string \| undefined` | — | Label text (hidden when omitted). |
83
- | `error` | `string \| undefined` | — | Error text shown when invalid. |
84
- | `hint` | `string \| undefined` | — | Hint text shown otherwise. |
85
- | `controlId` | `string \| undefined` | — | Explicit id (first priority for `effectiveId`). |
86
- | `id` | `string \| undefined` | — | Fallback id (second priority). |
87
- | `length` | `number` | `6` | Slot count; one `hlm-input-otp-slot` is rendered per unit. |
88
- | `disabled` | `boolean` | `false` | Locks interaction (form control stays enabled). |
89
- | `required` | `boolean` | `false` | Shows a red `*` next to the label. |
90
- | `class` | `ClassValue` | `''` | Extra host classes (base `tw:w-full`). |
91
- | `labelClass` | `string` | `''` | Extra label classes (base `tw:mb-1`). |
92
- | `otpClass` | `string` | `''` | Extra OTP container classes. |
93
- | `errorClass` | `string` | `''` | Extra error classes (base `tw:mt-0`). |
94
- | `hintClass` | `string` | `''` | Extra hint classes (base `tw:mt-0`). |
79
+ | Input | Type | Default | Description |
80
+ | ------------- | --------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
81
+ | `controlName` | `string` | `''` | `formControlName` key inside the parent `FormGroup`. **Required.** |
82
+ | `label` | `string \| undefined` | — | Label text (hidden when omitted). |
83
+ | `error` | `string \| undefined` | — | Error text shown when invalid. |
84
+ | `autoError` | `boolean` | `true` | Auto-resolve the message from the control's `ValidationErrors` when `error` is unset. Explicit `error` always wins; `error=""` counts as unset. Global wording via `provideEgFormErrorMessages`. |
85
+ | `hint` | `string \| undefined` | — | Hint text shown otherwise. |
86
+ | `controlId` | `string \| undefined` | — | Explicit id (first priority for `effectiveId`). |
87
+ | `id` | `string \| undefined` | — | Fallback id (second priority). |
88
+ | `length` | `number` | `6` | Slot count; one `hlm-input-otp-slot` is rendered per unit. |
89
+ | `disabled` | `boolean` | `false` | Locks interaction (form control stays enabled). |
90
+ | `required` | `boolean` | `false` | Shows a red `*` next to the label. |
91
+ | `class` | `ClassValue` | `''` | Extra host classes (base `tw:w-full`). |
92
+ | `labelClass` | `string` | `''` | Extra label classes (base `tw:mb-1`). |
93
+ | `otpClass` | `string` | `''` | Extra OTP container classes. |
94
+ | `errorClass` | `string` | `''` | Extra error classes (base `tw:mt-0`). |
95
+ | `hintClass` | `string` | `''` | Extra hint classes (base `tw:mt-0`). |
95
96
 
96
97
  | Member | Description |
97
98
  | -------------------- | -------------------------------------------------------------------------------- |
@@ -2,7 +2,7 @@ import * as i0 from '@angular/core';
2
2
  import { InjectionToken, inject, input, numberAttribute, computed, Component } from '@angular/core';
3
3
  import * as i1 from '@angular/forms';
4
4
  import { FormGroupDirective, ReactiveFormsModule, ControlContainer } from '@angular/forms';
5
- import { HlmFormIdGenerator, HlmFormField, HlmError, HlmHint } from '@egose/shadcn-theme-ng-tw/form-field';
5
+ import { injectEgFormErrorMessages, HlmFormIdGenerator, resolveEgFormError, HlmFormField, HlmError, HlmHint } from '@egose/shadcn-theme-ng-tw/form-field';
6
6
  import { HlmLabel } from '@egose/shadcn-theme-ng-tw/label';
7
7
  import { BrnInputOtp } from '@spartan-ng/brain/input-otp';
8
8
  import * as i2 from '@egose/shadcn-theme-ng-tw/input-otp';
@@ -19,6 +19,7 @@ function injectEgFormInputOtpConfig() {
19
19
 
20
20
  class EgFormInputOtp {
21
21
  formGroupDirective = inject(FormGroupDirective);
22
+ _errorMessages = injectEgFormErrorMessages();
22
23
  generatedId = inject(HlmFormIdGenerator).generate('eg-form-input-otp');
23
24
  _config = injectEgFormInputOtpConfig();
24
25
  label = input(undefined, /* @ts-ignore */
@@ -29,6 +30,13 @@ class EgFormInputOtp {
29
30
  ...(ngDevMode ? [{ debugName: "controlName" }] : /* istanbul ignore next */ []));
30
31
  error = input(undefined, /* @ts-ignore */
31
32
  ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
33
+ /**
34
+ * Auto-resolve the displayed message from the control's `ValidationErrors`
35
+ * when `error()` is unset. Explicit `error()` always wins. Set to `false`
36
+ * for manual-only messages.
37
+ */
38
+ autoError = input(true, /* @ts-ignore */
39
+ ...(ngDevMode ? [{ debugName: "autoError" }] : /* istanbul ignore next */ []));
32
40
  hint = input(undefined, /* @ts-ignore */
33
41
  ...(ngDevMode ? [{ debugName: "hint" }] : /* istanbul ignore next */ []));
34
42
  id = input(undefined, /* @ts-ignore */
@@ -44,6 +52,32 @@ class EgFormInputOtp {
44
52
  ...(ngDevMode ? [{ debugName: "errorId" }] : /* istanbul ignore next */ []));
45
53
  hintId = computed(() => `${this.effectiveId()}-hint`, /* @ts-ignore */
46
54
  ...(ngDevMode ? [{ debugName: "hintId" }] : /* istanbul ignore next */ []));
55
+ /**
56
+ * Displayed message: explicit `error()` wins; otherwise (when `autoError()`)
57
+ * auto-resolved from the control's `ValidationErrors`. Plain method (not a
58
+ * computed): `ValidationErrors` is not a signal, so the control must be read
59
+ * fresh on every change-detection pass.
60
+ */
61
+ resolvedError() {
62
+ const explicit = this.error();
63
+ if (explicit)
64
+ return explicit;
65
+ if (!this.autoError())
66
+ return undefined;
67
+ const control = this.formGroupDirective.form.get(this.controlName());
68
+ return resolveEgFormError(control?.errors ?? null, this.label(), this._errorMessages);
69
+ }
70
+ /**
71
+ * Whether the error is surfaced: message present + control invalid +
72
+ * touched/dirty/submitted. Gates the visual `<hlm-error>` (and
73
+ * `describedBy()`, where present) so sighted and screen-reader output agree.
74
+ */
75
+ showError() {
76
+ const control = this.formGroupDirective.form.get(this.controlName());
77
+ return (!!this.resolvedError() &&
78
+ !!control?.invalid &&
79
+ (control.dirty || control.touched || this.formGroupDirective.submitted));
80
+ }
47
81
  slotIndexes = computed(() => Array.from({ length: this.length() }, (_, i) => i), /* @ts-ignore */
48
82
  ...(ngDevMode ? [{ debugName: "slotIndexes" }] : /* istanbul ignore next */ []));
49
83
  /**
@@ -78,10 +112,9 @@ class EgFormInputOtp {
78
112
  $hintClass = computed(() => hlm('tw:mt-0', this._config.hintClass, this.hintClass()), /* @ts-ignore */
79
113
  ...(ngDevMode ? [{ debugName: "$hintClass" }] : /* istanbul ignore next */ []));
80
114
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: EgFormInputOtp, deps: [], target: i0.ɵɵFactoryTarget.Component });
81
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: EgFormInputOtp, isStandalone: true, selector: "eg-form-input-otp", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, controlId: { classPropertyName: "controlId", publicName: "controlId", isSignal: true, isRequired: false, transformFunction: null }, controlName: { classPropertyName: "controlName", publicName: "controlName", isSignal: true, isRequired: false, transformFunction: null }, error: { classPropertyName: "error", publicName: "error", isSignal: true, isRequired: false, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null }, id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, length: { classPropertyName: "length", publicName: "length", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, labelClass: { classPropertyName: "labelClass", publicName: "labelClass", isSignal: true, isRequired: false, transformFunction: null }, otpClass: { classPropertyName: "otpClass", publicName: "otpClass", isSignal: true, isRequired: false, transformFunction: null }, errorClass: { classPropertyName: "errorClass", publicName: "errorClass", isSignal: true, isRequired: false, transformFunction: null }, hintClass: { classPropertyName: "hintClass", publicName: "hintClass", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "$userClass()" } }, providers: [{ provide: ControlContainer, useExisting: FormGroupDirective }], ngImport: i0, template: `
115
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: EgFormInputOtp, isStandalone: true, selector: "eg-form-input-otp", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, controlId: { classPropertyName: "controlId", publicName: "controlId", isSignal: true, isRequired: false, transformFunction: null }, controlName: { classPropertyName: "controlName", publicName: "controlName", isSignal: true, isRequired: false, transformFunction: null }, error: { classPropertyName: "error", publicName: "error", isSignal: true, isRequired: false, transformFunction: null }, autoError: { classPropertyName: "autoError", publicName: "autoError", isSignal: true, isRequired: false, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null }, id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, length: { classPropertyName: "length", publicName: "length", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, labelClass: { classPropertyName: "labelClass", publicName: "labelClass", isSignal: true, isRequired: false, transformFunction: null }, otpClass: { classPropertyName: "otpClass", publicName: "otpClass", isSignal: true, isRequired: false, transformFunction: null }, errorClass: { classPropertyName: "errorClass", publicName: "errorClass", isSignal: true, isRequired: false, transformFunction: null }, hintClass: { classPropertyName: "hintClass", publicName: "hintClass", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "$userClass()" } }, providers: [{ provide: ControlContainer, useExisting: FormGroupDirective }], ngImport: i0, template: `
82
116
  @let lbl = label();
83
117
  @let cnm = controlName();
84
- @let err = error();
85
118
  @let hnt = hint();
86
119
  @let rqrd = required();
87
120
 
@@ -110,9 +143,9 @@ class EgFormInputOtp {
110
143
  </div>
111
144
  </brn-input-otp>
112
145
 
113
- @if (err) {
146
+ @if (showError()) {
114
147
  <hlm-error [id]="errorId()" [class]="$errorClass()">
115
- {{ err }}
148
+ {{ resolvedError() }}
116
149
  </hlm-error>
117
150
  }
118
151
 
@@ -137,7 +170,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
137
170
  template: `
138
171
  @let lbl = label();
139
172
  @let cnm = controlName();
140
- @let err = error();
141
173
  @let hnt = hint();
142
174
  @let rqrd = required();
143
175
 
@@ -166,9 +198,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
166
198
  </div>
167
199
  </brn-input-otp>
168
200
 
169
- @if (err) {
201
+ @if (showError()) {
170
202
  <hlm-error [id]="errorId()" [class]="$errorClass()">
171
- {{ err }}
203
+ {{ resolvedError() }}
172
204
  </hlm-error>
173
205
  }
174
206
 
@@ -180,7 +212,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
180
212
  </hlm-form-field>
181
213
  `,
182
214
  }]
183
- }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], controlId: [{ type: i0.Input, args: [{ isSignal: true, alias: "controlId", required: false }] }], controlName: [{ type: i0.Input, args: [{ isSignal: true, alias: "controlName", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], length: [{ type: i0.Input, args: [{ isSignal: true, alias: "length", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], labelClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelClass", required: false }] }], otpClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "otpClass", required: false }] }], errorClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorClass", required: false }] }], hintClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "hintClass", required: false }] }] } });
215
+ }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], controlId: [{ type: i0.Input, args: [{ isSignal: true, alias: "controlId", required: false }] }], controlName: [{ type: i0.Input, args: [{ isSignal: true, alias: "controlName", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], autoError: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoError", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], length: [{ type: i0.Input, args: [{ isSignal: true, alias: "length", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], labelClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelClass", required: false }] }], otpClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "otpClass", required: false }] }], errorClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorClass", required: false }] }], hintClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "hintClass", required: false }] }] } });
184
216
 
185
217
  /*
186
218
  * Public API Surface of form-input-otp
@@ -5,12 +5,19 @@ import { NumberInput } from '@angular/cdk/coercion';
5
5
 
6
6
  declare class EgFormInputOtp {
7
7
  private readonly formGroupDirective;
8
+ private readonly _errorMessages;
8
9
  private readonly generatedId;
9
10
  private readonly _config;
10
11
  label: _angular_core.InputSignal<string | undefined>;
11
12
  controlId: _angular_core.InputSignal<string | undefined>;
12
13
  controlName: _angular_core.InputSignal<string>;
13
14
  error: _angular_core.InputSignal<string | undefined>;
15
+ /**
16
+ * Auto-resolve the displayed message from the control's `ValidationErrors`
17
+ * when `error()` is unset. Explicit `error()` always wins. Set to `false`
18
+ * for manual-only messages.
19
+ */
20
+ autoError: _angular_core.InputSignal<boolean>;
14
21
  hint: _angular_core.InputSignal<string | undefined>;
15
22
  id: _angular_core.InputSignal<string | undefined>;
16
23
  length: _angular_core.InputSignalWithTransform<number, NumberInput>;
@@ -19,6 +26,19 @@ declare class EgFormInputOtp {
19
26
  readonly effectiveId: _angular_core.Signal<string>;
20
27
  readonly errorId: _angular_core.Signal<string>;
21
28
  readonly hintId: _angular_core.Signal<string>;
29
+ /**
30
+ * Displayed message: explicit `error()` wins; otherwise (when `autoError()`)
31
+ * auto-resolved from the control's `ValidationErrors`. Plain method (not a
32
+ * computed): `ValidationErrors` is not a signal, so the control must be read
33
+ * fresh on every change-detection pass.
34
+ */
35
+ protected resolvedError(): string | undefined;
36
+ /**
37
+ * Whether the error is surfaced: message present + control invalid +
38
+ * touched/dirty/submitted. Gates the visual `<hlm-error>` (and
39
+ * `describedBy()`, where present) so sighted and screen-reader output agree.
40
+ */
41
+ protected showError(): boolean;
22
42
  readonly slotIndexes: _angular_core.Signal<number[]>;
23
43
  /**
24
44
  * Combined lock for the brain `linkedSignal`-based disabled state: the raw
@@ -37,7 +57,7 @@ declare class EgFormInputOtp {
37
57
  $errorClass: _angular_core.Signal<string>;
38
58
  $hintClass: _angular_core.Signal<string>;
39
59
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<EgFormInputOtp, never>;
40
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<EgFormInputOtp, "eg-form-input-otp", never, { "label": { "alias": "label"; "required": false; "isSignal": true; }; "controlId": { "alias": "controlId"; "required": false; "isSignal": true; }; "controlName": { "alias": "controlName"; "required": false; "isSignal": true; }; "error": { "alias": "error"; "required": false; "isSignal": true; }; "hint": { "alias": "hint"; "required": false; "isSignal": true; }; "id": { "alias": "id"; "required": false; "isSignal": true; }; "length": { "alias": "length"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "required": { "alias": "required"; "required": false; "isSignal": true; }; "userClass": { "alias": "class"; "required": false; "isSignal": true; }; "labelClass": { "alias": "labelClass"; "required": false; "isSignal": true; }; "otpClass": { "alias": "otpClass"; "required": false; "isSignal": true; }; "errorClass": { "alias": "errorClass"; "required": false; "isSignal": true; }; "hintClass": { "alias": "hintClass"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
60
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<EgFormInputOtp, "eg-form-input-otp", never, { "label": { "alias": "label"; "required": false; "isSignal": true; }; "controlId": { "alias": "controlId"; "required": false; "isSignal": true; }; "controlName": { "alias": "controlName"; "required": false; "isSignal": true; }; "error": { "alias": "error"; "required": false; "isSignal": true; }; "autoError": { "alias": "autoError"; "required": false; "isSignal": true; }; "hint": { "alias": "hint"; "required": false; "isSignal": true; }; "id": { "alias": "id"; "required": false; "isSignal": true; }; "length": { "alias": "length"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "required": { "alias": "required"; "required": false; "isSignal": true; }; "userClass": { "alias": "class"; "required": false; "isSignal": true; }; "labelClass": { "alias": "labelClass"; "required": false; "isSignal": true; }; "otpClass": { "alias": "otpClass"; "required": false; "isSignal": true; }; "errorClass": { "alias": "errorClass"; "required": false; "isSignal": true; }; "hintClass": { "alias": "hintClass"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
41
61
  }
42
62
 
43
63
  interface EgFormInputOtpConfig {
@@ -61,31 +61,32 @@ Selector (from source): `eg-form-month-year-picker` (standalone component, host
61
61
 
62
62
  ### `EgFormMonthYearPicker` — `eg-form-month-year-picker`
63
63
 
64
- | Input | Type | Default | Description |
65
- | ------------------- | --------------------------------- | -------------- | ------------------------------------------------------------------------------------------- |
66
- | `controlName` | `string` | `''` | `formControlName` key inside the parent `FormGroup`. **Required.** |
67
- | `label` | `string \| undefined` | — | Label text (hidden when omitted). |
68
- | `error` | `string \| undefined` | — | Error text shown when invalid. |
69
- | `hint` | `string \| undefined` | — | Hint text shown otherwise. |
70
- | `controlId` | `string \| undefined` | — | Explicit id (first priority for `effectiveId`). |
71
- | `id` | `string \| undefined` | — | Fallback id (second priority). |
72
- | `name` | `string \| undefined` | — | Native `name` forwarded to the inner input (`controlName` wins). |
73
- | `placeholder` | `string` | `'MM/YYYY'` | Input placeholder. |
74
- | `readonly` | `boolean` | `false` | Native `readonly` forwarded to the inner input. |
75
- | `disabled` | `boolean` | `false` | Locks interaction (form control stays enabled). |
76
- | `required` | `boolean` | `false` | Shows a red `*` next to the label. |
77
- | `min` / `max` | `Date \| string \| null` | `null` | Date bounds forwarded to the picker. |
78
- | `autoCloseOnSelect` | `boolean` | config default | Forwarded to the picker (falls back to `provideHlmMonthYearPickerConfig`, default `false`). |
79
- | `formatDate` | `(date: Date) => string` | config default | Display format (falls back to `provideHlmMonthYearPickerConfig`, default `MM/YYYY`). |
80
- | `transformDate` | `(date: Date) => Date` | config default | Model transform (falls back to `provideHlmMonthYearPickerConfig`). |
81
- | `parseDate` | `(value: string) => Date \| null` | config default | Input parsing, forwarded to the input (falls back to `provideHlmMonthYearPickerConfig`). |
82
- | `formatInputDate` | `(date: Date) => string` | config default | Edit format while focused (falls back to `provideHlmMonthYearPickerConfig`). |
83
- | `class` | `ClassValue` | `''` | Extra host classes (base `tw:w-full`). |
84
- | `labelClass` | `string` | `''` | Extra label classes (base `tw:mb-1`). |
85
- | `pickerClass` | `string` | `''` | Extra picker-container classes (base `tw:mb-1`). |
86
- | `inputClass` | `string` | `''` | Extra inner-input classes (e.g. `tw:text-lg`). |
87
- | `errorClass` | `string` | `''` | Extra error classes (base `tw:mt-0`). |
88
- | `hintClass` | `string` | `''` | Extra hint classes (base `tw:mt-0`). |
64
+ | Input | Type | Default | Description |
65
+ | ------------------- | --------------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
66
+ | `controlName` | `string` | `''` | `formControlName` key inside the parent `FormGroup`. **Required.** |
67
+ | `label` | `string \| undefined` | — | Label text (hidden when omitted). |
68
+ | `error` | `string \| undefined` | — | Error text shown when invalid. |
69
+ | `autoError` | `boolean` | `true` | Auto-resolve the message from the control's `ValidationErrors` when `error` is unset. Explicit `error` always wins; `error=""` counts as unset. Global wording via `provideEgFormErrorMessages`. |
70
+ | `hint` | `string \| undefined` | — | Hint text shown otherwise. |
71
+ | `controlId` | `string \| undefined` | — | Explicit id (first priority for `effectiveId`). |
72
+ | `id` | `string \| undefined` | — | Fallback id (second priority). |
73
+ | `name` | `string \| undefined` | — | Native `name` forwarded to the inner input (`controlName` wins). |
74
+ | `placeholder` | `string` | `'MM/YYYY'` | Input placeholder. |
75
+ | `readonly` | `boolean` | `false` | Native `readonly` forwarded to the inner input. |
76
+ | `disabled` | `boolean` | `false` | Locks interaction (form control stays enabled). |
77
+ | `required` | `boolean` | `false` | Shows a red `*` next to the label. |
78
+ | `min` / `max` | `Date \| string \| null` | `null` | Date bounds forwarded to the picker. |
79
+ | `autoCloseOnSelect` | `boolean` | config default | Forwarded to the picker (falls back to `provideHlmMonthYearPickerConfig`, default `false`). |
80
+ | `formatDate` | `(date: Date) => string` | config default | Display format (falls back to `provideHlmMonthYearPickerConfig`, default `MM/YYYY`). |
81
+ | `transformDate` | `(date: Date) => Date` | config default | Model transform (falls back to `provideHlmMonthYearPickerConfig`). |
82
+ | `parseDate` | `(value: string) => Date \| null` | config default | Input parsing, forwarded to the input (falls back to `provideHlmMonthYearPickerConfig`). |
83
+ | `formatInputDate` | `(date: Date) => string` | config default | Edit format while focused (falls back to `provideHlmMonthYearPickerConfig`). |
84
+ | `class` | `ClassValue` | `''` | Extra host classes (base `tw:w-full`). |
85
+ | `labelClass` | `string` | `''` | Extra label classes (base `tw:mb-1`). |
86
+ | `pickerClass` | `string` | `''` | Extra picker-container classes (base `tw:mb-1`). |
87
+ | `inputClass` | `string` | `''` | Extra inner-input classes (e.g. `tw:text-lg`). |
88
+ | `errorClass` | `string` | `''` | Extra error classes (base `tw:mt-0`). |
89
+ | `hintClass` | `string` | `''` | Extra hint classes (base `tw:mt-0`). |
89
90
 
90
91
  | Member | Description |
91
92
  | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |