ngx-vest-forms 2.2.0 → 2.3.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.
package/README.md CHANGED
@@ -116,7 +116,8 @@ That's all you need. The directive automatically creates controls, wires validat
116
116
  - **Unidirectional state with signals** — Models are `NgxDeepPartial<T>` so values build up incrementally
117
117
  - **Type-safe with runtime shape validation** — Automatic control creation and validation wiring (dev mode checks)
118
118
  - **Vest.js validations** — Sync/async, conditional, composable patterns with `only(field)` optimization
119
- - **Error display modes** — Control when errors show: `on-blur`, `on-submit`, or `on-blur-or-submit` (default)
119
+ - **Error display modes** — Control when errors show: `on-blur`, `on-submit`, `on-blur-or-submit` (default), `on-dirty`, or `always`
120
+ - **Warning display modes** — Control when warnings show: `on-touch`, `on-validated-or-touch` (default), `on-dirty`, or `always`
120
121
  - **Form state tracking** — Access touched, dirty, valid/invalid states for individual fields or entire form
121
122
  - **Error display helpers** — `ngx-control-wrapper` component (recommended) plus directive building blocks for custom wrappers:
122
123
  - `ngx-form-group-wrapper` component (recommended for `ngModelGroup` containers)
@@ -125,23 +126,60 @@ That's all you need. The directive automatically creates controls, wires validat
125
126
  - **Cross-field dependencies** — `validationConfig` for field-to-field triggers, `ROOT_FORM` for form-level rules
126
127
  - **Utilities** — Field paths, field clearing, validation config builder
127
128
 
128
- ### Error Display Modes
129
+ ### Error & Warning Display Modes
129
130
 
130
- Control when validation errors are shown to users with three built-in modes:
131
+ Control when validation errors and warnings are shown to users with multiple built-in modes:
132
+
133
+ #### Error Display Modes
131
134
 
