@firestitch/form 18.0.34 → 18.0.36

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.
@@ -1589,11 +1589,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.7", ngImpor
1589
1589
  }] } });
1590
1590
 
1591
1591
  class FsFormDialogActionsComponent {
1592
- /** Leave unset inside an `fsFormContainer` to follow whether it has anything to save. */
1592
+ /** Leave unset to follow whether the enclosing form or container has anything to save. */
1593
1593
  save;
1594
1594
  create = false;
1595
1595
  close = false;
1596
- /** Leave unset inside an `fsFormContainer` to fill in whenever Save does not apply. */
1596
+ /** Leave unset to fill in whenever Save does not apply. */
1597
1597
  done;
1598
1598
  closeData = null;
1599
1599
  name;
@@ -1607,20 +1607,37 @@ class FsFormDialogActionsComponent {
1607
1607
  _form = this._ownDialogOnly(inject(FsFormDirective, { optional: true }));
1608
1608
  _cdRef = inject(ChangeDetectorRef);
1609
1609
  /**
1610
- * Inside a container these default to whether anything mounted can actually
1611
- * save, so a footer left as a bare `<fs-form-dialog-actions>` swaps Save for
1612
- * Done as tabs change without the dialog listing which tabs are savable. An
1613
- * explicit binding always wins, and outside a container Save stays on as it
1614
- * always did.
1610
+ * Whatever owns the form set these actions act on, or null when nothing does.
1611
+ * A container answers for every form mounted under it; failing that the
1612
+ * enclosing form's root answers for itself and anything linked into it.
1613
+ */
1614
+ get _owner() {
1615
+ return this._container || this._form?.rootForm || null;
1616
+ }
1617
+ /**
1618
+ * Save and Done are one decision - whether there is anything to save - rather
1619
+ * than two independent switches, so a bare `<fs-form-dialog-actions>` resolves
1620
+ * it off the form set it sits in and needs no bindings at all. A footer in a
1621
+ * container swaps Save for Done as tabs change without the dialog listing
1622
+ * which tabs are savable, and a dialog with no form at all - a read-only one
1623
+ * of labelled facts - gets Done, because a submit button there has nothing to
1624
+ * submit.
1625
+ *
1626
+ * An explicit binding always wins. `[done]="true"` on its own means Done and
1627
+ * only Done: turning Done on while leaving a Save beside it is never what the
1628
+ * caller meant, and the two-flag version of this let that happen silently.
1615
1629
  */
1616
1630
  get showSave() {
1617
- return this.save ?? (this._container ? this._container.submits : true);
1631
+ if (this.save !== undefined) {
1632
+ return this.save;
1633
+ }
1634
+ return this.done === true ? false : this._owner?.submits ?? false;
1618
1635
  }
1619
1636
  get showDone() {
1620
- return this.done ?? (this._container ? !this._container.submits && !this.create : false);
1637
+ return this.done ?? (!this.showSave && !this.create);
1621
1638
  }
1622
1639
  ngOnInit() {
1623
- const owner = this._container || this._form?.rootForm;
1640
+ const owner = this._owner;
1624
1641
  if (owner) {
1625
1642
  // Track the whole set, not one form's own controls: with a form scoped to a
1626
1643
  // tab's fields, the actions sit outside it in the dialog footer and that
@@ -1759,6 +1776,7 @@ function messageProviderFactory() {
1759
1776
  return { ...ERROR_MESSAGES };
1760
1777
  }
1761
1778
 
1779
+ const resolvedPromise = Promise.resolve();
1762
1780
  class FsControlDirective {
1763
1781
  wrapperSelector;
1764
1782
  messageSelector;
@@ -1774,7 +1792,6 @@ class FsControlDirective {
1774
1792
  };
1775
1793
  }
1776
1794
  errors = [];
1777
- _control;
1778
1795
  _elementRef = inject(ElementRef);
1779
1796
  _renderer2 = inject(Renderer2);
1780
1797
  _injector = inject(Injector);
@@ -1782,19 +1799,31 @@ class FsControlDirective {
1782
1799
  _ngControl = inject(NgControl, { optional: true });
1783
1800
  _formDirective = inject(FsFormDirective, { optional: true });
1784
1801
  _destroy$ = new Subject();
1802
+ _statusChanges$ = new Subject();
1803
+ _boundControl;
1785
1804
  constructor() {
1786
- const _ngControl = this._ngControl;
1787
- if (_ngControl) {
1788
- this._control = _ngControl.control;
1789
- }
1790
- else {
1805
+ if (!this._ngControl) {
1791
1806
  console.error('The element does not have a valid ngModel', this._elementRef.nativeElement);
1792
1807
  }
1793
1808
  }
1809
+ /**
1810
+ * Always read the control through the directive rather than caching it.
1811
+ *
1812
+ * NgForm.addControl() runs in a microtask and reassigns dir.control to
1813
+ * whatever FormGroup.registerControl() returns — and registerControl() hands
1814
+ * back the EXISTING control when the name is already taken (two fields
1815
+ * sharing a name, or lazily created content re-registering). A control
1816
+ * captured in the constructor is then an orphan: it never emits again, so
1817
+ * statusChanges never fires and the validation message is never rendered.
1818
+ */
1819
+ get _control() {
1820
+ return this._ngControl?.control;
1821
+ }
1794
1822
  ngOnInit() {
1795
1823
  this._setupValidators();
1796
1824
  }
1797
1825
  ngOnDestroy() {
1826
+ this._statusChanges$.complete();
1798
1827
  this._destroy$.next(null);
1799
1828
  this._destroy$.complete();
1800
1829
  }
@@ -1936,11 +1965,30 @@ class FsControlDirective {
1936
1965
  }
1937
1966
  }
1938
1967
  _subscribeToStatusChagnes() {
1939
- if (this._control) {
1940
- this._control.statusChanges
1941
- .pipe(takeUntil(this._destroy$))
1942
- .subscribe(this.render.bind(this));
1968
+ // Registration is deferred to a microtask by NgForm.addControl(), so wait
1969
+ // for it to settle before binding — otherwise we subscribe to the control
1970
+ // that is about to be replaced.
1971
+ resolvedPromise
1972
+ .then(() => this._bindStatusChanges());
1973
+ }
1974
+ _bindStatusChanges() {
1975
+ const control = this._control;
1976
+ if (!control || control === this._boundControl) {
1977
+ return;
1943
1978
  }
1979
+ this._boundControl = control;
1980
+ this._statusChanges$.next();
1981
+ control.statusChanges
1982
+ .pipe(takeUntil(this._statusChanges$), takeUntil(this._destroy$))
1983
+ .subscribe(() => {
1984
+ // The control can still be swapped out from under us (a sibling with
1985
+ // the same name registering later), so re-bind before rendering.
1986
+ if (this._control !== this._boundControl) {
1987
+ this._bindStatusChanges();
1988
+ }
1989
+ this.render();
1990
+ });
1991
+ this.render();
1944
1992
  }
1945
1993
  _getWrapper(node, count = 0) {
1946
1994
  if (!node || count > 10) {