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 +1 -0
- package/fesm2022/ngx-vest-forms.mjs +102 -9
- package/fesm2022/ngx-vest-forms.mjs.map +1 -1
- package/package.json +1 -1
- package/types/ngx-vest-forms.d.ts +42 -0
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,
|
|
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.#
|
|
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();
|