ngx-vest-forms 2.5.1 → 2.6.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
@@ -401,6 +401,7 @@ const shape: NgxDeepRequired<MyFormModel> = {
401
401
  ### Advanced Patterns
402
402
 
403
403
  - **[ValidationConfig vs Root-Form](./docs/VALIDATION-CONFIG-VS-ROOT-FORM.md)** - Cross-field dependencies and form-level rules
404
+ - **[Clear Submitted State](./docs/CLEAR-SUBMITTED-STATE.md)** - End a submit cycle without resetting values or control metadata
404
405
  - **[Field Path Types](./docs/FIELD-PATHS.md)** - Type-safe dot-notation paths for nested properties
405
406
  - **[Structure Change Detection](./docs/STRUCTURE_CHANGE_DETECTION.md)** - Handle dynamic form structure updates
406
407
  - **[Field Clearing Utilities](./docs/FIELD-CLEARING-UTILITIES.md)** - Type-safe utilities for clearing nested form values
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, isDevMode, inject, ElementRef, DestroyRef, ChangeDetectorRef, signal, linkedSignal, computed, input, effect, untracked, Directive, contentChild, Injector, afterEveryRender, ChangeDetectionStrategy, Component, booleanAttribute, Optional } from '@angular/core';
2
+ import { InjectionToken, isDevMode, signal, inject, ElementRef, DestroyRef, ChangeDetectorRef, linkedSignal, computed, input, effect, untracked, Directive, contentChild, Injector, afterEveryRender, ChangeDetectionStrategy, Component, booleanAttribute, Optional } from '@angular/core';
3
3
  import { isFormArray, isFormGroup, NgForm, StatusChangeEvent, ValueChangeEvent, PristineChangeEvent, FormGroup, FormArray, NgModel, NgModelGroup, FormSubmittedEvent, FormResetEvent, NG_ASYNC_VALIDATORS, ControlContainer, FormsModule } from '@angular/forms';
4
4
  import { toSignal, outputFromObservable, takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop';
5
5
  import { startWith, merge, filter, map, switchMap, take, of, scan, distinctUntilChanged, timer, Observable, catchError, EMPTY, debounceTime, tap, race, from } from 'rxjs';
@@ -896,6 +896,46 @@ function validateFormValueAgainstShape(formValue, shape, path = '') {
896
896
  }
897
897
  }
898
898
 