132
135
  ```typescript
133
136
  // Global configuration via DI token
134
137
  import { NGX_ERROR_DISPLAY_MODE_TOKEN } from 'ngx-vest-forms';
135
138
 
136
139
  providers: [
137
- { provide: NGX_ERROR_DISPLAY_MODE_TOKEN, useValue: 'on-blur-or-submit' }
140
+ { provide: NGX_ERROR_DISPLAY_MODE_TOKEN, useValue: 'on-dirty' }
138
141
  ]
139
142
 
140
143
  // Recommended: Use ngx-control-wrapper component
141
144
  <ngx-control-wrapper [errorDisplayMode]="'on-blur'">
142
145
  <input name="email" [ngModel]="formValue().email" />
143
146
  </ngx-control-wrapper>
147
+ ```
148
+
149
+ | Mode | Behavior |
150
+ | --------------------- | ---------------------------------------------------- |
151
+ | `'on-blur-or-submit'` | Show after blur OR form submit (default) |
152
+ | `'on-blur'` | Show only after blur/touch |
153
+ | `'on-submit'` | Show only after form submission |
154
+ | `'on-dirty'` | Show as soon as value changes (or after blur/submit) |
155
+ | `'always'` | Show immediately, even on pristine fields |
156
+
157
+ #### Warning Display Modes
158
+
159
+ ```typescript
160
+ // Global configuration via DI token
161
+ import { NGX_WARNING_DISPLAY_MODE_TOKEN } from 'ngx-vest-forms';
162
+
163
+ providers: [
164
+ { provide: NGX_WARNING_DISPLAY_MODE_TOKEN, useValue: 'always' }
165
+ ]
144
166
 
167
+ // Per-instance configuration
168
+ <ngx-control-wrapper [warningDisplayMode]="'on-dirty'">
169
+ <input name="username" [ngModel]="formValue().username" />
170
+ </ngx-control-wrapper>
171
+ ```
172
+
173
+ | Mode | Behavior |
174
+ | ------------------------- | ---------------------------------------------------- |
175
+ | `'on-validated-or-touch'` | Show after validation runs or touch (default) |
176
+ | `'on-touch'` | Show only after blur/touch |
177
+ | `'on-dirty'` | Show as soon as value changes (or after blur/submit) |
178
+ | `'always'` | Show immediately, even on pristine fields |
179
+
180
+ #### Group-Safe Mode Example
181
+
182
+ ```html
145
183
  // Group-safe mode (use this on an ngModelGroup container)
146
184
  <ngx-form-group-wrapper ngModelGroup="address">
147
185
  <ngx-control-wrapper>
@@ -175,14 +213,6 @@ For `ngModelGroup` containers, prefer using `<ngx-form-group-wrapper>` (group-sa
175
213
  > **Styling note**: `ngx-control-wrapper` uses Tailwind CSS utility classes for default styling.
176
214
  > If your project doesn't use Tailwind, see the [component docs](./projects/ngx-vest-forms/src/lib/components/control-wrapper/README.md#styling-dependency-tailwind-css) for alternatives.
177
215
 
178
- **Available modes:**
179
-
180
- - **`on-blur-or-submit`** (default) — Show errors after field is touched OR form is submitted
181
- - **`on-blur`** — Show errors only after field loses focus (touched)
182
- - **`on-submit`** — Show errors only after form submission
183
-
184
- **Tip**: Use `on-blur-or-submit` for best UX — users get immediate feedback on touched fields while preventing overwhelming errors on pristine forms.
185
-
186
216
  📖 **[Complete Guide: Custom Control Wrappers](./docs/CUSTOM-CONTROL-WRAPPERS.md)**
187
217
 
188
218
  ### Form State
@@ -212,14 +242,14 @@ Access complete form and field state through the `FormErrorDisplayDirective` or
212
242
  - `isValid()` / `isInvalid()` — Validation state
213
243
  - `isPending()` — Async validation in progress
214
244
  - `errorMessages()` / `warningMessages()` — Current validation messages
215
- - `shouldShowErrors()` — Computed based on display mode and state
245
+ - `shouldShowErrors()` / `shouldShowWarnings()` — Computed based on display mode and state
216
246
 
217
247
  **Warnings behavior:**
218
248
 
219
249
  - Warnings are **non-blocking** and do not make a field invalid.
220
- - Warnings are stored separately from `control.errors` and are cleared on `resetForm()`.
221
- - Warnings may appear after `validationConfig` triggers validation, even if the field
222
- - was not touched yet. Use `NGX_WARNING_DISPLAY_MODE_TOKEN` to require touch-only display.
250
+ - They are stored separately from `control.errors` and are cleared on `resetForm()`.
251
+ - These messages may appear after `validationConfig` triggers validation, even if the field was not touched yet.
252
+ - Use `NGX_WARNING_DISPLAY_MODE_TOKEN` to control when warnings display (see [Warning Display Modes](#warning-display-modes)).
223
253
 
224
254
  **Tip**: For async validations, use `createDebouncedPendingState()` to prevent "Validating..." messages from flashing when validation completes quickly (< 200ms).
225
255
 
@@ -13,7 +13,12 @@ const SC_ERROR_DISPLAY_MODE_TOKEN = new InjectionToken('SC_ERROR_DISPLAY_MODE_TO
13
13
  });
14
14
  /**
15
15
  * Injection token for configuring the default error display mode.
16
- * Values: 'on-blur' | 'on-submit' | 'on-blur-or-submit' (default)
16
+ * Values:
17
+ * - 'on-blur': Show errors after field is touched/blurred
18
+ * - 'on-submit': Show errors after form submission
19
+ * - 'on-blur-or-submit': Show errors after blur or form submission (default)
20
+ * - 'on-dirty': Show errors as soon as the field value changes
21
+ * - 'always': Show errors immediately, even on pristine fields
17
22
  */
18
23
  const NGX_ERROR_DISPLAY_MODE_TOKEN = new InjectionToken('NGX_ERROR_DISPLAY_MODE_TOKEN', {
19
24
  providedIn: 'root',
@@ -21,7 +26,11 @@ const NGX_ERROR_DISPLAY_MODE_TOKEN = new InjectionToken('NGX_ERROR_DISPLAY_MODE_
21
26
  });
22
27
  /**
23
28
  * Injection token for configuring the default warning display mode.
24
- * Values: 'on-touch' | 'on-validated-or-touch' (default)
29
+ * Values:
30
+ * - 'on-touch': Show warnings after field is touched/blurred
31
+ * - 'on-validated-or-touch': Show warnings after validation runs or field is touched (default)
32
+ * - 'on-dirty': Show warnings as soon as the field value changes
33
+ * - 'always': Show warnings immediately, even on pristine fields
25
34
  */
26
35
  const NGX_WARNING_DISPLAY_MODE_TOKEN = new InjectionToken('NGX_WARNING_DISPLAY_MODE_TOKEN', {
27
36
  providedIn: 'root',
@@ -1966,6 +1975,7 @@ class FormErrorDisplayDirective {
1966
1975
  this.shouldShowErrors = computed(() => {
1967
1976
  const mode = this.errorDisplayMode();
1968
1977
  const isTouched = this.isTouched();
1978
+ const isDirty = this.isDirty();
1969
1979
  const isInvalid = this.isInvalid();
1970
1980
  const hasErrors = this.errorMessages().length > 0;
1971
1981
  const updateOn = this.updateOn();
@@ -1977,16 +1987,25 @@ class FormErrorDisplayDirective {
1977
1987
  if (updateOn === 'submit') {
1978
1988
  return !!(formSubmitted && hasErrorState);
1979
1989
  }
1980
- // on-blur: show errors after blur (touch)
1981
- if (mode === 'on-blur') {
1982
- return !!(isTouched && hasErrorState);
1990
+ // Handle the new display modes
1991
+ switch (mode) {
1992
+ case 'always':
1993
+ // Always show errors immediately, even on pristine fields
1994
+ return hasErrorState;
1995
+ case 'on-dirty':
1996
+ // Show when value has changed, OR when touched/submitted (for backwards compat)
1997
+ return !!(isDirty || isTouched || formSubmitted) && hasErrorState;
1998
+ case 'on-blur':
1999
+ // Show after touch (blur) or form submission (traditional behavior, not dirty-based)
2000
+ return !!(isTouched || formSubmitted) && hasErrorState;
2001
+ case 'on-submit':
2002
+ // Show only after form submission
2003
+ return !!(formSubmitted && hasErrorState);
2004
+ case 'on-blur-or-submit':
2005
+ default:
2006
+ // Show after blur (touch) OR submit (default behavior)
2007
+ return !!((isTouched || formSubmitted) && hasErrorState);
1983
2008
  }
1984
- // on-submit: show errors after submit
1985
- if (mode === 'on-submit') {
1986
- return !!(formSubmitted && hasErrorState);
1987
- }
1988
- // on-blur-or-submit: show errors after blur (touch) OR submit
1989
- return !!((isTouched || formSubmitted) && hasErrorState);
1990
2009
  }, ...(ngDevMode ? [{ debugName: "shouldShowErrors" }] : []));
1991
2010
  /**
1992
2011
  * Errors to display (filtered for pending state)
@@ -2017,6 +2036,47 @@ class FormErrorDisplayDirective {
2017
2036
  }
2018
2037
  return this.hasPendingValidation();
2019
2038
  }, ...(ngDevMode ? [{ debugName: "isPending" }] : []));
2039
+ /**
2040
+ * Determines if warnings should be shown based on the specified display mode
2041
+ * and the control's state (touched/validated/dirty).
2042
+ *
2043
+ * NOTE: Unlike errors, warnings can exist on VALID fields (warnings-only scenario).
2044
+ * We don't require isInvalid() because Vest warn() tests don't affect field validity.
2045
+ *
2046
+ * UX Note: We include `hasBeenValidated` for `on-validated-or-touch` mode to support
2047
+ * cross-field validation. If Field A triggers validation on Field B (via validationConfig),
2048
+ * Field B should show warnings if it has them, even if the user hasn't touched Field B yet.
2049
+ * Unlike errors (which block submission), warnings are informational and safe to show.
2050
+ */
2051
+ this.shouldShowWarnings = computed(() => {
2052
+ const mode = this.warningDisplayMode();
2053
+ const isTouched = this.isTouched();
2054
+ const isDirty = this.isDirty();
2055
+ const hasBeenValidated = this.hasBeenValidated();
2056
+ const hasWarnings = this.warningMessages().length > 0;
2057
+ const isPending = this.hasPendingValidation();
2058
+ const formSubmitted = this.formSubmitted();
2059
+ // No warnings to show or still pending
2060
+ if (!hasWarnings || isPending) {
2061
+ return false;
2062
+ }
2063
+ // Handle the warning display modes
2064
+ switch (mode) {
2065
+ case 'always':
2066
+ // Always show warnings immediately, even on pristine fields
2067
+ return true;
2068
+ case 'on-dirty':
2069
+ // Show when value has changed, OR when touched/submitted (for backwards compat)
2070
+ return isDirty || isTouched || formSubmitted;
2071
+ case 'on-touch':
2072
+ // Show after touch (blur) or form submission (traditional behavior, not dirty-based)
2073
+ return isTouched || formSubmitted;
2074
+ case 'on-validated-or-touch':
2075
+ default:
2076
+ // Show after validation runs or after touch/submit (default behavior)
2077
+ return hasBeenValidated || isTouched || formSubmitted;
2078
+ }
2079
+ }, ...(ngDevMode ? [{ debugName: "shouldShowWarnings" }] : []));
2020
2080
  // Warn about problematic combinations of updateOn and errorDisplayMode
2021
2081
  effect(() => {
2022
2082
  const mode = this.errorDisplayMode();
@@ -2161,6 +2221,8 @@ let nextUniqueId$2 = 0;
2161
2221
  * - `'on-blur-or-submit'` (default): Show errors after blur OR form submit
2162
2222
  * - `'on-blur'`: Show errors only after blur
2163
2223
  * - `'on-submit'`: Show errors only after form submit
2224
+ * - `'on-dirty'`: Show errors as soon as the field value changes
2225
+ * - `'always'`: Show errors immediately, even on pristine fields
2164
2226
  *
2165
2227
  * ```html
