@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.
Files changed (32) hide show
  1. package/app/components/form-dialog-actions/form-dialog-actions.component.d.ts +14 -2
  2. package/app/directives/button.directive.d.ts +7 -2
  3. package/app/directives/form/form.directive.d.ts +87 -29
  4. package/app/directives/form-base/form-base.directive.d.ts +73 -5
  5. package/app/directives/form-container/form-container.directive.d.ts +80 -0
  6. package/app/directives/form-container/index.d.ts +1 -0
  7. package/app/directives/index.d.ts +1 -1
  8. package/app/fs-form.module.d.ts +2 -2
  9. package/app/interfaces/form-owner.d.ts +17 -0
  10. package/app/interfaces/index.d.ts +1 -0
  11. package/app/services/fsform.service.d.ts +5 -5
  12. package/esm2022/app/components/form-dialog-actions/form-dialog-actions.component.mjs +41 -17
  13. package/esm2022/app/directives/button.directive.mjs +39 -9
  14. package/esm2022/app/directives/form/form.directive.mjs +249 -242
  15. package/esm2022/app/directives/form-base/form-base.directive.mjs +243 -6
  16. package/esm2022/app/directives/form-container/form-container.directive.mjs +164 -0
  17. package/esm2022/app/directives/form-container/index.mjs +2 -0
  18. package/esm2022/app/directives/index.mjs +2 -2
  19. package/esm2022/app/fs-form.module.mjs +6 -6
  20. package/esm2022/app/guards/form-deactivate.guard.mjs +1 -1
  21. package/esm2022/app/interfaces/form-owner.mjs +2 -0
  22. package/esm2022/app/interfaces/index.mjs +2 -1
  23. package/esm2022/app/services/fsform.service.mjs +10 -7
  24. package/esm2022/public_api.mjs +2 -2
  25. package/fesm2022/firestitch-form.mjs +743 -319
  26. package/fesm2022/firestitch-form.mjs.map +1 -1
  27. package/package.json +1 -1
  28. package/public_api.d.ts +2 -1
  29. package/app/directives/form-group/form-group.directive.d.ts +0 -15
  30. package/app/directives/form-group/index.d.ts +0 -1
  31. package/esm2022/app/directives/form-group/form-group.directive.mjs +0 -35
  32. 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, ElementRef, EventEmitter, Output, HostBinding, HostListener, InjectionToken, Renderer2, Injector, ViewContainerRef, NgModule } from '@angular/core';
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, BehaviorSubject, throwError, fromEvent, iif, defer, from, isObservable } from 'rxjs';
12
- import { switchMap, map, catchError, takeUntil, filter, tap, take, mergeMap, delay, first, startWith, mapTo } from 'rxjs/operators';
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
- return Object.keys(form.ngForm.control.controls)
113
- .some((key) => {
114
- const control = form.ngForm.control.controls[key];
115
- return control.dirty;
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.ngForm.control.markAsPristine();
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
- function getFormErrors(control, key) {
209
- let errors = null;
210
- if (control.invalid && control.errors) {
211
- errors = {
212
- [key]: { ...control.errors },
213
- };
214
- }
215
- if (control instanceof UntypedFormGroup) {
216
- Object.entries(control.controls)
217
- .forEach(([name, childControl]) => {
218
- const childErrors = getFormErrors(childControl, name);
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._registerConfirmTabs();
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
- class FsFormGroupDirective extends FsFormBaseDirective {
333
- _formDirective;
334
- triggerSubmit() {
335
- return this._formDirective?.triggerSubmit();
336
- }
337
- triggerConfirm() {
338
- return this._formDirective?.triggerConfirm();
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._formDirective?.submitting;
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
- get confirm() {
344
- return this._formDirective?.confirm;
682
+ triggerSubmit() {
683
+ this.rootForm?.triggerSubmit();
345
684
  }
346
- registerForm(formDirective) {
347
- this._formDirective = formDirective;
685
+ triggerConfirm() {
686
+ return this.rootForm
687
+ ? this.rootForm.triggerConfirm()
688
+ : of(ConfirmResult.NoChanges);
348
689
  }
349
- deregisterForm() {
350
- this._formDirective = null;
690
+ submit$(event) {
691
+ return this.rootForm
692
+ ? this.rootForm.submit$(event)
693
+ : of(null);
351
694
  }
352
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.7", ngImport: i0, type: FsFormGroupDirective, deps: null, target: i0.ɵɵFactoryTarget.Directive });
353
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.7", type: FsFormGroupDirective, isStandalone: true, selector: "[fsFormGroup]", exportAs: ["fsFormGroup"], usesInheritance: true, ngImport: i0 });
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: FsFormGroupDirective, decorators: [{
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: '[fsFormGroup]',
359
- exportAs: 'fsFormGroup',
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
- deactivationGuard = true;
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
- const submit$ = this._submit$ ?
430
- this._submit$(this._submitEvent) : of(submittedEvent);
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._getFormGroup().activeSubmitButton?.name;
891
+ return this._getOwner().activeSubmitButton?.name;
451
892
  }
452
893
  ngOnInit() {
453
- this._formGroup?.registerForm(this);
454
- if (this.deactivationGuard) {
455
- this._registerCanDeactivateGuard();
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
- super.ngAfterContentInit();
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
- this._formGroup?.deregisterForm();
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.ngForm.resetForm();
505
- Object.keys(this.ngForm.controls)
506
- .forEach((name) => {
507
- const control = this.ngForm.controls[name];
508
- control.reset(this._snapshot[name]);
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._getFormGroup()
1032
+ this._getOwner()
539
1033
  .buttons.forEach((button) => {
540
1034
  button.disable();
541
1035
  });
542
1036
  }
543
1037
  validate() {
544
- Object.values(this.ngForm.controls)
545
- .forEach((control) => {
546
- control.markAsDirty();
547
- control.markAsTouched();
548
- control.updateValueAndValidity();
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
- if (this.ngForm.status === 'INVALID') {
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._getFormGroup().activeSubmitButton) {
658
- return this._getFormGroup().activeSubmitButton;
1095
+ if (this._getOwner().activeSubmitButton) {
1096
+ return this._getOwner().activeSubmitButton;
659
1097
  }
660
- return this._getFormGroup()
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.ngForm.control.markAsPristine();
676
- this.createSnapshot();
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._getFormGroup().activeSubmitButton) {
1114
+ if (this._getOwner().activeSubmitButton) {
683
1115
  if (success) {
684
- this._getFormGroup().activeSubmitButton.success();
1116
+ this._getOwner().activeSubmitButton.success();
685
1117
  }
686
1118
  else {
687
- this._getFormGroup().activeSubmitButton.error();
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
- if (this.ngForm.form.status === 'VALID') {
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._getFormGroup().activeSubmitButton = null;
1143
+ this._getOwner().activeSubmitButton = null;
710
1144
  this._updateDirtySubmitButtons();
711
1145
  });
712
1146
  }
713
- _getFormGroup() {
714
- return this._formGroup || this;
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._getFormGroup()
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._getFormGroup()
1209
+ this._getOwner()
823
1210
  .buttons.changes
824
1211
  .pipe(takeUntil(this.destroy$))
825
1212
  .subscribe(() => {
826
- this._updateDirtySubmitButtons();
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._getFormGroup()
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.ngForm.dirty || !submitButton.dirtySubmit) {
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._getFormGroup()
1247
+ this._getOwner()
850
1248
  .activeSubmitButton = this._getActiveSubmitButton();
851
1249
  this._resetButtons();
852
- if (this._getFormGroup().activeSubmitButton) {
853
- this._getFormGroup().activeSubmitButton.process();
1250
+ if (this._getOwner().activeSubmitButton) {
1251
+ this._getOwner().activeSubmitButton.process();
854
1252
  }
855
1253
  }
856
1254
  _disableButtons() {
857
- this._getFormGroup()
1255
+ this._getOwner()
858
1256
  .buttons.forEach((button) => {
859
1257
  button.disable();
860
1258
  });
861
1259
  }
862
1260
  _waitUntilStatusPending() {
863
- return this.ngForm.statusChanges
864
- .pipe(startWith(this.ngForm.status), first((state) => state !== 'PENDING'));
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", shortcuts: "shortcuts", confirm: "confirm", confirmDialog: "confirmDialog", confirmDrawer: "confirmDrawer", confirmBrowser: "confirmBrowser", dirtySubmitButton: "dirtySubmitButton", submit: "submit", successDelay: "successDelay", errorDelay: "errorDelay", deactivationGuard: "deactivationGuard" }, 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 });
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
- }], deactivationGuard: [{
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
- _formGroup = inject(FsFormGroupDirective, { optional: true });
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
- this._formBase = this._form || this._formGroup;
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._matButton.disabled = true;
1384
+ this._formDisabled = true;
995
1385
  this._matButton.disableRipple = true;
996
- this._cdRef.markForCheck();
1386
+ this._applyDisabled();
997
1387
  }
998
1388
  }
999
1389
  enable() {
1000
1390
  if (this._matButton) {
1001
- this._matButton.disabled = false;
1391
+ this._formDisabled = false;
1002
1392
  this._matButton.disableRipple = true;
1003
- this._cdRef.markForCheck();
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
- _form = inject(FsFormDirective, { optional: true });
1141
- _cdRef = inject(ChangeDetectorRef);
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
- done = false;
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
- if (this._form) {
1152
- this._form.ngForm.valueChanges
1153
- .pipe(filter(() => (!this.dirty)), takeUntil(this._destroy$))
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 = this._form.ngForm.dirty;
1577
+ this.dirty = owner.dirtyLinked;
1156
1578
  this._cdRef.markForCheck();
1157
1579
  });
1158
- this._form.submitted
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': save || create }\">\n @if (save || 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 (done) {\n <button\n mat-button\n type=\"button\"\n color=\"primary\"\n [mat-dialog-close]=\"null\">\n Done\n </button>\n }\n @if (save || 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 (!(save || create)) {\n <ng-template [ngTemplateOutlet]=\"content\"></ng-template>\n }\n</div>\n<ng-template #content>\n <ng-content></ng-content>\n</ng-template>", 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"] }, { 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 });
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': save || create }\">\n @if (save || 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 (done) {\n <button\n mat-button\n type=\"button\"\n color=\"primary\"\n [mat-dialog-close]=\"null\">\n Done\n </button>\n }\n @if (save || 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 (!(save || create)) {\n <ng-template [ngTemplateOutlet]=\"content\"></ng-template>\n }\n</div>\n<ng-template #content>\n <ng-content></ng-content>\n</ng-template>", styles: [":host{width:100%}.form-buttons.save-create .close-button{float:right}\n"] }]
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
- FsFormGroupDirective], exports: [FsFormDirective,
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
- FsFormGroupDirective] });
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
- FsFormGroupDirective,
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
- FsFormGroupDirective,
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, FsFormGroupDirective, FsFormIntegerDirective, FsFormLesserDirective, FsFormLesserEqualDirective, FsFormMaxDirective, FsFormMaxLengthDirective, FsFormMinDirective, FsFormMinLengthDirective, FsFormModule, FsFormNoFsValidatorsDirective, FsFormNumericDirective, FsFormPatternDirective, FsFormPhoneDirective, FsFormRequiredDirective, FsFormTemplateDirective, FsFormTemplateOutletDirective, FsFormUrlDirective, FsFormValidateDirective, FsSubmitButtonDirective, FsValidators };
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