@firestitch/form 18.0.28 → 18.0.30
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 +14 -2
- package/app/directives/button.directive.d.ts +7 -2
- package/app/directives/form/form.directive.d.ts +87 -29
- package/app/directives/form-base/form-base.directive.d.ts +73 -5
- 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 +1 -1
- package/app/fs-form.module.d.ts +2 -2
- 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 +41 -17
- package/esm2022/app/directives/button.directive.mjs +39 -9
- package/esm2022/app/directives/form/form.directive.mjs +249 -242
- package/esm2022/app/directives/form-base/form-base.directive.mjs +243 -6
- 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 +2 -2
- package/esm2022/app/fs-form.module.mjs +6 -6
- package/esm2022/app/guards/form-deactivate.guard.mjs +1 -1
- 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 +2 -2
- package/fesm2022/firestitch-form.mjs +743 -319
- package/fesm2022/firestitch-form.mjs.map +1 -1
- package/package.json +1 -1
- package/public_api.d.ts +2 -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,30 @@ 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
|
+
function getFormErrors(control, key) {
|
|
63
|
+
let errors = null;
|
|
64
|
+
if (control.invalid && control.errors) {
|
|
65
|
+
errors = {
|
|
66
|
+
[key]: { ...control.errors },
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
if (control instanceof UntypedFormGroup) {
|
|
70
|
+
Object.entries(control.controls)
|
|
71
|
+
.forEach(([name, childControl]) => {
|
|
72
|
+
const childErrors = getFormErrors(childControl, name);
|
|
73
|
+
if (childErrors) {
|
|
74
|
+
if (!errors) {
|
|
75
|
+
errors = { ...childErrors };
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
Object.assign(errors, childErrors);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return errors;
|
|
84
|
+
}
|
|
85
|
+
|
|
62
86
|
var ConfirmResult;
|
|
63
87
|
(function (ConfirmResult) {
|
|
64
88
|
ConfirmResult["Save"] = "save";
|
|
@@ -68,6 +92,18 @@ var ConfirmResult;
|
|
|
68
92
|
ConfirmResult["NoChanges"] = "noChanges";
|
|
69
93
|
})(ConfirmResult || (ConfirmResult = {}));
|
|
70
94
|
|
|
95
|
+
var FormStatus;
|
|
96
|
+
(function (FormStatus) {
|
|
97
|
+
FormStatus["Valid"] = "valid";
|
|
98
|
+
FormStatus["Invalid"] = "invalid";
|
|
99
|
+
FormStatus["Validating"] = "validating";
|
|
100
|
+
FormStatus["Submitting"] = "submitting";
|
|
101
|
+
FormStatus["Submitted"] = "submitted";
|
|
102
|
+
FormStatus["Error"] = "error";
|
|
103
|
+
FormStatus["Success"] = "success";
|
|
104
|
+
FormStatus["Completing"] = "completing";
|
|
105
|
+
})(FormStatus || (FormStatus = {}));
|
|
106
|
+
|
|
71
107
|
function confirmResultContinue(result) {
|
|
72
108
|
return (result === ConfirmResult.Discard ||
|
|
73
109
|
result === ConfirmResult.Save ||
|
|
@@ -81,18 +117,6 @@ function getActiveRoute(route) {
|
|
|
81
117
|
return route;
|
|
82
118
|
}
|
|
83
119
|
|
|
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
120
|
class FsForm {
|
|
97
121
|
// value is array for future possibilities of extension
|
|
98
122
|
_formDirectiveStore = new WeakMap();
|
|
@@ -109,11 +133,13 @@ class FsForm {
|
|
|
109
133
|
this._formDirectiveStore.delete(routeComponent);
|
|
110
134
|
}
|
|
111
135
|
hasChanges(form) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
136
|
+
// Spans the linked set, so a dialog whose fields live in a nested tab form
|
|
137
|
+
// still knows it has changes. A form linked out with `[link]="false"` is
|
|
138
|
+
// absent from that set and never triggers the prompt - which is the point of
|
|
139
|
+
// an instant-save panel: it has nothing outstanding to warn about.
|
|
140
|
+
return form.linkedForms
|
|
141
|
+
.some((linkedForm) => Object.keys(linkedForm.ngForm.control.controls)
|
|
142
|
+
.some((key) => linkedForm.ngForm.control.controls[key].dirty));
|
|
117
143
|
}
|
|
118
144
|
confirmUnsaved(form) {
|
|
119
145
|
if (!form.confirm || !this.hasChanges(form)) {
|
|
@@ -152,7 +178,8 @@ class FsForm {
|
|
|
152
178
|
return of(ConfirmResult.Discard);
|
|
153
179
|
}
|
|
154
180
|
if (result === 'save') {
|
|
155
|
-
form.
|
|
181
|
+
form.linkedForms
|
|
182
|
+
.forEach((linkedForm) => linkedForm.ngForm.control.markAsPristine());
|
|
156
183
|
return form.submit$({ confirmed: true })
|
|
157
184
|
.pipe(map((submitEvent) => {
|
|
158
185
|
if (submitEvent.error) {
|
|
@@ -205,46 +232,51 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.7", ngImpor
|
|
|
205
232
|
}]
|
|
206
233
|
}] });
|
|
207
234
|
|
|
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
|
-
|
|
235
|
+
/**
|
|
236
|
+
* Shared by the two things that can own a set of forms: an `fsForm` sitting at
|
|
237
|
+
* the root of its linked set, and an `fsFormContainer` holding forms that are
|
|
238
|
+
* siblings rather than nested.
|
|
239
|
+
*
|
|
240
|
+
* It holds the submit-button registry both of them expose to buttons, and the
|
|
241
|
+
* lifecycle that belongs to the set as a whole rather than to any one form -
|
|
242
|
+
* the dialog backdrop and Escape, Ctrl+S, the browser close prompt, the drawer
|
|
243
|
+
* and tab-switch confirms. Exactly one owner registers that lifecycle; see
|
|
244
|
+
* `_registerOwnerLifecycle`.
|
|
245
|
+
*/
|
|
232
246
|
class FsFormBaseDirective {
|
|
233
247
|
confirmTabs = true;
|
|
234
248
|
tabGroup;
|
|
249
|
+
confirm = true;
|
|
250
|
+
confirmDialog = true;
|
|
251
|
+
confirmDrawer = true;
|
|
252
|
+
confirmBrowser = true;
|
|
253
|
+
shortcuts = true; // Ctrl + s
|
|
254
|
+
deactivationGuard = true;
|
|
235
255
|
_ngZone = inject(NgZone);
|
|
236
256
|
_cdRef = inject(ChangeDetectorRef);
|
|
257
|
+
_element = inject(ElementRef);
|
|
258
|
+
_form = inject(FsForm);
|
|
259
|
+
_route = inject(ActivatedRoute);
|
|
260
|
+
_dialogRef = inject(MatDialogRef, { optional: true });
|
|
261
|
+
_drawerRef = inject(DrawerRef, { optional: true });
|
|
262
|
+
_dialogBackdropEscape = false;
|
|
263
|
+
_activatedRouteConfig;
|
|
237
264
|
_tabGroups = new QueryList();
|
|
238
265
|
_buttons = new QueryList();
|
|
239
266
|
_activeSubmitButton;
|
|
267
|
+
_confirmTabGroups = new WeakSet();
|
|
240
268
|
_destroy$ = new Subject();
|
|
269
|
+
ngOnInit() {
|
|
270
|
+
this._registerOwnerLifecycle();
|
|
271
|
+
}
|
|
241
272
|
ngAfterContentInit() {
|
|
242
|
-
this.
|
|
273
|
+
this._registerOwnerContentLifecycle();
|
|
243
274
|
}
|
|
244
275
|
get destroy$() {
|
|
245
276
|
return this._destroy$.asObservable();
|
|
246
277
|
}
|
|
247
278
|
ngOnDestroy() {
|
|
279
|
+
this._cleanupCanDeactivate();
|
|
248
280
|
this._destroy$.next(null);
|
|
249
281
|
this._destroy$.complete();
|
|
250
282
|
}
|
|
@@ -263,6 +295,9 @@ class FsFormBaseDirective {
|
|
|
263
295
|
...this._buttons.toArray(),
|
|
264
296
|
button,
|
|
265
297
|
]);
|
|
298
|
+
// QueryList.reset() does not emit `changes`; notify so subscribers (e.g. the
|
|
299
|
+
// form's dirty-submit-button tracking) react to buttons being registered.
|
|
300
|
+
this._buttons.notifyOnChanges();
|
|
266
301
|
}
|
|
267
302
|
removeButton(button) {
|
|
268
303
|
if (this._activeSubmitButton === button) {
|
|
@@ -272,6 +307,161 @@ class FsFormBaseDirective {
|
|
|
272
307
|
...this._buttons.toArray()
|
|
273
308
|
.filter((item) => (button !== item)),
|
|
274
309
|
]);
|
|
310
|
+
this._buttons.notifyOnChanges();
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Everything here acts on the form set as a whole, so exactly one thing may
|
|
314
|
+
* register it. A form linked into a parent defers to that parent; every form
|
|
315
|
+
* inside a container defers to the container. Registering twice would put two
|
|
316
|
+
* confirms on the same backdrop click and two handlers on the same Ctrl+S.
|
|
317
|
+
*/
|
|
318
|
+
_registerOwnerLifecycle() {
|
|
319
|
+
if (this.deactivationGuard) {
|
|
320
|
+
this._registerCanDeactivateGuard();
|
|
321
|
+
}
|
|
322
|
+
this._registerConfirmDialogBackdropEscape();
|
|
323
|
+
this._listenHotKeys();
|
|
324
|
+
this._listenWindowClose();
|
|
325
|
+
}
|
|
326
|
+
/** The half of the same ownership that needs projected content resolved. */
|
|
327
|
+
_registerOwnerContentLifecycle() {
|
|
328
|
+
this._registerConfirmTabs();
|
|
329
|
+
this._registerConfirmDrawerClose();
|
|
330
|
+
this._registerDrawerClose();
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Whether the element is inside what this owner covers. Overridden by the
|
|
334
|
+
* container, whose own element is a poor proxy: it may be an `<ng-container>`,
|
|
335
|
+
* which is a comment node and contains nothing.
|
|
336
|
+
*/
|
|
337
|
+
_containsElement(el) {
|
|
338
|
+
return this._nodeContains(this._element.nativeElement, el);
|
|
339
|
+
}
|
|
340
|
+
_nodeContains(root, el) {
|
|
341
|
+
if (!root || !el) {
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
if (el === root) {
|
|
345
|
+
return true;
|
|
346
|
+
}
|
|
347
|
+
return el.parentElement ? this._nodeContains(root, el.parentElement) : false;
|
|
348
|
+
}
|
|
349
|
+
_formClose() {
|
|
350
|
+
if (this.confirm && this.confirmDialog) {
|
|
351
|
+
this.triggerConfirm()
|
|
352
|
+
.pipe(filter((result) => confirmResultContinue(result)), switchMap((result) => {
|
|
353
|
+
return result === ConfirmResult.NoChanges || result === ConfirmResult.Discard
|
|
354
|
+
? of(null)
|
|
355
|
+
: this.submitted$;
|
|
356
|
+
}), takeUntil(this._destroy$))
|
|
357
|
+
.subscribe((result) => {
|
|
358
|
+
this._dialogRef.close(result?.response);
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
this._dialogRef.close(null);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
_listenWindowClose() {
|
|
366
|
+
fromEvent(window, 'beforeunload')
|
|
367
|
+
.pipe(takeUntil(this._destroy$))
|
|
368
|
+
.subscribe((event) => {
|
|
369
|
+
if (this.confirm && this.confirmBrowser && this.dirtyLinked) {
|
|
370
|
+
event.returnValue = false;
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
_activeDialog(el, dialog) {
|
|
375
|
+
return this._nodeContains(dialog, el);
|
|
376
|
+
}
|
|
377
|
+
_listenHotKeys() {
|
|
378
|
+
this._ngZone.runOutsideAngular(() => {
|
|
379
|
+
fromEvent(document, 'keydown')
|
|
380
|
+
.pipe(takeUntil(this._destroy$))
|
|
381
|
+
.subscribe((event) => {
|
|
382
|
+
if (this._dialogBackdropEscape && event.code === 'Escape') {
|
|
383
|
+
const cdkOverlayPane = Array
|
|
384
|
+
.from(document.querySelectorAll('.cdk-overlay-pane')).pop();
|
|
385
|
+
const activeDialog = this
|
|
386
|
+
._activeDialog(document.getElementById(this._dialogRef.id), cdkOverlayPane);
|
|
387
|
+
if (activeDialog) {
|
|
388
|
+
this._ngZone.run(() => {
|
|
389
|
+
this._formClose();
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
if ((event.ctrlKey || event.metaKey) && event.code === 'KeyS') {
|
|
394
|
+
event.preventDefault();
|
|
395
|
+
if (this.shortcuts) {
|
|
396
|
+
if (this._containsElement(document.activeElement)) {
|
|
397
|
+
this._ngZone.run(() => {
|
|
398
|
+
this.triggerSubmit();
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
_registerConfirmDialogBackdropEscape() {
|
|
407
|
+
if (this._dialogRef) {
|
|
408
|
+
this._dialogBackdropEscape = !this._dialogRef?.disableClose;
|
|
409
|
+
if (this._dialogBackdropEscape) {
|
|
410
|
+
this._dialogRef.backdropClick()
|
|
411
|
+
.pipe(takeUntil(this._destroy$))
|
|
412
|
+
.subscribe(() => {
|
|
413
|
+
this._formClose();
|
|
414
|
+
});
|
|
415
|
+
this._destroy$
|
|
416
|
+
.subscribe(() => {
|
|
417
|
+
this._dialogRef.disableClose = false;
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
_registerDrawerClose() {
|
|
423
|
+
if (this._drawerRef) {
|
|
424
|
+
this._drawerRef.closeStart$
|
|
425
|
+
.pipe(takeUntil(this._destroy$))
|
|
426
|
+
.subscribe((subscriber) => {
|
|
427
|
+
if (this.submitting) {
|
|
428
|
+
this.status$
|
|
429
|
+
.pipe(filter((status) => status === FormStatus.Success || status === FormStatus.Error), takeUntil(this._destroy$))
|
|
430
|
+
.subscribe((status) => {
|
|
431
|
+
if (status === FormStatus.Success) {
|
|
432
|
+
subscriber.next(null);
|
|
433
|
+
subscriber.complete();
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
subscriber.error();
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
subscriber.next(null);
|
|
442
|
+
subscriber.complete();
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
_registerConfirmDrawerClose() {
|
|
448
|
+
if (this._drawerRef) {
|
|
449
|
+
this._drawerRef.closeStart$
|
|
450
|
+
.pipe(switchMap((subscriber) => {
|
|
451
|
+
return iif(() => !!this.confirm && this.confirmDrawer, this.triggerConfirm()
|
|
452
|
+
.pipe(map((result) => confirmResultContinue(result)), tap((result) => {
|
|
453
|
+
if (result) {
|
|
454
|
+
subscriber.next(null);
|
|
455
|
+
subscriber.complete();
|
|
456
|
+
}
|
|
457
|
+
})), defer(() => {
|
|
458
|
+
subscriber.next(null);
|
|
459
|
+
subscriber.complete();
|
|
460
|
+
return of(null);
|
|
461
|
+
}));
|
|
462
|
+
}), takeUntil(this._destroy$))
|
|
463
|
+
.subscribe();
|
|
464
|
+
}
|
|
275
465
|
}
|
|
276
466
|
_registerConfirmTabs() {
|
|
277
467
|
this._registerConfirmTabGroups(this._tabGroups.toArray());
|
|
@@ -287,6 +477,15 @@ class FsFormBaseDirective {
|
|
|
287
477
|
});
|
|
288
478
|
}
|
|
289
479
|
_registerConfirmTabGroup(tabGroup) {
|
|
480
|
+
// `_tabGroups.changes` re-emits the whole list, not just what arrived, and it
|
|
481
|
+
// emits every time a lazy tab body mounts a nested group. Groups already
|
|
482
|
+
// wired therefore come back around - so without this the outer group
|
|
483
|
+
// accumulates one subscription per emission, and a single click on it opens
|
|
484
|
+
// that many stacked confirm dialogs.
|
|
485
|
+
if (this._confirmTabGroups.has(tabGroup)) {
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
this._confirmTabGroups.add(tabGroup);
|
|
290
489
|
const confirmTabGroup = tabGroup;
|
|
291
490
|
if (!confirmTabGroup._originalHandleClick) {
|
|
292
491
|
confirmTabGroup._originalHandleClick = tabGroup._handleClick;
|
|
@@ -315,8 +514,29 @@ class FsFormBaseDirective {
|
|
|
315
514
|
}), takeUntil(this._destroy$))
|
|
316
515
|
.subscribe();
|
|
317
516
|
}
|
|
517
|
+
_registerCanDeactivateGuard() {
|
|
518
|
+
this._activatedRouteConfig = getActiveRoute(this._route)?.routeConfig;
|
|
519
|
+
if (!this._activatedRouteConfig) {
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
this._form.registerFormDirective(this._activatedRouteConfig.component, this);
|
|
523
|
+
if (!Array.isArray(this._activatedRouteConfig.canDeactivate)) {
|
|
524
|
+
this._activatedRouteConfig.canDeactivate = [];
|
|
525
|
+
}
|
|
526
|
+
if (this._activatedRouteConfig.canDeactivate.indexOf(FormDeactivateGuard) === -1) {
|
|
527
|
+
this._activatedRouteConfig.canDeactivate.push(FormDeactivateGuard);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
_cleanupCanDeactivate() {
|
|
531
|
+
if (!this._activatedRouteConfig) {
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
const guardIndex = this._activatedRouteConfig.canDeactivate.indexOf(FormDeactivateGuard);
|
|
535
|
+
this._activatedRouteConfig.canDeactivate.splice(guardIndex, 1);
|
|
536
|
+
this._form.removeFormDirective(this._activatedRouteConfig.component);
|
|
537
|
+
}
|
|
318
538
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.7", ngImport: i0, type: FsFormBaseDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
319
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.7", type: FsFormBaseDirective, inputs: { confirmTabs: "confirmTabs", tabGroup: "tabGroup" }, queries: [{ propertyName: "_tabGroups", predicate: MatTabGroup, descendants: true }], ngImport: i0 });
|
|
539
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.7", type: FsFormBaseDirective, inputs: { confirmTabs: "confirmTabs", tabGroup: "tabGroup", confirm: "confirm", confirmDialog: "confirmDialog", confirmDrawer: "confirmDrawer", confirmBrowser: "confirmBrowser", shortcuts: "shortcuts", deactivationGuard: "deactivationGuard" }, queries: [{ propertyName: "_tabGroups", predicate: MatTabGroup, descendants: true }], ngImport: i0 });
|
|
320
540
|
}
|
|
321
541
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.7", ngImport: i0, type: FsFormBaseDirective, decorators: [{
|
|
322
542
|
type: Directive
|
|
@@ -324,62 +544,187 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.7", ngImpor
|
|
|
324
544
|
type: Input
|
|
325
545
|
}], tabGroup: [{
|
|
326
546
|
type: Input
|
|
547
|
+
}], confirm: [{
|
|
548
|
+
type: Input
|
|
549
|
+
}], confirmDialog: [{
|
|
550
|
+
type: Input
|
|
551
|
+
}], confirmDrawer: [{
|
|
552
|
+
type: Input
|
|
553
|
+
}], confirmBrowser: [{
|
|
554
|
+
type: Input
|
|
555
|
+
}], shortcuts: [{
|
|
556
|
+
type: Input
|
|
557
|
+
}], deactivationGuard: [{
|
|
558
|
+
type: Input
|
|
327
559
|
}], _tabGroups: [{
|
|
328
560
|
type: ContentChildren,
|
|
329
561
|
args: [MatTabGroup, { descendants: true }]
|
|
330
562
|
}] } });
|
|
331
563
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
564
|
+
/**
|
|
565
|
+
* Bounds a set of forms without being one.
|
|
566
|
+
*
|
|
567
|
+
* An `fsForm` can only reach the forms nested inside it through DI, which forces
|
|
568
|
+
* a wrapper carrying `ngForm` around anything that needs to see them - and that
|
|
569
|
+
* wrapper then owns a real, empty `NgForm` whose control namespace every field
|
|
570
|
+
* below it falls into. A container has no `ngForm` and no controls. It only
|
|
571
|
+
* watches: forms register themselves as they mount, buttons resolve to it, and
|
|
572
|
+
* `fs-form-dialog-actions` reads Save-vs-Done off it. Put it on whatever element
|
|
573
|
+
* already frames the layout - or on an `<ng-container>` for no element at all.
|
|
574
|
+
*
|
|
575
|
+
* Forms in a container are siblings rather than nested, so on registration each
|
|
576
|
+
* one is linked into the first. That reproduces exactly the structure DI nesting
|
|
577
|
+
* builds, which is why validation, dirty state, the unsaved-changes confirm and
|
|
578
|
+
* the submit chain all span a container without any of that machinery knowing
|
|
579
|
+
* containers exist.
|
|
580
|
+
*
|
|
581
|
+
* A form marked `[link]="false"` never registers, so an instant-save panel stays
|
|
582
|
+
* out of the set the same way it does when nested.
|
|
583
|
+
*/
|
|
584
|
+
class FsFormContainerDirective extends FsFormBaseDirective {
|
|
585
|
+
_forms = [];
|
|
586
|
+
_formsChange$ = new Subject();
|
|
587
|
+
/** The forms registered into this container, in registration order. */
|
|
588
|
+
get forms() {
|
|
589
|
+
return [...this._forms];
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* The form the rest of the set is linked into, or null while the container
|
|
593
|
+
* holds none - an open tab that has nothing to save, say.
|
|
594
|
+
*/
|
|
595
|
+
get rootForm() {
|
|
596
|
+
return this._forms[0] || null;
|
|
597
|
+
}
|
|
598
|
+
/** Emits whenever a form registers or deregisters. */
|
|
599
|
+
get formsChange$() {
|
|
600
|
+
return this._formsChange$.asObservable();
|
|
601
|
+
}
|
|
602
|
+
get linkedForms() {
|
|
603
|
+
return this.rootForm?.linkedForms || [];
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* True when anything in the container has a submit handler - i.e. there is
|
|
607
|
+
* something for a Save button to do. `fs-form-dialog-actions` reads this to
|
|
608
|
+
* swap Save for Done as the mounted tab changes, so a dialog never has to keep
|
|
609
|
+
* a list of which tabs happen to be savable.
|
|
610
|
+
*/
|
|
611
|
+
get submits() {
|
|
612
|
+
return this.linkedForms
|
|
613
|
+
.some((form) => form.hasSubmit);
|
|
614
|
+
}
|
|
615
|
+
get dirtyLinked() {
|
|
616
|
+
return this.rootForm?.dirtyLinked || false;
|
|
339
617
|
}
|
|
340
618
|
get submitting() {
|
|
341
|
-
return this.
|
|
619
|
+
return this.rootForm?.submitting || false;
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Emits whenever the set's dirty state may have moved, including when the set
|
|
623
|
+
* itself changes. Anything rendering off it - the dialog actions - subscribes
|
|
624
|
+
* once and outlives any particular form, so this has to follow the root across
|
|
625
|
+
* tab switches rather than bind to whichever form happened to be first.
|
|
626
|
+
*/
|
|
627
|
+
get dirtyChange$() {
|
|
628
|
+
return this._formsChange$
|
|
629
|
+
.pipe(startWith(null), switchMap(() => (this.rootForm?.dirtyChange$ || EMPTY)
|
|
630
|
+
.pipe(startWith(null))), map(() => undefined));
|
|
631
|
+
}
|
|
632
|
+
// Resolved at subscribe time rather than switch-mapped: a submit in flight has
|
|
633
|
+
// to survive the set changing under it, which is routine when saving unmounts
|
|
634
|
+
// a tab.
|
|
635
|
+
get submitted$() {
|
|
636
|
+
return defer(() => this.rootForm?.submitted$ || EMPTY);
|
|
637
|
+
}
|
|
638
|
+
get status$() {
|
|
639
|
+
return defer(() => this.rootForm?.status$ || of(FormStatus.Valid));
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Take a form into the container. Called by the form on init - consumers never
|
|
643
|
+
* call it directly.
|
|
644
|
+
*/
|
|
645
|
+
registerForm(form) {
|
|
646
|
+
if (this._forms.indexOf(form) !== -1) {
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
this._forms = [...this._forms, form];
|
|
650
|
+
if (this.rootForm !== form) {
|
|
651
|
+
form.linkTo(this.rootForm);
|
|
652
|
+
}
|
|
653
|
+
this._formsChange$.next();
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Material attaches an incoming tab body before it detaches the outgoing one,
|
|
657
|
+
* so losing the root is a routine reshuffle rather than teardown. Re-elect and
|
|
658
|
+
* re-link the survivors instead of leaving them pointing at a dead root, and
|
|
659
|
+
* only let go of forms actually registered - clearing unconditionally would
|
|
660
|
+
* drop the form that just arrived.
|
|
661
|
+
*/
|
|
662
|
+
deregisterForm(form) {
|
|
663
|
+
if (form && this._forms.indexOf(form) === -1) {
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
const removing = form ? [form] : [...this._forms];
|
|
667
|
+
const rootRemoved = removing.indexOf(this.rootForm) !== -1;
|
|
668
|
+
removing.forEach((item) => item.unlinkFrom());
|
|
669
|
+
this._forms = this._forms
|
|
670
|
+
.filter((item) => removing.indexOf(item) === -1);
|
|
671
|
+
if (rootRemoved) {
|
|
672
|
+
const root = this.rootForm;
|
|
673
|
+
this._forms
|
|
674
|
+
.filter((item) => item !== root)
|
|
675
|
+
.forEach((item) => {
|
|
676
|
+
item.unlinkFrom();
|
|
677
|
+
item.linkTo(root);
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
this._formsChange$.next();
|
|
342
681
|
}
|
|
343
|
-
|
|
344
|
-
|
|
682
|
+
triggerSubmit() {
|
|
683
|
+
this.rootForm?.triggerSubmit();
|
|
345
684
|
}
|
|
346
|
-
|
|
347
|
-
this.
|
|
685
|
+
triggerConfirm() {
|
|
686
|
+
return this.rootForm
|
|
687
|
+
? this.rootForm.triggerConfirm()
|
|
688
|
+
: of(ConfirmResult.NoChanges);
|
|
348
689
|
}
|
|
349
|
-
|
|
350
|
-
this.
|
|
690
|
+
submit$(event) {
|
|
691
|
+
return this.rootForm
|
|
692
|
+
? this.rootForm.submit$(event)
|
|
693
|
+
: of(null);
|
|
351
694
|
}
|
|
352
|
-
|
|
353
|
-
|
|
695
|
+
reset() {
|
|
696
|
+
this.rootForm?.reset();
|
|
697
|
+
}
|
|
698
|
+
validate() {
|
|
699
|
+
this.rootForm?.validate();
|
|
700
|
+
}
|
|
701
|
+
// The container's own element is a poor proxy for what it covers: it is
|
|
702
|
+
// typically layout chrome wrapping far more than the forms, and on an
|
|
703
|
+
// `<ng-container>` it is a comment node containing nothing at all. Scope to the
|
|
704
|
+
// registered forms instead.
|
|
705
|
+
_containsElement(el) {
|
|
706
|
+
return this._forms
|
|
707
|
+
.some((form) => this._nodeContains(form.element, el));
|
|
708
|
+
}
|
|
709
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.7", ngImport: i0, type: FsFormContainerDirective, deps: null, target: i0.ɵɵFactoryTarget.Directive });
|
|
710
|
+
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 });
|
|
354
711
|
}
|
|
355
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.7", ngImport: i0, type:
|
|
712
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.7", ngImport: i0, type: FsFormContainerDirective, decorators: [{
|
|
356
713
|
type: Directive,
|
|
357
714
|
args: [{
|
|
358
|
-
selector: '[
|
|
359
|
-
exportAs: '
|
|
715
|
+
selector: 'fs-form-container,[fsFormContainer]',
|
|
716
|
+
exportAs: 'fsFormContainer',
|
|
360
717
|
standalone: true,
|
|
361
718
|
}]
|
|
362
719
|
}] });
|
|
363
720
|
|
|
364
721
|
class FsFormDirective extends FsFormBaseDirective {
|
|
365
722
|
ngForm = inject(NgForm);
|
|
366
|
-
_form = inject(FsForm);
|
|
367
|
-
_element = inject(ElementRef);
|
|
368
|
-
_message = inject(FsMessage);
|
|
369
|
-
_formGroup = inject(FsFormGroupDirective, { optional: true });
|
|
370
|
-
_dialogRef = inject(MatDialogRef, { optional: true });
|
|
371
|
-
_drawerRef = inject(DrawerRef, { optional: true });
|
|
372
|
-
_route = inject(ActivatedRoute);
|
|
373
723
|
wrapperSelector = '.fs-form-wrapper,.mat-mdc-form-field';
|
|
374
724
|
messageSelector = '.fs-form-message,.mat-mdc-form-field-subscript-wrapper';
|
|
375
725
|
hintSelector = '.fs-form-hint,.mat-mdc-form-field-hint-wrapper';
|
|
376
726
|
labelSelector = '.fs-form-label,.mdc-floating-label>mat-label';
|
|
377
727
|
autocomplete = false;
|
|
378
|
-
shortcuts = true; // Ctrl + s
|
|
379
|
-
confirm = true;
|
|
380
|
-
confirmDialog = true;
|
|
381
|
-
confirmDrawer = true;
|
|
382
|
-
confirmBrowser = true;
|
|
383
728
|
dirtySubmitButton = true;
|
|
384
729
|
set submit(submit$) {
|
|
385
730
|
this._submit$ = submit$;
|
|
@@ -389,7 +734,21 @@ class FsFormDirective extends FsFormBaseDirective {
|
|
|
389
734
|
}
|
|
390
735
|
successDelay = 0;
|
|
391
736
|
errorDelay = 1000;
|
|
392
|
-
|
|
737
|
+
/**
|
|
738
|
+
* Whether this form joins the set around it - the `fsForm` it is nested inside,
|
|
739
|
+
* or the `fsFormContainer` enclosing it.
|
|
740
|
+
*
|
|
741
|
+
* Forms in a set behave as one submittable unit: the outermost form is what the
|
|
742
|
+
* submit buttons drive, and validation, dirty state, the unsaved-changes
|
|
743
|
+
* confirm and the pristine reset all span the set. That is what lets a form
|
|
744
|
+
* wrap only one tab's fields while the dialog footer stays outside it.
|
|
745
|
+
*
|
|
746
|
+
* Set `[link]="false"` for a panel that owns its own saving - an instant-save
|
|
747
|
+
* side panel, say. It keeps every fsForm feature for itself, but its dirty
|
|
748
|
+
* state never enables the outer Save button, its validation never blocks the
|
|
749
|
+
* outer submit, and the outer submit never touches it.
|
|
750
|
+
*/
|
|
751
|
+
link = true;
|
|
393
752
|
submitEvent = new EventEmitter();
|
|
394
753
|
invalid = new EventEmitter();
|
|
395
754
|
valid = new EventEmitter();
|
|
@@ -398,11 +757,75 @@ class FsFormDirective extends FsFormBaseDirective {
|
|
|
398
757
|
cleared = new EventEmitter();
|
|
399
758
|
fsFormClass = true;
|
|
400
759
|
_registerControl;
|
|
401
|
-
_dialogBackdropEscape = false;
|
|
402
760
|
_snapshot = {};
|
|
403
|
-
_activatedRouteConfig;
|
|
404
761
|
_status$ = new BehaviorSubject(FormStatus.Valid);
|
|
405
762
|
_submit$ = null;
|
|
763
|
+
_linkedForms = new Set();
|
|
764
|
+
_containerParent = null;
|
|
765
|
+
_dirtyChange$ = new Subject();
|
|
766
|
+
_message = inject(FsMessage);
|
|
767
|
+
_container = inject(FsFormContainerDirective, { optional: true });
|
|
768
|
+
_ancestorForm = inject(FsFormDirective, { optional: true, skipSelf: true });
|
|
769
|
+
/**
|
|
770
|
+
* Emits whenever the set's dirty state may have moved. Anything outside the
|
|
771
|
+
* form that renders off it - the dialog actions, say - can't watch
|
|
772
|
+
* `ngForm.valueChanges` on its own, because the edits happen in a nested form's
|
|
773
|
+
* NgForm, not this one's.
|
|
774
|
+
*/
|
|
775
|
+
get dirtyChange$() {
|
|
776
|
+
return this._dirtyChange$.asObservable();
|
|
777
|
+
}
|
|
778
|
+
get submitted$() {
|
|
779
|
+
return this.submitted.asObservable();
|
|
780
|
+
}
|
|
781
|
+
get status$() {
|
|
782
|
+
return this._status$.asObservable();
|
|
783
|
+
}
|
|
784
|
+
get element() {
|
|
785
|
+
return this._element.nativeElement;
|
|
786
|
+
}
|
|
787
|
+
/** True when a Save button driving this form would have something to run. */
|
|
788
|
+
get hasSubmit() {
|
|
789
|
+
return !!this._submit$;
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* The form this one is linked into, or null when it is a root - either because
|
|
793
|
+
* nothing encloses it or because `[link]="false"` cut it loose. A container
|
|
794
|
+
* supplies the parent when the forms are siblings rather than nested.
|
|
795
|
+
*/
|
|
796
|
+
get parentForm() {
|
|
797
|
+
if (!this.link) {
|
|
798
|
+
return null;
|
|
799
|
+
}
|
|
800
|
+
return this._ancestorForm || this._containerParent;
|
|
801
|
+
}
|
|
802
|
+
/** The outermost form of this set. Submit buttons drive this one. */
|
|
803
|
+
get rootForm() {
|
|
804
|
+
return this.parentForm?.rootForm || this;
|
|
805
|
+
}
|
|
806
|
+
/** This form plus every form linked into it, at any depth. */
|
|
807
|
+
get linkedForms() {
|
|
808
|
+
return [
|
|
809
|
+
this,
|
|
810
|
+
...[...this._linkedForms]
|
|
811
|
+
.reduce((forms, form) => [...forms, ...form.linkedForms], []),
|
|
812
|
+
];
|
|
813
|
+
}
|
|
814
|
+
/** True when this form or anything linked into it has unsaved changes. */
|
|
815
|
+
get dirtyLinked() {
|
|
816
|
+
return this.linkedForms
|
|
817
|
+
.some((form) => form.ngForm.dirty);
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
820
|
+
* True when this form or anything linked into it has a submit handler - i.e.
|
|
821
|
+
* there is something for a Save button to do. A dialog footer can bind to this
|
|
822
|
+
* to swap Save for Done as the mounted tab changes, instead of hard-coding
|
|
823
|
+
* which tab names happen to be savable.
|
|
824
|
+
*/
|
|
825
|
+
get submits() {
|
|
826
|
+
return this.linkedForms
|
|
827
|
+
.some((form) => form.hasSubmit);
|
|
828
|
+
}
|
|
406
829
|
get submitting() {
|
|
407
830
|
return this._status$.getValue() === FormStatus.Submitting;
|
|
408
831
|
}
|
|
@@ -412,6 +835,15 @@ class FsFormDirective extends FsFormBaseDirective {
|
|
|
412
835
|
get completing() {
|
|
413
836
|
return this._status$.getValue() === FormStatus.Completing;
|
|
414
837
|
}
|
|
838
|
+
/**
|
|
839
|
+
* True when nothing above this form speaks for the set it belongs to. Reads the
|
|
840
|
+
* raw injections rather than `parentForm`, so an unlinked panel inside a dialog
|
|
841
|
+
* still defers to whatever encloses it instead of registering a second backdrop
|
|
842
|
+
* confirm on the same dialog.
|
|
843
|
+
*/
|
|
844
|
+
get _owns() {
|
|
845
|
+
return !this._ancestorForm && !this._container;
|
|
846
|
+
}
|
|
415
847
|
get _submitEvent() {
|
|
416
848
|
return {
|
|
417
849
|
ngForm: this.ngForm,
|
|
@@ -426,8 +858,17 @@ class FsFormDirective extends FsFormBaseDirective {
|
|
|
426
858
|
submitter: this._submitter,
|
|
427
859
|
response: null,
|
|
428
860
|
};
|
|
429
|
-
|
|
430
|
-
|
|
861
|
+
// Every form in the set gets to save. A form that only frames the layout - a
|
|
862
|
+
// dialog wrapper around a tab group - carries no submit of its own and simply
|
|
863
|
+
// defers to the tab form linked into it. When several are linked they run in
|
|
864
|
+
// nesting order, so an outer save lands before an inner one, and the response
|
|
865
|
+
// of the last is what the submit resolves with.
|
|
866
|
+
const submits = this.linkedForms
|
|
867
|
+
.filter((form) => form.hasSubmit)
|
|
868
|
+
.map((form) => form._submit$(this._submitEvent));
|
|
869
|
+
const submit$ = submits.length ?
|
|
870
|
+
concat(...submits).pipe(toArray(), map((responses) => responses[responses.length - 1])) :
|
|
871
|
+
of(submittedEvent);
|
|
431
872
|
return submit$
|
|
432
873
|
.pipe(map((response) => {
|
|
433
874
|
submittedEvent.response = response;
|
|
@@ -447,22 +888,67 @@ class FsFormDirective extends FsFormBaseDirective {
|
|
|
447
888
|
return throwError(() => new Error('Form validation error'));
|
|
448
889
|
}
|
|
449
890
|
get _submitter() {
|
|
450
|
-
return this.
|
|
891
|
+
return this._getOwner().activeSubmitButton?.name;
|
|
451
892
|
}
|
|
452
893
|
ngOnInit() {
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
894
|
+
// A form already nested inside another links to that one, which is itself in
|
|
895
|
+
// the container - so it joins the set transitively and must not also register
|
|
896
|
+
// as a container sibling.
|
|
897
|
+
if (this.link) {
|
|
898
|
+
if (this._ancestorForm) {
|
|
899
|
+
this._ancestorForm.linkForm(this);
|
|
900
|
+
}
|
|
901
|
+
else {
|
|
902
|
+
this._container?.registerForm(this);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
// The route guard, dialog backdrop, Ctrl+S and browser close prompt all act
|
|
906
|
+
// on the set as a whole, so exactly one thing registers them: a linked child
|
|
907
|
+
// defers to its parent, and every form in a container defers to the container.
|
|
908
|
+
if (this._owns) {
|
|
909
|
+
super.ngOnInit();
|
|
456
910
|
}
|
|
457
|
-
this._registerConfirmDialogBackdropEscape();
|
|
458
|
-
this._listenHotKeys();
|
|
459
|
-
this._listenWindowClose();
|
|
460
911
|
this._listenSubmit();
|
|
461
912
|
this._listenFormStatus();
|
|
462
913
|
if (!this.autocomplete) {
|
|
463
914
|
this._registerAutocomplete();
|
|
464
915
|
}
|
|
465
916
|
}
|
|
917
|
+
/**
|
|
918
|
+
* Take a nested form into this one's set. Called by the child on init, or by a
|
|
919
|
+
* container linking its siblings - consumers never call it directly.
|
|
920
|
+
*/
|
|
921
|
+
linkForm(form) {
|
|
922
|
+
this._linkedForms.add(form);
|
|
923
|
+
Promise.resolve()
|
|
924
|
+
.then(() => {
|
|
925
|
+
this.rootForm._updateDirtySubmitButtons();
|
|
926
|
+
this._cdRef.markForCheck();
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
unlinkForm(form) {
|
|
930
|
+
this._linkedForms.delete(form);
|
|
931
|
+
Promise.resolve()
|
|
932
|
+
.then(() => {
|
|
933
|
+
this.rootForm._updateDirtySubmitButtons();
|
|
934
|
+
this._cdRef.markForCheck();
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* Adopt a parent that DI could not supply. Only a container calls this, to link
|
|
939
|
+
* forms that are siblings in the template into one set.
|
|
940
|
+
*/
|
|
941
|
+
linkTo(form) {
|
|
942
|
+
if (!form || form === this) {
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
this._containerParent = form;
|
|
946
|
+
form.linkForm(this);
|
|
947
|
+
}
|
|
948
|
+
unlinkFrom() {
|
|
949
|
+
this._containerParent?.unlinkForm(this);
|
|
950
|
+
this._containerParent = null;
|
|
951
|
+
}
|
|
466
952
|
ngOnChanges(changes) {
|
|
467
953
|
if (changes.confirm) {
|
|
468
954
|
this._updateDirtySubmitButtons();
|
|
@@ -483,16 +969,21 @@ class FsFormDirective extends FsFormBaseDirective {
|
|
|
483
969
|
});
|
|
484
970
|
}
|
|
485
971
|
ngAfterContentInit() {
|
|
486
|
-
|
|
972
|
+
// Tab and drawer confirmation belong to whoever owns the set. A linked child
|
|
973
|
+
// shares the same tab groups and the same drawer, so letting it register too
|
|
974
|
+
// would put a second confirm on the same click.
|
|
975
|
+
if (this._owns) {
|
|
976
|
+
super.ngAfterContentInit();
|
|
977
|
+
}
|
|
487
978
|
this._registerConfirm();
|
|
488
|
-
this._registerConfirmDrawerClose();
|
|
489
|
-
this._registerDrawerClose();
|
|
490
979
|
this._registerDirtySubmitButton();
|
|
491
980
|
}
|
|
492
981
|
ngOnDestroy() {
|
|
493
|
-
|
|
982
|
+
// Deregister first: it clears the container-supplied parent, so the unlink
|
|
983
|
+
// below falls through to the DI ancestor rather than repeating the same work.
|
|
984
|
+
this._container?.deregisterForm(this);
|
|
985
|
+
this.parentForm?.unlinkForm(this);
|
|
494
986
|
super.ngOnDestroy();
|
|
495
|
-
this._cleanupCanDeactivate();
|
|
496
987
|
}
|
|
497
988
|
createSnapshot() {
|
|
498
989
|
this._snapshot = this.ngForm.value;
|
|
@@ -501,11 +992,14 @@ class FsFormDirective extends FsFormBaseDirective {
|
|
|
501
992
|
return this._snapshot || {};
|
|
502
993
|
}
|
|
503
994
|
reset() {
|
|
504
|
-
this.
|
|
505
|
-
|
|
506
|
-
.
|
|
507
|
-
|
|
508
|
-
|
|
995
|
+
this.linkedForms
|
|
996
|
+
.forEach((form) => {
|
|
997
|
+
form.ngForm.resetForm();
|
|
998
|
+
Object.keys(form.ngForm.controls)
|
|
999
|
+
.forEach((name) => {
|
|
1000
|
+
const control = form.ngForm.controls[name];
|
|
1001
|
+
control.reset(form._snapshot[name]);
|
|
1002
|
+
});
|
|
509
1003
|
});
|
|
510
1004
|
this.reseted.emit();
|
|
511
1005
|
}
|
|
@@ -535,24 +1029,29 @@ class FsFormDirective extends FsFormBaseDirective {
|
|
|
535
1029
|
}
|
|
536
1030
|
disable() {
|
|
537
1031
|
this.ngForm.control.disable();
|
|
538
|
-
this.
|
|
1032
|
+
this._getOwner()
|
|
539
1033
|
.buttons.forEach((button) => {
|
|
540
1034
|
button.disable();
|
|
541
1035
|
});
|
|
542
1036
|
}
|
|
543
1037
|
validate() {
|
|
544
|
-
|
|
545
|
-
.forEach((
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
1038
|
+
this.linkedForms
|
|
1039
|
+
.forEach((form) => {
|
|
1040
|
+
Object.values(form.ngForm.controls)
|
|
1041
|
+
.forEach((control) => {
|
|
1042
|
+
control.markAsDirty();
|
|
1043
|
+
control.markAsTouched();
|
|
1044
|
+
control.updateValueAndValidity();
|
|
1045
|
+
});
|
|
549
1046
|
});
|
|
550
1047
|
}
|
|
551
1048
|
submit$(submitEvent) {
|
|
552
1049
|
return of(submitEvent)
|
|
553
1050
|
.pipe(tap(() => this._statusValidating()), tap(() => this.validate()), tap(() => this._statusSubmitting()), tap(() => this._setupActiveSubmitButton()), tap(() => this._disableButtons()), switchMap((data) => this._waitUntilStatusPending()
|
|
554
1051
|
.pipe(map(() => data))), mergeMap(() => {
|
|
555
|
-
|
|
1052
|
+
const invalid = this.linkedForms
|
|
1053
|
+
.some((form) => form.ngForm.status === 'INVALID');
|
|
1054
|
+
if (invalid) {
|
|
556
1055
|
return this._formInvalidState$;
|
|
557
1056
|
}
|
|
558
1057
|
return this._formValidState$;
|
|
@@ -592,99 +1091,32 @@ class FsFormDirective extends FsFormBaseDirective {
|
|
|
592
1091
|
}
|
|
593
1092
|
});
|
|
594
1093
|
}
|
|
595
|
-
_listenWindowClose() {
|
|
596
|
-
fromEvent(window, 'beforeunload')
|
|
597
|
-
.pipe(takeUntil(this.destroy$))
|
|
598
|
-
.subscribe((event) => {
|
|
599
|
-
if (this.confirm && this.confirmBrowser && this.ngForm.dirty) {
|
|
600
|
-
event.returnValue = false;
|
|
601
|
-
}
|
|
602
|
-
});
|
|
603
|
-
}
|
|
604
|
-
_activeDialog(el, dialog) {
|
|
605
|
-
if (el.isSameNode(dialog)) {
|
|
606
|
-
return true;
|
|
607
|
-
}
|
|
608
|
-
else if (el.parentElement) {
|
|
609
|
-
return this._activeDialog(el.parentElement, dialog);
|
|
610
|
-
}
|
|
611
|
-
return false;
|
|
612
|
-
}
|
|
613
|
-
_listenHotKeys() {
|
|
614
|
-
this._ngZone.runOutsideAngular(() => {
|
|
615
|
-
fromEvent(document, 'keydown')
|
|
616
|
-
.pipe(takeUntil(this.destroy$))
|
|
617
|
-
.subscribe((event) => {
|
|
618
|
-
if (this._dialogBackdropEscape && event.code === 'Escape') {
|
|
619
|
-
const cdkOverlayPane = Array
|
|
620
|
-
.from(document.querySelectorAll('.cdk-overlay-pane')).pop();
|
|
621
|
-
const activeDialog = this
|
|
622
|
-
._activeDialog(document.getElementById(this._dialogRef.id), cdkOverlayPane);
|
|
623
|
-
if (activeDialog) {
|
|
624
|
-
this._ngZone.run(() => {
|
|
625
|
-
this._formClose();
|
|
626
|
-
});
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
if ((event.ctrlKey || event.metaKey) && event.code === 'KeyS') {
|
|
630
|
-
event.preventDefault();
|
|
631
|
-
if (this.shortcuts) {
|
|
632
|
-
if (this._elementInForm(document.activeElement)) {
|
|
633
|
-
this.ngForm.ngSubmit.next(null);
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
});
|
|
638
|
-
});
|
|
639
|
-
}
|
|
640
|
-
_formClose() {
|
|
641
|
-
if (this.confirm && this.confirmDialog) {
|
|
642
|
-
this.triggerConfirm()
|
|
643
|
-
.pipe(filter((result) => confirmResultContinue(result)), switchMap((result) => {
|
|
644
|
-
return result === ConfirmResult.NoChanges || result === ConfirmResult.Discard
|
|
645
|
-
? of(null)
|
|
646
|
-
: this.submitted.asObservable();
|
|
647
|
-
}), takeUntil(this.destroy$))
|
|
648
|
-
.subscribe((result) => {
|
|
649
|
-
this._dialogRef.close(result?.response);
|
|
650
|
-
});
|
|
651
|
-
}
|
|
652
|
-
else {
|
|
653
|
-
this._dialogRef.close(null);
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
1094
|
_getActiveSubmitButton() {
|
|
657
|
-
if (this.
|
|
658
|
-
return this.
|
|
1095
|
+
if (this._getOwner().activeSubmitButton) {
|
|
1096
|
+
return this._getOwner().activeSubmitButton;
|
|
659
1097
|
}
|
|
660
|
-
return this.
|
|
1098
|
+
return this._getOwner()
|
|
661
1099
|
.buttons
|
|
662
1100
|
.filter((button) => button.submit)[0];
|
|
663
1101
|
}
|
|
664
|
-
_elementInForm(el) {
|
|
665
|
-
if (el.isSameNode(this._element.nativeElement)) {
|
|
666
|
-
return true;
|
|
667
|
-
}
|
|
668
|
-
else if (el.parentElement) {
|
|
669
|
-
return this._elementInForm(el.parentElement);
|
|
670
|
-
}
|
|
671
|
-
return false;
|
|
672
|
-
}
|
|
673
1102
|
_completeSubmit(success, submitEvent) {
|
|
674
1103
|
if (success) {
|
|
675
|
-
this.
|
|
676
|
-
|
|
1104
|
+
this.linkedForms
|
|
1105
|
+
.forEach((form) => {
|
|
1106
|
+
form.ngForm.control.markAsPristine();
|
|
1107
|
+
form.createSnapshot();
|
|
1108
|
+
});
|
|
677
1109
|
this.submitted.emit(submitEvent);
|
|
678
1110
|
}
|
|
679
1111
|
else {
|
|
680
1112
|
this._resetButtons();
|
|
681
1113
|
}
|
|
682
|
-
if (this.
|
|
1114
|
+
if (this._getOwner().activeSubmitButton) {
|
|
683
1115
|
if (success) {
|
|
684
|
-
this.
|
|
1116
|
+
this._getOwner().activeSubmitButton.success();
|
|
685
1117
|
}
|
|
686
1118
|
else {
|
|
687
|
-
this.
|
|
1119
|
+
this._getOwner().activeSubmitButton.error();
|
|
688
1120
|
}
|
|
689
1121
|
}
|
|
690
1122
|
this._status$.next(FormStatus.Submitted);
|
|
@@ -699,22 +1131,35 @@ class FsFormDirective extends FsFormBaseDirective {
|
|
|
699
1131
|
of(true)
|
|
700
1132
|
.pipe(delay(resetDelay), first(), takeUntil(this.destroy$))
|
|
701
1133
|
.subscribe(() => {
|
|
702
|
-
|
|
1134
|
+
const valid = this.linkedForms
|
|
1135
|
+
.every((form) => form.ngForm.form.status === 'VALID');
|
|
1136
|
+
if (valid) {
|
|
703
1137
|
this._status$.next(FormStatus.Valid);
|
|
704
1138
|
}
|
|
705
1139
|
else {
|
|
706
1140
|
this._status$.next(FormStatus.Invalid);
|
|
707
1141
|
}
|
|
708
1142
|
this._resetButtons();
|
|
709
|
-
this.
|
|
1143
|
+
this._getOwner().activeSubmitButton = null;
|
|
710
1144
|
this._updateDirtySubmitButtons();
|
|
711
1145
|
});
|
|
712
1146
|
}
|
|
713
|
-
|
|
714
|
-
|
|
1147
|
+
/**
|
|
1148
|
+
* Whoever holds the submit buttons for this form. A container when one encloses
|
|
1149
|
+
* it, otherwise the root of its linked set - the button that saves a tab's form
|
|
1150
|
+
* typically sits outside it, down in the dialog footer.
|
|
1151
|
+
*
|
|
1152
|
+
* Only a form that actually joined the container may drive its buttons. DI
|
|
1153
|
+
* hands `_container` to every form inside, including one cut loose with
|
|
1154
|
+
* `[link]="false"`, and an instant-save panel is usually `[confirm]="false"`
|
|
1155
|
+
* too - so without this it would take the `!confirm` branch below and
|
|
1156
|
+
* re-enable the footer's Save button that the linked set had just disabled.
|
|
1157
|
+
*/
|
|
1158
|
+
_getOwner() {
|
|
1159
|
+
return (this.link ? this._container : null) || this.rootForm;
|
|
715
1160
|
}
|
|
716
1161
|
_resetButtons() {
|
|
717
|
-
this.
|
|
1162
|
+
this._getOwner()
|
|
718
1163
|
.buttons.forEach((button) => {
|
|
719
1164
|
button.reset();
|
|
720
1165
|
});
|
|
@@ -737,66 +1182,6 @@ class FsFormDirective extends FsFormBaseDirective {
|
|
|
737
1182
|
}
|
|
738
1183
|
});
|
|
739
1184
|
}
|
|
740
|
-
_registerDrawerClose() {
|
|
741
|
-
if (this._drawerRef) {
|
|
742
|
-
this._drawerRef.closeStart$
|
|
743
|
-
.pipe(takeUntil(this.destroy$))
|
|
744
|
-
.subscribe((subscriber) => {
|
|
745
|
-
if (this.submitting) {
|
|
746
|
-
this._status$
|
|
747
|
-
.pipe(filter((status) => status === FormStatus.Success || status === FormStatus.Error), takeUntil(this.destroy$))
|
|
748
|
-
.subscribe((status) => {
|
|
749
|
-
if (status === FormStatus.Success) {
|
|
750
|
-
subscriber.next(null);
|
|
751
|
-
subscriber.complete();
|
|
752
|
-
}
|
|
753
|
-
else {
|
|
754
|
-
subscriber.error();
|
|
755
|
-
}
|
|
756
|
-
});
|
|
757
|
-
}
|
|
758
|
-
else {
|
|
759
|
-
subscriber.next(null);
|
|
760
|
-
subscriber.complete();
|
|
761
|
-
}
|
|
762
|
-
});
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
_registerConfirmDrawerClose() {
|
|
766
|
-
if (this._drawerRef) {
|
|
767
|
-
this._drawerRef.closeStart$
|
|
768
|
-
.pipe(switchMap((subscriber) => {
|
|
769
|
-
return iif(() => this.confirm && this.confirmDrawer, this.triggerConfirm()
|
|
770
|
-
.pipe(map((result) => confirmResultContinue(result)), tap((result) => {
|
|
771
|
-
if (result) {
|
|
772
|
-
subscriber.next(null);
|
|
773
|
-
subscriber.complete();
|
|
774
|
-
}
|
|
775
|
-
})), defer(() => {
|
|
776
|
-
subscriber.next(null);
|
|
777
|
-
subscriber.complete();
|
|
778
|
-
return of(null);
|
|
779
|
-
}));
|
|
780
|
-
}), takeUntil(this.destroy$))
|
|
781
|
-
.subscribe();
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
_registerConfirmDialogBackdropEscape() {
|
|
785
|
-
if (this._dialogRef) {
|
|
786
|
-
this._dialogBackdropEscape = !this._dialogRef?.disableClose;
|
|
787
|
-
if (this._dialogBackdropEscape) {
|
|
788
|
-
this._dialogRef.backdropClick()
|
|
789
|
-
.pipe(takeUntil(this.destroy$))
|
|
790
|
-
.subscribe(() => {
|
|
791
|
-
this._formClose();
|
|
792
|
-
});
|
|
793
|
-
this.destroy$
|
|
794
|
-
.subscribe(() => {
|
|
795
|
-
this._dialogRef.disableClose = false;
|
|
796
|
-
});
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
1185
|
_registerAutocomplete() {
|
|
801
1186
|
this._registerControl = this.ngForm.form.registerControl.bind(this.ngForm.form);
|
|
802
1187
|
this.ngForm.form.registerControl = (name, control) => {
|
|
@@ -814,24 +1199,37 @@ class FsFormDirective extends FsFormBaseDirective {
|
|
|
814
1199
|
if (!this.ngForm) {
|
|
815
1200
|
return;
|
|
816
1201
|
}
|
|
1202
|
+
// Report up: a linked child's edits have to reach the root, since that is
|
|
1203
|
+
// where the submit buttons live and what decides whether they are enabled.
|
|
817
1204
|
this.ngForm.form.valueChanges
|
|
818
1205
|
.pipe(takeUntil(this.destroy$))
|
|
819
1206
|
.subscribe(() => {
|
|
820
|
-
this._updateDirtySubmitButtons();
|
|
1207
|
+
this.rootForm._updateDirtySubmitButtons();
|
|
821
1208
|
});
|
|
822
|
-
this.
|
|
1209
|
+
this._getOwner()
|
|
823
1210
|
.buttons.changes
|
|
824
1211
|
.pipe(takeUntil(this.destroy$))
|
|
825
1212
|
.subscribe(() => {
|
|
826
|
-
|
|
1213
|
+
// Buttons (de)register during change detection (their ngOnInit/ngOnDestroy),
|
|
1214
|
+
// so defer to a microtask to update the disabled state outside that pass and
|
|
1215
|
+
// avoid ExpressionChangedAfterItHasBeenChecked on the button's disabled binding.
|
|
1216
|
+
Promise.resolve().then(() => this._updateDirtySubmitButtons());
|
|
827
1217
|
});
|
|
1218
|
+
// The initial content buttons register (addButton) before the subscription above
|
|
1219
|
+
// exists, so their `changes` emission is missed. Apply the initial pristine/dirty
|
|
1220
|
+
// disabled state once, deferred so it runs after the current change-detection pass.
|
|
1221
|
+
Promise.resolve().then(() => this._updateDirtySubmitButtons());
|
|
828
1222
|
}
|
|
829
1223
|
_updateDirtySubmitButtons() {
|
|
830
|
-
this.
|
|
1224
|
+
if (!this.ngForm) {
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1227
|
+
this._dirtyChange$.next();
|
|
1228
|
+
this._getOwner()
|
|
831
1229
|
.buttons
|
|
832
1230
|
.filter((button) => button.submit)
|
|
833
1231
|
.forEach((submitButton) => {
|
|
834
|
-
if (!this.confirm || !this.dirtySubmitButton || this.
|
|
1232
|
+
if (!this.confirm || !this.dirtySubmitButton || this.dirtyLinked || !submitButton.dirtySubmit) {
|
|
835
1233
|
submitButton.enable();
|
|
836
1234
|
}
|
|
837
1235
|
else {
|
|
@@ -846,22 +1244,26 @@ class FsFormDirective extends FsFormBaseDirective {
|
|
|
846
1244
|
this._status$.next(FormStatus.Validating);
|
|
847
1245
|
}
|
|
848
1246
|
_setupActiveSubmitButton() {
|
|
849
|
-
this.
|
|
1247
|
+
this._getOwner()
|
|
850
1248
|
.activeSubmitButton = this._getActiveSubmitButton();
|
|
851
1249
|
this._resetButtons();
|
|
852
|
-
if (this.
|
|
853
|
-
this.
|
|
1250
|
+
if (this._getOwner().activeSubmitButton) {
|
|
1251
|
+
this._getOwner().activeSubmitButton.process();
|
|
854
1252
|
}
|
|
855
1253
|
}
|
|
856
1254
|
_disableButtons() {
|
|
857
|
-
this.
|
|
1255
|
+
this._getOwner()
|
|
858
1256
|
.buttons.forEach((button) => {
|
|
859
1257
|
button.disable();
|
|
860
1258
|
});
|
|
861
1259
|
}
|
|
862
1260
|
_waitUntilStatusPending() {
|
|
863
|
-
|
|
864
|
-
|
|
1261
|
+
// Async validators can be pending in any linked form, so hold the submit
|
|
1262
|
+
// until every one of them has settled - not just this form's.
|
|
1263
|
+
return combineLatest(this.linkedForms
|
|
1264
|
+
.map((form) => form.ngForm.statusChanges
|
|
1265
|
+
.pipe(startWith(form.ngForm.status))))
|
|
1266
|
+
.pipe(first((states) => states.every((state) => state !== 'PENDING')));
|
|
865
1267
|
}
|
|
866
1268
|
_handleError(e) {
|
|
867
1269
|
console.log('%c Form Submit ', 'color: white; background-color: #D33F49', 'Error occured');
|
|
@@ -875,29 +1277,8 @@ class FsFormDirective extends FsFormBaseDirective {
|
|
|
875
1277
|
console.groupEnd();
|
|
876
1278
|
this._completeSubmit(false, null);
|
|
877
1279
|
}
|
|
878
|
-
_registerCanDeactivateGuard() {
|
|
879
|
-
this._activatedRouteConfig = getActiveRoute(this._route).routeConfig;
|
|
880
|
-
if (!this._activatedRouteConfig) {
|
|
881
|
-
return;
|
|
882
|
-
}
|
|
883
|
-
this._form.registerFormDirective(this._activatedRouteConfig.component, this);
|
|
884
|
-
if (!Array.isArray(this._activatedRouteConfig.canDeactivate)) {
|
|
885
|
-
this._activatedRouteConfig.canDeactivate = [];
|
|
886
|
-
}
|
|
887
|
-
if (this._activatedRouteConfig.canDeactivate.indexOf(FormDeactivateGuard) === -1) {
|
|
888
|
-
this._activatedRouteConfig.canDeactivate.push(FormDeactivateGuard);
|
|
889
|
-
}
|
|
890
|
-
}
|
|
891
|
-
_cleanupCanDeactivate() {
|
|
892
|
-
if (!this._activatedRouteConfig) {
|
|
893
|
-
return;
|
|
894
|
-
}
|
|
895
|
-
const guardIndex = this._activatedRouteConfig.canDeactivate.indexOf(FormDeactivateGuard);
|
|
896
|
-
this._activatedRouteConfig.canDeactivate.splice(guardIndex, 1);
|
|
897
|
-
this._form.removeFormDirective(this._activatedRouteConfig.component);
|
|
898
|
-
}
|
|
899
1280
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.7", ngImport: i0, type: FsFormDirective, deps: null, target: i0.ɵɵFactoryTarget.Directive });
|
|
900
|
-
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",
|
|
1281
|
+
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 });
|
|
901
1282
|
}
|
|
902
1283
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.7", ngImport: i0, type: FsFormDirective, decorators: [{
|
|
903
1284
|
type: Directive,
|
|
@@ -916,16 +1297,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.7", ngImpor
|
|
|
916
1297
|
type: Input
|
|
917
1298
|
}], autocomplete: [{
|
|
918
1299
|
type: Input
|
|
919
|
-
}], shortcuts: [{
|
|
920
|
-
type: Input
|
|
921
|
-
}], confirm: [{
|
|
922
|
-
type: Input
|
|
923
|
-
}], confirmDialog: [{
|
|
924
|
-
type: Input
|
|
925
|
-
}], confirmDrawer: [{
|
|
926
|
-
type: Input
|
|
927
|
-
}], confirmBrowser: [{
|
|
928
|
-
type: Input
|
|
929
1300
|
}], dirtySubmitButton: [{
|
|
930
1301
|
type: Input
|
|
931
1302
|
}], submit: [{
|
|
@@ -934,7 +1305,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.7", ngImpor
|
|
|
934
1305
|
type: Input
|
|
935
1306
|
}], errorDelay: [{
|
|
936
1307
|
type: Input
|
|
937
|
-
}],
|
|
1308
|
+
}], link: [{
|
|
938
1309
|
type: Input
|
|
939
1310
|
}], submitEvent: [{
|
|
940
1311
|
type: Output,
|
|
@@ -957,11 +1328,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.7", ngImpor
|
|
|
957
1328
|
class FsButtonDirective {
|
|
958
1329
|
name;
|
|
959
1330
|
dirtySubmit = true;
|
|
1331
|
+
set submitDisabled(value) {
|
|
1332
|
+
this._submitDisabled = value;
|
|
1333
|
+
this._applyDisabled();
|
|
1334
|
+
}
|
|
1335
|
+
get submitDisabled() {
|
|
1336
|
+
return this._submitDisabled;
|
|
1337
|
+
}
|
|
960
1338
|
transitionStyle = null;
|
|
961
1339
|
submit = false;
|
|
1340
|
+
_submitDisabled = false;
|
|
1341
|
+
_formDisabled = false;
|
|
962
1342
|
_previousDisabled = false;
|
|
963
1343
|
_destroy$ = new Subject();
|
|
964
|
-
|
|
1344
|
+
_container = inject(FsFormContainerDirective, { optional: true });
|
|
965
1345
|
_form = inject(FsFormDirective, { optional: true });
|
|
966
1346
|
_matButton = inject(MatButton, { optional: true, host: true });
|
|
967
1347
|
_elementRef = inject(ElementRef);
|
|
@@ -969,7 +1349,17 @@ class FsButtonDirective {
|
|
|
969
1349
|
_formBase;
|
|
970
1350
|
ngOnInit() {
|
|
971
1351
|
this.submit = this._elementRef.nativeElement.getAttribute('type') === 'submit';
|
|
972
|
-
|
|
1352
|
+
// Prefer the enclosing container over the form: the form's own button
|
|
1353
|
+
// management (dirty-submit tracking, activeSubmitButton, reset) resolves its
|
|
1354
|
+
// registry the same way, so buttons must register with the same owner.
|
|
1355
|
+
// Registering with the form while the form reads the container leaves the
|
|
1356
|
+
// container's button list empty and the submit button never gets
|
|
1357
|
+
// dirty-disabled.
|
|
1358
|
+
//
|
|
1359
|
+
// Note this resolves the ROOT of the linked set, not the nearest form: the
|
|
1360
|
+
// button that saves a tab's form typically sits outside it, down in the
|
|
1361
|
+
// dialog footer, and drives the whole linked set from there.
|
|
1362
|
+
this._formBase = this._container || this._form?.rootForm;
|
|
973
1363
|
if (this._formBase) {
|
|
974
1364
|
this._formBase.addButton(this);
|
|
975
1365
|
if (this.submit) {
|
|
@@ -991,16 +1381,16 @@ class FsButtonDirective {
|
|
|
991
1381
|
disable() {
|
|
992
1382
|
if (this._matButton && !this._formBase.activeSubmitButton) {
|
|
993
1383
|
this._previousDisabled = this._matButton.disabled;
|
|
994
|
-
this.
|
|
1384
|
+
this._formDisabled = true;
|
|
995
1385
|
this._matButton.disableRipple = true;
|
|
996
|
-
this.
|
|
1386
|
+
this._applyDisabled();
|
|
997
1387
|
}
|
|
998
1388
|
}
|
|
999
1389
|
enable() {
|
|
1000
1390
|
if (this._matButton) {
|
|
1001
|
-
this.
|
|
1391
|
+
this._formDisabled = false;
|
|
1002
1392
|
this._matButton.disableRipple = true;
|
|
1003
|
-
this.
|
|
1393
|
+
this._applyDisabled();
|
|
1004
1394
|
}
|
|
1005
1395
|
}
|
|
1006
1396
|
process() {
|
|
@@ -1048,6 +1438,15 @@ class FsButtonDirective {
|
|
|
1048
1438
|
this._destroy$.complete();
|
|
1049
1439
|
this._formBase?.removeButton(this);
|
|
1050
1440
|
}
|
|
1441
|
+
// Effective disabled combines the consumer-controlled submitDisabled input with
|
|
1442
|
+
// the form's submit-lifecycle state, so form enable/disable never clobbers an
|
|
1443
|
+
// explicit [submitDisabled] and the two channels stay independent.
|
|
1444
|
+
_applyDisabled() {
|
|
1445
|
+
if (this._matButton) {
|
|
1446
|
+
this._matButton.disabled = this._submitDisabled || this._formDisabled;
|
|
1447
|
+
this._cdRef.markForCheck();
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1051
1450
|
_resetClass() {
|
|
1052
1451
|
this.element.classList
|
|
1053
1452
|
.remove('fs-form-submit-button-success', 'fs-form-submit-button-error', 'fs-form-submit-button-process');
|
|
@@ -1078,7 +1477,7 @@ class FsButtonDirective {
|
|
|
1078
1477
|
}
|
|
1079
1478
|
}
|
|
1080
1479
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.7", ngImport: i0, type: FsButtonDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1081
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.7", type: FsButtonDirective, isStandalone: true, selector: "[mat-raised-button],[mat-button],[mat-flat-button],[mat-stroked-button]", inputs: { name: "name", dirtySubmit: "dirtySubmit" }, host: { properties: { "style.transition": "this.transitionStyle" } }, ngImport: i0 });
|
|
1480
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.7", type: FsButtonDirective, isStandalone: true, selector: "[mat-raised-button],[mat-button],[mat-flat-button],[mat-stroked-button]", inputs: { name: "name", dirtySubmit: "dirtySubmit", submitDisabled: "submitDisabled" }, host: { properties: { "style.transition": "this.transitionStyle" } }, ngImport: i0 });
|
|
1082
1481
|
}
|
|
1083
1482
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.7", ngImport: i0, type: FsButtonDirective, decorators: [{
|
|
1084
1483
|
type: Directive,
|
|
@@ -1090,6 +1489,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.7", ngImpor
|
|
|
1090
1489
|
type: Input
|
|
1091
1490
|
}], dirtySubmit: [{
|
|
1092
1491
|
type: Input
|
|
1492
|
+
}], submitDisabled: [{
|
|
1493
|
+
type: Input
|
|
1093
1494
|
}], transitionStyle: [{
|
|
1094
1495
|
type: HostBinding,
|
|
1095
1496
|
args: ['style.transition']
|
|
@@ -1137,30 +1538,53 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.7", ngImpor
|
|
|
1137
1538
|
}] } });
|
|
1138
1539
|
|
|
1139
1540
|
class FsFormDialogActionsComponent {
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
save = true;
|
|
1541
|
+
/** Leave unset inside an `fsFormContainer` to follow whether it has anything to save. */
|
|
1542
|
+
save;
|
|
1143
1543
|
create = false;
|
|
1144
1544
|
close = false;
|
|
1145
|
-
|
|
1545
|
+
/** Leave unset inside an `fsFormContainer` to fill in whenever Save does not apply. */
|
|
1546
|
+
done;
|
|
1146
1547
|
closeData = null;
|
|
1147
1548
|
name;
|
|
1148
1549
|
dirty = false;
|
|
1149
1550
|
_destroy$ = new Subject();
|
|
1551
|
+
_container = inject(FsFormContainerDirective, { optional: true });
|
|
1552
|
+
_form = inject(FsFormDirective, { optional: true });
|
|
1553
|
+
_cdRef = inject(ChangeDetectorRef);
|
|
1554
|
+
/**
|
|
1555
|
+
* Inside a container these default to whether anything mounted can actually
|
|
1556
|
+
* save, so a footer left as a bare `<fs-form-dialog-actions>` swaps Save for
|
|
1557
|
+
* Done as tabs change without the dialog listing which tabs are savable. An
|
|
1558
|
+
* explicit binding always wins, and outside a container Save stays on as it
|
|
1559
|
+
* always did.
|
|
1560
|
+
*/
|
|
1561
|
+
get showSave() {
|
|
1562
|
+
return this.save ?? (this._container ? this._container.submits : true);
|
|
1563
|
+
}
|
|
1564
|
+
get showDone() {
|
|
1565
|
+
return this.done ?? (this._container ? !this._container.submits && !this.create : false);
|
|
1566
|
+
}
|
|
1150
1567
|
ngOnInit() {
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1568
|
+
const owner = this._container || this._form?.rootForm;
|
|
1569
|
+
if (owner) {
|
|
1570
|
+
// Track the whole set, not one form's own controls: with a form scoped to a
|
|
1571
|
+
// tab's fields, the actions sit outside it in the dialog footer and that
|
|
1572
|
+
// form's NgForm holds nothing of its own. Recomputed on every emission
|
|
1573
|
+
// rather than latched, so a switch to a pristine tab clears it too.
|
|
1574
|
+
owner.dirtyChange$
|
|
1575
|
+
.pipe(takeUntil(this._destroy$))
|
|
1154
1576
|
.subscribe(() => {
|
|
1155
|
-
this.dirty =
|
|
1577
|
+
this.dirty = owner.dirtyLinked;
|
|
1156
1578
|
this._cdRef.markForCheck();
|
|
1157
1579
|
});
|
|
1158
|
-
|
|
1580
|
+
owner.submitted$
|
|
1159
1581
|
.pipe(delay(50), takeUntil(this._destroy$))
|
|
1160
1582
|
.subscribe(() => {
|
|
1161
1583
|
this.dirty = false;
|
|
1162
1584
|
this._cdRef.markForCheck();
|
|
1163
1585
|
});
|
|
1586
|
+
}
|
|
1587
|
+
if (this._form) {
|
|
1164
1588
|
this._form.reseted
|
|
1165
1589
|
.pipe(takeUntil(this._destroy$))
|
|
1166
1590
|
.subscribe(() => {
|
|
@@ -1174,7 +1598,7 @@ class FsFormDialogActionsComponent {
|
|
|
1174
1598
|
this._destroy$.complete();
|
|
1175
1599
|
}
|
|
1176
1600
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.7", ngImport: i0, type: FsFormDialogActionsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1177
|
-
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':
|
|
1601
|
+
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 });
|
|
1178
1602
|
}
|
|
1179
1603
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.7", ngImport: i0, type: FsFormDialogActionsComponent, decorators: [{
|
|
1180
1604
|
type: Component,
|
|
@@ -1185,7 +1609,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.7", ngImpor
|
|
|
1185
1609
|
MatDialogClose,
|
|
1186
1610
|
NgTemplateOutlet,
|
|
1187
1611
|
FsFormDialogCloseDirective,
|
|
1188
|
-
], template: "<div\n class=\"form-buttons\"\n [ngClass]=\"{ 'save-create':
|
|
1612
|
+
], 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"] }]
|
|
1189
1613
|
}], propDecorators: { save: [{
|
|
1190
1614
|
type: Input
|
|
1191
1615
|
}], create: [{
|
|
@@ -2484,7 +2908,7 @@ class FsFormModule {
|
|
|
2484
2908
|
FsFormTemplateDirective,
|
|
2485
2909
|
FsFormTemplateDirective,
|
|
2486
2910
|
FsFormTemplateOutletDirective,
|
|
2487
|
-
|
|
2911
|
+
FsFormContainerDirective], exports: [FsFormDirective,
|
|
2488
2912
|
FsControlDirective,
|
|
2489
2913
|
FsFormRequiredDirective,
|
|
2490
2914
|
FsFormMinDirective,
|
|
@@ -2514,7 +2938,7 @@ class FsFormModule {
|
|
|
2514
2938
|
FsFormTemplateDirective,
|
|
2515
2939
|
FsFormTemplateDirective,
|
|
2516
2940
|
FsFormTemplateOutletDirective,
|
|
2517
|
-
|
|
2941
|
+
FsFormContainerDirective] });
|
|
2518
2942
|
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "18.2.7", ngImport: i0, type: FsFormModule, providers: [
|
|
2519
2943
|
{
|
|
2520
2944
|
provide: ErrorStateMatcher,
|
|
@@ -2568,7 +2992,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.7", ngImpor
|
|
|
2568
2992
|
FsFormTemplateDirective,
|
|
2569
2993
|
FsFormTemplateDirective,
|
|
2570
2994
|
FsFormTemplateOutletDirective,
|
|
2571
|
-
|
|
2995
|
+
FsFormContainerDirective,
|
|
2572
2996
|
],
|
|
2573
2997
|
exports: [
|
|
2574
2998
|
FsFormDirective,
|
|
@@ -2601,7 +3025,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.7", ngImpor
|
|
|
2601
3025
|
FsFormTemplateDirective,
|
|
2602
3026
|
FsFormTemplateDirective,
|
|
2603
3027
|
FsFormTemplateOutletDirective,
|
|
2604
|
-
|
|
3028
|
+
FsFormContainerDirective,
|
|
2605
3029
|
],
|
|
2606
3030
|
providers: [
|
|
2607
3031
|
{
|
|
@@ -2618,5 +3042,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.7", ngImpor
|
|
|
2618
3042
|
* Generated bundle index. Do not edit.
|
|
2619
3043
|
*/
|
|
2620
3044
|
|
|
2621
|
-
export { ConfirmResult, FormDeactivateGuard, FormStatus, FsButtonDirective, FsControlDirective, FsForm, FsFormCompareDirective, FsFormDateRangeDirective, FsFormDialogActionsComponent, FsFormDialogCloseDirective, FsFormDirective, FsFormEmailDirective, FsFormEmailsDirective, FsFormFunctionDirective, FsFormGreaterDirective, FsFormGreaterEqualDirective,
|
|
3045
|
+
export { ConfirmResult, FormDeactivateGuard, FormStatus, FsButtonDirective, FsControlDirective, FsForm, FsFormCompareDirective, 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 };
|
|
2622
3046
|
//# sourceMappingURL=firestitch-form.mjs.map
|