899
+ const formSubmittedSignals = new WeakMap();
900
+ function getFormSubmittedSignal(ngForm) {
901
+ let submitted = formSubmittedSignals.get(ngForm);
902
+ if (!submitted) {
903
+ submitted = signal(ngForm.submitted);
904
+ formSubmittedSignals.set(ngForm, submitted);
905
+ }
906
+ return submitted;
907
+ }
908
+ function setAngularFormSubmittedState(ngForm, submitted) {
909
+ // Angular 21.x's concrete NgForm stores submitted state on
910
+ // `submittedReactive`, while AbstractFormDirective-backed implementations
911
+ // expose `_submittedReactive` and may also provide a public setter on
912
+ // `submitted`. This helper is verified against Angular 21.x in this
913
+ // repository and falls back to the first writable `submitted` setter it can
914
+ // find if a future Angular version changes the concrete field names.
915
+ //
916
+ // We try the concrete/internal signal first because NgForm overrides the
917
+ // getter-only `submitted` property at runtime in this workspace. If neither
918
+ // signal exists, fall back to the first writable `submitted` setter we can
919
+ // find on the prototype chain. If no setter exists either, callers should
920
+ // still update ngx-vest-forms' shared signal so error display state remains
921
+ // reactive even though Angular's native submitted flag cannot be changed.
922
+ const signalHost = ngForm;
923
+ const angularSignal = signalHost.submittedReactive ?? signalHost._submittedReactive;
924
+ if (angularSignal) {
925
+ angularSignal.set(submitted);
926
+ return;
927
+ }
928
+ let prototype = Object.getPrototypeOf(ngForm);
929
+ while (prototype) {
930
+ const submittedDescriptor = Object.getOwnPropertyDescriptor(prototype, 'submitted');
931
+ if (submittedDescriptor?.set) {
932
+ submittedDescriptor.set.call(ngForm, submitted);
933
+ return;
934
+ }
935
+ prototype = Object.getPrototypeOf(prototype);
936
+ }
937
+ }
938
+
899
939
  /**
900
940
  * Duration (in milliseconds) to keep fields marked as "in-progress" after validation.
901
941
  * This prevents immediate re-triggering of bidirectional validations.
@@ -1364,6 +1404,52 @@ class FormDirective {
1364
1404
  this.ngForm.form.markAllAsTouched();
1365
1405
  this.#blurTick.update((v) => v + 1);
1366
1406
  }
1407
+ /**
1408
+ * Clears the current submit cycle without resetting control values or metadata.
1409
+ *
1410
+ * Unlike {@link resetForm}, this only flips the submitted gate back to `false`.
1411
+ * Touched/dirty/pristine state is preserved so consumers can end `'on-submit'`
1412
+ * error visibility without a full form reset.
1413
+ *
1414
+ * **When to use:**
1415
+ * - You use submit-gated error visibility such as `'on-submit'`
1416
+ * - A submit attempt already happened
1417
+ * - The user resolved the current submit-time errors
1418
+ * - You want future untouched fields to wait for the next submit before showing errors
1419
+ *
1420
+ * **Why this exists:**
1421
+ * `resetForm()` would also clear touched/dirty/pristine metadata, which is often
1422
+ * too disruptive for long-form, multi-form, or mixed error-display flows.
1423
+ *
1424
+ * **What it does NOT do:**
1425
+ * - Does not change field values
1426
+ * - Does not mark controls pristine or untouched
1427
+ * - Does not re-run validation
1428
+ *
1429
+ * @example
1430
+ * ```typescript
1431
+ * submitAll(): void {
1432
+ * for (const form of this.submitForms()) {
1433
+ * form.ngForm.onSubmit(new Event('submit'));
1434
+ * }
1435
+ *
1436
+ * if (this.submitForms().every((form) => form.formState().valid)) {
1437
+ * for (const form of this.submitForms()) {
1438
+ * form.clearSubmittedState();
1439
+ * }
1440
+ * }
1441
+ * }
1442
+ * ```
1443
+ *
1444
+ * @see {@link resetForm} to fully reset values and control metadata
1445
+ * @see {@link markAllAsTouched} to manually show all errors
1446
+ * @see {@link triggerFormValidation} to re-run validation after structure changes
1447
+ */
1448
+ clearSubmittedState() {
1449
+ setAngularFormSubmittedState(this.ngForm, false);
1450
+ getFormSubmittedSignal(this.ngForm).set(false);
1451
+ this.#blurTick.update((v) => v + 1);
1452
+ }
1367
1453
  /**
1368
1454
  * Finds the first invalid element in this form, scrolls it into view, and focuses it.
1369
1455
  *
@@ -2178,10 +2264,14 @@ class FormErrorDisplayDirective {
2178
2264
  #formControlState;
2179
2265
  // Optionally inject NgForm for form submission tracking
2180
2266
  #ngForm;
2267
+ #formSubmittedState;
2181
2268
  constructor() {
2182
2269
  this.#formControlState = inject(FormControlStateDirective);
2183
2270
  // Optionally inject NgForm for form submission tracking
2184
2271
  this.#ngForm = inject(NgForm, { optional: true });
2272
+ this.#formSubmittedState = this.#ngForm
2273
+ ? getFormSubmittedSignal(this.#ngForm)
2274
+ : signal(false);
2185
2275
  /**
2186
2276
  * Input signal for error display mode.
2187
2277
  * Works seamlessly with hostDirectives in Angular 19+.
@@ -2220,14 +2310,7 @@ class FormErrorDisplayDirective {
2220
2310
  * avoids depending on `NgForm.submitted`, whose getter intentionally reads an
2221
2311
  * internal signal with `untracked()`.
2222
2312
  */
2223
- this.formSubmitted = this.#ngForm
2224
- ? (() => {
2225
- const ngForm = this.#ngForm;
2226
- return toSignal(ngForm.form.events.pipe(filter((event) => event.source === ngForm.form &&
2227
- (event instanceof FormSubmittedEvent ||
2228
- event instanceof FormResetEvent)), map((event) => event instanceof FormSubmittedEvent), startWith(ngForm.submitted)), { initialValue: ngForm.submitted });
2229
- })()
2230
- : signal(false);
2313
+ this.formSubmitted = this.#formSubmittedState;
2231
2314
  /**
2232
2315
  * Determines if errors should be shown based on the specified display mode
2233
2316
  * and the control's state (touched/submitted/validated).
@@ -2346,6 +2429,16 @@ class FormErrorDisplayDirective {
2346
2429
  return hasBeenValidated || isTouched || formSubmitted;
2347
2430
  }
2348
2431
  }, ...(ngDevMode ? [{ debugName: "shouldShowWarnings" }] : []));
2432
+ const ngForm = this.#ngForm;
2433
+ if (ngForm) {
2434
+ ngForm.form.events
2435
+ .pipe(filter((event) => event.source === ngForm.form &&
2436
+ (event instanceof FormSubmittedEvent ||
2437
+ event instanceof FormResetEvent)), map((event) => event instanceof FormSubmittedEvent), startWith(ngForm.submitted), takeUntilDestroyed())
2438
+ .subscribe((submitted) => {
2439
+ this.#formSubmittedState.set(submitted);
2440
+ });
2441
+ }
2349
2442
  // Warn about problematic combinations of updateOn and errorDisplayMode
2350
2443
  effect(() => {
2351
2444
  const mode = this.errorDisplayMode();