2166
2228
  * <ngx-control-wrapper [errorDisplayMode]="'on-submit'">
@@ -2168,6 +2230,19 @@ let nextUniqueId$2 = 0;
2168
2230
  * </ngx-control-wrapper>
2169
2231
  * ```
2170
2232
  *
2233
+ * ### Warning Display Modes
2234
+ * Control when warnings appear using the `warningDisplayMode` input:
2235
+ * - `'on-validated-or-touch'` (default): Show warnings after validation runs or touch
2236
+ * - `'on-touch'`: Show warnings only after touch/blur
2237
+ * - `'on-dirty'`: Show warnings as soon as the field value changes
2238
+ * - `'always'`: Show warnings immediately, even on pristine fields
2239
+ *
2240
+ * ```html
2241
+ * <ngx-control-wrapper [warningDisplayMode]="'on-dirty'">
2242
+ * <input name="email" [ngModel]="formValue().email" />
2243
+ * </ngx-control-wrapper>
2244
+ * ```
2245
+ *
2171
2246
  * ### Accessibility Features (Automatic)
2172
2247
  * - Unique IDs for error/warning/pending regions
2173
2248
  * - `aria-describedby` linking errors to form controls
@@ -2200,9 +2275,9 @@ let nextUniqueId$2 = 0;
2200
2275
  *
2201
2276
  * Error & Warning Display Behavior:
2202
2277
  * - The error display mode can be configured globally using the NGX_ERROR_DISPLAY_MODE_TOKEN injection token, or per instance using the `errorDisplayMode` input on FormErrorDisplayDirective (which this component uses as a hostDirective).
2203
- * - Possible values: 'on-blur' | 'on-submit' | 'on-blur-or-submit' (default: 'on-blur-or-submit')
2278
+ * - Possible error display values: 'on-blur' | 'on-submit' | 'on-blur-or-submit' | 'on-dirty' | 'always' (default: 'on-blur-or-submit')
2204
2279
  * - The warning display mode can be configured globally using NGX_WARNING_DISPLAY_MODE_TOKEN, or per instance using the `warningDisplayMode` input on FormErrorDisplayDirective.
2205
- * - Possible values: 'on-touch' | 'on-validated-or-touch' (default: 'on-validated-or-touch')
2280
+ * - Possible warning display values: 'on-touch' | 'on-validated-or-touch' | 'on-dirty' | 'always' (default: 'on-validated-or-touch')
2206
2281
  *
2207
2282
  * Example (per instance):
2208
2283
  * <div ngxControlWrapper>
@@ -2296,28 +2371,12 @@ class ControlWrapperComponent {
2296
2371
  this.showPendingMessage = this.pendingState.showPendingMessage;
2297
2372
  /**
2298
2373
  * Whether to display warnings.
2299
- * Warnings are shown when:
2300
- * 1. The field has been touched (user has interacted with it)
2301
- * 2. The field has warnings to display
2302
- * 3. The field is not currently pending validation
2303
- *
2304
- * NOTE: Unlike errors, warnings can exist on VALID fields (warnings-only scenario).
2305
- * We don't require isInvalid() because Vest warn() tests don't affect field validity.
2374
+ * Delegates to FormErrorDisplayDirective's centralized shouldShowWarnings signal.
2306
2375
  *
2307
- * UX Note: We include `hasBeenValidated` here to support cross-field validation.
2308
- * If Field A triggers validation on Field B (via validationConfig), Field B should
2309
- * show warnings if it has them, even if the user hasn't touched Field B yet.
2310
- * Unlike errors (which block submission), warnings are informational and safe to safe to show.
2376
+ * This ensures consistent warning display behavior across all form components
2377
+ * and supports the new 'on-dirty' and 'always' display modes.
2311
2378
  */
2312
- this.shouldShowWarnings = computed(() => {
2313
- const isTouched = this.errorDisplay.isTouched();
2314
- const hasBeenValidated = this.errorDisplay.hasBeenValidated();
2315
- const isPending = this.errorDisplay.isPending();
2316
- const hasWarnings = this.errorDisplay.warnings().length > 0;
2317
- const mode = this.errorDisplay.warningDisplayMode();
2318
- const shouldShowAfterInteraction = mode === 'on-touch' ? isTouched : isTouched || hasBeenValidated;
2319
- return shouldShowAfterInteraction && hasWarnings && !isPending;
2320
- }, ...(ngDevMode ? [{ debugName: "shouldShowWarnings" }] : []));
2379
+ this.shouldShowWarnings = this.errorDisplay.shouldShowWarnings;
2321
2380
  /**
2322
2381
  * Computed signal that builds aria-describedby string based on visible regions
2323
2382
  */