ngx-vest-forms 2.5.0 → 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 +1 -0
- package/fesm2022/ngx-vest-forms.mjs +107 -23
- package/fesm2022/ngx-vest-forms.mjs.map +1 -1
- package/package.json +1 -1
- package/types/ngx-vest-forms.d.ts +46 -4
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,6 +1,6 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken, isDevMode, inject, ElementRef, DestroyRef, ChangeDetectorRef,
|
|
3
|
-
import { isFormArray, isFormGroup, NgForm, StatusChangeEvent, ValueChangeEvent, PristineChangeEvent, FormGroup, FormArray, NgModel, NgModelGroup, NG_ASYNC_VALIDATORS, ControlContainer, FormsModule } from '@angular/forms';
|
|
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
|
+
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';
|
|
6
6
|
|
|
@@ -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,15 +2264,14 @@ class FormErrorDisplayDirective {
|
|
|
2178
2264
|
#formControlState;
|
|
2179
2265
|
// Optionally inject NgForm for form submission tracking
|
|
2180
2266
|
#ngForm;
|
|
2181
|
-
|
|
2182
|
-
* Internal trigger signal that updates whenever form submit or status changes.
|
|
2183
|
-
* Used to ensure reactive tracking for the formSubmitted computed signal.
|
|
2184
|
-
*/
|
|
2185
|
-
#formEventTrigger;
|
|
2267
|
+
#formSubmittedState;
|
|
2186
2268
|
constructor() {
|
|
2187
2269
|
this.#formControlState = inject(FormControlStateDirective);
|
|
2188
2270
|
// Optionally inject NgForm for form submission tracking
|
|
2189
2271
|
this.#ngForm = inject(NgForm, { optional: true });
|
|
2272
|
+
this.#formSubmittedState = this.#ngForm
|
|
2273
|
+
? getFormSubmittedSignal(this.#ngForm)
|
|
2274
|
+
: signal(false);
|
|
2190
2275
|
/**
|
|
2191
2276
|
* Input signal for error display mode.
|
|
2192
2277
|
* Works seamlessly with hostDirectives in Angular 19+.
|
|
@@ -2216,27 +2301,16 @@ class FormErrorDisplayDirective {
|
|
|
2216
2301
|
* formSubmitted: true after the form is submitted (if NgForm is present)
|
|
2217
2302
|
*/
|
|
2218
2303
|
this.updateOn = this.#formControlState.updateOn;
|
|
2219
|
-
/**
|
|
2220
|
-
* Internal trigger signal that updates whenever form submit or status changes.
|
|
2221
|
-
* Used to ensure reactive tracking for the formSubmitted computed signal.
|
|
2222
|
-
*/
|
|
2223
|
-
this.#formEventTrigger = this.#ngForm
|
|
2224
|
-
? toSignal(merge(this.#ngForm.ngSubmit, this.#ngForm.statusChanges ?? of()).pipe(startWith(null)), { initialValue: null })
|
|
2225
|
-
: signal(null);
|
|
2226
2304
|
/**
|
|
2227
2305
|
* Signal that tracks NgForm.submitted state reactively.
|
|
2228
2306
|
*
|
|
2229
|
-
*
|
|
2230
|
-
* - ngSubmit: fires when form is submitted (sets NgForm.submitted = true)
|
|
2231
|
-
* - statusChanges: fires after resetForm() (which sets NgForm.submitted = false)
|
|
2307
|
+
* Map form-level submit/reset events directly to boolean state.
|
|
2232
2308
|
*
|
|
2233
|
-
* This
|
|
2309
|
+
* This keeps programmatic `NgForm.onSubmit()` reactive in zoneless mode and
|
|
2310
|
+
* avoids depending on `NgForm.submitted`, whose getter intentionally reads an
|
|
2311
|
+
* internal signal with `untracked()`.
|
|
2234
2312
|
*/
|
|
2235
|
-
this.formSubmitted =
|
|
2236
|
-
// Trigger signal ensures this recomputes on submit/status changes
|
|
2237
|
-
this.#formEventTrigger();
|
|
2238
|
-
return this.#ngForm?.submitted ?? false;
|
|
2239
|
-
}, ...(ngDevMode ? [{ debugName: "formSubmitted" }] : []));
|
|
2313
|
+
this.formSubmitted = this.#formSubmittedState;
|
|
2240
2314
|
/**
|
|
2241
2315
|
* Determines if errors should be shown based on the specified display mode
|
|
2242
2316
|
* and the control's state (touched/submitted/validated).
|
|
@@ -2355,6 +2429,16 @@ class FormErrorDisplayDirective {
|
|
|
2355
2429
|
return hasBeenValidated || isTouched || formSubmitted;
|
|
2356
2430
|
}
|
|
2357
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
|
+
}
|
|
2358
2442
|
// Warn about problematic combinations of updateOn and errorDisplayMode
|
|
2359
2443
|
effect(() => {
|
|
2360
2444
|
const mode = this.errorDisplayMode();
|