@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.
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 +1 -1
  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 +13 -8
  14. package/esm2022/app/directives/form/form.directive.mjs +238 -241
  15. package/esm2022/app/directives/form-base/form-base.directive.mjs +239 -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 +702 -317
  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
  }
@@ -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
- class FsFormGroupDirective extends FsFormBaseDirective {
337
- _formDirective;
338
- triggerSubmit() {
339
- return this._formDirective?.triggerSubmit();
340
- }
341
- triggerConfirm() {
342
- 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;
343
617
  }
344
618
  get submitting() {
345
- 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();
681
+ }
682
+ triggerSubmit() {
683
+ this.rootForm?.triggerSubmit();
346
684
  }
347
- get confirm() {
348
- return this._formDirective?.confirm;
685
+ triggerConfirm() {
686
+ return this.rootForm
687
+ ? this.rootForm.triggerConfirm()
688
+ : of(ConfirmResult.NoChanges);
349
689
  }
350
- registerForm(formDirective) {
351
- this._formDirective = formDirective;
690
+ submit$(event) {
691
+ return this.rootForm
692
+ ? this.rootForm.submit$(event)
693
+ : of(null);
352
694
  }
353
- deregisterForm() {
354
- this._formDirective = null;
695
+ reset() {
696
+ this.rootForm?.reset();
355
697
  }
356
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.7", ngImport: i0, type: FsFormGroupDirective, deps: null, target: i0.ɵɵFactoryTarget.Directive });
357
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.7", type: FsFormGroupDirective, isStandalone: true, selector: "[fsFormGroup]", exportAs: ["fsFormGroup"], usesInheritance: true, ngImport: i0 });
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: FsFormGroupDirective, decorators: [{
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: '[fsFormGroup]',
363
- exportAs: 'fsFormGroup',
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
- 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;
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
- const submit$ = this._submit$ ?
434
- 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);
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._getFormGroup().activeSubmitButton?.name;
891
+ return this._getOwner().activeSubmitButton?.name;
455
892
  }
456
893
  ngOnInit() {
457
- this._formGroup?.registerForm(this);
458
- if (this.deactivationGuard) {
459
- 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();
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
- 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
+ }
491
978
  this._registerConfirm();
492
- this._registerConfirmDrawerClose();
493
- this._registerDrawerClose();
494
979
  this._registerDirtySubmitButton();
495
980
  }
496
981
  ngOnDestroy() {
497
- 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);
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.ngForm.resetForm();
509
- Object.keys(this.ngForm.controls)
510
- .forEach((name) => {
511
- const control = this.ngForm.controls[name];
512
- 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
+ });
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._getFormGroup()
1032
+ this._getOwner()
543
1033
  .buttons.forEach((button) => {
544
1034
  button.disable();
545
1035
  });
546
1036
  }
547
1037
  validate() {
548
- Object.values(this.ngForm.controls)
549
- .forEach((control) => {
550
- control.markAsDirty();
551
- control.markAsTouched();
552
- 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
+ });
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
- if (this.ngForm.status === 'INVALID') {
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._getFormGroup().activeSubmitButton) {
662
- return this._getFormGroup().activeSubmitButton;
1095
+ if (this._getOwner().activeSubmitButton) {
1096
+ return this._getOwner().activeSubmitButton;
663
1097
  }
664
- return this._getFormGroup()
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.ngForm.control.markAsPristine();
680
- this.createSnapshot();
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._getFormGroup().activeSubmitButton) {
1114
+ if (this._getOwner().activeSubmitButton) {
687
1115
  if (success) {
688
- this._getFormGroup().activeSubmitButton.success();
1116
+ this._getOwner().activeSubmitButton.success();
689
1117
  }
690
1118
  else {
691
- this._getFormGroup().activeSubmitButton.error();
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
- if (this.ngForm.form.status === 'VALID') {
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._getFormGroup().activeSubmitButton = null;
1143
+ this._getOwner().activeSubmitButton = null;
714
1144
  this._updateDirtySubmitButtons();
715
1145
  });
716
1146
  }
717
- _getFormGroup() {
718
- 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;
719
1160
  }
720
1161
  _resetButtons() {
721
- this._getFormGroup()
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._getFormGroup()
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._getFormGroup()
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.ngForm.dirty || !submitButton.dirtySubmit) {
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._getFormGroup()
1247
+ this._getOwner()
864
1248
  .activeSubmitButton = this._getActiveSubmitButton();
865
1249
  this._resetButtons();
866
- if (this._getFormGroup().activeSubmitButton) {
867
- this._getFormGroup().activeSubmitButton.process();
1250
+ if (this._getOwner().activeSubmitButton) {
1251
+ this._getOwner().activeSubmitButton.process();
868
1252
  }
869
1253
  }
870
1254
  _disableButtons() {
871
- this._getFormGroup()
1255
+ this._getOwner()
872
1256
  .buttons.forEach((button) => {
873
1257
  button.disable();
874
1258
  });
875
1259
  }
876
1260
  _waitUntilStatusPending() {
877
- return this.ngForm.statusChanges
878
- .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')));
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", 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 });
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
- }], deactivationGuard: [{
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
- _formGroup = inject(FsFormGroupDirective, { optional: true });
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 form group over the form: the form's own button
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 via `_formGroup || this`, so buttons must register with the same
998
- // owner. Registering with the form while the form reads the group leaves the
999
- // group's button list empty and the submit button never gets dirty-disabled.
1000
- this._formBase = this._formGroup || this._form;
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
- _form = inject(FsFormDirective, { optional: true });
1180
- _cdRef = inject(ChangeDetectorRef);
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
- done = false;
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
- if (this._form) {
1191
- this._form.ngForm.valueChanges
1192
- .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$))
1193
1576
  .subscribe(() => {
1194
- this.dirty = this._form.ngForm.dirty;
1577
+ this.dirty = owner.dirtyLinked;
1195
1578
  this._cdRef.markForCheck();
1196
1579
  });
1197
- this._form.submitted
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': 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", "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 });
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': 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"] }]
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
- FsFormGroupDirective], exports: [FsFormDirective,
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
- FsFormGroupDirective] });
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
- FsFormGroupDirective,
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
- FsFormGroupDirective,
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, 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 };
2661
3046
  //# sourceMappingURL=firestitch-form.mjs.map