@firestitch/form 18.0.29 → 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 +1 -1
- 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 +13 -8
- package/esm2022/app/directives/form/form.directive.mjs +238 -241
- package/esm2022/app/directives/form-base/form-base.directive.mjs +239 -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 +702 -317
- 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
|
}
|
|
@@ -277,6 +309,160 @@ class FsFormBaseDirective {
|
|
|
277
309
|
]);
|
|
278
310
|
this._buttons.notifyOnChanges();
|
|
279
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
|
+
}
|
|
465
|
+
}
|
|
280
466
|
_registerConfirmTabs() {
|
|
281
467
|
this._registerConfirmTabGroups(this._tabGroups.toArray());
|
|
282
468
|
this._tabGroups.changes
|
|
@@ -291,6 +477,15 @@ class FsFormBaseDirective {
|
|
|
291
477
|
});
|
|
292
478
|
}
|
|
293
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);
|
|
294
489
|
const confirmTabGroup = tabGroup;
|
|
295
490
|
if (!confirmTabGroup._originalHandleClick) {
|
|
296
491
|
confirmTabGroup._originalHandleClick = tabGroup._handleClick;
|
|
@@ -319,8 +514,29 @@ class FsFormBaseDirective {
|
|
|
319
514
|
}), takeUntil(this._destroy$))
|
|
320
515
|
.subscribe();
|
|
321
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
|
+
}
|
|
322
538
|
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", 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 });
|
|
324
540
|
}
|
|
325
541
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.7", ngImport: i0, type: FsFormBaseDirective, decorators: [{
|
|
326
542
|
type: Directive
|
|
@@ -328,62 +544,187 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.7", ngImpor
|
|
|
328
544
|
type: Input
|
|
329
545
|
}], tabGroup: [{
|
|
330
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
|
|
331
559
|
}], _tabGroups: [{
|
|
332
560
|
type: ContentChildren,
|
|
333
561
|
args: [MatTabGroup, { descendants: true }]
|
|
334
562
|
}] } });
|
|
335
563
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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;
|
|
343
617
|
}
|
|
344
618
|
get submitting() {
|
|
345
|
-
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();
|
|
681
|
+
}
|
|
682
|
+
triggerSubmit() {
|
|
683
|
+
this.rootForm?.triggerSubmit();
|
|
346
684
|
}
|
|
347
|
-
|
|
348
|
-
return this.
|
|
685
|
+
triggerConfirm() {
|
|
686
|
+
return this.rootForm
|
|
687
|
+
? this.rootForm.triggerConfirm()
|
|
688
|
+
: of(ConfirmResult.NoChanges);
|
|
349
689
|
}
|
|
350
|
-
|
|
351
|
-
this.
|
|
690
|
+
submit$(event) {
|
|
691
|
+
return this.rootForm
|
|
692
|
+
? this.rootForm.submit$(event)
|
|
693
|
+
: of(null);
|
|
352
694
|
}
|
|
353
|
-
|
|
354
|
-
this.
|
|
695
|
+
reset() {
|
|
696
|
+
this.rootForm?.reset();
|
|
355
697
|
}
|
|
356
|
-
|
|
357
|
-
|
|
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 });
|
|
358
711
|
}
|
|
359
|
-
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: [{
|
|
360
713
|
type: Directive,
|
|
361
714
|
args: [{
|
|
362
|
-
selector: '[
|
|
363
|
-
exportAs: '
|
|
715
|
+
selector: 'fs-form-container,[fsFormContainer]',
|
|
716
|
+
exportAs: 'fsFormContainer',
|
|
364
717
|
standalone: true,
|
|
365
718
|
}]
|
|
366
719
|
}] });
|
|
367
720
|
|
|
368
721
|
class FsFormDirective extends FsFormBaseDirective {
|
|
369
722
|
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
723
|
wrapperSelector = '.fs-form-wrapper,.mat-mdc-form-field';
|
|
378
724
|
messageSelector = '.fs-form-message,.mat-mdc-form-field-subscript-wrapper';
|
|
379
725
|
hintSelector = '.fs-form-hint,.mat-mdc-form-field-hint-wrapper';
|
|
380
726
|
labelSelector = '.fs-form-label,.mdc-floating-label>mat-label';
|
|
381
727
|
autocomplete = false;
|
|
382
|
-
shortcuts = true; // Ctrl + s
|
|
383
|
-
confirm = true;
|
|
384
|
-
confirmDialog = true;
|
|
385
|
-
confirmDrawer = true;
|
|
386
|
-
confirmBrowser = true;
|
|
387
728
|
dirtySubmitButton = true;
|
|
388
729
|
set submit(submit$) {
|
|
389
730
|
this._submit$ = submit$;
|
|
@@ -393,7 +734,21 @@ class FsFormDirective extends FsFormBaseDirective {
|
|
|
393
734
|
}
|
|
394
735
|
successDelay = 0;
|
|
395
736
|
errorDelay = 1000;
|
|
396
|
-
|
|
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;
|
|
397
752
|
submitEvent = new EventEmitter();
|
|
398
753
|
invalid = new EventEmitter();
|
|
399
754
|
valid = new EventEmitter();
|
|
@@ -402,11 +757,75 @@ class FsFormDirective extends FsFormBaseDirective {
|
|
|
402
757
|
cleared = new EventEmitter();
|
|
403
758
|
fsFormClass = true;
|
|
404
759
|
_registerControl;
|
|
405
|
-
_dialogBackdropEscape = false;
|
|
406
760
|
_snapshot = {};
|
|
407
|
-
_activatedRouteConfig;
|
|
408
761
|
_status$ = new BehaviorSubject(FormStatus.Valid);
|
|
409
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
|
+
}
|
|
410
829
|
get submitting() {
|
|
411
830
|
return this._status$.getValue() === FormStatus.Submitting;
|
|
412
831
|
}
|
|
@@ -416,6 +835,15 @@ class FsFormDirective extends FsFormBaseDirective {
|
|
|
416
835
|
get completing() {
|
|
417
836
|
return this._status$.getValue() === FormStatus.Completing;
|
|
418
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
|
+
}
|
|
419
847
|
get _submitEvent() {
|
|
420
848
|
return {
|
|
421
849
|
ngForm: this.ngForm,
|
|
@@ -430,8 +858,17 @@ class FsFormDirective extends FsFormBaseDirective {
|
|
|
430
858
|
submitter: this._submitter,
|
|
431
859
|
response: null,
|
|
432
860
|
};
|
|
433
|
-
|
|
434
|
-
|
|
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);
|
|
435
872
|
return submit$
|
|
436
873
|
.pipe(map((response) => {
|
|
437
874
|
submittedEvent.response = response;
|
|
@@ -451,22 +888,67 @@ class FsFormDirective extends FsFormBaseDirective {
|
|
|
451
888
|
return throwError(() => new Error('Form validation error'));
|
|
452
889
|
}
|
|
453
890
|
get _submitter() {
|
|
454
|
-
return this.
|
|
891
|
+
return this._getOwner().activeSubmitButton?.name;
|
|
455
892
|
}
|
|
456
893
|
ngOnInit() {
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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();
|
|
460
910
|
}
|
|
461
|
-
this._registerConfirmDialogBackdropEscape();
|
|
462
|
-
this._listenHotKeys();
|
|
463
|
-
this._listenWindowClose();
|
|
464
911
|
this._listenSubmit();
|
|
465
912
|
this._listenFormStatus();
|
|
466
913
|
if (!this.autocomplete) {
|
|
467
914
|
this._registerAutocomplete();
|
|
468
915
|
}
|
|
469
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
|
+
}
|
|
470
952
|
ngOnChanges(changes) {
|
|
471
953
|
if (changes.confirm) {
|
|
472
954
|
this._updateDirtySubmitButtons();
|
|
@@ -487,16 +969,21 @@ class FsFormDirective extends FsFormBaseDirective {
|
|
|
487
969
|
});
|
|
488
970
|
}
|
|
489
971
|
ngAfterContentInit() {
|
|
490
|
-
|
|
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
|
+
}
|
|
491
978
|
this._registerConfirm();
|
|
492
|
-
this._registerConfirmDrawerClose();
|
|
493
|
-
this._registerDrawerClose();
|
|
494
979
|
this._registerDirtySubmitButton();
|
|
495
980
|
}
|
|
496
981
|
ngOnDestroy() {
|
|
497
|
-
|
|
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);
|
|
498
986
|
super.ngOnDestroy();
|
|
499
|
-
this._cleanupCanDeactivate();
|
|
500
987
|
}
|
|
501
988
|
createSnapshot() {
|
|
502
989
|
this._snapshot = this.ngForm.value;
|
|
@@ -505,11 +992,14 @@ class FsFormDirective extends FsFormBaseDirective {
|
|
|
505
992
|
return this._snapshot || {};
|
|
506
993
|
}
|
|
507
994
|
reset() {
|
|
508
|
-
this.
|
|
509
|
-
|
|
510
|
-
.
|
|
511
|
-
|
|
512
|
-
|
|
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
|
+
});
|
|
513
1003
|
});
|
|
514
1004
|
this.reseted.emit();
|
|
515
1005
|
}
|
|
@@ -539,24 +1029,29 @@ class FsFormDirective extends FsFormBaseDirective {
|
|
|
539
1029
|
}
|
|
540
1030
|
disable() {
|
|
541
1031
|
this.ngForm.control.disable();
|
|
542
|
-
this.
|
|
1032
|
+
this._getOwner()
|
|
543
1033
|
.buttons.forEach((button) => {
|
|
544
1034
|
button.disable();
|
|
545
1035
|
});
|
|
546
1036
|
}
|
|
547
1037
|
validate() {
|
|
548
|
-
|
|
549
|
-
.forEach((
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
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
|
+
});
|
|
553
1046
|
});
|
|
554
1047
|
}
|
|
555
1048
|
submit$(submitEvent) {
|
|
556
1049
|
return of(submitEvent)
|
|
557
1050
|
.pipe(tap(() => this._statusValidating()), tap(() => this.validate()), tap(() => this._statusSubmitting()), tap(() => this._setupActiveSubmitButton()), tap(() => this._disableButtons()), switchMap((data) => this._waitUntilStatusPending()
|
|
558
1051
|
.pipe(map(() => data))), mergeMap(() => {
|
|
559
|
-
|
|
1052
|
+
const invalid = this.linkedForms
|
|
1053
|
+
.some((form) => form.ngForm.status === 'INVALID');
|
|
1054
|
+
if (invalid) {
|
|
560
1055
|
return this._formInvalidState$;
|
|
561
1056
|
}
|
|
562
1057
|
return this._formValidState$;
|
|
@@ -596,99 +1091,32 @@ class FsFormDirective extends FsFormBaseDirective {
|
|
|
596
1091
|
}
|
|
597
1092
|
});
|
|
598
1093
|
}
|
|
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
1094
|
_getActiveSubmitButton() {
|
|
661
|
-
if (this.
|
|
662
|
-
return this.
|
|
1095
|
+
if (this._getOwner().activeSubmitButton) {
|
|
1096
|
+
return this._getOwner().activeSubmitButton;
|
|
663
1097
|
}
|
|
664
|
-
return this.
|
|
1098
|
+
return this._getOwner()
|
|
665
1099
|
.buttons
|
|
666
1100
|
.filter((button) => button.submit)[0];
|
|
667
1101
|
}
|
|
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
1102
|
_completeSubmit(success, submitEvent) {
|
|
678
1103
|
if (success) {
|
|
679
|
-
this.
|
|
680
|
-
|
|
1104
|
+
this.linkedForms
|
|
1105
|
+
.forEach((form) => {
|
|
1106
|
+
form.ngForm.control.markAsPristine();
|
|
1107
|
+
form.createSnapshot();
|
|
1108
|
+
});
|
|
681
1109
|
this.submitted.emit(submitEvent);
|
|
682
1110
|
}
|
|
683
1111
|
else {
|
|
684
1112
|
this._resetButtons();
|
|
685
1113
|
}
|
|
686
|
-
if (this.
|
|
1114
|
+
if (this._getOwner().activeSubmitButton) {
|
|
687
1115
|
if (success) {
|
|
688
|
-
this.
|
|
1116
|
+
this._getOwner().activeSubmitButton.success();
|
|
689
1117
|
}
|
|
690
1118
|
else {
|
|
691
|
-
this.
|
|
1119
|
+
this._getOwner().activeSubmitButton.error();
|
|
692
1120
|
}
|
|
693
1121
|
}
|
|
694
1122
|
this._status$.next(FormStatus.Submitted);
|
|
@@ -703,22 +1131,35 @@ class FsFormDirective extends FsFormBaseDirective {
|
|
|
703
1131
|
of(true)
|
|
704
1132
|
.pipe(delay(resetDelay), first(), takeUntil(this.destroy$))
|
|
705
1133
|
.subscribe(() => {
|
|
706
|
-
|
|
1134
|
+
const valid = this.linkedForms
|
|
1135
|
+
.every((form) => form.ngForm.form.status === 'VALID');
|
|
1136
|
+
if (valid) {
|
|
707
1137
|
this._status$.next(FormStatus.Valid);
|
|
708
1138
|
}
|
|
709
1139
|
else {
|
|
710
1140
|
this._status$.next(FormStatus.Invalid);
|
|
711
1141
|
}
|
|
712
1142
|
this._resetButtons();
|
|
713
|
-
this.
|
|
1143
|
+
this._getOwner().activeSubmitButton = null;
|
|
714
1144
|
this._updateDirtySubmitButtons();
|
|
715
1145
|
});
|
|
716
1146
|
}
|
|
717
|
-
|
|
718
|
-
|
|
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;
|
|
719
1160
|
}
|
|
720
1161
|
_resetButtons() {
|
|
721
|
-
this.
|
|
1162
|
+
this._getOwner()
|
|
722
1163
|
.buttons.forEach((button) => {
|
|
723
1164
|
button.reset();
|
|
724
1165
|
});
|
|
@@ -741,66 +1182,6 @@ class FsFormDirective extends FsFormBaseDirective {
|
|
|
741
1182
|
}
|
|
742
1183
|
});
|
|
743
1184
|
}
|
|
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
1185
|
_registerAutocomplete() {
|
|
805
1186
|
this._registerControl = this.ngForm.form.registerControl.bind(this.ngForm.form);
|
|
806
1187
|
this.ngForm.form.registerControl = (name, control) => {
|
|
@@ -818,12 +1199,14 @@ class FsFormDirective extends FsFormBaseDirective {
|
|
|
818
1199
|
if (!this.ngForm) {
|
|
819
1200
|
return;
|
|
820
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.
|
|
821
1204
|
this.ngForm.form.valueChanges
|
|
822
1205
|
.pipe(takeUntil(this.destroy$))
|
|
823
1206
|
.subscribe(() => {
|
|
824
|
-
this._updateDirtySubmitButtons();
|
|
1207
|
+
this.rootForm._updateDirtySubmitButtons();
|
|
825
1208
|
});
|
|
826
|
-
this.
|
|
1209
|
+
this._getOwner()
|
|
827
1210
|
.buttons.changes
|
|
828
1211
|
.pipe(takeUntil(this.destroy$))
|
|
829
1212
|
.subscribe(() => {
|
|
@@ -841,11 +1224,12 @@ class FsFormDirective extends FsFormBaseDirective {
|
|
|
841
1224
|
if (!this.ngForm) {
|
|
842
1225
|
return;
|
|
843
1226
|
}
|
|
844
|
-
this.
|
|
1227
|
+
this._dirtyChange$.next();
|
|
1228
|
+
this._getOwner()
|
|
845
1229
|
.buttons
|
|
846
1230
|
.filter((button) => button.submit)
|
|
847
1231
|
.forEach((submitButton) => {
|
|
848
|
-
if (!this.confirm || !this.dirtySubmitButton || this.
|
|
1232
|
+
if (!this.confirm || !this.dirtySubmitButton || this.dirtyLinked || !submitButton.dirtySubmit) {
|
|
849
1233
|
submitButton.enable();
|
|
850
1234
|
}
|
|
851
1235
|
else {
|
|
@@ -860,22 +1244,26 @@ class FsFormDirective extends FsFormBaseDirective {
|
|
|
860
1244
|
this._status$.next(FormStatus.Validating);
|
|
861
1245
|
}
|
|
862
1246
|
_setupActiveSubmitButton() {
|
|
863
|
-
this.
|
|
1247
|
+
this._getOwner()
|
|
864
1248
|
.activeSubmitButton = this._getActiveSubmitButton();
|
|
865
1249
|
this._resetButtons();
|
|
866
|
-
if (this.
|
|
867
|
-
this.
|
|
1250
|
+
if (this._getOwner().activeSubmitButton) {
|
|
1251
|
+
this._getOwner().activeSubmitButton.process();
|
|
868
1252
|
}
|
|
869
1253
|
}
|
|
870
1254
|
_disableButtons() {
|
|
871
|
-
this.
|
|
1255
|
+
this._getOwner()
|
|
872
1256
|
.buttons.forEach((button) => {
|
|
873
1257
|
button.disable();
|
|
874
1258
|
});
|
|
875
1259
|
}
|
|
876
1260
|
_waitUntilStatusPending() {
|
|
877
|
-
|
|
878
|
-
|
|
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')));
|
|
879
1267
|
}
|
|
880
1268
|
_handleError(e) {
|
|
881
1269
|
console.log('%c Form Submit ', 'color: white; background-color: #D33F49', 'Error occured');
|
|
@@ -889,29 +1277,8 @@ class FsFormDirective extends FsFormBaseDirective {
|
|
|
889
1277
|
console.groupEnd();
|
|
890
1278
|
this._completeSubmit(false, null);
|
|
891
1279
|
}
|
|
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
1280
|
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",
|
|
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 });
|
|
915
1282
|
}
|
|
916
1283
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.7", ngImport: i0, type: FsFormDirective, decorators: [{
|
|
917
1284
|
type: Directive,
|
|
@@ -930,16 +1297,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.7", ngImpor
|
|
|
930
1297
|
type: Input
|
|
931
1298
|
}], autocomplete: [{
|
|
932
1299
|
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
1300
|
}], dirtySubmitButton: [{
|
|
944
1301
|
type: Input
|
|
945
1302
|
}], submit: [{
|
|
@@ -948,7 +1305,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.7", ngImpor
|
|
|
948
1305
|
type: Input
|
|
949
1306
|
}], errorDelay: [{
|
|
950
1307
|
type: Input
|
|
951
|
-
}],
|
|
1308
|
+
}], link: [{
|
|
952
1309
|
type: Input
|
|
953
1310
|
}], submitEvent: [{
|
|
954
1311
|
type: Output,
|
|
@@ -984,7 +1341,7 @@ class FsButtonDirective {
|
|
|
984
1341
|
_formDisabled = false;
|
|
985
1342
|
_previousDisabled = false;
|
|
986
1343
|
_destroy$ = new Subject();
|
|
987
|
-
|
|
1344
|
+
_container = inject(FsFormContainerDirective, { optional: true });
|
|
988
1345
|
_form = inject(FsFormDirective, { optional: true });
|
|
989
1346
|
_matButton = inject(MatButton, { optional: true, host: true });
|
|
990
1347
|
_elementRef = inject(ElementRef);
|
|
@@ -992,12 +1349,17 @@ class FsButtonDirective {
|
|
|
992
1349
|
_formBase;
|
|
993
1350
|
ngOnInit() {
|
|
994
1351
|
this.submit = this._elementRef.nativeElement.getAttribute('type') === 'submit';
|
|
995
|
-
// Prefer the enclosing
|
|
1352
|
+
// Prefer the enclosing container over the form: the form's own button
|
|
996
1353
|
// management (dirty-submit tracking, activeSubmitButton, reset) resolves its
|
|
997
|
-
// registry
|
|
998
|
-
//
|
|
999
|
-
//
|
|
1000
|
-
|
|
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;
|
|
1001
1363
|
if (this._formBase) {
|
|
1002
1364
|
this._formBase.addButton(this);
|
|
1003
1365
|
if (this.submit) {
|
|
@@ -1176,30 +1538,53 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.7", ngImpor
|
|
|
1176
1538
|
}] } });
|
|
1177
1539
|
|
|
1178
1540
|
class FsFormDialogActionsComponent {
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
save = true;
|
|
1541
|
+
/** Leave unset inside an `fsFormContainer` to follow whether it has anything to save. */
|
|
1542
|
+
save;
|
|
1182
1543
|
create = false;
|
|
1183
1544
|
close = false;
|
|
1184
|
-
|
|
1545
|
+
/** Leave unset inside an `fsFormContainer` to fill in whenever Save does not apply. */
|
|
1546
|
+
done;
|
|
1185
1547
|
closeData = null;
|
|
1186
1548
|
name;
|
|
1187
1549
|
dirty = false;
|
|
1188
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
|
+
}
|
|
1189
1567
|
ngOnInit() {
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
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$))
|
|
1193
1576
|
.subscribe(() => {
|
|
1194
|
-
this.dirty =
|
|
1577
|
+
this.dirty = owner.dirtyLinked;
|
|
1195
1578
|
this._cdRef.markForCheck();
|
|
1196
1579
|
});
|
|
1197
|
-
|
|
1580
|
+
owner.submitted$
|
|
1198
1581
|
.pipe(delay(50), takeUntil(this._destroy$))
|
|
1199
1582
|
.subscribe(() => {
|
|
1200
1583
|
this.dirty = false;
|
|
1201
1584
|
this._cdRef.markForCheck();
|
|
1202
1585
|
});
|
|
1586
|
+
}
|
|
1587
|
+
if (this._form) {
|
|
1203
1588
|
this._form.reseted
|
|
1204
1589
|
.pipe(takeUntil(this._destroy$))
|
|
1205
1590
|
.subscribe(() => {
|
|
@@ -1213,7 +1598,7 @@ class FsFormDialogActionsComponent {
|
|
|
1213
1598
|
this._destroy$.complete();
|
|
1214
1599
|
}
|
|
1215
1600
|
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':
|
|
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 });
|
|
1217
1602
|
}
|
|
1218
1603
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.7", ngImport: i0, type: FsFormDialogActionsComponent, decorators: [{
|
|
1219
1604
|
type: Component,
|
|
@@ -1224,7 +1609,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.7", ngImpor
|
|
|
1224
1609
|
MatDialogClose,
|
|
1225
1610
|
NgTemplateOutlet,
|
|
1226
1611
|
FsFormDialogCloseDirective,
|
|
1227
|
-
], 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"] }]
|
|
1228
1613
|
}], propDecorators: { save: [{
|
|
1229
1614
|
type: Input
|
|
1230
1615
|
}], create: [{
|
|
@@ -2523,7 +2908,7 @@ class FsFormModule {
|
|
|
2523
2908
|
FsFormTemplateDirective,
|
|
2524
2909
|
FsFormTemplateDirective,
|
|
2525
2910
|
FsFormTemplateOutletDirective,
|
|
2526
|
-
|
|
2911
|
+
FsFormContainerDirective], exports: [FsFormDirective,
|
|
2527
2912
|
FsControlDirective,
|
|
2528
2913
|
FsFormRequiredDirective,
|
|
2529
2914
|
FsFormMinDirective,
|
|
@@ -2553,7 +2938,7 @@ class FsFormModule {
|
|
|
2553
2938
|
FsFormTemplateDirective,
|
|
2554
2939
|
FsFormTemplateDirective,
|
|
2555
2940
|
FsFormTemplateOutletDirective,
|
|
2556
|
-
|
|
2941
|
+
FsFormContainerDirective] });
|
|
2557
2942
|
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "18.2.7", ngImport: i0, type: FsFormModule, providers: [
|
|
2558
2943
|
{
|
|
2559
2944
|
provide: ErrorStateMatcher,
|
|
@@ -2607,7 +2992,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.7", ngImpor
|
|
|
2607
2992
|
FsFormTemplateDirective,
|
|
2608
2993
|
FsFormTemplateDirective,
|
|
2609
2994
|
FsFormTemplateOutletDirective,
|
|
2610
|
-
|
|
2995
|
+
FsFormContainerDirective,
|
|
2611
2996
|
],
|
|
2612
2997
|
exports: [
|
|
2613
2998
|
FsFormDirective,
|
|
@@ -2640,7 +3025,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.7", ngImpor
|
|
|
2640
3025
|
FsFormTemplateDirective,
|
|
2641
3026
|
FsFormTemplateDirective,
|
|
2642
3027
|
FsFormTemplateOutletDirective,
|
|
2643
|
-
|
|
3028
|
+
FsFormContainerDirective,
|
|
2644
3029
|
],
|
|
2645
3030
|
providers: [
|
|
2646
3031
|
{
|
|
@@ -2657,5 +3042,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.7", ngImpor
|
|
|
2657
3042
|
* Generated bundle index. Do not edit.
|
|
2658
3043
|
*/
|
|
2659
3044
|
|
|
2660
|
-
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 };
|
|
2661
3046
|
//# sourceMappingURL=firestitch-form.mjs.map
|