@firestitch/form 18.0.29 → 18.0.31
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/app/components/form-dialog-actions/form-dialog-actions.component.d.ts +17 -2
- package/app/directives/button.directive.d.ts +9 -1
- package/app/directives/confirm-tabs/confirm-tabs.directive.d.ts +32 -0
- package/app/directives/confirm-tabs/index.d.ts +1 -0
- package/app/directives/form/form.directive.d.ts +89 -29
- package/app/directives/form-base/form-base.directive.d.ts +86 -6
- package/app/directives/form-container/form-container.directive.d.ts +80 -0
- package/app/directives/form-container/index.d.ts +1 -0
- package/app/directives/index.d.ts +2 -1
- package/app/fs-form.module.d.ts +3 -2
- package/app/helpers/index.d.ts +1 -0
- package/app/helpers/within-same-dialog.d.ts +18 -0
- package/app/interfaces/form-owner.d.ts +17 -0
- package/app/interfaces/index.d.ts +1 -0
- package/app/services/fsform.service.d.ts +5 -5
- package/esm2022/app/components/form-dialog-actions/form-dialog-actions.component.mjs +50 -17
- package/esm2022/app/directives/button.directive.mjs +26 -9
- package/esm2022/app/directives/confirm-tabs/confirm-tabs.directive.mjs +50 -0
- package/esm2022/app/directives/confirm-tabs/index.mjs +2 -0
- package/esm2022/app/directives/form/form.directive.mjs +247 -241
- package/esm2022/app/directives/form-base/form-base.directive.mjs +255 -8
- package/esm2022/app/directives/form-container/form-container.directive.mjs +164 -0
- package/esm2022/app/directives/form-container/index.mjs +2 -0
- package/esm2022/app/directives/index.mjs +3 -2
- package/esm2022/app/fs-form.module.mjs +11 -6
- package/esm2022/app/guards/form-deactivate.guard.mjs +1 -1
- package/esm2022/app/helpers/index.mjs +2 -1
- package/esm2022/app/helpers/within-same-dialog.mjs +19 -0
- package/esm2022/app/interfaces/form-owner.mjs +2 -0
- package/esm2022/app/interfaces/index.mjs +2 -1
- package/esm2022/app/services/fsform.service.mjs +10 -7
- package/esm2022/public_api.mjs +3 -2
- package/fesm2022/firestitch-form.mjs +811 -320
- package/fesm2022/firestitch-form.mjs.map +1 -1
- package/package.json +1 -1
- package/public_api.d.ts +3 -1
- package/app/directives/form-group/form-group.directive.d.ts +0 -15
- package/app/directives/form-group/index.d.ts +0 -1
- package/esm2022/app/directives/form-group/form-group.directive.mjs +0 -35
- package/esm2022/app/directives/form-group/index.mjs +0 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { NgClass, NgTemplateOutlet, CommonModule } from '@angular/common';
|
|
2
2
|
import * as i0 from '@angular/core';
|
|
3
|
-
import { inject, Component, ChangeDetectionStrategy, Injectable, NgZone, ChangeDetectorRef, QueryList, Directive, Input, ContentChildren,
|
|
3
|
+
import { inject, Component, ChangeDetectionStrategy, Injectable, NgZone, ChangeDetectorRef, ElementRef, QueryList, Directive, Input, ContentChildren, EventEmitter, Output, HostBinding, HostListener, InjectionToken, Renderer2, Injector, ViewContainerRef, NgModule } from '@angular/core';
|
|
4
4
|
import { UntypedFormGroup, NgForm, NgControl, Validators, NgModel, FormsModule } from '@angular/forms';
|
|
5
5
|
import { MatButton, MatButtonModule } from '@angular/material/button';
|
|
6
6
|
import { ErrorStateMatcher, ShowOnDirtyErrorStateMatcher } from '@angular/material/core';
|
|
@@ -8,13 +8,13 @@ import * as i1 from '@angular/material/dialog';
|
|
|
8
8
|
import { MAT_DIALOG_DATA, MatDialogRef, MatDialogModule, MatDialog, MatDialogClose } from '@angular/material/dialog';
|
|
9
9
|
import * as i2 from '@firestitch/dialog';
|
|
10
10
|
import { FsDialogModule } from '@firestitch/dialog';
|
|
11
|
-
import { Subject, of,
|
|
12
|
-
import { switchMap, map, catchError,
|
|
13
|
-
import { ActivatedRoute } from '@angular/router';
|
|
11
|
+
import { Subject, of, fromEvent, iif, defer, EMPTY, BehaviorSubject, concat, throwError, combineLatest, from, isObservable } from 'rxjs';
|
|
12
|
+
import { switchMap, map, catchError, filter, takeUntil, tap, startWith, toArray, take, mergeMap, delay, first, mapTo } from 'rxjs/operators';
|
|
14
13
|
import { guid, email, isEmpty, isNumeric, phone, url } from '@firestitch/common';
|
|
15
|
-
import { DrawerRef } from '@firestitch/drawer';
|
|
16
14
|
import { FsMessage, MessageMode } from '@firestitch/message';
|
|
15
|
+
import { ActivatedRoute } from '@angular/router';
|
|
17
16
|
import { MatTabGroup } from '@angular/material/tabs';
|
|
17
|
+
import { DrawerRef } from '@firestitch/drawer';
|
|
18
18
|
import { values, keys, isObject } from 'lodash-es';
|
|
19
19
|
import { isValid } from 'date-fns';
|
|
20
20
|
|
|
@@ -59,6 +59,49 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.7", ngImpor
|
|
|
59
59
|
], template: "<fs-dialog>\n <h1 mat-dialog-title>\n {{ title }}\n </h1>\n <mat-dialog-content>\n {{ message }}\n </mat-dialog-content>\n <mat-dialog-actions>\n <button\n type=\"button\"\n color=\"primary\"\n (click)=\"save()\"\n mat-button>\n {{ saveLabel }}\n </button>\n <button\n type=\"button\"\n (click)=\"discard()\"\n mat-button>\n {{ discardLabel }}\n </button>\n <button\n type=\"button\"\n (click)=\"cancel()\"\n mat-button>\n {{ cancelLabel }}\n </button>\n </mat-dialog-actions>\n</fs-dialog>", styles: [":host{width:100%}.form-buttons.save-create .close-button:not(.cancel-button){float:right}\n"] }]
|
|
60
60
|
}] });
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Whether an owner found through DI belongs to the same dialog as whoever found
|
|
64
|
+
* it - and may therefore be joined, or driven by a button.
|
|
65
|
+
*
|
|
66
|
+
* A form set must never span a dialog boundary. Owners are resolved by walking
|
|
67
|
+
* the element injector, and CDK parents a dialog opened with `viewContainerRef`
|
|
68
|
+
* to the opener's injector, so a dialog opened from inside another dialog's form
|
|
69
|
+
* would otherwise register into that form's set: the outer footer would flip to
|
|
70
|
+
* Save the moment the inner dialog opened and would then submit the inner form,
|
|
71
|
+
* while the inner dialog - no longer the owner of its own set - would stop
|
|
72
|
+
* confirming its unsaved changes on backdrop click.
|
|
73
|
+
*
|
|
74
|
+
* Identity of the `MatDialogRef` is the test: everything inside one dialog
|
|
75
|
+
* injects the same instance, and everything outside any dialog gets null.
|
|
76
|
+
*/
|
|
77
|
+
function withinSameDialog(owner, dialogRef) {
|
|
78
|
+
return !!owner && owner.dialogRef === dialogRef;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function getFormErrors(control, key) {
|
|
82
|
+
let errors = null;
|
|
83
|
+
if (control.invalid && control.errors) {
|
|
84
|
+
errors = {
|
|
85
|
+
[key]: { ...control.errors },
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
if (control instanceof UntypedFormGroup) {
|
|
89
|
+
Object.entries(control.controls)
|
|
90
|
+
.forEach(([name, childControl]) => {
|
|
91
|
+
const childErrors = getFormErrors(childControl, name);
|
|
92
|
+
if (childErrors) {
|
|
93
|
+
if (!errors) {
|
|
94
|
+
errors = { ...childErrors };
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
Object.assign(errors, childErrors);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return errors;
|
|
103
|
+
}
|
|
104
|
+
|
|
62
105
|
var ConfirmResult;
|
|
63
106
|
(function (ConfirmResult) {
|
|
64
107
|
ConfirmResult["Save"] = "save";
|
|
@@ -68,6 +111,18 @@ var ConfirmResult;
|
|
|
68
111
|
ConfirmResult["NoChanges"] = "noChanges";
|
|
69
112
|
})(ConfirmResult || (ConfirmResult = {}));
|
|
70
113
|
|
|
114
|
+
var FormStatus;
|
|
115
|
+
(function (FormStatus) {
|
|
116
|
+
FormStatus["Valid"] = "valid";
|
|
117
|
+
FormStatus["Invalid"] = "invalid";
|
|
118
|
+
FormStatus["Validating"] = "validating";
|
|
119
|
+
FormStatus["Submitting"] = "submitting";
|
|
120
|
+
FormStatus["Submitted"] = "submitted";
|
|
121
|
+
FormStatus["Error"] = "error";
|
|
122
|
+
FormStatus["Success"] = "success";
|
|
123
|
+
FormStatus["Completing"] = "completing";
|
|
124
|
+
})(FormStatus || (FormStatus = {}));
|
|
125
|
+
|
|
71
126
|
function confirmResultContinue(result) {
|
|
72
127
|
return (result === ConfirmResult.Discard ||
|
|
73
128
|
result === ConfirmResult.Save ||
|
|
@@ -81,18 +136,6 @@ function getActiveRoute(route) {
|
|
|
81
136
|
return route;
|
|
82
137
|
}
|
|
83
138
|
|
|
84
|
-
var FormStatus;
|
|
85
|
-
(function (FormStatus) {
|
|
86
|
-
FormStatus["Valid"] = "valid";
|
|
87
|
-
FormStatus["Invalid"] = "invalid";
|
|
88
|
-
FormStatus["Validating"] = "validating";
|
|
89
|
-
FormStatus["Submitting"] = "submitting";
|
|
90
|
-
FormStatus["Submitted"] = "submitted";
|
|
91
|
-
FormStatus["Error"] = "error";
|
|
92
|
-
FormStatus["Success"] = "success";
|
|
93
|
-
FormStatus["Completing"] = "completing";
|
|
94
|
-
})(FormStatus || (FormStatus = {}));
|
|
95
|
-
|
|
96
139
|
class FsForm {
|
|
97
140
|
// value is array for future possibilities of extension
|
|
98
141
|
_formDirectiveStore = new WeakMap();
|
|
@@ -109,11 +152,13 @@ class FsForm {
|
|
|
109
152
|
this._formDirectiveStore.delete(routeComponent);
|
|
110
153
|
}
|
|
111
154
|
hasChanges(form) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
155
|
+
// Spans the linked set, so a dialog whose fields live in a nested tab form
|
|
156
|
+
// still knows it has changes. A form linked out with `[link]="false"` is
|
|
157
|
+
// absent from that set and never triggers the prompt - which is the point of
|
|
158
|
+
// an instant-save panel: it has nothing outstanding to warn about.
|
|
159
|
+
return form.linkedForms
|
|
160
|
+
.some((linkedForm) => Object.keys(linkedForm.ngForm.control.controls)
|
|
161
|
+
.some((key) => linkedForm.ngForm.control.controls[key].dirty));
|
|
117
162
|
}
|
|
118
163
|
confirmUnsaved(form) {
|
|
119
164
|
if (!form.confirm || !this.hasChanges(form)) {
|
|
@@ -152,7 +197,8 @@ class FsForm {
|
|
|
152
197
|
return of(ConfirmResult.Discard);
|
|
153
198
|
}
|
|
154
199
|
if (result === 'save') {
|
|
155
|
-
form.
|
|
200
|
+
form.linkedForms
|
|
201
|
+
.forEach((linkedForm) => linkedForm.ngForm.control.markAsPristine());
|
|
156
202
|
return form.submit$({ confirmed: true })
|
|
157
203
|
.pipe(map((submitEvent) => {
|
|
158
204
|
if (submitEvent.error) {
|
|
@@ -205,46 +251,57 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.7", ngImpor
|
|
|
205
251
|
}]
|
|
206
252
|
}] });
|
|
207
253
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
if (childErrors) {
|
|
220
|
-
if (!errors) {
|
|
221
|
-
errors = { ...childErrors };
|
|
222
|
-
}
|
|
223
|
-
else {
|
|
224
|
-
Object.assign(errors, childErrors);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
});
|
|
228
|
-
}
|
|
229
|
-
return errors;
|
|
230
|
-
}
|
|
231
|
-
|
|
254
|
+
/**
|
|
255
|
+
* Shared by the two things that can own a set of forms: an `fsForm` sitting at
|
|
256
|
+
* the root of its linked set, and an `fsFormContainer` holding forms that are
|
|
257
|
+
* siblings rather than nested.
|
|
258
|
+
*
|
|
259
|
+
* It holds the submit-button registry both of them expose to buttons, and the
|
|
260
|
+
* lifecycle that belongs to the set as a whole rather than to any one form -
|
|
261
|
+
* the dialog backdrop and Escape, Ctrl+S, the browser close prompt, the drawer
|
|
262
|
+
* and tab-switch confirms. Exactly one owner registers that lifecycle; see
|
|
263
|
+
* `_registerOwnerLifecycle`.
|
|
264
|
+
*/
|
|
232
265
|
class FsFormBaseDirective {
|
|
233
266
|
confirmTabs = true;
|
|
234
|
-
|
|
267
|
+
confirm = true;
|
|
268
|
+
confirmDialog = true;
|
|
269
|
+
confirmDrawer = true;
|
|
270
|
+
confirmBrowser = true;
|
|
271
|
+
shortcuts = true; // Ctrl + s
|
|
272
|
+
deactivationGuard = true;
|
|
235
273
|
_ngZone = inject(NgZone);
|
|
236
274
|
_cdRef = inject(ChangeDetectorRef);
|
|
275
|
+
_element = inject(ElementRef);
|
|
276
|
+
_form = inject(FsForm);
|
|
277
|
+
_route = inject(ActivatedRoute);
|
|
278
|
+
_dialogRef = inject(MatDialogRef, { optional: true });
|
|
279
|
+
_drawerRef = inject(DrawerRef, { optional: true });
|
|
280
|
+
_dialogBackdropEscape = false;
|
|
281
|
+
_activatedRouteConfig;
|
|
237
282
|
_tabGroups = new QueryList();
|
|
238
283
|
_buttons = new QueryList();
|
|
239
284
|
_activeSubmitButton;
|
|
285
|
+
_confirmTabGroups = new WeakSet();
|
|
240
286
|
_destroy$ = new Subject();
|
|
287
|
+
/**
|
|
288
|
+
* The dialog this owner lives in, or null outside one. Used to keep a form set
|
|
289
|
+
* from spanning a dialog boundary - see `withinSameDialog`.
|
|
290
|
+
*/
|
|
291
|
+
get dialogRef() {
|
|
292
|
+
return this._dialogRef;
|
|
293
|
+
}
|
|
294
|
+
ngOnInit() {
|
|
295
|
+
this._registerOwnerLifecycle();
|
|
296
|
+
}
|
|
241
297
|
ngAfterContentInit() {
|
|
242
|
-
this.
|
|
298
|
+
this._registerOwnerContentLifecycle();
|
|
243
299
|
}
|
|
244
300
|
get destroy$() {
|
|
245
301
|
return this._destroy$.asObservable();
|
|
246
302
|
}
|
|
247
303
|
ngOnDestroy() {
|
|
304
|
+
this._cleanupCanDeactivate();
|
|
248
305
|
this._destroy$.next(null);
|
|
249
306
|
this._destroy$.complete();
|
|
250
307
|
}
|
|
@@ -257,6 +314,16 @@ class FsFormBaseDirective {
|
|
|
257
314
|
set activeSubmitButton(button) {
|
|
258
315
|
this._activeSubmitButton = button;
|
|
259
316
|
}
|
|
317
|
+
/**
|
|
318
|
+
* Bring a tab group into this owner's unsaved-changes confirm.
|
|
319
|
+
*
|
|
320
|
+
* `_tabGroups` only reaches groups in this owner's projected content, so a group
|
|
321
|
+
* declared inside a child component's own template has to announce itself.
|
|
322
|
+
* `fsFormConfirmTabs` calls this - consumers never call it directly.
|
|
323
|
+
*/
|
|
324
|
+
registerTabGroup(tabGroup) {
|
|
325
|
+
this._registerConfirmTabGroup(tabGroup);
|
|
326
|
+
}
|
|
260
327
|
addButton(button) {
|
|
261
328
|
this._buttons
|
|
262
329
|
.reset([
|
|
@@ -277,6 +344,160 @@ class FsFormBaseDirective {
|
|
|
277
344
|
]);
|
|
278
345
|
this._buttons.notifyOnChanges();
|
|
279
346
|
}
|
|
347
|
+
/**
|
|
348
|
+
* Everything here acts on the form set as a whole, so exactly one thing may
|
|
349
|
+
* register it. A form linked into a parent defers to that parent; every form
|
|
350
|
+
* inside a container defers to the container. Registering twice would put two
|
|
351
|
+
* confirms on the same backdrop click and two handlers on the same Ctrl+S.
|
|
352
|
+
*/
|
|
353
|
+
_registerOwnerLifecycle() {
|
|
354
|
+
if (this.deactivationGuard) {
|
|
355
|
+
this._registerCanDeactivateGuard();
|
|
356
|
+
}
|
|
357
|
+
this._registerConfirmDialogBackdropEscape();
|
|
358
|
+
this._listenHotKeys();
|
|
359
|
+
this._listenWindowClose();
|
|
360
|
+
}
|
|
361
|
+
/** The half of the same ownership that needs projected content resolved. */
|
|
362
|
+
_registerOwnerContentLifecycle() {
|
|
363
|
+
this._registerConfirmTabs();
|
|
364
|
+
this._registerConfirmDrawerClose();
|
|
365
|
+
this._registerDrawerClose();
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Whether the element is inside what this owner covers. Overridden by the
|
|
369
|
+
* container, whose own element is a poor proxy: it may be an `<ng-container>`,
|
|
370
|
+
* which is a comment node and contains nothing.
|
|
371
|
+
*/
|
|
372
|
+
_containsElement(el) {
|
|
373
|
+
return this._nodeContains(this._element.nativeElement, el);
|
|
374
|
+
}
|
|
375
|
+
_nodeContains(root, el) {
|
|
376
|
+
if (!root || !el) {
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
if (el === root) {
|
|
380
|
+
return true;
|
|
381
|
+
}
|
|
382
|
+
return el.parentElement ? this._nodeContains(root, el.parentElement) : false;
|
|
383
|
+
}
|
|
384
|
+
_formClose() {
|
|
385
|
+
if (this.confirm && this.confirmDialog) {
|
|
386
|
+
this.triggerConfirm()
|
|
387
|
+
.pipe(filter((result) => confirmResultContinue(result)), switchMap((result) => {
|
|
388
|
+
return result === ConfirmResult.NoChanges || result === ConfirmResult.Discard
|
|
389
|
+
? of(null)
|
|
390
|
+
: this.submitted$;
|
|
391
|
+
}), takeUntil(this._destroy$))
|
|
392
|
+
.subscribe((result) => {
|
|
393
|
+
this._dialogRef.close(result?.response);
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
this._dialogRef.close(null);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
_listenWindowClose() {
|
|
401
|
+
fromEvent(window, 'beforeunload')
|
|
402
|
+
.pipe(takeUntil(this._destroy$))
|
|
403
|
+
.subscribe((event) => {
|
|
404
|
+
if (this.confirm && this.confirmBrowser && this.dirtyLinked) {
|
|
405
|
+
event.returnValue = false;
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
_activeDialog(el, dialog) {
|
|
410
|
+
return this._nodeContains(dialog, el);
|
|
411
|
+
}
|
|
412
|
+
_listenHotKeys() {
|
|
413
|
+
this._ngZone.runOutsideAngular(() => {
|
|
414
|
+
fromEvent(document, 'keydown')
|
|
415
|
+
.pipe(takeUntil(this._destroy$))
|
|
416
|
+
.subscribe((event) => {
|
|
417
|
+
if (this._dialogBackdropEscape && event.code === 'Escape') {
|
|
418
|
+
const cdkOverlayPane = Array
|
|
419
|
+
.from(document.querySelectorAll('.cdk-overlay-pane')).pop();
|
|
420
|
+
const activeDialog = this
|
|
421
|
+
._activeDialog(document.getElementById(this._dialogRef.id), cdkOverlayPane);
|
|
422
|
+
if (activeDialog) {
|
|
423
|
+
this._ngZone.run(() => {
|
|
424
|
+
this._formClose();
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if ((event.ctrlKey || event.metaKey) && event.code === 'KeyS') {
|
|
429
|
+
event.preventDefault();
|
|
430
|
+
if (this.shortcuts) {
|
|
431
|
+
if (this._containsElement(document.activeElement)) {
|
|
432
|
+
this._ngZone.run(() => {
|
|
433
|
+
this.triggerSubmit();
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
_registerConfirmDialogBackdropEscape() {
|
|
442
|
+
if (this._dialogRef) {
|
|
443
|
+
this._dialogBackdropEscape = !this._dialogRef?.disableClose;
|
|
444
|
+
if (this._dialogBackdropEscape) {
|
|
445
|
+
this._dialogRef.backdropClick()
|
|
446
|
+
.pipe(takeUntil(this._destroy$))
|
|
447
|
+
.subscribe(() => {
|
|
448
|
+
this._formClose();
|
|
449
|
+
});
|
|
450
|
+
this._destroy$
|
|
451
|
+
.subscribe(() => {
|
|
452
|
+
this._dialogRef.disableClose = false;
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
_registerDrawerClose() {
|
|
458
|
+
if (this._drawerRef) {
|
|
459
|
+
this._drawerRef.closeStart$
|
|
460
|
+
.pipe(takeUntil(this._destroy$))
|
|
461
|
+
.subscribe((subscriber) => {
|
|
462
|
+
if (this.submitting) {
|
|
463
|
+
this.status$
|
|
464
|
+
.pipe(filter((status) => status === FormStatus.Success || status === FormStatus.Error), takeUntil(this._destroy$))
|
|
465
|
+
.subscribe((status) => {
|
|
466
|
+
if (status === FormStatus.Success) {
|
|
467
|
+
subscriber.next(null);
|
|
468
|
+
subscriber.complete();
|
|
469
|
+
}
|
|
470
|
+
else {
|
|
471
|
+
subscriber.error();
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
else {
|
|
476
|
+
subscriber.next(null);
|
|
477
|
+
subscriber.complete();
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
_registerConfirmDrawerClose() {
|
|
483
|
+
if (this._drawerRef) {
|
|
484
|
+
this._drawerRef.closeStart$
|
|
485
|
+
.pipe(switchMap((subscriber) => {
|
|
486
|
+
return iif(() => !!this.confirm && this.confirmDrawer, this.triggerConfirm()
|
|
487
|
+
.pipe(map((result) => confirmResultContinue(result)), tap((result) => {
|
|
488
|
+
if (result) {
|
|
489
|
+
subscriber.next(null);
|
|
490
|
+
subscriber.complete();
|
|
491
|
+
}
|
|
492
|
+
})), defer(() => {
|
|
493
|
+
subscriber.next(null);
|
|
494
|
+
subscriber.complete();
|
|
495
|
+
return of(null);
|
|
496
|
+
}));
|
|
497
|
+
}), takeUntil(this._destroy$))
|
|
498
|
+
.subscribe();
|
|
499
|
+
}
|
|
500
|
+
}
|
|
280
501
|
_registerConfirmTabs() {
|
|
281
502
|
this._registerConfirmTabGroups(this._tabGroups.toArray());
|
|
282
503
|
this._tabGroups.changes
|
|
@@ -291,6 +512,15 @@ class FsFormBaseDirective {
|
|
|
291
512
|
});
|
|
292
513
|
}
|
|
293
514
|
_registerConfirmTabGroup(tabGroup) {
|
|
515
|
+
// `_tabGroups.changes` re-emits the whole list, not just what arrived, and it
|
|
516
|
+
// emits every time a lazy tab body mounts a nested group. Groups already
|
|
517
|
+
// wired therefore come back around - so without this the outer group
|
|
518
|
+
// accumulates one subscription per emission, and a single click on it opens
|
|
519
|
+
// that many stacked confirm dialogs.
|
|
520
|
+
if (this._confirmTabGroups.has(tabGroup)) {
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
this._confirmTabGroups.add(tabGroup);
|
|
294
524
|
const confirmTabGroup = tabGroup;
|
|
295
525
|
if (!confirmTabGroup._originalHandleClick) {
|
|
296
526
|
confirmTabGroup._originalHandleClick = tabGroup._handleClick;
|
|
@@ -319,71 +549,215 @@ class FsFormBaseDirective {
|
|
|
319
549
|
}), takeUntil(this._destroy$))
|
|
320
550
|
.subscribe();
|
|
321
551
|
}
|
|
552
|
+
_registerCanDeactivateGuard() {
|
|
553
|
+
this._activatedRouteConfig = getActiveRoute(this._route)?.routeConfig;
|
|
554
|
+
if (!this._activatedRouteConfig) {
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
this._form.registerFormDirective(this._activatedRouteConfig.component, this);
|
|
558
|
+
if (!Array.isArray(this._activatedRouteConfig.canDeactivate)) {
|
|
559
|
+
this._activatedRouteConfig.canDeactivate = [];
|
|
560
|
+
}
|
|
561
|
+
if (this._activatedRouteConfig.canDeactivate.indexOf(FormDeactivateGuard) === -1) {
|
|
562
|
+
this._activatedRouteConfig.canDeactivate.push(FormDeactivateGuard);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
_cleanupCanDeactivate() {
|
|
566
|
+
if (!this._activatedRouteConfig) {
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
const guardIndex = this._activatedRouteConfig.canDeactivate.indexOf(FormDeactivateGuard);
|
|
570
|
+
this._activatedRouteConfig.canDeactivate.splice(guardIndex, 1);
|
|
571
|
+
this._form.removeFormDirective(this._activatedRouteConfig.component);
|
|
572
|
+
}
|
|
322
573
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.7", ngImport: i0, type: FsFormBaseDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
323
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.7", type: FsFormBaseDirective, inputs: { confirmTabs: "confirmTabs",
|
|
574
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.7", type: FsFormBaseDirective, inputs: { confirmTabs: "confirmTabs", confirm: "confirm", confirmDialog: "confirmDialog", confirmDrawer: "confirmDrawer", confirmBrowser: "confirmBrowser", shortcuts: "shortcuts", deactivationGuard: "deactivationGuard" }, queries: [{ propertyName: "_tabGroups", predicate: MatTabGroup, descendants: true }], ngImport: i0 });
|
|
324
575
|
}
|
|
325
576
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.7", ngImport: i0, type: FsFormBaseDirective, decorators: [{
|
|
326
577
|
type: Directive
|
|
327
578
|
}], propDecorators: { confirmTabs: [{
|
|
328
579
|
type: Input
|
|
329
|
-
}],
|
|
580
|
+
}], confirm: [{
|
|
581
|
+
type: Input
|
|
582
|
+
}], confirmDialog: [{
|
|
583
|
+
type: Input
|
|
584
|
+
}], confirmDrawer: [{
|
|
585
|
+
type: Input
|
|
586
|
+
}], confirmBrowser: [{
|
|
587
|
+
type: Input
|
|
588
|
+
}], shortcuts: [{
|
|
589
|
+
type: Input
|
|
590
|
+
}], deactivationGuard: [{
|
|
330
591
|
type: Input
|
|
331
592
|
}], _tabGroups: [{
|
|
332
593
|
type: ContentChildren,
|
|
333
594
|
args: [MatTabGroup, { descendants: true }]
|
|
334
595
|
}] } });
|
|
335
596
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
597
|
+
/**
|
|
598
|
+
* Bounds a set of forms without being one.
|
|
599
|
+
*
|
|
600
|
+
* An `fsForm` can only reach the forms nested inside it through DI, which forces
|
|
601
|
+
* a wrapper carrying `ngForm` around anything that needs to see them - and that
|
|
602
|
+
* wrapper then owns a real, empty `NgForm` whose control namespace every field
|
|
603
|
+
* below it falls into. A container has no `ngForm` and no controls. It only
|
|
604
|
+
* watches: forms register themselves as they mount, buttons resolve to it, and
|
|
605
|
+
* `fs-form-dialog-actions` reads Save-vs-Done off it. Put it on whatever element
|
|
606
|
+
* already frames the layout - or on an `<ng-container>` for no element at all.
|
|
607
|
+
*
|
|
608
|
+
* Forms in a container are siblings rather than nested, so on registration each
|
|
609
|
+
* one is linked into the first. That reproduces exactly the structure DI nesting
|
|
610
|
+
* builds, which is why validation, dirty state, the unsaved-changes confirm and
|
|
611
|
+
* the submit chain all span a container without any of that machinery knowing
|
|
612
|
+
* containers exist.
|
|
613
|
+
*
|
|
614
|
+
* A form marked `[link]="false"` never registers, so an instant-save panel stays
|
|
615
|
+
* out of the set the same way it does when nested.
|
|
616
|
+
*/
|
|
617
|
+
class FsFormContainerDirective extends FsFormBaseDirective {
|
|
618
|
+
_forms = [];
|
|
619
|
+
_formsChange$ = new Subject();
|
|
620
|
+
/** The forms registered into this container, in registration order. */
|
|
621
|
+
get forms() {
|
|
622
|
+
return [...this._forms];
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* The form the rest of the set is linked into, or null while the container
|
|
626
|
+
* holds none - an open tab that has nothing to save, say.
|
|
627
|
+
*/
|
|
628
|
+
get rootForm() {
|
|
629
|
+
return this._forms[0] || null;
|
|
630
|
+
}
|
|
631
|
+
/** Emits whenever a form registers or deregisters. */
|
|
632
|
+
get formsChange$() {
|
|
633
|
+
return this._formsChange$.asObservable();
|
|
634
|
+
}
|
|
635
|
+
get linkedForms() {
|
|
636
|
+
return this.rootForm?.linkedForms || [];
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* True when anything in the container has a submit handler - i.e. there is
|
|
640
|
+
* something for a Save button to do. `fs-form-dialog-actions` reads this to
|
|
641
|
+
* swap Save for Done as the mounted tab changes, so a dialog never has to keep
|
|
642
|
+
* a list of which tabs happen to be savable.
|
|
643
|
+
*/
|
|
644
|
+
get submits() {
|
|
645
|
+
return this.linkedForms
|
|
646
|
+
.some((form) => form.hasSubmit);
|
|
647
|
+
}
|
|
648
|
+
get dirtyLinked() {
|
|
649
|
+
return this.rootForm?.dirtyLinked || false;
|
|
343
650
|
}
|
|
344
651
|
get submitting() {
|
|
345
|
-
return this.
|
|
652
|
+
return this.rootForm?.submitting || false;
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Emits whenever the set's dirty state may have moved, including when the set
|
|
656
|
+
* itself changes. Anything rendering off it - the dialog actions - subscribes
|
|
657
|
+
* once and outlives any particular form, so this has to follow the root across
|
|
658
|
+
* tab switches rather than bind to whichever form happened to be first.
|
|
659
|
+
*/
|
|
660
|
+
get dirtyChange$() {
|
|
661
|
+
return this._formsChange$
|
|
662
|
+
.pipe(startWith(null), switchMap(() => (this.rootForm?.dirtyChange$ || EMPTY)
|
|
663
|
+
.pipe(startWith(null))), map(() => undefined));
|
|
664
|
+
}
|
|
665
|
+
// Resolved at subscribe time rather than switch-mapped: a submit in flight has
|
|
666
|
+
// to survive the set changing under it, which is routine when saving unmounts
|
|
667
|
+
// a tab.
|
|
668
|
+
get submitted$() {
|
|
669
|
+
return defer(() => this.rootForm?.submitted$ || EMPTY);
|
|
670
|
+
}
|
|
671
|
+
get status$() {
|
|
672
|
+
return defer(() => this.rootForm?.status$ || of(FormStatus.Valid));
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* Take a form into the container. Called by the form on init - consumers never
|
|
676
|
+
* call it directly.
|
|
677
|
+
*/
|
|
678
|
+
registerForm(form) {
|
|
679
|
+
if (this._forms.indexOf(form) !== -1) {
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
this._forms = [...this._forms, form];
|
|
683
|
+
if (this.rootForm !== form) {
|
|
684
|
+
form.linkTo(this.rootForm);
|
|
685
|
+
}
|
|
686
|
+
this._formsChange$.next();
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Material attaches an incoming tab body before it detaches the outgoing one,
|
|
690
|
+
* so losing the root is a routine reshuffle rather than teardown. Re-elect and
|
|
691
|
+
* re-link the survivors instead of leaving them pointing at a dead root, and
|
|
692
|
+
* only let go of forms actually registered - clearing unconditionally would
|
|
693
|
+
* drop the form that just arrived.
|
|
694
|
+
*/
|
|
695
|
+
deregisterForm(form) {
|
|
696
|
+
if (form && this._forms.indexOf(form) === -1) {
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
const removing = form ? [form] : [...this._forms];
|
|
700
|
+
const rootRemoved = removing.indexOf(this.rootForm) !== -1;
|
|
701
|
+
removing.forEach((item) => item.unlinkFrom());
|
|
702
|
+
this._forms = this._forms
|
|
703
|
+
.filter((item) => removing.indexOf(item) === -1);
|
|
704
|
+
if (rootRemoved) {
|
|
705
|
+
const root = this.rootForm;
|
|
706
|
+
this._forms
|
|
707
|
+
.filter((item) => item !== root)
|
|
708
|
+
.forEach((item) => {
|
|
709
|
+
item.unlinkFrom();
|
|
710
|
+
item.linkTo(root);
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
this._formsChange$.next();
|
|
714
|
+
}
|
|
715
|
+
triggerSubmit() {
|
|
716
|
+
this.rootForm?.triggerSubmit();
|
|
346
717
|
}
|
|
347
|
-
|
|
348
|
-
return this.
|
|
718
|
+
triggerConfirm() {
|
|
719
|
+
return this.rootForm
|
|
720
|
+
? this.rootForm.triggerConfirm()
|
|
721
|
+
: of(ConfirmResult.NoChanges);
|
|
349
722
|
}
|
|
350
|
-
|
|
351
|
-
this.
|
|
723
|
+
submit$(event) {
|
|
724
|
+
return this.rootForm
|
|
725
|
+
? this.rootForm.submit$(event)
|
|
726
|
+
: of(null);
|
|
352
727
|
}
|
|
353
|
-
|
|
354
|
-
this.
|
|
728
|
+
reset() {
|
|
729
|
+
this.rootForm?.reset();
|
|
355
730
|
}
|
|
356
|
-
|
|
357
|
-
|
|
731
|
+
validate() {
|
|
732
|
+
this.rootForm?.validate();
|
|
733
|
+
}
|
|
734
|
+
// The container's own element is a poor proxy for what it covers: it is
|
|
735
|
+
// typically layout chrome wrapping far more than the forms, and on an
|
|
736
|
+
// `<ng-container>` it is a comment node containing nothing at all. Scope to the
|
|
737
|
+
// registered forms instead.
|
|
738
|
+
_containsElement(el) {
|
|
739
|
+
return this._forms
|
|
740
|
+
.some((form) => this._nodeContains(form.element, el));
|
|
741
|
+
}
|
|
742
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.7", ngImport: i0, type: FsFormContainerDirective, deps: null, target: i0.ɵɵFactoryTarget.Directive });
|
|
743
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.7", type: FsFormContainerDirective, isStandalone: true, selector: "fs-form-container,[fsFormContainer]", exportAs: ["fsFormContainer"], usesInheritance: true, ngImport: i0 });
|
|
358
744
|
}
|
|
359
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.7", ngImport: i0, type:
|
|
745
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.7", ngImport: i0, type: FsFormContainerDirective, decorators: [{
|
|
360
746
|
type: Directive,
|
|
361
747
|
args: [{
|
|
362
|
-
selector: '[
|
|
363
|
-
exportAs: '
|
|
748
|
+
selector: 'fs-form-container,[fsFormContainer]',
|
|
749
|
+
exportAs: 'fsFormContainer',
|
|
364
750
|
standalone: true,
|
|
365
751
|
}]
|
|
366
752
|
}] });
|
|
367
753
|
|
|
368
754
|
class FsFormDirective extends FsFormBaseDirective {
|
|
369
755
|
ngForm = inject(NgForm);
|
|
370
|
-
_form = inject(FsForm);
|
|
371
|
-
_element = inject(ElementRef);
|
|
372
|
-
_message = inject(FsMessage);
|
|
373
|
-
_formGroup = inject(FsFormGroupDirective, { optional: true });
|
|
374
|
-
_dialogRef = inject(MatDialogRef, { optional: true });
|
|
375
|
-
_drawerRef = inject(DrawerRef, { optional: true });
|
|
376
|
-
_route = inject(ActivatedRoute);
|
|
377
756
|
wrapperSelector = '.fs-form-wrapper,.mat-mdc-form-field';
|
|
378
757
|
messageSelector = '.fs-form-message,.mat-mdc-form-field-subscript-wrapper';
|
|
379
758
|
hintSelector = '.fs-form-hint,.mat-mdc-form-field-hint-wrapper';
|
|
380
759
|
labelSelector = '.fs-form-label,.mdc-floating-label>mat-label';
|
|
381
760
|
autocomplete = false;
|
|
382
|
-
shortcuts = true; // Ctrl + s
|
|
383
|
-
confirm = true;
|
|
384
|
-
confirmDialog = true;
|
|
385
|
-
confirmDrawer = true;
|
|
386
|
-
confirmBrowser = true;
|
|
387
761
|
dirtySubmitButton = true;
|
|
388
762
|
set submit(submit$) {
|
|
389
763
|
this._submit$ = submit$;
|
|
@@ -393,7 +767,21 @@ class FsFormDirective extends FsFormBaseDirective {
|
|
|
393
767
|
}
|
|
394
768
|
successDelay = 0;
|
|
395
769
|
errorDelay = 1000;
|
|
396
|
-
|
|
770
|
+
/**
|
|
771
|
+
* Whether this form joins the set around it - the `fsForm` it is nested inside,
|
|
772
|
+
* or the `fsFormContainer` enclosing it.
|
|
773
|
+
*
|
|
774
|
+
* Forms in a set behave as one submittable unit: the outermost form is what the
|
|
775
|
+
* submit buttons drive, and validation, dirty state, the unsaved-changes
|
|
776
|
+
* confirm and the pristine reset all span the set. That is what lets a form
|
|
777
|
+
* wrap only one tab's fields while the dialog footer stays outside it.
|
|
778
|
+
*
|
|
779
|
+
* Set `[link]="false"` for a panel that owns its own saving - an instant-save
|
|
780
|
+
* side panel, say. It keeps every fsForm feature for itself, but its dirty
|
|
781
|
+
* state never enables the outer Save button, its validation never blocks the
|
|
782
|
+
* outer submit, and the outer submit never touches it.
|
|
783
|
+
*/
|
|
784
|
+
link = true;
|
|
397
785
|
submitEvent = new EventEmitter();
|
|
398
786
|
invalid = new EventEmitter();
|
|
399
787
|
valid = new EventEmitter();
|
|
@@ -402,11 +790,79 @@ class FsFormDirective extends FsFormBaseDirective {
|
|
|
402
790
|
cleared = new EventEmitter();
|
|
403
791
|
fsFormClass = true;
|
|
404
792
|
_registerControl;
|
|
405
|
-
_dialogBackdropEscape = false;
|
|
406
793
|
_snapshot = {};
|
|
407
|
-
_activatedRouteConfig;
|
|
408
794
|
_status$ = new BehaviorSubject(FormStatus.Valid);
|
|
409
795
|
_submit$ = null;
|
|
796
|
+
_linkedForms = new Set();
|
|
797
|
+
_containerParent = null;
|
|
798
|
+
_dirtyChange$ = new Subject();
|
|
799
|
+
_message = inject(FsMessage);
|
|
800
|
+
// Both lookups walk the element injector, which reaches straight through a
|
|
801
|
+
// dialog opened with `viewContainerRef` into whatever opened it. Anything
|
|
802
|
+
// belonging to a different dialog is discarded, so a set never spans two.
|
|
803
|
+
_container = this._ownDialogOnly(inject(FsFormContainerDirective, { optional: true }));
|
|
804
|
+
_ancestorForm = this
|
|
805
|
+
._ownDialogOnly(inject(FsFormDirective, { optional: true, skipSelf: true }));
|
|
806
|
+
/**
|
|
807
|
+
* Emits whenever the set's dirty state may have moved. Anything outside the
|
|
808
|
+
* form that renders off it - the dialog actions, say - can't watch
|
|
809
|
+
* `ngForm.valueChanges` on its own, because the edits happen in a nested form's
|
|
810
|
+
* NgForm, not this one's.
|
|
811
|
+
*/
|
|
812
|
+
get dirtyChange$() {
|
|
813
|
+
return this._dirtyChange$.asObservable();
|
|
814
|
+
}
|
|
815
|
+
get submitted$() {
|
|
816
|
+
return this.submitted.asObservable();
|
|
817
|
+
}
|
|
818
|
+
get status$() {
|
|
819
|
+
return this._status$.asObservable();
|
|
820
|
+
}
|
|
821
|
+
get element() {
|
|
822
|
+
return this._element.nativeElement;
|
|
823
|
+
}
|
|
824
|
+
/** True when a Save button driving this form would have something to run. */
|
|
825
|
+
get hasSubmit() {
|
|
826
|
+
return !!this._submit$;
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* The form this one is linked into, or null when it is a root - either because
|
|
830
|
+
* nothing encloses it or because `[link]="false"` cut it loose. A container
|
|
831
|
+
* supplies the parent when the forms are siblings rather than nested.
|
|
832
|
+
*/
|
|
833
|
+
get parentForm() {
|
|
834
|
+
if (!this.link) {
|
|
835
|
+
return null;
|
|
836
|
+
}
|
|
837
|
+
return this._ancestorForm || this._containerParent;
|
|
838
|
+
}
|
|
839
|
+
/** The outermost form of this set. Submit buttons drive this one. */
|
|
840
|
+
get rootForm() {
|
|
841
|
+
return this.parentForm?.rootForm || this;
|
|
842
|
+
}
|
|
843
|
+
/** This form plus every form linked into it, at any depth. */
|
|
844
|
+
get linkedForms() {
|
|
845
|
+
return [
|
|
846
|
+
this,
|
|
847
|
+
...[...this._linkedForms]
|
|
848
|
+
.reduce((forms, form) => [...forms, ...form.linkedForms], []),
|
|
849
|
+
];
|
|
850
|
+
}
|
|
851
|
+
/** True when this form or anything linked into it has unsaved changes. */
|
|
852
|
+
get dirtyLinked() {
|
|
853
|
+
return this.linkedForms
|
|
854
|
+
.some((form) => form.ngForm.dirty);
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* True when this form or anything linked into it has a submit handler - i.e.
|
|
858
|
+
* there is something for a Save button to do. A dialog footer can bind to this
|
|
859
|
+
* to swap Save for Done as the mounted tab changes, instead of hard-coding
|
|
860
|
+
* which tab names happen to be savable.
|
|
861
|
+
*/
|
|
862
|
+
get submits() {
|
|
863
|
+
return this.linkedForms
|
|
864
|
+
.some((form) => form.hasSubmit);
|
|
865
|
+
}
|
|
410
866
|
get submitting() {
|
|
411
867
|
return this._status$.getValue() === FormStatus.Submitting;
|
|
412
868
|
}
|
|
@@ -416,6 +872,15 @@ class FsFormDirective extends FsFormBaseDirective {
|
|
|
416
872
|
get completing() {
|
|
417
873
|
return this._status$.getValue() === FormStatus.Completing;
|
|
418
874
|
}
|
|
875
|
+
/**
|
|
876
|
+
* True when nothing above this form speaks for the set it belongs to. Reads the
|
|
877
|
+
* raw injections rather than `parentForm`, so an unlinked panel inside a dialog
|
|
878
|
+
* still defers to whatever encloses it instead of registering a second backdrop
|
|
879
|
+
* confirm on the same dialog.
|
|
880
|
+
*/
|
|
881
|
+
get _owns() {
|
|
882
|
+
return !this._ancestorForm && !this._container;
|
|
883
|
+
}
|
|
419
884
|
get _submitEvent() {
|
|
420
885
|
return {
|
|
421
886
|
ngForm: this.ngForm,
|
|
@@ -430,8 +895,17 @@ class FsFormDirective extends FsFormBaseDirective {
|
|
|
430
895
|
submitter: this._submitter,
|
|
431
896
|
response: null,
|
|
432
897
|
};
|
|
433
|
-
|
|
434
|
-
|
|
898
|
+
// Every form in the set gets to save. A form that only frames the layout - a
|
|
899
|
+
// dialog wrapper around a tab group - carries no submit of its own and simply
|
|
900
|
+
// defers to the tab form linked into it. When several are linked they run in
|
|
901
|
+
// nesting order, so an outer save lands before an inner one, and the response
|
|
902
|
+
// of the last is what the submit resolves with.
|
|
903
|
+
const submits = this.linkedForms
|
|
904
|
+
.filter((form) => form.hasSubmit)
|
|
905
|
+
.map((form) => form._submit$(this._submitEvent));
|
|
906
|
+
const submit$ = submits.length ?
|
|
907
|
+
concat(...submits).pipe(toArray(), map((responses) => responses[responses.length - 1])) :
|
|
908
|
+
of(submittedEvent);
|
|
435
909
|
return submit$
|
|
436
910
|
.pipe(map((response) => {
|
|
437
911
|
submittedEvent.response = response;
|
|
@@ -451,22 +925,67 @@ class FsFormDirective extends FsFormBaseDirective {
|
|
|
451
925
|
return throwError(() => new Error('Form validation error'));
|
|
452
926
|
}
|
|
453
927
|
get _submitter() {
|
|
454
|
-
return this.
|
|
928
|
+
return this._getOwner().activeSubmitButton?.name;
|
|
455
929
|
}
|
|
456
930
|
ngOnInit() {
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
931
|
+
// A form already nested inside another links to that one, which is itself in
|
|
932
|
+
// the container - so it joins the set transitively and must not also register
|
|
933
|
+
// as a container sibling.
|
|
934
|
+
if (this.link) {
|
|
935
|
+
if (this._ancestorForm) {
|
|
936
|
+
this._ancestorForm.linkForm(this);
|
|
937
|
+
}
|
|
938
|
+
else {
|
|
939
|
+
this._container?.registerForm(this);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
// The route guard, dialog backdrop, Ctrl+S and browser close prompt all act
|
|
943
|
+
// on the set as a whole, so exactly one thing registers them: a linked child
|
|
944
|
+
// defers to its parent, and every form in a container defers to the container.
|
|
945
|
+
if (this._owns) {
|
|
946
|
+
super.ngOnInit();
|
|
460
947
|
}
|
|
461
|
-
this._registerConfirmDialogBackdropEscape();
|
|
462
|
-
this._listenHotKeys();
|
|
463
|
-
this._listenWindowClose();
|
|
464
948
|
this._listenSubmit();
|
|
465
949
|
this._listenFormStatus();
|
|
466
950
|
if (!this.autocomplete) {
|
|
467
951
|
this._registerAutocomplete();
|
|
468
952
|
}
|
|
469
953
|
}
|
|
954
|
+
/**
|
|
955
|
+
* Take a nested form into this one's set. Called by the child on init, or by a
|
|
956
|
+
* container linking its siblings - consumers never call it directly.
|
|
957
|
+
*/
|
|
958
|
+
linkForm(form) {
|
|
959
|
+
this._linkedForms.add(form);
|
|
960
|
+
Promise.resolve()
|
|
961
|
+
.then(() => {
|
|
962
|
+
this.rootForm._updateDirtySubmitButtons();
|
|
963
|
+
this._cdRef.markForCheck();
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
unlinkForm(form) {
|
|
967
|
+
this._linkedForms.delete(form);
|
|
968
|
+
Promise.resolve()
|
|
969
|
+
.then(() => {
|
|
970
|
+
this.rootForm._updateDirtySubmitButtons();
|
|
971
|
+
this._cdRef.markForCheck();
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
/**
|
|
975
|
+
* Adopt a parent that DI could not supply. Only a container calls this, to link
|
|
976
|
+
* forms that are siblings in the template into one set.
|
|
977
|
+
*/
|
|
978
|
+
linkTo(form) {
|
|
979
|
+
if (!form || form === this) {
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
this._containerParent = form;
|
|
983
|
+
form.linkForm(this);
|
|
984
|
+
}
|
|
985
|
+
unlinkFrom() {
|
|
986
|
+
this._containerParent?.unlinkForm(this);
|
|
987
|
+
this._containerParent = null;
|
|
988
|
+
}
|
|
470
989
|
ngOnChanges(changes) {
|
|
471
990
|
if (changes.confirm) {
|
|
472
991
|
this._updateDirtySubmitButtons();
|
|
@@ -487,16 +1006,21 @@ class FsFormDirective extends FsFormBaseDirective {
|
|
|
487
1006
|
});
|
|
488
1007
|
}
|
|
489
1008
|
ngAfterContentInit() {
|
|
490
|
-
|
|
1009
|
+
// Tab and drawer confirmation belong to whoever owns the set. A linked child
|
|
1010
|
+
// shares the same tab groups and the same drawer, so letting it register too
|
|
1011
|
+
// would put a second confirm on the same click.
|
|
1012
|
+
if (this._owns) {
|
|
1013
|
+
super.ngAfterContentInit();
|
|
1014
|
+
}
|
|
491
1015
|
this._registerConfirm();
|
|
492
|
-
this._registerConfirmDrawerClose();
|
|
493
|
-
this._registerDrawerClose();
|
|
494
1016
|
this._registerDirtySubmitButton();
|
|
495
1017
|
}
|
|
496
1018
|
ngOnDestroy() {
|
|
497
|
-
|
|
1019
|
+
// Deregister first: it clears the container-supplied parent, so the unlink
|
|
1020
|
+
// below falls through to the DI ancestor rather than repeating the same work.
|
|
1021
|
+
this._container?.deregisterForm(this);
|
|
1022
|
+
this.parentForm?.unlinkForm(this);
|
|
498
1023
|
super.ngOnDestroy();
|
|
499
|
-
this._cleanupCanDeactivate();
|
|
500
1024
|
}
|
|
501
1025
|
createSnapshot() {
|
|
502
1026
|
this._snapshot = this.ngForm.value;
|
|
@@ -505,11 +1029,14 @@ class FsFormDirective extends FsFormBaseDirective {
|
|
|
505
1029
|
return this._snapshot || {};
|
|
506
1030
|
}
|
|
507
1031
|
reset() {
|
|
508
|
-
this.
|
|
509
|
-
|
|
510
|
-
.
|
|
511
|
-
|
|
512
|
-
|
|
1032
|
+
this.linkedForms
|
|
1033
|
+
.forEach((form) => {
|
|
1034
|
+
form.ngForm.resetForm();
|
|
1035
|
+
Object.keys(form.ngForm.controls)
|
|
1036
|
+
.forEach((name) => {
|
|
1037
|
+
const control = form.ngForm.controls[name];
|
|
1038
|
+
control.reset(form._snapshot[name]);
|
|
1039
|
+
});
|
|
513
1040
|
});
|
|
514
1041
|
this.reseted.emit();
|
|
515
1042
|
}
|
|
@@ -539,24 +1066,29 @@ class FsFormDirective extends FsFormBaseDirective {
|
|
|
539
1066
|
}
|
|
540
1067
|
disable() {
|
|
541
1068
|
this.ngForm.control.disable();
|
|
542
|
-
this.
|
|
1069
|
+
this._getOwner()
|
|
543
1070
|
.buttons.forEach((button) => {
|
|
544
1071
|
button.disable();
|
|
545
1072
|
});
|
|
546
1073
|
}
|
|
547
1074
|
validate() {
|
|
548
|
-
|
|
549
|
-
.forEach((
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
1075
|
+
this.linkedForms
|
|
1076
|
+
.forEach((form) => {
|
|
1077
|
+
Object.values(form.ngForm.controls)
|
|
1078
|
+
.forEach((control) => {
|
|
1079
|
+
control.markAsDirty();
|
|
1080
|
+
control.markAsTouched();
|
|
1081
|
+
control.updateValueAndValidity();
|
|
1082
|
+
});
|
|
553
1083
|
});
|
|
554
1084
|
}
|
|
555
1085
|
submit$(submitEvent) {
|
|
556
1086
|
return of(submitEvent)
|
|
557
1087
|
.pipe(tap(() => this._statusValidating()), tap(() => this.validate()), tap(() => this._statusSubmitting()), tap(() => this._setupActiveSubmitButton()), tap(() => this._disableButtons()), switchMap((data) => this._waitUntilStatusPending()
|
|
558
1088
|
.pipe(map(() => data))), mergeMap(() => {
|
|
559
|
-
|
|
1089
|
+
const invalid = this.linkedForms
|
|
1090
|
+
.some((form) => form.ngForm.status === 'INVALID');
|
|
1091
|
+
if (invalid) {
|
|
560
1092
|
return this._formInvalidState$;
|
|
561
1093
|
}
|
|
562
1094
|
return this._formValidState$;
|
|
@@ -596,99 +1128,32 @@ class FsFormDirective extends FsFormBaseDirective {
|
|
|
596
1128
|
}
|
|
597
1129
|
});
|
|
598
1130
|
}
|
|
599
|
-
_listenWindowClose() {
|
|
600
|
-
fromEvent(window, 'beforeunload')
|
|
601
|
-
.pipe(takeUntil(this.destroy$))
|
|
602
|
-
.subscribe((event) => {
|
|
603
|
-
if (this.confirm && this.confirmBrowser && this.ngForm.dirty) {
|
|
604
|
-
event.returnValue = false;
|
|
605
|
-
}
|
|
606
|
-
});
|
|
607
|
-
}
|
|
608
|
-
_activeDialog(el, dialog) {
|
|
609
|
-
if (el.isSameNode(dialog)) {
|
|
610
|
-
return true;
|
|
611
|
-
}
|
|
612
|
-
else if (el.parentElement) {
|
|
613
|
-
return this._activeDialog(el.parentElement, dialog);
|
|
614
|
-
}
|
|
615
|
-
return false;
|
|
616
|
-
}
|
|
617
|
-
_listenHotKeys() {
|
|
618
|
-
this._ngZone.runOutsideAngular(() => {
|
|
619
|
-
fromEvent(document, 'keydown')
|
|
620
|
-
.pipe(takeUntil(this.destroy$))
|
|
621
|
-
.subscribe((event) => {
|
|
622
|
-
if (this._dialogBackdropEscape && event.code === 'Escape') {
|
|
623
|
-
const cdkOverlayPane = Array
|
|
624
|
-
.from(document.querySelectorAll('.cdk-overlay-pane')).pop();
|
|
625
|
-
const activeDialog = this
|
|
626
|
-
._activeDialog(document.getElementById(this._dialogRef.id), cdkOverlayPane);
|
|
627
|
-
if (activeDialog) {
|
|
628
|
-
this._ngZone.run(() => {
|
|
629
|
-
this._formClose();
|
|
630
|
-
});
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
if ((event.ctrlKey || event.metaKey) && event.code === 'KeyS') {
|
|
634
|
-
event.preventDefault();
|
|
635
|
-
if (this.shortcuts) {
|
|
636
|
-
if (this._elementInForm(document.activeElement)) {
|
|
637
|
-
this.ngForm.ngSubmit.next(null);
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
});
|
|
642
|
-
});
|
|
643
|
-
}
|
|
644
|
-
_formClose() {
|
|
645
|
-
if (this.confirm && this.confirmDialog) {
|
|
646
|
-
this.triggerConfirm()
|
|
647
|
-
.pipe(filter((result) => confirmResultContinue(result)), switchMap((result) => {
|
|
648
|
-
return result === ConfirmResult.NoChanges || result === ConfirmResult.Discard
|
|
649
|
-
? of(null)
|
|
650
|
-
: this.submitted.asObservable();
|
|
651
|
-
}), takeUntil(this.destroy$))
|
|
652
|
-
.subscribe((result) => {
|
|
653
|
-
this._dialogRef.close(result?.response);
|
|
654
|
-
});
|
|
655
|
-
}
|
|
656
|
-
else {
|
|
657
|
-
this._dialogRef.close(null);
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
1131
|
_getActiveSubmitButton() {
|
|
661
|
-
if (this.
|
|
662
|
-
return this.
|
|
1132
|
+
if (this._getOwner().activeSubmitButton) {
|
|
1133
|
+
return this._getOwner().activeSubmitButton;
|
|
663
1134
|
}
|
|
664
|
-
return this.
|
|
1135
|
+
return this._getOwner()
|
|
665
1136
|
.buttons
|
|
666
1137
|
.filter((button) => button.submit)[0];
|
|
667
1138
|
}
|
|
668
|
-
_elementInForm(el) {
|
|
669
|
-
if (el.isSameNode(this._element.nativeElement)) {
|
|
670
|
-
return true;
|
|
671
|
-
}
|
|
672
|
-
else if (el.parentElement) {
|
|
673
|
-
return this._elementInForm(el.parentElement);
|
|
674
|
-
}
|
|
675
|
-
return false;
|
|
676
|
-
}
|
|
677
1139
|
_completeSubmit(success, submitEvent) {
|
|
678
1140
|
if (success) {
|
|
679
|
-
this.
|
|
680
|
-
|
|
1141
|
+
this.linkedForms
|
|
1142
|
+
.forEach((form) => {
|
|
1143
|
+
form.ngForm.control.markAsPristine();
|
|
1144
|
+
form.createSnapshot();
|
|
1145
|
+
});
|
|
681
1146
|
this.submitted.emit(submitEvent);
|
|
682
1147
|
}
|
|
683
1148
|
else {
|
|
684
1149
|
this._resetButtons();
|
|
685
1150
|
}
|
|
686
|
-
if (this.
|
|
1151
|
+
if (this._getOwner().activeSubmitButton) {
|
|
687
1152
|
if (success) {
|
|
688
|
-
this.
|
|
1153
|
+
this._getOwner().activeSubmitButton.success();
|
|
689
1154
|
}
|
|
690
1155
|
else {
|
|
691
|
-
this.
|
|
1156
|
+
this._getOwner().activeSubmitButton.error();
|
|
692
1157
|
}
|
|
693
1158
|
}
|
|
694
1159
|
this._status$.next(FormStatus.Submitted);
|
|
@@ -703,22 +1168,39 @@ class FsFormDirective extends FsFormBaseDirective {
|
|
|
703
1168
|
of(true)
|
|
704
1169
|
.pipe(delay(resetDelay), first(), takeUntil(this.destroy$))
|
|
705
1170
|
.subscribe(() => {
|
|
706
|
-
|
|
1171
|
+
const valid = this.linkedForms
|
|
1172
|
+
.every((form) => form.ngForm.form.status === 'VALID');
|
|
1173
|
+
if (valid) {
|
|
707
1174
|
this._status$.next(FormStatus.Valid);
|
|
708
1175
|
}
|
|
709
1176
|
else {
|
|
710
1177
|
this._status$.next(FormStatus.Invalid);
|
|
711
1178
|
}
|
|
712
1179
|
this._resetButtons();
|
|
713
|
-
this.
|
|
1180
|
+
this._getOwner().activeSubmitButton = null;
|
|
714
1181
|
this._updateDirtySubmitButtons();
|
|
715
1182
|
});
|
|
716
1183
|
}
|
|
717
|
-
|
|
718
|
-
|
|
1184
|
+
/**
|
|
1185
|
+
* Whoever holds the submit buttons for this form. A container when one encloses
|
|
1186
|
+
* it, otherwise the root of its linked set - the button that saves a tab's form
|
|
1187
|
+
* typically sits outside it, down in the dialog footer.
|
|
1188
|
+
*
|
|
1189
|
+
* Only a form that actually joined the container may drive its buttons. DI
|
|
1190
|
+
* hands `_container` to every form inside, including one cut loose with
|
|
1191
|
+
* `[link]="false"`, and an instant-save panel is usually `[confirm]="false"`
|
|
1192
|
+
* too - so without this it would take the `!confirm` branch below and
|
|
1193
|
+
* re-enable the footer's Save button that the linked set had just disabled.
|
|
1194
|
+
*/
|
|
1195
|
+
_getOwner() {
|
|
1196
|
+
return (this.link ? this._container : null) || this.rootForm;
|
|
1197
|
+
}
|
|
1198
|
+
/** Keeps a DI-resolved owner only when it belongs to this form's dialog. */
|
|
1199
|
+
_ownDialogOnly(owner) {
|
|
1200
|
+
return withinSameDialog(owner, this._dialogRef) ? owner : null;
|
|
719
1201
|
}
|
|
720
1202
|
_resetButtons() {
|
|
721
|
-
this.
|
|
1203
|
+
this._getOwner()
|
|
722
1204
|
.buttons.forEach((button) => {
|
|
723
1205
|
button.reset();
|
|
724
1206
|
});
|
|
@@ -741,66 +1223,6 @@ class FsFormDirective extends FsFormBaseDirective {
|
|
|
741
1223
|
}
|
|
742
1224
|
});
|
|
743
1225
|
}
|
|
744
|
-
_registerDrawerClose() {
|
|
745
|
-
if (this._drawerRef) {
|
|
746
|
-
this._drawerRef.closeStart$
|
|
747
|
-
.pipe(takeUntil(this.destroy$))
|
|
748
|
-
.subscribe((subscriber) => {
|
|
749
|
-
if (this.submitting) {
|
|
750
|
-
this._status$
|
|
751
|
-
.pipe(filter((status) => status === FormStatus.Success || status === FormStatus.Error), takeUntil(this.destroy$))
|
|
752
|
-
.subscribe((status) => {
|
|
753
|
-
if (status === FormStatus.Success) {
|
|
754
|
-
subscriber.next(null);
|
|
755
|
-
subscriber.complete();
|
|
756
|
-
}
|
|
757
|
-
else {
|
|
758
|
-
subscriber.error();
|
|
759
|
-
}
|
|
760
|
-
});
|
|
761
|
-
}
|
|
762
|
-
else {
|
|
763
|
-
subscriber.next(null);
|
|
764
|
-
subscriber.complete();
|
|
765
|
-
}
|
|
766
|
-
});
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
_registerConfirmDrawerClose() {
|
|
770
|
-
if (this._drawerRef) {
|
|
771
|
-
this._drawerRef.closeStart$
|
|
772
|
-
.pipe(switchMap((subscriber) => {
|
|
773
|
-
return iif(() => this.confirm && this.confirmDrawer, this.triggerConfirm()
|
|
774
|
-
.pipe(map((result) => confirmResultContinue(result)), tap((result) => {
|
|
775
|
-
if (result) {
|
|
776
|
-
subscriber.next(null);
|
|
777
|
-
subscriber.complete();
|
|
778
|
-
}
|
|
779
|
-
})), defer(() => {
|
|
780
|
-
subscriber.next(null);
|
|
781
|
-
subscriber.complete();
|
|
782
|
-
return of(null);
|
|
783
|
-
}));
|
|
784
|
-
}), takeUntil(this.destroy$))
|
|
785
|
-
.subscribe();
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
_registerConfirmDialogBackdropEscape() {
|
|
789
|
-
if (this._dialogRef) {
|
|
790
|
-
this._dialogBackdropEscape = !this._dialogRef?.disableClose;
|
|
791
|
-
if (this._dialogBackdropEscape) {
|
|
792
|
-
this._dialogRef.backdropClick()
|
|
793
|
-
.pipe(takeUntil(this.destroy$))
|
|
794
|
-
.subscribe(() => {
|
|
795
|
-
this._formClose();
|
|
796
|
-
});
|
|
797
|
-
this.destroy$
|
|
798
|
-
.subscribe(() => {
|
|
799
|
-
this._dialogRef.disableClose = false;
|
|
800
|
-
});
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
1226
|
_registerAutocomplete() {
|
|
805
1227
|
this._registerControl = this.ngForm.form.registerControl.bind(this.ngForm.form);
|
|
806
1228
|
this.ngForm.form.registerControl = (name, control) => {
|
|
@@ -818,12 +1240,14 @@ class FsFormDirective extends FsFormBaseDirective {
|
|
|
818
1240
|
if (!this.ngForm) {
|
|
819
1241
|
return;
|
|
820
1242
|
}
|
|
1243
|
+
// Report up: a linked child's edits have to reach the root, since that is
|
|
1244
|
+
// where the submit buttons live and what decides whether they are enabled.
|
|
821
1245
|
this.ngForm.form.valueChanges
|
|
822
1246
|
.pipe(takeUntil(this.destroy$))
|
|
823
1247
|
.subscribe(() => {
|
|
824
|
-
this._updateDirtySubmitButtons();
|
|
1248
|
+
this.rootForm._updateDirtySubmitButtons();
|
|
825
1249
|
});
|
|
826
|
-
this.
|
|
1250
|
+
this._getOwner()
|
|
827
1251
|
.buttons.changes
|
|
828
1252
|
.pipe(takeUntil(this.destroy$))
|
|
829
1253
|
.subscribe(() => {
|
|
@@ -841,11 +1265,12 @@ class FsFormDirective extends FsFormBaseDirective {
|
|
|
841
1265
|
if (!this.ngForm) {
|
|
842
1266
|
return;
|
|
843
1267
|
}
|
|
844
|
-
this.
|
|
1268
|
+
this._dirtyChange$.next();
|
|
1269
|
+
this._getOwner()
|
|
845
1270
|
.buttons
|
|
846
1271
|
.filter((button) => button.submit)
|
|
847
1272
|
.forEach((submitButton) => {
|
|
848
|
-
if (!this.confirm || !this.dirtySubmitButton || this.
|
|
1273
|
+
if (!this.confirm || !this.dirtySubmitButton || this.dirtyLinked || !submitButton.dirtySubmit) {
|
|
849
1274
|
submitButton.enable();
|
|
850
1275
|
}
|
|
851
1276
|
else {
|
|
@@ -860,22 +1285,26 @@ class FsFormDirective extends FsFormBaseDirective {
|
|
|
860
1285
|
this._status$.next(FormStatus.Validating);
|
|
861
1286
|
}
|
|
862
1287
|
_setupActiveSubmitButton() {
|
|
863
|
-
this.
|
|
1288
|
+
this._getOwner()
|
|
864
1289
|
.activeSubmitButton = this._getActiveSubmitButton();
|
|
865
1290
|
this._resetButtons();
|
|
866
|
-
if (this.
|
|
867
|
-
this.
|
|
1291
|
+
if (this._getOwner().activeSubmitButton) {
|
|
1292
|
+
this._getOwner().activeSubmitButton.process();
|
|
868
1293
|
}
|
|
869
1294
|
}
|
|
870
1295
|
_disableButtons() {
|
|
871
|
-
this.
|
|
1296
|
+
this._getOwner()
|
|
872
1297
|
.buttons.forEach((button) => {
|
|
873
1298
|
button.disable();
|
|
874
1299
|
});
|
|
875
1300
|
}
|
|
876
1301
|
_waitUntilStatusPending() {
|
|
877
|
-
|
|
878
|
-
|
|
1302
|
+
// Async validators can be pending in any linked form, so hold the submit
|
|
1303
|
+
// until every one of them has settled - not just this form's.
|
|
1304
|
+
return combineLatest(this.linkedForms
|
|
1305
|
+
.map((form) => form.ngForm.statusChanges
|
|
1306
|
+
.pipe(startWith(form.ngForm.status))))
|
|
1307
|
+
.pipe(first((states) => states.every((state) => state !== 'PENDING')));
|
|
879
1308
|
}
|
|
880
1309
|
_handleError(e) {
|
|
881
1310
|
console.log('%c Form Submit ', 'color: white; background-color: #D33F49', 'Error occured');
|
|
@@ -889,29 +1318,8 @@ class FsFormDirective extends FsFormBaseDirective {
|
|
|
889
1318
|
console.groupEnd();
|
|
890
1319
|
this._completeSubmit(false, null);
|
|
891
1320
|
}
|
|
892
|
-
_registerCanDeactivateGuard() {
|
|
893
|
-
this._activatedRouteConfig = getActiveRoute(this._route).routeConfig;
|
|
894
|
-
if (!this._activatedRouteConfig) {
|
|
895
|
-
return;
|
|
896
|
-
}
|
|
897
|
-
this._form.registerFormDirective(this._activatedRouteConfig.component, this);
|
|
898
|
-
if (!Array.isArray(this._activatedRouteConfig.canDeactivate)) {
|
|
899
|
-
this._activatedRouteConfig.canDeactivate = [];
|
|
900
|
-
}
|
|
901
|
-
if (this._activatedRouteConfig.canDeactivate.indexOf(FormDeactivateGuard) === -1) {
|
|
902
|
-
this._activatedRouteConfig.canDeactivate.push(FormDeactivateGuard);
|
|
903
|
-
}
|
|
904
|
-
}
|
|
905
|
-
_cleanupCanDeactivate() {
|
|
906
|
-
if (!this._activatedRouteConfig) {
|
|
907
|
-
return;
|
|
908
|
-
}
|
|
909
|
-
const guardIndex = this._activatedRouteConfig.canDeactivate.indexOf(FormDeactivateGuard);
|
|
910
|
-
this._activatedRouteConfig.canDeactivate.splice(guardIndex, 1);
|
|
911
|
-
this._form.removeFormDirective(this._activatedRouteConfig.component);
|
|
912
|
-
}
|
|
913
1321
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.7", ngImport: i0, type: FsFormDirective, deps: null, target: i0.ɵɵFactoryTarget.Directive });
|
|
914
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.7", type: FsFormDirective, isStandalone: true, selector: "[fsForm]", inputs: { wrapperSelector: "wrapperSelector", messageSelector: "messageSelector", hintSelector: "hintSelector", labelSelector: "labelSelector", autocomplete: "autocomplete",
|
|
1322
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.7", type: FsFormDirective, isStandalone: true, selector: "[fsForm]", inputs: { wrapperSelector: "wrapperSelector", messageSelector: "messageSelector", hintSelector: "hintSelector", labelSelector: "labelSelector", autocomplete: "autocomplete", dirtySubmitButton: "dirtySubmitButton", submit: "submit", successDelay: "successDelay", errorDelay: "errorDelay", link: "link" }, outputs: { submitEvent: "fsForm", invalid: "invalid", valid: "valid", submitted: "submitted", reseted: "reseted", cleared: "cleared" }, host: { properties: { "class.fs-form": "this.fsFormClass" } }, exportAs: ["fsForm"], usesInheritance: true, usesOnChanges: true, ngImport: i0 });
|
|
915
1323
|
}
|
|
916
1324
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.7", ngImport: i0, type: FsFormDirective, decorators: [{
|
|
917
1325
|
type: Directive,
|
|
@@ -930,16 +1338,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.7", ngImpor
|
|
|
930
1338
|
type: Input
|
|
931
1339
|
}], autocomplete: [{
|
|
932
1340
|
type: Input
|
|
933
|
-
}], shortcuts: [{
|
|
934
|
-
type: Input
|
|
935
|
-
}], confirm: [{
|
|
936
|
-
type: Input
|
|
937
|
-
}], confirmDialog: [{
|
|
938
|
-
type: Input
|
|
939
|
-
}], confirmDrawer: [{
|
|
940
|
-
type: Input
|
|
941
|
-
}], confirmBrowser: [{
|
|
942
|
-
type: Input
|
|
943
1341
|
}], dirtySubmitButton: [{
|
|
944
1342
|
type: Input
|
|
945
1343
|
}], submit: [{
|
|
@@ -948,7 +1346,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.7", ngImpor
|
|
|
948
1346
|
type: Input
|
|
949
1347
|
}], errorDelay: [{
|
|
950
1348
|
type: Input
|
|
951
|
-
}],
|
|
1349
|
+
}], link: [{
|
|
952
1350
|
type: Input
|
|
953
1351
|
}], submitEvent: [{
|
|
954
1352
|
type: Output,
|
|
@@ -984,20 +1382,26 @@ class FsButtonDirective {
|
|
|
984
1382
|
_formDisabled = false;
|
|
985
1383
|
_previousDisabled = false;
|
|
986
1384
|
_destroy$ = new Subject();
|
|
987
|
-
|
|
988
|
-
|
|
1385
|
+
_dialogRef = inject(MatDialogRef, { optional: true });
|
|
1386
|
+
_container = this._ownDialogOnly(inject(FsFormContainerDirective, { optional: true }));
|
|
1387
|
+
_form = this._ownDialogOnly(inject(FsFormDirective, { optional: true }));
|
|
989
1388
|
_matButton = inject(MatButton, { optional: true, host: true });
|
|
990
1389
|
_elementRef = inject(ElementRef);
|
|
991
1390
|
_cdRef = inject(ChangeDetectorRef);
|
|
992
1391
|
_formBase;
|
|
993
1392
|
ngOnInit() {
|
|
994
1393
|
this.submit = this._elementRef.nativeElement.getAttribute('type') === 'submit';
|
|
995
|
-
// Prefer the enclosing
|
|
1394
|
+
// Prefer the enclosing container over the form: the form's own button
|
|
996
1395
|
// management (dirty-submit tracking, activeSubmitButton, reset) resolves its
|
|
997
|
-
// registry
|
|
998
|
-
//
|
|
999
|
-
//
|
|
1000
|
-
|
|
1396
|
+
// registry the same way, so buttons must register with the same owner.
|
|
1397
|
+
// Registering with the form while the form reads the container leaves the
|
|
1398
|
+
// container's button list empty and the submit button never gets
|
|
1399
|
+
// dirty-disabled.
|
|
1400
|
+
//
|
|
1401
|
+
// Note this resolves the ROOT of the linked set, not the nearest form: the
|
|
1402
|
+
// button that saves a tab's form typically sits outside it, down in the
|
|
1403
|
+
// dialog footer, and drives the whole linked set from there.
|
|
1404
|
+
this._formBase = this._container || this._form?.rootForm;
|
|
1001
1405
|
if (this._formBase) {
|
|
1002
1406
|
this._formBase.addButton(this);
|
|
1003
1407
|
if (this.submit) {
|
|
@@ -1076,6 +1480,15 @@ class FsButtonDirective {
|
|
|
1076
1480
|
this._destroy$.complete();
|
|
1077
1481
|
this._formBase?.removeButton(this);
|
|
1078
1482
|
}
|
|
1483
|
+
/**
|
|
1484
|
+
* Keeps a DI-resolved owner only when it belongs to this button's dialog. A
|
|
1485
|
+
* dialog opened with `viewContainerRef` resolves through to whatever opened
|
|
1486
|
+
* it, so without this the inner dialog's Save would register with - and
|
|
1487
|
+
* submit - the outer dialog's form set.
|
|
1488
|
+
*/
|
|
1489
|
+
_ownDialogOnly(owner) {
|
|
1490
|
+
return withinSameDialog(owner, this._dialogRef) ? owner : null;
|
|
1491
|
+
}
|
|
1079
1492
|
// Effective disabled combines the consumer-controlled submitDisabled input with
|
|
1080
1493
|
// the form's submit-lifecycle state, so form enable/disable never clobbers an
|
|
1081
1494
|
// explicit [submitDisabled] and the two channels stay independent.
|
|
@@ -1176,30 +1589,57 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.7", ngImpor
|
|
|
1176
1589
|
}] } });
|
|
1177
1590
|
|
|
1178
1591
|
class FsFormDialogActionsComponent {
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
save = true;
|
|
1592
|
+
/** Leave unset inside an `fsFormContainer` to follow whether it has anything to save. */
|
|
1593
|
+
save;
|
|
1182
1594
|
create = false;
|
|
1183
1595
|
close = false;
|
|
1184
|
-
|
|
1596
|
+
/** Leave unset inside an `fsFormContainer` to fill in whenever Save does not apply. */
|
|
1597
|
+
done;
|
|
1185
1598
|
closeData = null;
|
|
1186
1599
|
name;
|
|
1187
1600
|
dirty = false;
|
|
1188
1601
|
_destroy$ = new Subject();
|
|
1602
|
+
_dialogRef = inject(MatDialogRef, { optional: true });
|
|
1603
|
+
// A dialog opened with `viewContainerRef` resolves through to whatever opened
|
|
1604
|
+
// it, so an owner from another dialog is discarded - these actions belong to
|
|
1605
|
+
// their own dialog's form set and no one else's.
|
|
1606
|
+
_container = this._ownDialogOnly(inject(FsFormContainerDirective, { optional: true }));
|
|
1607
|
+
_form = this._ownDialogOnly(inject(FsFormDirective, { optional: true }));
|
|
1608
|
+
_cdRef = inject(ChangeDetectorRef);
|
|
1609
|
+
/**
|
|
1610
|
+
* Inside a container these default to whether anything mounted can actually
|
|
1611
|
+
* save, so a footer left as a bare `<fs-form-dialog-actions>` swaps Save for
|
|
1612
|
+
* Done as tabs change without the dialog listing which tabs are savable. An
|
|
1613
|
+
* explicit binding always wins, and outside a container Save stays on as it
|
|
1614
|
+
* always did.
|
|
1615
|
+
*/
|
|
1616
|
+
get showSave() {
|
|
1617
|
+
return this.save ?? (this._container ? this._container.submits : true);
|
|
1618
|
+
}
|
|
1619
|
+
get showDone() {
|
|
1620
|
+
return this.done ?? (this._container ? !this._container.submits && !this.create : false);
|
|
1621
|
+
}
|
|
1189
1622
|
ngOnInit() {
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1623
|
+
const owner = this._container || this._form?.rootForm;
|
|
1624
|
+
if (owner) {
|
|
1625
|
+
// Track the whole set, not one form's own controls: with a form scoped to a
|
|
1626
|
+
// tab's fields, the actions sit outside it in the dialog footer and that
|
|
1627
|
+
// form's NgForm holds nothing of its own. Recomputed on every emission
|
|
1628
|
+
// rather than latched, so a switch to a pristine tab clears it too.
|
|
1629
|
+
owner.dirtyChange$
|
|
1630
|
+
.pipe(takeUntil(this._destroy$))
|
|
1193
1631
|
.subscribe(() => {
|
|
1194
|
-
this.dirty =
|
|
1632
|
+
this.dirty = owner.dirtyLinked;
|
|
1195
1633
|
this._cdRef.markForCheck();
|
|
1196
1634
|
});
|
|
1197
|
-
|
|
1635
|
+
owner.submitted$
|
|
1198
1636
|
.pipe(delay(50), takeUntil(this._destroy$))
|
|
1199
1637
|
.subscribe(() => {
|
|
1200
1638
|
this.dirty = false;
|
|
1201
1639
|
this._cdRef.markForCheck();
|
|
1202
1640
|
});
|
|
1641
|
+
}
|
|
1642
|
+
if (this._form) {
|
|
1203
1643
|
this._form.reseted
|
|
1204
1644
|
.pipe(takeUntil(this._destroy$))
|
|
1205
1645
|
.subscribe(() => {
|
|
@@ -1212,8 +1652,12 @@ class FsFormDialogActionsComponent {
|
|
|
1212
1652
|
this._destroy$.next(null);
|
|
1213
1653
|
this._destroy$.complete();
|
|
1214
1654
|
}
|
|
1655
|
+
/** Keeps a DI-resolved owner only when it belongs to these actions' dialog. */
|
|
1656
|
+
_ownDialogOnly(owner) {
|
|
1657
|
+
return withinSameDialog(owner, this._dialogRef) ? owner : null;
|
|
1658
|
+
}
|
|
1215
1659
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.7", ngImport: i0, type: FsFormDialogActionsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1216
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.7", type: FsFormDialogActionsComponent, isStandalone: true, selector: "fs-form-dialog-actions", inputs: { save: "save", create: "create", close: "close", done: "done", closeData: "closeData", name: "name" }, ngImport: i0, template: "<div\n class=\"form-buttons\"\n [ngClass]=\"{ 'save-create':
|
|
1660
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.7", type: FsFormDialogActionsComponent, isStandalone: true, selector: "fs-form-dialog-actions", inputs: { save: "save", create: "create", close: "close", done: "done", closeData: "closeData", name: "name" }, ngImport: i0, template: "<div\n class=\"form-buttons\"\n [ngClass]=\"{ 'save-create': showSave || create }\">\n @if (showSave || create) {\n <button\n mat-button\n type=\"submit\"\n color=\"primary\"\n [name]=\"name\">\n {{ create ? 'Create' : 'Save' }}\n </button>\n <button\n mat-button\n type=\"button\"\n class=\"cancel-button\"\n [disabled]=\"close && !dirty && !create\"\n [mat-dialog-close]=\"null\">\n Cancel\n </button>\n }\n @if (showDone) {\n <button\n mat-button\n type=\"button\"\n color=\"primary\"\n [mat-dialog-close]=\"null\">\n Done\n </button>\n }\n @if (showSave || create) {\n <ng-template [ngTemplateOutlet]=\"content\"></ng-template>\n }\n @if (close) {\n <button\n mat-button\n type=\"button\"\n class=\"close-button\"\n fsFormDialogClose\n [closeData]=\"closeData\"\n [color]=\"dirty ? 'basic' : 'primary'\">\n Close\n </button>\n }\n @if (!(showSave || create)) {\n <ng-template [ngTemplateOutlet]=\"content\"></ng-template>\n }\n</div>\n<ng-template #content>\n <ng-content></ng-content>\n</ng-template>\n", styles: [":host{width:100%}.form-buttons.save-create .close-button{float:right}\n"], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "directive", type: FsButtonDirective, selector: "[mat-raised-button],[mat-button],[mat-flat-button],[mat-stroked-button]", inputs: ["name", "dirtySubmit", "submitDisabled"] }, { kind: "directive", type: MatDialogClose, selector: "[mat-dialog-close], [matDialogClose]", inputs: ["aria-label", "type", "mat-dialog-close", "matDialogClose"], exportAs: ["matDialogClose"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: FsFormDialogCloseDirective, selector: "[fsFormDialogClose],[fs-form-dialog-close]", inputs: ["closeData"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1217
1661
|
}
|
|
1218
1662
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.7", ngImport: i0, type: FsFormDialogActionsComponent, decorators: [{
|
|
1219
1663
|
type: Component,
|
|
@@ -1224,7 +1668,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.7", ngImpor
|
|
|
1224
1668
|
MatDialogClose,
|
|
1225
1669
|
NgTemplateOutlet,
|
|
1226
1670
|
FsFormDialogCloseDirective,
|
|
1227
|
-
], template: "<div\n class=\"form-buttons\"\n [ngClass]=\"{ 'save-create':
|
|
1671
|
+
], template: "<div\n class=\"form-buttons\"\n [ngClass]=\"{ 'save-create': showSave || create }\">\n @if (showSave || create) {\n <button\n mat-button\n type=\"submit\"\n color=\"primary\"\n [name]=\"name\">\n {{ create ? 'Create' : 'Save' }}\n </button>\n <button\n mat-button\n type=\"button\"\n class=\"cancel-button\"\n [disabled]=\"close && !dirty && !create\"\n [mat-dialog-close]=\"null\">\n Cancel\n </button>\n }\n @if (showDone) {\n <button\n mat-button\n type=\"button\"\n color=\"primary\"\n [mat-dialog-close]=\"null\">\n Done\n </button>\n }\n @if (showSave || create) {\n <ng-template [ngTemplateOutlet]=\"content\"></ng-template>\n }\n @if (close) {\n <button\n mat-button\n type=\"button\"\n class=\"close-button\"\n fsFormDialogClose\n [closeData]=\"closeData\"\n [color]=\"dirty ? 'basic' : 'primary'\">\n Close\n </button>\n }\n @if (!(showSave || create)) {\n <ng-template [ngTemplateOutlet]=\"content\"></ng-template>\n }\n</div>\n<ng-template #content>\n <ng-content></ng-content>\n</ng-template>\n", styles: [":host{width:100%}.form-buttons.save-create .close-button{float:right}\n"] }]
|
|
1228
1672
|
}], propDecorators: { save: [{
|
|
1229
1673
|
type: Input
|
|
1230
1674
|
}], create: [{
|
|
@@ -1239,6 +1683,49 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.7", ngImpor
|
|
|
1239
1683
|
type: Input
|
|
1240
1684
|
}] } });
|
|
1241
1685
|
|
|
1686
|
+
/**
|
|
1687
|
+
* Brings a tab group into the enclosing form set's unsaved-changes confirm.
|
|
1688
|
+
*
|
|
1689
|
+
* An owner picks up tab groups with `@ContentChildren(MatTabGroup)`, which reaches
|
|
1690
|
+
* through its own projected content - including lazily mounted tab bodies - but
|
|
1691
|
+
* stops at a child component's view. So a tab group declared inside a child
|
|
1692
|
+
* component's own template is invisible to the dialog that contains it, and
|
|
1693
|
+
* switching between those tabs never prompts even though switching the outer ones
|
|
1694
|
+
* does. That is not a shape worth avoiding: a dialog whose Profile tab is its own
|
|
1695
|
+
* component, with its own tabs inside, is the normal way to keep a large dialog
|
|
1696
|
+
* readable.
|
|
1697
|
+
*
|
|
1698
|
+
* DI does cross the boundary a content query cannot, so put this on the nested
|
|
1699
|
+
* group and it registers itself with the owner above it:
|
|
1700
|
+
*
|
|
1701
|
+
* <mat-tab-group fsFormConfirmTabs [(selected)]="tab">
|
|
1702
|
+
*
|
|
1703
|
+
* Confirming stays gated on the owner's `confirm` and `confirmTabs` inputs, so
|
|
1704
|
+
* this only opts the group in - it never forces a prompt on.
|
|
1705
|
+
*/
|
|
1706
|
+
class FsFormConfirmTabsDirective {
|
|
1707
|
+
_tabGroup = inject(MatTabGroup, { self: true });
|
|
1708
|
+
_dialogRef = inject(MatDialogRef, { optional: true });
|
|
1709
|
+
_container = this._ownDialogOnly(inject(FsFormContainerDirective, { optional: true }));
|
|
1710
|
+
_form = this._ownDialogOnly(inject(FsFormDirective, { optional: true }));
|
|
1711
|
+
ngOnInit() {
|
|
1712
|
+
const owner = this._container || this._form?.rootForm;
|
|
1713
|
+
owner?.registerTabGroup(this._tabGroup);
|
|
1714
|
+
}
|
|
1715
|
+
_ownDialogOnly(owner) {
|
|
1716
|
+
return withinSameDialog(owner, this._dialogRef) ? owner : null;
|
|
1717
|
+
}
|
|
1718
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.7", ngImport: i0, type: FsFormConfirmTabsDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1719
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.7", type: FsFormConfirmTabsDirective, isStandalone: true, selector: "mat-tab-group[fsFormConfirmTabs]", ngImport: i0 });
|
|
1720
|
+
}
|
|
1721
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.7", ngImport: i0, type: FsFormConfirmTabsDirective, decorators: [{
|
|
1722
|
+
type: Directive,
|
|
1723
|
+
args: [{
|
|
1724
|
+
selector: 'mat-tab-group[fsFormConfirmTabs]',
|
|
1725
|
+
standalone: true,
|
|
1726
|
+
}]
|
|
1727
|
+
}] });
|
|
1728
|
+
|
|
1242
1729
|
const ERROR_MESSAGES = {
|
|
1243
1730
|
required: 'This field is required',
|
|
1244
1731
|
email: 'This is not a valid email address',
|
|
@@ -2523,7 +3010,8 @@ class FsFormModule {
|
|
|
2523
3010
|
FsFormTemplateDirective,
|
|
2524
3011
|
FsFormTemplateDirective,
|
|
2525
3012
|
FsFormTemplateOutletDirective,
|
|
2526
|
-
|
|
3013
|
+
FsFormContainerDirective,
|
|
3014
|
+
FsFormConfirmTabsDirective], exports: [FsFormDirective,
|
|
2527
3015
|
FsControlDirective,
|
|
2528
3016
|
FsFormRequiredDirective,
|
|
2529
3017
|
FsFormMinDirective,
|
|
@@ -2553,7 +3041,8 @@ class FsFormModule {
|
|
|
2553
3041
|
FsFormTemplateDirective,
|
|
2554
3042
|
FsFormTemplateDirective,
|
|
2555
3043
|
FsFormTemplateOutletDirective,
|
|
2556
|
-
|
|
3044
|
+
FsFormContainerDirective,
|
|
3045
|
+
FsFormConfirmTabsDirective] });
|
|
2557
3046
|
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "18.2.7", ngImport: i0, type: FsFormModule, providers: [
|
|
2558
3047
|
{
|
|
2559
3048
|
provide: ErrorStateMatcher,
|
|
@@ -2607,7 +3096,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.7", ngImpor
|
|
|
2607
3096
|
FsFormTemplateDirective,
|
|
2608
3097
|
FsFormTemplateDirective,
|
|
2609
3098
|
FsFormTemplateOutletDirective,
|
|
2610
|
-
|
|
3099
|
+
FsFormContainerDirective,
|
|
3100
|
+
FsFormConfirmTabsDirective,
|
|
2611
3101
|
],
|
|
2612
3102
|
exports: [
|
|
2613
3103
|
FsFormDirective,
|
|
@@ -2640,7 +3130,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.7", ngImpor
|
|
|
2640
3130
|
FsFormTemplateDirective,
|
|
2641
3131
|
FsFormTemplateDirective,
|
|
2642
3132
|
FsFormTemplateOutletDirective,
|
|
2643
|
-
|
|
3133
|
+
FsFormContainerDirective,
|
|
3134
|
+
FsFormConfirmTabsDirective,
|
|
2644
3135
|
],
|
|
2645
3136
|
providers: [
|
|
2646
3137
|
{
|
|
@@ -2657,5 +3148,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.7", ngImpor
|
|
|
2657
3148
|
* Generated bundle index. Do not edit.
|
|
2658
3149
|
*/
|
|
2659
3150
|
|
|
2660
|
-
export { ConfirmResult, FormDeactivateGuard, FormStatus, FsButtonDirective, FsControlDirective, FsForm, FsFormCompareDirective, FsFormDateRangeDirective, FsFormDialogActionsComponent, FsFormDialogCloseDirective, FsFormDirective, FsFormEmailDirective, FsFormEmailsDirective, FsFormFunctionDirective, FsFormGreaterDirective, FsFormGreaterEqualDirective,
|
|
3151
|
+
export { ConfirmResult, FormDeactivateGuard, FormStatus, FsButtonDirective, FsControlDirective, FsForm, FsFormCompareDirective, FsFormConfirmTabsDirective, FsFormContainerDirective, FsFormDateRangeDirective, FsFormDialogActionsComponent, FsFormDialogCloseDirective, FsFormDirective, FsFormEmailDirective, FsFormEmailsDirective, FsFormFunctionDirective, FsFormGreaterDirective, FsFormGreaterEqualDirective, FsFormIntegerDirective, FsFormLesserDirective, FsFormLesserEqualDirective, FsFormMaxDirective, FsFormMaxLengthDirective, FsFormMinDirective, FsFormMinLengthDirective, FsFormModule, FsFormNoFsValidatorsDirective, FsFormNumericDirective, FsFormPatternDirective, FsFormPhoneDirective, FsFormRequiredDirective, FsFormTemplateDirective, FsFormTemplateOutletDirective, FsFormUrlDirective, FsFormValidateDirective, FsSubmitButtonDirective, FsValidators };
|
|
2661
3152
|
//# sourceMappingURL=firestitch-form.mjs.map
|