@ng-forge/dynamic-forms 1.1.0-next.2 → 1.1.0-next.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/fesm2022/{ng-forge-dynamic-forms-array-field.component-CftX-bCB.mjs → ng-forge-dynamic-forms-array-field.component-DCHrAcfd.mjs} +2 -2
- package/fesm2022/{ng-forge-dynamic-forms-array-field.component-CftX-bCB.mjs.map → ng-forge-dynamic-forms-array-field.component-DCHrAcfd.mjs.map} +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-bootstrap-derivation-orchestrator-CFSJLZPZ.mjs → ng-forge-dynamic-forms-bootstrap-derivation-orchestrator-DWIWQjjZ.mjs} +2 -2
- package/fesm2022/{ng-forge-dynamic-forms-bootstrap-derivation-orchestrator-CFSJLZPZ.mjs.map → ng-forge-dynamic-forms-bootstrap-derivation-orchestrator-DWIWQjjZ.mjs.map} +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-container-field.component-B_bQ-f2g.mjs → ng-forge-dynamic-forms-container-field.component-CSYhRBe6.mjs} +2 -2
- package/fesm2022/{ng-forge-dynamic-forms-container-field.component-B_bQ-f2g.mjs.map → ng-forge-dynamic-forms-container-field.component-CSYhRBe6.mjs.map} +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-group-field.component-Bl9WRPvF.mjs → ng-forge-dynamic-forms-group-field.component-DqhHzVsN.mjs} +2 -2
- package/fesm2022/{ng-forge-dynamic-forms-group-field.component-Bl9WRPvF.mjs.map → ng-forge-dynamic-forms-group-field.component-DqhHzVsN.mjs.map} +1 -1
- package/fesm2022/ng-forge-dynamic-forms-integration.mjs +17 -67
- package/fesm2022/ng-forge-dynamic-forms-integration.mjs.map +1 -1
- package/fesm2022/ng-forge-dynamic-forms-internal.mjs +41 -35
- package/fesm2022/ng-forge-dynamic-forms-internal.mjs.map +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-ng-forge-dynamic-forms-fMB33l2f.mjs → ng-forge-dynamic-forms-ng-forge-dynamic-forms-BYyZ_cuA.mjs} +49 -135
- package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-BYyZ_cuA.mjs.map +1 -0
- package/fesm2022/{ng-forge-dynamic-forms-page-field.component-C9bQkVjQ.mjs → ng-forge-dynamic-forms-page-field.component-Vvk3fYZp.mjs} +2 -2
- package/fesm2022/{ng-forge-dynamic-forms-page-field.component-C9bQkVjQ.mjs.map → ng-forge-dynamic-forms-page-field.component-Vvk3fYZp.mjs.map} +1 -1
- package/fesm2022/ng-forge-dynamic-forms-testing.mjs +84 -0
- package/fesm2022/ng-forge-dynamic-forms-testing.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms.mjs +1 -1
- package/package.json +5 -1
- package/types/ng-forge-dynamic-forms-integration.d.ts +6 -54
- package/types/ng-forge-dynamic-forms-internal.d.ts +41 -10
- package/types/ng-forge-dynamic-forms-testing.d.ts +58 -0
- package/types/ng-forge-dynamic-forms.d.ts +1 -1
- package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-fMB33l2f.mjs.map +0 -1
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ng-forge-dynamic-forms-ng-forge-dynamic-forms-fMB33l2f.mjs","mappings":";;;;;;;;;;;AAOA;AASM;;AACS;AACb;AAA2B;AAC3B;;AACqB;;AAErB;AACF;;AAQE;AACE;;;AAIF;AACA;AAEA;AACE;;;AAIJ;;ACxBA;AAEA;;;AAGI;;AAEA;;AAEF;AACF;;AAIE;AACE;;AAEJ;AAaM;AACJ;AACF;AAGM;AACJ;AACF;AAGO;;AAML;AAAY;;AAGZ;AAAiB;;AAGjB;AAAe;AACf;AACF;AAQM;AAMJ;AAA0B;AAE1B;;;AAOU;;AAEF;AACF;AAIR;AA8BM;;AAEJ;AACA;AACF;AAEA;AAME;AACE;;;;;;;AAcA;AACD;AACD;AAEA;gBAGE;;AACA;;AAGF;;;YAOA;AAEA;;;;AAKM;;AAIgB;;;;AAKxB;AASA;AACE;AACA;AACE;;AACA;AACA;;AAEJ;AAOM;;AACmC;AACvC;AACF;AAGA;AAEA;AAEqB;AACA;;AAEjB;;;;AAIO;AACP;;AACiC;AACjC;;AAEH;;AC3KK;AACJ;AACA;AAEA;;YAMA;AACG;AACA;wBAIC;AACE;;;;AAGA;;AAEA;;AAEJ;;AAGF;AACF;AAiCA;;AAEI;AACA;AACA;AACA;AACA;;AAEJ;AAEA;AACE;AAEI;AACA;AACA;AACD;AAGL;AAMA;;AAEA;AAEA;AACE;;;;gBAOE;;;AAIF;AACF;AAQA;;AAEE;;YAMA;AAAyC;;AAOvC;;AAEA;;AAGF;AAAoC;IAEpC;AAA4B;;AAG1B;AACA;;;AAGA;AACA;;AAED;AACD;AACA;AACF;AAEA;;AACwB;AACtB;AAA4C;;AAE9C;AAOA;;;AAEE;AACE;AAA6B;AAC/B;AACF;;;ACpNE;;AAIA;;AAME;;;AAKA;AACA;AACA;;;AAGJ;;ACPA;;;;AAWE;;;AAcA;AAcE;AAEA;;AAEE;;AAEA;AACA;AAAkC;;;gCAQpC;AAA+B;AAC/B;AACA;AACA;;;AAGC;;AAED;;AAGF;AAQE;AACA;;AACA;;;AAEA;AACE;;;;wCAQF;;;AAGA;;AAGF;AAOE;AACA;;;AAEA;;AACc;AACd;;;AAIF;AAME;AACA;AAAkC;AAClC;AACA;;AAGM;AACN;AACA;AACA;AAAkE;;;AAGhE;AACA;AACA;;;AAII;;;AAEN;AACA;AACG;;;AAGN;;;;;;;ACpJkB;;AAEA;AACA;;AAEA;IAGA;;;AAGjB;;AAOiB;;AAEjB,IAKiB;AAKjB;;AAWiB;IAEA;;;;AAIjB;AACE;;;;;;;;AAQE;AACE;AACA;;AASH;gCAMD;AAIA;;;;;;;AAtFO;AACT;AACD;;;ACqBD;AAGE;AACE;QAEA;AACE;;;AAGF;AACF;AACF;AAQM;AAIJ;gBAMA;AAEI;gBAIA;AACE;;gBAKF;AACE;AACA;;;wDAcF;AAEI;AACA;;;AAKV;;;;ACpGC;;;;ACAA;;;ACGmB;AAEA;AAEA;;AANlB;;;;;;;;AAQD;;;;ACTA;;;;ACAA;;;;ACAA;;;ACkBmB;AAMA;;AARlB;;AAGE;;;;AAOH;;;AClBmB;AAMA;;AARlB;;AAGE;;;;AAOH;;;ACRmB;AAEA;AAMA;;AAVlB;;;AAKE;;;;;AAOH;;;ACfmB;;AAFlB;;;;AAID;;;ACFmB;;AAFlB;;;;AAID;;;ACFmB;AAEA;AAEA;;AANlB;;;;;;;;AAQD;;;ACNmB;AAEA;;AAJlB;;;;;;AAMD;;;ACNoB;;AAAnB;;;AACD;;;ACuEkB;AACA;AACA;;;;AAMjB;;;;AAUA,IAKS;AACP;AAEA;;;AAGF,IAIS;AACP;AACA;AACG;;;;;AAKL,IAIiB;;;AAGO;QAGtB;;;IAIO;AACP;;AAEA;;;;;AAQE;;AAEA;AACA;AACA;;;;AAIJ,IAKS;AACP;AACA;AACA;QAGA;AACE;;AAGF;AACA;QAGA;gBAIA;AACE;AACA;AACE;AACA;AACE;;;;AAKN;;;IAIO;;;;;AAKT;;QAKE;;;gBAII;AACA;AACE;;;AAGN;;AAGF;gBAQE;;;AAGI;AACA;AACA;;;AAIJ;AACA;AAEA;;AAEI;;AAEA;;;AAIJ;;AAEI;;AAEA;;;;AAMJ;;AAEI;;AAEA;;;;AAKJ;;AAGF;AAME;AACA;AAEA;;AAEI;;AAEA;;;AAIJ;;AAEI;;AAEA;;;;AAMJ;;AAEI;;AAEA;;;;AAKJ;;AAGF,IAMA;AACE;AACA;;;AAKI;;AAEA;;;QAKJ;;;AAGI;;;;;QAOJ;;AAEI;AACA;;;QAKJ;AACA;QAGA;;AAGE;AACA;;;AAIJ;AAKE;;AAGA;;YAGE;;;;;AAMF;;;QAMA;;;;;AAKE;QAGF;;;;;AAKE;;;AAKJ,IAOQ;QAEN;AACE;;QAIF;QAGA;AACE;;QAIF;YAEE;AACE;AACE;;;;YAMJ;;AAGA;AACE;;;AAIJ;;;AAlWS;AART;AACE;;;AAGD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAZF;;;;;;;AAnCS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsCR;AACA;AACA;;AAGA;AACE;;AAEA;AACD;;;AA4WL;;AAGE;AACE;;;AAGO;AACL;;;AAIJ;AACF;;ACpaO;AAMD;AACJ;AACF;AAMM;AACJ;AACF;;AC/BM;AACJ;QAEE;AACE;;;AAKA;;;QAOF;;AAEE;;;AAKJ;AACF;AAYA;AAMA;AACE;;;;AAKE;;AAII;AAEI;;;;;;;;AASF;;;AAIV;AAGA;;;;;;;;;AAyBI;AACF;;AAME;;;IASF;;AAEE;AACA;;;AAGA;;AAEF;AACE;;AAEF;AACE;;AAEF;AACE;;AAEF;AACE;;AAEF;AACE;;;4CAiBF;AACA;;;AAIE;AACA;AACD;IAGD;AACA;AACE;QAEA;;AAEE;AACA;AACA;AACA;AACA;wBAGA;;;AAIJ;AACF;AAGA;AACE;AACF;AAGA;;AAQQ;;AAED;AAGL;AACE;;;AAIJ;AAGA;AAIE;AACE;;;AAIJ;AAGA;AACE;;AAGE;AACA;AACA;AACA;;AAEJ;;AC7LA;;AAEE;;AAEA;;;;;;AAME;;AAEE;;AAEF;AACE;;AAEF;AACE;;;AAGA;;AAEF;AACE;;AAC2B;;;AAG3B;;;;AAGF;;;AAE4B;;;;AAI1B;;AAEA;;;AAGN;;AAUmB;IAGjB;;;;;;AAME;;;;;AAMA;;;;;;;;;;ACnHe;IAGjB;;;IAKA;;;;AAME;;;AAKA;;IAIF;AACE;;;AAGA;;;;;;;;;AC7BG;;ACEA;;;AAoBL;;AAOF;AAYM;;;;AAMN;;AChCA;AACE;AACE;;AAEF;AACF;AAGA;AACE;AACE;;IAGF;;;AAGA;AACF;AAEM;;;IAKJ;;AAEI;AAEI;;;;;AAOR;;AAGA;AACE;;;IAKF;;AAGI;;AAGN;AAEA;;AAEI;AACE;;AAEF;AACE;;AAEF;AACE;;AAEF;;;;;AAKE;;;AAGN;AAEM;AACJ;AACF;;ACnFM;;AAEF;YAEE;AACA;;;;AAGA;;AAEJ;AACF;;ACNM;AACJ;AACA;;;;AAKE;AAEI;AACA;;;AAKN;;AAKA;AACE;AACE;;AAGF;AACE;;AAEE;;;AAIJ;AACE;;AAEE;;;AAIJ;AACE;;;AAGN;AAGM;;;AAIA;AACF;;AAIE;AACF;;AAIE;AACF;AACF;AACF;;ACpCM;AACJ;AAAyB;AACzB;AAAyB;AACzB;AACF;AAYM;AAIJ;AAA+C;AAC/C;AAAsE;;AAEpE;;;AAGJ;AAUM;AAIJ;AACA;;;;;;AAQA;AACE;;;AAGE;AAEA;;;;;;;;;;;wBAgBA;;;;;AAMJ;AAAc;;;;;;AAOhB;;ACrFA;AACE;AACA;AACA;;AA0BF;AACE;;;AAGA;AACE;;AAEF;AACE;;AAEF;AACE;AACA;AACE;;;;;;;;AAMA;;AAEF;;AAEJ;AAMA;AACE;AACE;;AAEF;AACE;;AAEJ;AAMA;;AAEI;;;;;;;;AAQJ;AAOA;;;;AAKI;;;AAGJ;AAWM;;YAUJ;;;;;gBAUA;;;;;IAOA;;AAEE;;;YAOF;AACF;AAGA;AAKE;;;AAIA;AACE;;;;;;;;;;AAaE;;;AAGN;AAOA;;AAEA;AASA;;;YAOE;AAEA;AACE;AACE;AACE;;;;AAKN;AACE;AACE;;;IAKJ;AACE;AAEA;AACE;AACE;;;AAIJ;AACE;AACE;AACE;;;;AAIR;AAEA;;;;QAOA;;;;AAWA;AAGI;AAAwB;AACxB;AAAqD;AACrD;AACF;AAGJ;AAMA;AACE;;;IAKA;;AAEE;;IAIF;;AAEE;;AAEJ;AAGA;AACE;;;AAIA;;;AAIA;;;AAGF;AAGA;AACE;;;IAKA;AACE;;AAEF;AACE;;IAIF;;;;AASI;AACE;;;;;AAQJ;YAEA;AACA;;;;;AAQF;AACE;YAEE;;;;AAIA;oCAKA;;;;;;;AASF;YAEA;AACA;;;QAMF;;QAIE;AACE;;;;;AAKO;gBAEL;;;;AAIE;;;;;;;;;;gBAUF;;AAEE;;AAEE;;;;;AAKV;AAEA;AACF;AAOA;;AAGE;QAEE;YAEE;;AAEE;AACA;;;;AAGF;;AAEE;AACA;;;;AAIR;;ACrdM;;IAIJ;AACE;;YAMF;AACE;;AAIA;;;;;;;AAOE;gBAEE;;;AAEA;;;QAKJ;;;AAKA;;AAEE;;;AAIF;;;;;AAME;gCAGE;AACE;;;;AAGF;AACE;;;;AAIN;;;AAMF;IAGA;AACE;;;;AAME;;;;AAQF;;IAIF;AACE;;AAGF;;AAKE;;AAGE;;QAIF;;AAII;;;;AAOF;;;AAIE;oBAEE;;AACK;oBAEL;;;;AAGK;AACL;;;AAIJ;AACF;;;AASA;AACA;AACE;;;AAIJ;AACF;;AC7IM;AACJ;wBAIG;;AACI;;;AAMT;AASM;AACJ;AACE;AACF;AACF;;ACpCO;AACL;AACA;AACE;;AAEH;;ACUD;AAGA;;AAGI;;;AAKF;AAEA;gBAGE;AACE;;AAEF;;AAGF;AACF;;QA8BE;AACA;AACA;IAGA;AACE;AACA;AAEF;AACA;AACE;AACA;AACA;;AAGF;AACE;;YAIE;;;;;;;;;;;AAgBM;;;;;;;;;oCAgBN;;;;;;AAUA;;AAEJ;AACF;AAUA;IAOE;AAEA;AACE;;QAKA;;AAGA;AACE;AAEA;AACE;;AAGE;;;;;;;;;;AAYE;AACD;;;;AAKP;AACF;AACF;AAGA;AAQE;;IAIA;;;AAGE;;;;IAMF;;;;;;AAMF;AAGA;AAME;AACE;;;QAOF;AACE;;;AAGE;;;AAGD;;;;IAQH;;AAIE;;;;;AAOA;;AAGF;AACE;AACA;;IAIF;AACE;AACE;AACE;;;AAEA;;;;AAKN;AACF;AAMA;;QAUE;AACE;;;AAGE;;;AAGD;;;;;;;;;AAiBD;AACA;;AAGF;;AAEE;;;;;AASF;AACF;AAMA;AACE;AACE;;AAGI;;AAEF;AACE;;AAEF;AAEF;;;;AAMI;;AAEF;AAEF;;AAGI;;;AAGA;;AAEF;AAEF;YAEE;AACE;;;;;AAKF;AAEF;YAEE;AACE;;;;;AAKF;AAEF;YAEE;AACE;;;AAKJ;YAEE;AACE;;;;;;AAMF;AAEF;YAEE;;AAEN;AAGM;;AAGJ;;;;AAKE;;;;AAKA;AACE;;;AAIJ;AACF;;;;ACnaI;AACA;AACA;;AAEJ;AAGM;AACJ;AACA;AACA;AACF;AAEA;AACE;AACE;QAGA;4DAME;;AAEA;;;AAGN;AAEA;AACE;AACA;;AAEA;;IAKA;AACE;;;AAGI;;;;IAMN;AACE;;;AAGI;;;;IAMN;AACE;;;AAGI;;;;AAIR;AAGA;IAEE;;AAEE;;AAEI;;;AAGA;;;;IAMN;;;AAGI;AACA;;;AAGA;;;IAKJ;AACE;;AAEE;;;AAGA;AACA;;;AAIJ;AACF;AAGA;QAGE;AACE;;AAGF;AACE;;AAGF;;AAEE;;;AAGA;AACA;AACA;;AAEJ;AAGA;AACE;AACE;;AAGF;;AAEE;;;AAGA;AACA;;AAEJ;AAGA;AACE;;;;AAIA;AAEA;AACE;AACE;;AAEF;AACE;;AAEF;AACE;;AAEF;AACE;;AAEF;YAEE;AACE;;;;;;;AAQJ;;;;AAKA;AACA;;;AAGA;AACE;AACD;;AAEL;;ACtLA;AACE;AACA;AACA;AAEA;AACA;;AAGE;AAEF;AACE;;;AAIA;AACA;AACE;;QAGF;AACA;AACA;;AAEJ;AAWM;;;IASJ;;wBAQE;AACE;gBAEE;gCAIA;;AAEE;AACA;;AAE4B;;;;wBAKhC;AACE;AACA;;;;AAUG;;;;AAQH;AACA;;AAEiB;;AACd;;;oBAYD;AACA;;;AAIJ;;AAIE;AACA;;AAEiB;;;;;AAMjB;;AAEiB;;;AAIvB;AACF;;;AClKE;AACA;;AAEE;AACE;;;AAGF;AACA;AACF;AAEA;AACA;;AAKE;AACE;;AAEF;AACA;AACA;AACF;AAEA;AACA;;AAEE;AACE;;;AAGF;AACA;AACF;AAEA;AACA;;AAKE;AACE;;AAEF;AACA;AACA;AACF;;AAGF;;AAUI;AACA;AACD;;ACnCH;;AAQE;AACA;;;;AAKI;AACA;;AAEF;;;AAIA;AACE;;AAGF;AACE;AACA;AAEA;gBAEE;AACE;gBAGF;;;;gBAIA;AACA;AACE;AAEF;;;AAIJ;AACE;;AAGF;AACE;;;;;;;AAQN;;;ACxFA;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BA;AASM;;AAEJ;AACE;;AAGF;AAEA;AACE;;;;AAQE;AACE;;AAGI;;;AAKR;YAEE;;;;;YAOA;AACE;AAEI;;YAKN;AACA;AACA;AACA;;;AAIJ;AACF;;ACnGO;AAWD;AAMJ;;AAEE;;AAEF;AACF;;ACFA;;AAIqB;AACnB;AACA;AAAkC;AAClC;;AACkE;;AAElE;AACF;AASA;AACE;;AAEE;;;AAMF;AACE;;YAGE;;;;AAMF;;IAIF;AACA;AACE;AACA;;;AAIA;AACA;;AAEJ;AAUA;AACE;AACA;AACE;;;AAIA;AACA;;AAEJ;AAOA;AACE;;AAEE;;;AAIA;AACA;;AAEJ;AAOA;AACE;;AAEE;;;AAIA;AACA;;AAEJ;AAUM;;IAQJ;AACE;;QAKF;YAKA;IAGA;QAIA;;AAKA;;IAGA;;AAEA;QAGA;wBAQA;IAGA;AACE;;;AAKA;;AAIE;;;;;;AAaA;AACA;;;;;;gDAoBA;;AAEE;AAMA;;AAEF;;AAGF;AACF;AACF;;AC5NM;AAIJ;;AAGA;AACE;;;AAIA;AACA;AACF;AACF;AAQM;;AAEF;AACA;wBAIA;AACE;;AAEE;;;gBAKJ;AACF;AACF;AA2BM;AACJ;QAGI;AACE;;;AAKA;;;AAKF;;AAIE;;;;;;;;AASA;AACA;;AAEJ;QAGE;;;AAGA;;AAGN;AAmBM;;;AAIF;;;AAIF;;AAGE;;;;;;;;AASA;AACA;;AAEJ;AAUM;;AAGJ;;AAGE;;gBAGI;;;AAQA;;;;;;;QASJ;AACF;AACF;AAMM;AACJ;;AAGM;;AAEF;AACA;AACF;AAIJ;;;AC5NE;AAOE;;;YAME;AACE;AACA;AACE;AACE;;;;;;;;;AAaN;AACA;;;;AAKJ;;AASE;AACE;AAIA;;;AAIJ,IAOQ;;QAON;AACE;;QAIF;AACE;AACE;AACA;AACE;AACE;;;;;AAMR;;AAEH;AAcK;;AAEN;;ACxGO;AACL;AACA;AACD;;AA6BC;AACA;;AAGE,QAMA;AACE;;AAGF,QAMA;AACE;;AAGF;;;AAWI;;YAIF;AACE;;;;AAMA;;AAGF;;;AAII;;AAGF;;;;;;AAMJ,QAMA;AACE;;AAGF;;;AASA,QAKA;AACE;;AAEA;;AAGF,QAKA;AACE;;;AAGN;;AC5GA;AAOE;;;QAQE;;;;AAKE;AACA;wBAGA;;AAEF;;;;;AAME;;;AAIA;AACE;AACE;;;;AAKN;;wBAME;;AAEA;AACE;;;;AAGE;;;;;AAKV;AAGA;AACE;AACE;;;;;AAIJ;AAQA;AACE;AACA;AAEA;AACE;AACE;;AAEF;;AAGF;AACE;;;AAGA;;AAIJ;AAOA;AACE;AACA;AAEA;AACE;AACE;;AAEF;;AAGF;AACE;;;AAGA;;AAGI;;AAGR;AAOA;AACE;;;AAIA;;AAEI;;;AAIJ;;AAEE;;AAIJ;;AAWE;AACE;AACA;;AAEA;AACA;;;;AAMA;;;AAGA;AAEI;;AAIN;AACA;;;;AAMM;;;AAIR;;ACjNM;;AAOJ;AACA;AACE;;AAGF;;;AAGE;AACA;;AAGF;;AAEE;AACA;;AAGF;AACA;;;AAIA;;;AAIM;;;AAGA;;AAED;;;;;;AASC;;;;AAID;;;QAKH;;AAEI;;;;;AAKD;;;;;AAQC;;;AAGD;;;;;;AAWD;AAAe;;AAEjB;;wBAWA;AACE;;AAEI;AACA;;AAEE;;AAEA;AACD;;AAEL;AACA;AACE;;;AAGE;;AAEA;AACD;;;;;;wDAcL;AACE;;AAC2C;;AAEzC;;AAEA;AACD;;;AAID;;;QAKF;AACE;;;;;AAII;;;AAGA;AACD;;;;AAKL;;AAGF;AACF;AAMM;;;;AAUF;AACE;AACA;AACA;;gBAKF;AACA;AACE;AAMA;;;AAIF;AACF;;AAGF;;AC5MM;AACJ;AACE;AACE;AACF;AACE;AACF;AACE;AACF;AACE;AACF;AACE;AACF;AACE;AACF;AACE;AACF;AACE;;AAEN;AAGM;AACJ;AACE;;AAEA;;AAGI;AAEJ;AACE;;AAIF;;;AAII;AAEJ;AACE;;AAIF;AACE;AACF;;AAEA;;;AAGJ;;AChEO;AACL;AACA;AACE;AACA;AACA;;AAEH;;;;;;;;ACWD;AAoBA;AACE;AACA;AAEA;AACA;AACE;;AAGF;AACA;AACE;;AAGF;AACA;AACE;;AAGF;AACF;AAcM;;AAUJ;AACE;AACA;;;AAIA;;QAGA;;QAGA;;;;;;QAQA;;AAEE;;AAGE;AACA;;;;;;;;;;AAcF;AACA;;;AAGE;;;AASA;;;;gBAOJ;;;;;;AASF;AACF;;ACjJO;AACL;AACA;AACA;AACA;AACA;;AAWK;AACL;AACA;AACA;;AAWK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAWK;AACL;AACA;AACA;AACA;;AA4JI;AAGJ;AACF;AAGM;AAGJ;AACF;;AAIE;AACF;AAGM;;AAIN;AAGM;;AAKN;AAGM;AAQJ;AACF;;AAIE;AACF;;;AC1PmB;AACA;IAGjB;AACE;;AAGF;AACE;AAEA;AACE;AACF;;IAIF;AACE;AACE;;;;AAKA;AACE;AACA;;;;AAGA;;AAEJ;;AAGF;AAKE;AACE;;AAGF;AACE;;;;AAKA;AACE;;;;AAKA;AACE;AACA;;;;AAGA;;AAEJ;YAGA;;AAEA;AACF;;IAIF;AACE;AACE;;;;AAKA;;;;;;AASI;AACE;AACA;;;;AAGA;;;YAON;;AAEA;AACF;;AAEH;AAGK;;;AAGN;;;AClEmB;AACA;AACA;IAGR;IAGA;IAGT;AACE;;AAGF;AACE;AACA;;;AAEA;;;IAMF;AACE;;;AAKA;;AAKU;AACA;AACA;AACF;AAKL;;AAGL,IAKQ;AACN;;AAGA;AACE;;;AAGA;AACD;;;;AAMH;AAKE;;;;AAIO;AACL;;;;;AAQF;;;;;;;;;;AAcI;;;;AAMA;;AAGA;AAEF;AACE;;;;;AAMF;;;AAIA;;;;;;;AAQE;;;;;;AAOA;;;;;;AASF;;AAGF;;AAEI;AACA;;;AAIJ;;;AAWI;;;AAIJ;;;;;;AAOI;;;AAIJ;;;AAIA;AACE;;;;AAYA;;;AAII;AACN;AACE;;;;;;;;AAgBF;AACE;;AAGF;;;AAUI;;;;;AAMF;;;AAII;AACN;AACE;;AAGF;AACE;AAEI;;AAGN;;;AAIE;;;;;;AAOA;;;AAIJ,IAIQ;AACN;AACE;;AAGF;;AAMM;AACN;AAEA;AACE;AACE;;AAEE;AACF;;AAGF;AACE;;;;AAQF;AACE;AACE;AAEA;;AAEE;;AACK;AACL;;AAGF;;;;;AAME;;AACK;AACL;;AAEJ;;AAGF;AACE;AACE;AACA;;AAEA;;;AAGA;gFAMA;;AAEE;;;AAEF;AACA;AACE;AAA+D;;;;AAKnE;;AAGF;AACE;;;AAGP;AAGK;AAGJ;AACF;;AChWO;AAGP;AACE;AACF;AAGA;AACE;AACA;AACA;;AAWF;AACE;AACF;AAOA;AAME;AACE;AACA;;AACA;AACA;;AAEA;;AAEA;;AAGE;;;AAGN;AAMA;;AAEE;AACE;AAAqD;AACrD;;AAEF;AACF;AAOA;AACE;AACF;AAMA;AACE;AAA2B;;AAE3B;AACA;AACE;;;AAEA;AACA;AACA;;AAEF;AACF;;YAiBmB;AACA;AACA;AACA;AACA;AACA;;;AAUf;AACA;;AAGM;;wBAMN;;IASe;;IAKA;AACjB,IAMQ;YAMS;AACA;YAMA;;;AASjB,IAKiB;YAMjB,IAMS;;;AAIL;;;;AAIF;AACE;;;;AAGF;;AACA;;;IAIO;;;;AAGP;;;;;IAOO;AACP;;AAEE;;;;;IAOa;AACf;AACA;AAAa;;;;IAKN;AACP;AACA;;AAEA;;;IAIO;AACP;;;;;AAOA;;;;;IAUO;AACP;;AAGA;;;AAIA;AACE;;;;;wCAWF;;AAEE;;;AAGA;;AAGF;AACE;AACA;;AAGF;;;;;IAOe;;AAEf;AAAgD;AAChD;;;AAGF,IAKiB;AACf;;AAC4D;;AAG5D;AACA;;;AAGF;;;;gBAiBS;;AAGL;AACA;AACA;;;;;;AAYI;;;;AAIF;;AAGF;AACF;AAEE;AACA;AACD;AAGH,IAKiB;AAEb;AACA;AAEA;AAAa;AAEb;;;;;AAKI;AACD;;AAGH;AACE;;AAGF;AACF;;IAIO;AACP;AACA;AACA;;;IAIM;AAER,IAMS;AACP;;AAEA;;;AAIE;;AAEA;AACE;AACA;AAEA;;;AAIA;AACA;;;;;;;IAaG;;IAGA;AACP;AACA;AACA;AAEA;AACE;;wBAMF;AAEA;;;;;wBASA;;;;IAae;AACf;AACA;AACA;AAEA;AACE;;AAGF;AACA;;;;;AAKA;AAEA;;;IAWO;;IAGA;;IAGA;;IAGA;;IAGA;;IAGA;;;;;;IAOA;;YAMT,IAIiB;;AAIb;;;AAGE;AAEA;AACE;;;AAIA;;;AAIJ;;;AAIe;AACb;AAA2B;AAC3B;AACF;;;AAQA;;;IAKK;YAMT,IASS;IAGA;IAGA;IAGT;;;YAYA;AACE;;AAKQ;;AAGF;AACA;AAEA;AACE;;;;;;;AAUF;AACE;;AAEE;AACA;;;AAGE;AACD;AACF;;;AAMH;AAEA;AACE;AACA;AACA;;;;;AAKE;AACA;AAEI;;;;;AAKD;;;AAKP;AACE;;AAGF;AAEI;;;AAGA;;;gBAYV;AACE;;;AAGF;AAEI;AACE;AACA;;AAEF;AACF;;AAIE;AACA;AAEA;AACE;;AAGI;AAEJ;;AAGF;;AAKJ;AAKA;QAMA;;;;;AAKE;AACE;;;AAOF;;AAEA;AACA;;AAEE;AACE;AACE;;;;;;AAWA;AAAyB;;AAE3B;AACF;;AAEF;;;AAGA;AACE;;AAEH;AAED;;;AAGA;;gBAQF;;;;AAME;AACA;AACA;AACC;;;;;AAOD;AACA;;AAEC;;;;;AAOD;;;AAQA;AACE;AAEA;;AAEE;;;AAEA;;AAEJ;gEAUA;AACE;AACA;AACA;AACA;;AAGA;AACE;;;;AAIE;;;;;;AAOJ;AACE;;AAEJ;;;AAOE;AACA;AACE;;AAEF;AACE;;AACK;;AAEL;;;AAII;;AAGR;gBAIA;;;;;AAKA;AAEA;AACE;;;;;;AAMF;;;AAIA;;AAEG;;AAGH;;AAEG;;;AAIG;;AAEJ;AACE;AACF;;;;;AAOF;AACE;;AAEA;;AAGF;;;AAIA;;;AAGA;;;;;;;AASM;AACN;;;;AAqBA;AACA;oCAIE;AACA;;;;;AAKF;;AAGA;AACA;;AAEA;AACA;;AAGE;AACA;AACA;;;;;;AAOI;;AAEJ;AACA;AACA;AACA;;;;;AAUF;;;;;;;;;AC3jCE;AAMJ;AACE;AAEA;;AAEA;AACE;AACA;;;;AAKF;;AAEA;;AAEJ;AAUM;AACJ;;AACwB;AACtB;AAA4B;;AAE5B;AACF;AACF;AAQA;;AAKE;;;AAGI;AACE;;;;;AAMN;AACE;AACE;;;AAEA;;;AAGJ;AACF;AAGA;AACE;AACF;;ACrFM;AACJ;;AAIkB;;AAEhB;AACF;AACF;;ACZO;;AAoBL;AACA;AACA;AACA;AAEA;AACE;;;;gBAQF;;;AAGE;;AACA;;AAIK;;AAEC;AACF;AACC;wBAGC;AACA;;AAGR;;;AAKyB;AACvB;AACA;AACF;AACF;;ACvEO;;ACPP;;AAgBmB;;;IAIR;;;;;;;AAMc;;;AAGrB;;;IAIF;;AAEE;AAA6B;;AAE7B;AACA;;AAGF;AACE;AACA;;;AA5BS;;;;;;;ACTb;AAEA;;AAEA;AAEA;;AACyB;AACvB;AAAgD;AAChD;;AAEF;;AAamB;;;;;;;AAaA;;AAER;AACP;AACA;AAAc;AACd;;;AAGF;AACE;AACA;gBAIA;;AAEE;;;;AAKA;AACE;AACA;;;AAGF;;;;;;;;;;;AC5DJ;AACA;AACD;;ACuBD;;;AAGI;;;AAGA;AACE;;AAED;;AAED;AACE;;;AAOD;;;AAGD;AACE;AACA;;AAED;AACD;AACE;AACA;;AAED;AACD;AACE;AACA;;AAED;AACD;AACE;;;AAGD;AACD;AACE;AACA;;AAED;;AAID;AACE;;;AAGD;AACD;gCAKA;;AAEJ;AAWA;;AAEI;gDAOA;;AAEJ;AASA;;AAEA;AAQA;;AAEA;AAQA;;AAEA;AASA;;AAEA;AASA;;AAEA;;;AAYI;AACA;AACA;AACA;AACA;AACA;AACA;;AAEJ;;;AClJU;AAER,IAMA;AACE;;IAIF;AACE;;;AAKA;;;;;;;;;;;;ICnDO;;;;;;AATA;AACT;AACA;AACD;;;AAoBkB;;IAGR;IAGT;AACE;;;;;;;;;;;;;;;ACoGF;;YAYQ;AACA;AACE;AACF;AACA;;AAEA;;;AASS;AAEjB,IAKiB;IAGE;;gBAanB;IAGA;IAGA;IAGA;IAGA;IAGA;IAGA;IAGA;IAGA;IAGA;IAGA;IAGA;IAGA;IAGA;IAGA;IAGA;IAGA;YAKQ;AACR;;IAGU;YAMV,IAIQ;AACN;AACA;;;;;;;;AAYD;;;;IAgBD;IAGA;;AAKA,IAIA;IAGA;IAGA;YAMA;;AAEE;;;;;AAcF,IAQU;;AAER;;;;;AAWI;;AAEJ;;;;AAOQ;;;AAKR;gBAIA;AACE;AACA;AACE;;AAEF;AACF;;AAGF;AAOE;AACA;AACA;AACA;AACA;AACA;;;AAIA;;;;;;;AAOG;AACA;AACC;AACD;;;AAtRM;AAhBT;;AAEA;AACD;;;;;;;;;;;;;;;;;;;;;;;;;;AA9BS;;;;;;;;;;;;;;;;;;;;;;;AA2BR;;AAEA;;AAGA;wCAEA;AACA;AACA;AACA;AACA;AACA;;;AAmSJ;;AAEE;AACE;;AAEE;AACE;AACA;AACE;AACE;4BAEE;;;AAEA;;;;;;;AAOZ;AACF;;ACvaO;AACL;AACE;AACA;AACA;AACE;;AAME;;;AAGA;;;AAGL;AACD;AACE;AACA;AACA;AACE;;AAEE;;;AAGL;AACD;AACE;gBAGA;AACA;AACA;AACE;AACA;AACE;;;AAGL;;;ACrBH;;;AAQA;AACE;;AAIK;AACL;AACE;wBAIA;AACA;AACA;AACA;AACuC;AACzC;AACE;AACA;AACA;AACA;AACA;AACyC;AAC3C;AACE;AACA;AACA;AACA;AACA;AACyC;AAC3C;AACE;AACA;AACA;AACA;AACA;AACwC;AAC1C;AACE;AACA;AACA;AACA;AACA;AACwC;AAC1C;AACE;QAEA;AAC0C;AAC5C;AACE;AACA;AACA;AACA;AACA;AAC6C;;AAG1C;AACL;AACE;AACA;AAC2C;AAC7C;AACE;AACA;AAC2C;;;ACrEzC;AACJ;AAEE;AACA;AACA;AACA;;AAGJ;AAGM;;AAEF;AACA;;AAEJ;;;;ACvCE;AACE;;AAGF;AACE;;AAGF;AACE;;AAGF;AACE;;AAEH;;ACOK;;AAEF;AACA;AACA;;;;AAIC;;AAEL;AAGM;AACJ;AAEE;AACA;;AAEA;;AAGJ;;ACQM;IAIJ;;;;;;;AAYA;AACA;;AAEA;;AAKA;AACE;AACA;AACE;;AAEE;AACA;gBAEA;;;;;AAKA;gDAIA;AACE;AACA;AACA;AACA;AACA;AACA;;AAEF;;;AAEoC;;AAEpC;;AAEH;QAED;AACE;;AAEE;AACA;gBAEA;;;;;AAKA;AACA;;AAEH;wBAID;AACE;;AAEE;AACA;;;AAEE;;;AAGE;;;AAGJ;;AAEH;wCAMD;AACE;;AAEE;AACA;AACA;AACA;;;AAGA;;;;;;AAMA;;AAEH;QAED;AACA;AACD;AACH;;;AClKE;AACA;AACD;AAyBK;;AAGF;AACA;AACA;;IAIF;AACA;AAEA;IAGA;;AAEI;AACA;AACD;;AAGH;AACF;;;AC/CE;AACF;;ACDM;AACJ;AACE;AACA;AACA;;AAGF;AACF;;ACNM;AACJ;AACE;;AAGF;AACF;;;ACZE;AACF;;;ACXE;AACF;;;;;ICgBW;;;AAHE;;;;AATF;AACT;AACA;AACA;AACA;AACE;AACD;;AAEF;;;;;;;;;;ACMkB;;;IAIR;;QAIU;AACjB;;;;;IAMiB;;AAEnB;gBAGE;;;;AAMA;;;AA5BS;;;;AAHV;;;;AAPQ;AACT;;AAEA;;;;AAIC;;AAEF;;;;;;;;;ACOkB;;;IAIR;;AAEU;;AAEnB;;AAUmB;AACjB;AACA;AACA;;;AAGF;AACE;AACE;AACG;AACC;AACA;;AACF;AACC;AACC;AAGF;AACJ;;;AArCS;;;;AAHV;;;;AAPQ;AACT;;AAEA;;;;AAIC;;AAEF;;;;;;;;ACjBK;;AAEF;AACE;AACA;AACA;AACD;AACF;AACH;;ACCM;;AAEF;AACE;AACA;AACA;AACD;AACF;AACH;;;ACqBE;AACA;AACA;;;;;;AAaF;AAQM;AACJ;;;;;;;;AAOF;AAOM;AAMJ;;AAOA;AACF;;AClFM;;AAEF,QASA;AAEA,QASA;AAEA,QAUA;AAEA;AAQA;AAQA,QAOA;AAEA,QASA;;AAEJ","names":[],"ignoreList":[],"sources":["../../../../packages/dynamic-forms/src/lib/utils/resolve-wrappers/resolve-wrappers.ts","../../../../packages/dynamic-forms/src/lib/utils/wrapper-chain/wrapper-chain.ts","../../../../packages/dynamic-forms/src/lib/utils/wrapper-chain/wrapper-chain-controller.ts","../../../../packages/dynamic-forms/src/lib/utils/build-field-inputs/build-field-inputs.ts","../../../../packages/dynamic-forms/src/lib/directives/df-field-outlet/field-component-slot.ts","../../../../packages/dynamic-forms/src/lib/directives/df-field-outlet/df-field-outlet.directive.ts","../../../../packages/dynamic-forms/src/lib/utils/submission-handler/submission-handler.ts","../../../../packages/dynamic-forms/src/lib/events/constants/submit.event.ts","../../../../packages/dynamic-forms/src/lib/events/constants/next-page.event.ts","../../../../packages/dynamic-forms/src/lib/events/constants/page-change.event.ts","../../../../packages/dynamic-forms/src/lib/events/constants/previous-page.event.ts","../../../../packages/dynamic-forms/src/lib/events/constants/form-reset.event.ts","../../../../packages/dynamic-forms/src/lib/events/constants/form-clear.event.ts","../../../../packages/dynamic-forms/src/lib/events/constants/append-array-item.event.ts","../../../../packages/dynamic-forms/src/lib/events/constants/prepend-array-item.event.ts","../../../../packages/dynamic-forms/src/lib/events/constants/insert-array-item.event.ts","../../../../packages/dynamic-forms/src/lib/events/constants/pop-array-item.event.ts","../../../../packages/dynamic-forms/src/lib/events/constants/shift-array-item.event.ts","../../../../packages/dynamic-forms/src/lib/events/constants/move-array-item.event.ts","../../../../packages/dynamic-forms/src/lib/events/constants/remove-at-index.event.ts","../../../../packages/dynamic-forms/src/lib/events/constants/page-navigation-state-change.event.ts","../../../../packages/dynamic-forms/src/lib/core/page-orchestrator/page-orchestrator.component.ts","../../../../packages/dynamic-forms/src/lib/utils/array-field/normalized-array-metadata.ts","../../../../packages/dynamic-forms/src/lib/utils/array-field/normalize-simplified-arrays.ts","../../../../packages/dynamic-forms/src/lib/core/registry/array-item-registry.service.ts","../../../../packages/dynamic-forms/src/lib/core/registry/schema-registry.service.ts","../../../../packages/dynamic-forms/src/lib/utils/dev-mode.ts","../../../../packages/dynamic-forms/src/lib/utils/debounce/debounce.ts","../../../../packages/dynamic-forms/src/lib/core/logic/logic-applicator.ts","../../../../packages/dynamic-forms/src/lib/core/values/type-predicate-factory.ts","../../../../packages/dynamic-forms/src/lib/core/schema-application.ts","../../../../packages/dynamic-forms/src/lib/core/field-tree-mapping-context.ts","../../../../packages/dynamic-forms/src/lib/core/form-mapping.ts","../../../../packages/dynamic-forms/src/lib/utils/default-value/default-value.ts","../../../../packages/dynamic-forms/src/lib/core/form-schema-merger.ts","../../../../packages/dynamic-forms/src/lib/providers/features/validation-execution/validation-execution.token.ts","../../../../packages/dynamic-forms/src/lib/core/schema-builder.ts","../../../../packages/dynamic-forms/src/lib/core/cross-field/cross-field-collector.ts","../../../../packages/dynamic-forms/src/lib/utils/flattener/field-flattener.ts","../../../../packages/dynamic-forms/src/lib/utils/container-utils/container-field-processors.ts","../../../../packages/dynamic-forms/src/lib/core/property-derivation/property-override-store.ts","../../../../packages/dynamic-forms/src/lib/core/property-derivation/apply-property-overrides.ts","../../../../packages/dynamic-forms/src/lib/core/property-derivation/property-override-key.ts","../../../../packages/dynamic-forms/src/lib/utils/field-mapper/field-mapper.ts","../../../../packages/dynamic-forms/src/lib/utils/resolve-field/resolve-field.ts","../../../../packages/dynamic-forms/src/lib/utils/form-validation/form-mode-validator.ts","../../../../packages/dynamic-forms/src/lib/utils/inject-field-registry/inject-field-registry.ts","../../../../packages/dynamic-forms/src/lib/utils/config-validation/config-validator.ts","../../../../packages/dynamic-forms/src/lib/utils/validate-form-config/validate-field-addons.ts","../../../../packages/dynamic-forms/src/lib/utils/validate-form-config/addon-warning.ts","../../../../packages/dynamic-forms/src/lib/providers/features/value-exclusion/value-exclusion.token.ts","../../../../packages/dynamic-forms/src/lib/utils/value-filter/value-filter.ts","../../../../packages/dynamic-forms/src/lib/state/state-types.ts","../../../../packages/dynamic-forms/src/lib/state/side-effect-scheduler.ts","../../../../packages/dynamic-forms/src/lib/state/form-state-machine.ts","../../../../packages/dynamic-forms/src/lib/state/form-state-manager.ts","../../../../packages/dynamic-forms/src/lib/core/derivation/field-traversal.ts","../../../../packages/dynamic-forms/src/lib/core/derivation/config-has-derivations.ts","../../../../packages/dynamic-forms/src/lib/core/derivation/derivation-render-gate.ts","../../../../packages/dynamic-forms/src/lib/providers/form-initializer.token.ts","../../../../packages/dynamic-forms/src/lib/core/registry/dynamic-form-instance-registry.service.ts","../../../../packages/dynamic-forms/src/lib/core/registry/form-id-prefix.service.ts","../../../../packages/dynamic-forms/src/lib/core/derivation/derivation-warning-tracker.ts","../../../../packages/dynamic-forms/src/lib/providers/dynamic-form-di.ts","../../../../packages/dynamic-forms/src/lib/events/event-dispatcher.ts","../../../../packages/dynamic-forms/src/lib/directives/df-template.directive.ts","../../../../packages/dynamic-forms/src/lib/dynamic-form.component.ts","../../../../packages/dynamic-forms/src/lib/providers/built-in-addons.ts","../../../../packages/dynamic-forms/src/lib/providers/built-in-fields.ts","../../../../packages/dynamic-forms/src/lib/providers/features/dynamic-form-feature.ts","../../../../packages/dynamic-forms/src/lib/providers/features/logger/console-logger.ts","../../../../packages/dynamic-forms/src/lib/wrappers/create-wrappers.ts","../../../../packages/dynamic-forms/src/lib/providers/dynamic-form-providers.ts","../../../../packages/dynamic-forms/src/lib/providers/features/logger/with-logger-config.ts","../../../../packages/dynamic-forms/src/lib/providers/features/event-form-value/with-event-form-value.ts","../../../../packages/dynamic-forms/src/lib/providers/features/value-exclusion/with-value-exclusion-defaults.ts","../../../../packages/dynamic-forms/src/lib/providers/features/validation-execution/with-validation-execution-defaults.ts","../../../../packages/dynamic-forms/src/lib/providers/features/legacy-status-classes/with-legacy-status-classes.ts","../../../../packages/dynamic-forms/src/lib/wrappers/wrapper-props.ts","../../../../packages/dynamic-forms/src/lib/addons/text-addon.component.ts","../../../../packages/dynamic-forms/src/lib/addons/template-addon.component.ts","../../../../packages/dynamic-forms/src/lib/addons/component-addon.component.ts","../../../../packages/dynamic-forms/src/lib/providers/features/addons/with-custom-addon.ts","../../../../packages/dynamic-forms/src/lib/providers/features/addons/with-addon-actions.ts","../../../../packages/dynamic-forms/src/lib/utils/validate-form-config/validate-form-config.ts","../../../../packages/dynamic-forms/src/lib/events/array-event.ts"],"sourcesContent":["import { FieldDef } from '@ng-forge/dynamic-forms/internal';\nimport { WrapperAutoAssociations, WrapperConfig } from '@ng-forge/dynamic-forms/internal';\n\n/**\n * Shared empty chain — returned when no wrappers apply so callers get a\n * stable reference. Freezing makes accidental mutation observable.\n */\nconst EMPTY_WRAPPERS: readonly WrapperConfig[] = Object.freeze([]);\n\n/**\n * Element-wise identity (`===`) comparator for wrapper chains. Used as the\n * `equal` option on signal memoisation so reconciled `FieldDef`s with the\n * same chain don't churn downstream — relies on `WrapperConfig` objects\n * being stable across ticks, which holds for configs declared in\n * `FormConfig` / `createWrappers(...)`.\n */\nexport function isSameWrapperChain(a: readonly WrapperConfig[], b: readonly WrapperConfig[]): boolean {\n if (a === b) return true;\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (a[i] !== b[i]) return false;\n }\n return true;\n}\n\n/** Resolves the wrapper chain for a field. */\nexport function resolveWrappers(\n field: Pick<FieldDef<unknown>, 'type' | 'wrappers' | 'skipAutoWrappers' | 'skipDefaultWrappers'>,\n defaultWrappers: readonly WrapperConfig[] | undefined,\n autoAssociations: WrapperAutoAssociations,\n): readonly WrapperConfig[] {\n if (field.wrappers === null) {\n return EMPTY_WRAPPERS;\n }\n\n const autoWrappers = field.skipAutoWrappers ? EMPTY_WRAPPERS : (autoAssociations.get(field.type) ?? EMPTY_WRAPPERS);\n const defaults = field.skipDefaultWrappers ? EMPTY_WRAPPERS : (defaultWrappers ?? EMPTY_WRAPPERS);\n const fieldLevel = field.wrappers ?? EMPTY_WRAPPERS;\n\n if (autoWrappers.length === 0 && defaults.length === 0 && fieldLevel.length === 0) {\n return EMPTY_WRAPPERS;\n }\n\n return [...autoWrappers, ...defaults, ...fieldLevel];\n}\n","import {\n ComponentRef,\n EnvironmentInjector,\n InjectOptions,\n Injector,\n ProviderToken,\n reflectComponentType,\n Type,\n ViewContainerRef,\n} from '@angular/core';\nimport { catchError, forkJoin, from, map, Observable, of } from 'rxjs';\nimport { FieldWrapper, WrapperConfig, WrapperTypeDefinition } from '@ng-forge/dynamic-forms/internal';\nimport { Logger } from '@ng-forge/dynamic-forms/internal';\nimport { WrapperFieldInputs } from '@ng-forge/dynamic-forms/internal';\n\n/**\n * Module-level cache keyed by component class. `reflectComponentType` returns\n * immutable metadata per class, so we probe it once and reuse the resulting\n * `Set<templateName>` for every `setInputIfDeclared` call afterwards.\n */\nconst inputNamesCache = new WeakMap<Type<unknown>, Set<string>>();\n\nfunction getDeclaredInputs(componentType: Type<unknown>): Set<string> {\n let inputs = inputNamesCache.get(componentType);\n if (!inputs) {\n const meta = reflectComponentType(componentType);\n inputs = new Set(meta?.inputs.map((i) => i.templateName) ?? []);\n inputNamesCache.set(componentType, inputs);\n }\n return inputs;\n}\n\n/** Set an input on a ComponentRef only when the target component actually declares it. */\nexport function setInputIfDeclared(ref: ComponentRef<unknown>, inputName: string, value: unknown): void {\n if (getDeclaredInputs(ref.componentType).has(inputName)) {\n ref.setInput(inputName, value);\n }\n}\n\n/** A wrapper whose component has been resolved and loaded. */\nexport interface LoadedWrapper {\n readonly config: WrapperConfig;\n readonly component: Type<unknown>;\n}\n\n/**\n * Narrows an ES-module-with-default-export from a bare component class.\n * Lazy component loaders may return either shape depending on how the user\n * wrote the import expression (`import('./x')` vs `import('./x').then(m => m.Foo)`).\n */\nexport function hasDefaultExport<T>(value: unknown): value is { default: T } {\n return typeof value === 'object' && value !== null && 'default' in value && !!(value as { default: unknown }).default;\n}\n\n/** Pick the component class out of whatever a lazy loader returned. */\nexport function resolveDefaultExport<T>(result: Type<T> | { default: Type<T> }): Type<T> {\n return hasDefaultExport<Type<T>>(result) ? result.default : result;\n}\n\n/** Resolve a wrapper type name to its component class, with DI-scoped caching. */\nexport async function loadWrapperComponent(\n type: string,\n registry: ReadonlyMap<string, WrapperTypeDefinition>,\n cache: Map<string, Type<unknown>>,\n): Promise<Type<unknown> | undefined> {\n const cached = cache.get(type);\n if (cached) return cached;\n\n const definition = registry.get(type);\n if (!definition) return undefined;\n\n const component = resolveDefaultExport(await definition.loadComponent());\n if (component) cache.set(type, component);\n return component;\n}\n\n/**\n * Load every wrapper for a chain, all-or-nothing. A single failed load\n * aborts the whole chain (emits `[]`) so the field renders bare rather\n * than in a partially-wrapped, visually-misleading state. Each failure\n * is logged; field-type resolution still throws — this isn't silent.\n */\nexport function loadWrapperComponents(\n configs: readonly WrapperConfig[],\n registry: ReadonlyMap<string, WrapperTypeDefinition>,\n cache: Map<string, Type<unknown>>,\n logger: Logger,\n): Observable<LoadedWrapper[]> {\n if (configs.length === 0) return of([]);\n\n return forkJoin(\n configs.map((config) =>\n from(loadWrapperComponent(config.type, registry, cache)).pipe(\n catchError(() => of(undefined)),\n map((component) => {\n if (!component) {\n logger.error(`Wrapper type '${config.type}' could not be loaded. Ensure it is registered via provideDynamicForm().`);\n return null;\n }\n return { config, component } satisfies LoadedWrapper;\n }),\n ),\n ),\n ).pipe(map((results) => (results.some((r) => r === null) ? [] : (results as LoadedWrapper[]))));\n}\n\n/** Options for building a wrapper chain. */\nexport interface RenderWrapperChainOptions {\n readonly outerContainer: ViewContainerRef;\n readonly loadedWrappers: readonly LoadedWrapper[];\n readonly environmentInjector: EnvironmentInjector;\n readonly logger: Logger;\n /**\n * Optional mapper outputs to set as `fieldInputs` on each wrapper in the chain.\n * Container usages omit this (the children template is the innermost content,\n * not a single field).\n */\n readonly fieldInputs?: WrapperFieldInputs;\n /**\n * Optional field-level injector (e.g. `ResolvedField.injector`). When provided,\n * each wrapper is created with a merged injector that checks the field's\n * tokens first (`FIELD_SIGNAL_CONTEXT`, `ARRAY_CONTEXT`, …) and falls back to\n * the element injector — so wrappers can inject both field context AND outer\n * wrappers up the element hierarchy.\n */\n readonly fieldInjector?: Injector;\n /** Renders whatever belongs at the innermost slot (a field component, a children template, …). */\n readonly renderInnermost: (slot: ViewContainerRef) => void;\n}\n\n/**\n * Recursively create each wrapper component, threading the next one (or the\n * innermost content) into its `#fieldComponent` slot.\n */\nexport function renderWrapperChain(options: RenderWrapperChainOptions): ComponentRef<unknown>[] {\n const refs: ComponentRef<unknown>[] = [];\n renderStep(options.outerContainer, options.loadedWrappers, options, refs);\n return refs;\n}\n\nfunction renderStep(\n slot: ViewContainerRef,\n remaining: readonly LoadedWrapper[],\n options: RenderWrapperChainOptions,\n refs: ComponentRef<unknown>[],\n): void {\n if (remaining.length === 0) {\n options.renderInnermost(slot);\n return;\n }\n\n const [wrapper, ...rest] = remaining;\n\n // `slot.injector` is the element injector seen from inside the current slot —\n // for nested slots that includes the outer wrapper's component, enabling\n // `inject(OuterWrapper)`. When a field injector is provided we merge it in\n // front so tokens like `ARRAY_CONTEXT` also resolve.\n const wrapperInjector = options.fieldInjector ? createWrapperAwareInjector(options.fieldInjector, slot.injector) : slot.injector;\n\n const ref = slot.createComponent(wrapper.component, {\n environmentInjector: options.environmentInjector,\n injector: wrapperInjector,\n });\n refs.push(ref);\n\n for (const [key, value] of Object.entries(wrapper.config)) {\n // `type` is the registry discriminant; `fieldInputs` is set explicitly\n // below. Don't let a stray config key override either.\n if (key === 'type' || key === 'fieldInputs') continue;\n setInputIfDeclared(ref, key, value);\n }\n\n if (options.fieldInputs !== undefined) {\n setInputIfDeclared(ref, 'fieldInputs', options.fieldInputs);\n }\n\n // Required to resolve `viewChild.required('fieldComponent')` before we\n // recurse into the slot. O(chain length) CD passes per rebuild — acceptable\n // for typical chains (≤3); refactor to a template-based render if it bites.\n ref.changeDetectorRef.detectChanges();\n\n const inner = resolveInnerSlot(ref);\n if (!inner) {\n options.logger.error(\n `Wrapper component for type '${wrapper.config.type}' does not provide a 'fieldComponent' ViewContainerRef. ` +\n `Ensure the wrapper component has a viewChild('fieldComponent', { read: ViewContainerRef }) query ` +\n `and that #fieldComponent is not inside a conditional (@if, @defer).`,\n );\n // Unwind: destroy every wrapper built so far, including this one, so the\n // caller doesn't end up with a partial chain left in the DOM.\n while (refs.length) refs.pop()!.destroy();\n return;\n }\n\n renderStep(inner, rest, options, refs);\n}\n\n/**\n * Read a wrapper's `#fieldComponent` ViewContainerRef. `viewChild.required`\n * throws when the query can't resolve (missing template ref, or ref sitting\n * inside `@if` / `@defer` / `@for`); any throw from this pure signal read\n * means the slot is unusable, so we return undefined and let the caller log\n * the actionable \"wrapper missing fieldComponent\" message.\n */\nfunction resolveInnerSlot(ref: ComponentRef<unknown>): ViewContainerRef | undefined {\n const contract = ref.instance as FieldWrapper;\n try {\n return contract.fieldComponent?.();\n } catch {\n return undefined;\n }\n}\n\n/**\n * Merged injector: check the field-level injector first (for tokens like\n * `ARRAY_CONTEXT` / `FIELD_SIGNAL_CONTEXT`), fall back to the element-chain\n * injector (so nested wrappers can still `inject(OuterWrapper)`).\n */\nexport function createWrapperAwareInjector(fieldInjector: Injector, elementInjector: Injector): Injector {\n if (fieldInjector === elementInjector) return fieldInjector;\n return new WrapperAwareInjector(fieldInjector, elementInjector);\n}\n\n/** Sentinel — distinguishes \"token not found\" from a legitimate `undefined` provider value. */\nconst WRAPPER_NOT_FOUND = {};\n\nclass WrapperAwareInjector extends Injector {\n constructor(\n private readonly fieldInjector: Injector,\n private readonly elementInjector: Injector,\n ) {\n super();\n }\n\n override get<T>(token: ProviderToken<T>, notFoundValue?: T, options?: InjectOptions): T;\n override get(token: unknown, notFoundValue?: unknown, options?: InjectOptions): unknown {\n const field = this.fieldInjector.get(token as ProviderToken<unknown>, WRAPPER_NOT_FOUND, options);\n if (field !== WRAPPER_NOT_FOUND) return field;\n return this.elementInjector.get(token as ProviderToken<unknown>, notFoundValue, options);\n }\n}\n","import { ComponentRef, computed, DestroyRef, EnvironmentInjector, inject, Injector, Signal, ViewContainerRef } from '@angular/core';\nimport { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop';\nimport { explicitEffect } from 'ngxtension/explicit-effect';\nimport { map, Observable, of, switchMap } from 'rxjs';\nimport { WRAPPER_COMPONENT_CACHE, WRAPPER_REGISTRY, WrapperConfig, WrapperTypeDefinition } from '@ng-forge/dynamic-forms/internal';\nimport { Logger } from '@ng-forge/dynamic-forms/internal';\nimport { DynamicFormLogger } from '@ng-forge/dynamic-forms/internal';\nimport { WrapperFieldInputs } from '@ng-forge/dynamic-forms/internal';\nimport { isSameWrapperChain } from '../resolve-wrappers/resolve-wrappers';\nimport { LoadedWrapper, loadWrapperComponents, renderWrapperChain, setInputIfDeclared } from './wrapper-chain';\n\nexport interface WrapperChainControllerOptions {\n /**\n * The outer slot where the wrapper chain is mounted. Sampled only at render\n * time — safe to pass a `viewChild.required` signal that resolves post-init.\n */\n readonly vcr: Signal<ViewContainerRef>;\n /**\n * Signal of the wrapper chain. Ref-stable memoisation (e.g.\n * `isSameWrapperChain`) is recommended — `WrapperConfig` entries should be\n * singletons so the controller can tell a real chain change from ref churn.\n */\n readonly wrappers: Signal<readonly WrapperConfig[]>;\n /**\n * Optional gating signal. When `false`, the chain is not mounted (initial)\n * or left untouched (post-mount flicker). `DfFieldOutlet` passes\n * `renderReady`; containers omit the gate.\n */\n readonly gate?: Signal<boolean>;\n /**\n * Optional mapper-outputs signal. When present, the controller pushes it to\n * each wrapper's `fieldInputs` input at render time AND on every subsequent\n * change. Must always produce a value once subscribed — don't emit undefined.\n * Containers render a children template (no field), so they omit this.\n */\n readonly fieldInputs?: Signal<WrapperFieldInputs>;\n /**\n * Optional extra rebuild trigger. `DfFieldOutlet` passes the innermost\n * component class so that a reconciled field with the same wrapper chain\n * but a different `component` forces a rebuild. Compared by identity.\n */\n readonly rebuildKey?: Signal<unknown>;\n /**\n * Optional field-level injector signal (e.g. `ResolvedField.injector`). When\n * provided, wrapper components see a merged injector — field tokens first\n * (`ARRAY_CONTEXT`, `FIELD_SIGNAL_CONTEXT`), element chain behind\n * (parent wrappers). Containers omit this.\n */\n readonly fieldInjector?: Signal<Injector>;\n /**\n * Renders whatever belongs at the innermost slot (a field component, a children\n * template, …). Called each time the chain mounts or structurally rebuilds.\n */\n readonly renderInnermost: (slot: ViewContainerRef) => void;\n /**\n * Called right before the mounted chain is cleared on a structural change.\n * Lets the caller detach views it wants to preserve — e.g. `DfFieldOutlet`\n * detaches the innermost field's hostView so focus / caret / scroll survive\n * when only the wrapper chain changes. Not invoked on the pre-first-render path.\n */\n readonly beforeRebuild?: () => void;\n}\n\n/**\n * Owns the wrapper-chain lifecycle for a field or container: async component\n * loading, switchMap-based cancellation of stale loads, flicker-tolerant\n * rebuilds (only structural changes tear down), and `fieldInputs` push-through.\n * Must be called from a DI injection context.\n */\nexport function createWrapperChainController(opts: WrapperChainControllerOptions): void {\n const deps = injectChainDeps();\n const state = createStateSignal(opts);\n\n const mounted = { value: null as MountedChain | null };\n let refs: ComponentRef<unknown>[] = [];\n\n // INVARIANT: toObservable schedules emissions as microtasks, so `renderInnermost`\n // runs outside any parent CD pass. Don't swap for sync signal wiring without\n // restoring df-field-outlet's detectChanges — NG0950 would re-surface.\n buildEmissionStream(state, deps)\n .pipe(takeUntilDestroyed(deps.destroyRef))\n .subscribe((emission) => {\n // Wrap the whole render path — a throw here (bad wrapper template,\n // caller's renderInnermost blew up, etc.) would otherwise terminate\n // the subscription and silently freeze subsequent chain updates.\n try {\n refs = applyEmission(emission, { opts, deps, mounted, refs });\n } catch (err) {\n deps.logger.error('Wrapper chain render failed; tearing down partial state.', err);\n opts.vcr().clear();\n refs = [];\n mounted.value = null;\n }\n });\n\n pushFieldInputsOnChange(opts, () => refs);\n deps.destroyRef.onDestroy(() => opts.vcr().clear());\n}\n\ninterface ChainDeps {\n readonly registry: ReadonlyMap<string, WrapperTypeDefinition>;\n readonly cache: Map<string, import('@angular/core').Type<unknown>>;\n readonly logger: Logger;\n readonly destroyRef: DestroyRef;\n readonly environmentInjector: EnvironmentInjector;\n}\n\ninterface ChainState {\n readonly open: boolean;\n readonly wrappers: readonly WrapperConfig[];\n readonly rebuildKey: unknown;\n}\n\ninterface MountedChain {\n readonly wrappers: readonly WrapperConfig[];\n readonly rebuildKey: unknown;\n}\n\ninterface ChainEmission {\n readonly state: ChainState;\n readonly loaded: readonly LoadedWrapper[] | null;\n}\n\ninterface EmissionApplyContext {\n readonly opts: WrapperChainControllerOptions;\n readonly deps: ChainDeps;\n readonly mounted: { value: MountedChain | null };\n readonly refs: ComponentRef<unknown>[];\n}\n\nfunction injectChainDeps(): ChainDeps {\n return {\n registry: inject(WRAPPER_REGISTRY),\n cache: inject(WRAPPER_COMPONENT_CACHE),\n logger: inject(DynamicFormLogger),\n destroyRef: inject(DestroyRef),\n environmentInjector: inject(EnvironmentInjector),\n };\n}\n\nfunction createStateSignal(opts: WrapperChainControllerOptions): Signal<ChainState> {\n return computed(\n () => ({\n open: opts.gate?.() ?? true,\n wrappers: opts.wrappers(),\n rebuildKey: opts.rebuildKey?.(),\n }),\n { equal: (a, b) => a.open === b.open && a.rebuildKey === b.rebuildKey && isSameWrapperChain(a.wrappers, b.wrappers) },\n );\n}\n\n/**\n * Resolve a `ChainState` into a `ChainEmission`. `switchMap` cancellation\n * guarantees in-flight async loads are discarded when a newer state arrives.\n */\nfunction buildEmissionStream(state: Signal<ChainState>, deps: ChainDeps): Observable<ChainEmission> {\n return toObservable(state).pipe(switchMap((s) => resolveLoadedWrappers(s, deps)));\n}\n\nfunction resolveLoadedWrappers(state: ChainState, deps: ChainDeps): Observable<ChainEmission> {\n if (!state.open) {\n return of({ state, loaded: null });\n }\n\n if (state.wrappers.every((w) => deps.cache.has(w.type))) {\n // Sync fast-path — the cache only holds SUCCESSFUL loads, so \"every cached\"\n // means every wrapper was previously resolved cleanly. No need to re-log.\n const loaded = state.wrappers.map((config) => ({ config, component: deps.cache.get(config.type)! }) satisfies LoadedWrapper);\n return of({ state, loaded });\n }\n\n return loadWrapperComponents(state.wrappers, deps.registry, deps.cache, deps.logger).pipe(map((loaded) => ({ state, loaded })));\n}\n\n/**\n * Diff what's mounted against the next state and act:\n * - Gate-only flicker → no-op (preserve the mounted chain)\n * - Structural change → beforeRebuild → vcr.clear → render fresh\n * - Idempotent re-emission → no-op\n */\nfunction applyEmission({ state, loaded }: ChainEmission, ctx: EmissionApplyContext): ComponentRef<unknown>[] {\n const { opts, deps, mounted, refs } = ctx;\n const vcr = opts.vcr();\n const structurallyChanged = isStructurallyDifferent(mounted.value, state);\n\n // Gate-only flicker after first mount — keep the chain alive so the user's\n // focus / caret / scroll survive. The caller's rawInputs effect continues\n // to push live mapper outputs through the still-mounted innermost.\n if (!structurallyChanged && !state.open) return refs;\n\n // Real structural change — tear down. Angular cascades destroy through\n // every nested ComponentRef, so walking `refs` manually is redundant.\n // `beforeRebuild` gives the caller a chance to detach views it wants to\n // preserve (e.g. the innermost field when only wrappers changed).\n if (structurallyChanged && mounted.value !== null) {\n opts.beforeRebuild?.();\n vcr.clear();\n mounted.value = null;\n }\n\n if (!state.open || loaded === null) return [];\n // Same structure + already mounted — idempotent re-emission, nothing to do.\n if (mounted.value !== null) return refs;\n\n const newRefs = renderWrapperChain({\n outerContainer: vcr,\n loadedWrappers: loaded,\n environmentInjector: deps.environmentInjector,\n logger: deps.logger,\n fieldInputs: opts.fieldInputs?.(),\n fieldInjector: opts.fieldInjector?.(),\n renderInnermost: opts.renderInnermost,\n });\n mounted.value = { wrappers: state.wrappers, rebuildKey: state.rebuildKey };\n return newRefs;\n}\n\nfunction isStructurallyDifferent(mounted: MountedChain | null, next: ChainState): boolean {\n if (mounted === null) return true;\n if (mounted.rebuildKey !== next.rebuildKey) return true;\n return !isSameWrapperChain(mounted.wrappers, next.wrappers);\n}\n\n/**\n * Push the latest `fieldInputs` bag to every mounted wrapper whenever it\n * changes — without rebuilding the chain. No-op when the chain is empty\n * (gated off, or nothing rendered yet).\n */\nfunction pushFieldInputsOnChange(opts: WrapperChainControllerOptions, getRefs: () => readonly ComponentRef<unknown>[]): void {\n if (!opts.fieldInputs) return;\n explicitEffect([opts.fieldInputs], ([fi]) => {\n for (const ref of getRefs()) setInputIfDeclared(ref, 'fieldInputs', fi);\n });\n}\n","import { FieldTree } from '@angular/forms/signals';\nimport { ReadonlyFieldTree, toReadonlyFieldTreeCached, writeToFieldValue } from '@ng-forge/dynamic-forms/internal';\nimport { WrapperFieldInputs } from '@ng-forge/dynamic-forms/internal';\n\n/**\n * Convert a field's raw mapper outputs into the {@link WrapperFieldInputs}\n * bag consumed by wrappers and addons.\n *\n * @param rawInputs Mapper outputs (`{ key, label, props, field?: FieldTree, ... }`).\n * @param cache `WeakMap<FieldTree, ReadonlyFieldTree>` provided via\n * {@link READONLY_FIELD_TREE_CACHE}.\n * @param fieldType Optional field-type discriminant. Injected here (rather\n * than via the field mapper) because `type` is the\n * registry key, not a component input — but addons and\n * wrappers legitimately need it.\n */\nexport function buildFieldInputs(\n rawInputs: Record<string, unknown>,\n cache: WeakMap<FieldTree<unknown>, ReadonlyFieldTree<unknown>>,\n fieldType?: string,\n): WrapperFieldInputs {\n const fieldTreeCandidate = rawInputs['field'];\n // FieldTree is callable: `(): FieldState`. Anything else (undefined,\n // primitive) means the field is rendered before the form is bound.\n const hasFieldTree = fieldTreeCandidate !== undefined && typeof fieldTreeCandidate === 'function';\n const readonlyField = hasFieldTree ? toReadonlyFieldTreeCached(cache, fieldTreeCandidate as FieldTree<unknown>) : undefined;\n // Lazy writer keyed on the raw FieldTree. Addons (e.g., `prime-button`\n // presets) call this to mutate the host field. `writeToFieldValue`\n // centralises the `Signal → WritableSignal` cast and warns at runtime\n // if Signal Forms ever changes the contract.\n const setValue = hasFieldTree\n ? (next: unknown) => writeToFieldValue((fieldTreeCandidate as FieldTree<unknown>)().value, next)\n : undefined;\n // Shallow spread — relies on the mapper contract (see WrapperFieldInputs)\n // that rawInputs are emitted as fresh snapshots, not mutated in place.\n return {\n ...rawInputs,\n type: fieldType,\n field: readonlyField,\n setValue,\n } as WrapperFieldInputs;\n}\n","import { ComponentRef, EnvironmentInjector, Injector, signal, Type, ViewContainerRef } from '@angular/core';\nimport { createWrapperAwareInjector, setInputIfDeclared } from '../../utils/wrapper-chain/wrapper-chain';\nimport type { WrapperFieldInputs } from '@ng-forge/dynamic-forms/internal';\n\n/**\n * Captured focus + caret for one detach/reinsert cycle. Tied to a single\n * mounted-detached-mounted round trip; never persisted across a component-\n * class swap (the fresh `Mounted` state starts with no snapshot).\n */\ninterface FocusSnapshot {\n readonly element: HTMLElement;\n readonly selectionStart: number | null;\n readonly selectionEnd: number | null;\n}\n\n/**\n * Explicit lifecycle states for `FieldComponentSlot`. The discriminant carries\n * exactly the data each phase needs, so nothing in the slot is \"optional state\n * that might be undefined\" — the type forces every code path to handle the\n * empty/mounted/detached cases distinctly.\n */\nexport type FieldSlotState =\n | { readonly phase: 'empty' }\n | {\n readonly phase: 'mounted';\n readonly ref: ComponentRef<unknown>;\n readonly slot: ViewContainerRef;\n }\n | {\n readonly phase: 'detached';\n readonly ref: ComponentRef<unknown>;\n readonly focusSnapshot: FocusSnapshot | null;\n };\n\nconst EMPTY_STATE: FieldSlotState = Object.freeze({ phase: 'empty' });\n\n/**\n * Encapsulates the imperative `ViewContainerRef` / `ComponentRef` lifecycle\n * for the innermost field component. Pulled out of `DfFieldOutlet` so the\n * directive itself stays declarative — signals + constructor wiring — while\n * the unavoidable mutable bookkeeping (Angular's view container API is\n * imperative) lives in one place behind a small, focused surface.\n */\nexport class FieldComponentSlot {\n private readonly state = signal<FieldSlotState>(EMPTY_STATE);\n /**\n * Per-key cache for `pushInputs`. Lives outside the lifecycle state machine on\n * purpose — it's a `setInput`-call optimization that changes on every keystroke,\n * not a phase transition. Keeping it here means the `state` signal only emits\n * on real `empty ⇄ mounted ⇄ detached` transitions (≈ once per\n * mount/destroy cycle), not once per input emission.\n */\n private lastInputs: Record<string, unknown> | null = null;\n\n /** Read-only view of the lifecycle phase. Useful for tests and assertions. */\n readonly phase = () => this.state().phase;\n /** Read-only view of the full state. Exposed so tests can assert ref stability. */\n readonly snapshot = () => this.state();\n\n /**\n * Mount the field component into `slot`. Reuses the existing `ComponentRef`\n * when its component type matches (preserving DOM/focus/caret across a\n * wrapper-chain rebuild); otherwise discards the old ref and creates a fresh\n * one.\n */\n mountOrReuse(\n slot: ViewContainerRef,\n component: Type<unknown>,\n fieldInjector: Injector,\n environmentInjector: EnvironmentInjector,\n rawInputs: Record<string, unknown>,\n fieldInputs?: WrapperFieldInputs,\n ): void {\n const current = this.state();\n\n if (current.phase !== 'empty' && current.ref.componentType === component) {\n slot.insert(current.ref.hostView);\n this.lastInputs = null;\n this.state.set(Object.freeze({ phase: 'mounted', ref: current.ref, slot }));\n this.pushInputs(rawInputs, fieldInputs);\n if (current.phase === 'detached') this.replayFocus(current.focusSnapshot);\n return;\n }\n\n // Different component class — destroy the prior ref (if any) and create\n // a fresh one. The `empty` transition before `createComponent` ensures a\n // throw inside the factory doesn't leave a dangling ref the next mount\n // would try to re-insert.\n if (current.phase !== 'empty') current.ref.destroy();\n this.lastInputs = null;\n this.state.set(EMPTY_STATE);\n const ref = slot.createComponent(component, {\n environmentInjector,\n injector: createWrapperAwareInjector(fieldInjector, slot.injector),\n });\n this.state.set(Object.freeze({ phase: 'mounted', ref, slot }));\n this.pushInputs(rawInputs, fieldInputs);\n }\n\n /**\n * Push mapper inputs onto the mounted component. Per-key reference compare\n * skips `setInput` for unchanged values — necessary in dev mode where\n * `setInput` on unknown keys logs NG0303 per call (Angular 21 is lenient\n * at runtime but warns); without the cache, every emission would re-warn\n * for every undeclared mapper key.\n */\n pushInputs(rawInputs: Record<string, unknown>, fieldInputs?: WrapperFieldInputs): void {\n const current = this.state();\n if (current.phase !== 'mounted') return;\n const prev = this.lastInputs;\n if (prev === rawInputs) return;\n for (const [key, value] of Object.entries(rawInputs)) {\n if (prev && prev[key] === value) continue;\n current.ref.setInput(key, value);\n }\n // Side-channel forwarding for the `fieldInputs` bag — gated on\n // `setInputIfDeclared` so leaf fields that don't declare it (most\n // non-addon-aware components) skip silently rather than triggering\n // NG0303 in dev. Pushed unconditionally on every emission since the bag\n // changes identity when `rawInputs` does.\n if (fieldInputs !== undefined) {\n setInputIfDeclared(current.ref, 'fieldInputs', fieldInputs);\n }\n this.lastInputs = rawInputs;\n }\n\n /**\n * Detach the host view from its current slot, capturing focus + caret so the\n * next `mountOrReuse()` can replay them after re-inserting into the new\n * slot. A stranded `mounted` state with a bad slot index (re-entrancy during\n * a partial rebuild) is treated like `empty` to avoid leaks.\n */\n detach(): void {\n const current = this.state();\n if (current.phase !== 'mounted') return;\n const focusSnapshot = this.captureFocus(current.ref);\n const idx = current.slot.indexOf(current.ref.hostView);\n if (idx >= 0) current.slot.detach(idx);\n this.lastInputs = null;\n this.state.set(Object.freeze({ phase: 'detached', ref: current.ref, focusSnapshot }));\n }\n\n /**\n * Called on directive teardown. Only destroys when the host view is still\n * detached — otherwise the outer `vcr.clear()` cascade has already destroyed\n * it (calling `.destroy()` again would throw).\n */\n destroyOnTeardown(): void {\n const current = this.state();\n if (current.phase === 'detached') current.ref.destroy();\n this.lastInputs = null;\n this.state.set(EMPTY_STATE);\n }\n\n private captureFocus(ref: ComponentRef<unknown>): FocusSnapshot | null {\n const hostEl = ref.location.nativeElement as HTMLElement;\n const active = document.activeElement;\n if (!(active instanceof HTMLElement) || !hostEl.contains(active)) return null;\n const sel = active as HTMLInputElement & HTMLTextAreaElement;\n return {\n element: active,\n selectionStart: typeof sel.selectionStart === 'number' ? sel.selectionStart : null,\n selectionEnd: typeof sel.selectionEnd === 'number' ? sel.selectionEnd : null,\n };\n }\n\n private replayFocus(snap: FocusSnapshot | null): void {\n if (!snap || !snap.element.isConnected) return;\n snap.element.focus();\n if (snap.selectionStart !== null && snap.selectionEnd !== null && 'setSelectionRange' in snap.element) {\n (snap.element as HTMLInputElement).setSelectionRange(snap.selectionStart, snap.selectionEnd);\n }\n }\n}\n","import { computed, DestroyRef, Directive, EnvironmentInjector, inject, input, Signal, signal, Type, ViewContainerRef } from '@angular/core';\nimport { explicitEffect } from 'ngxtension/explicit-effect';\nimport { ResolvedField } from '../../utils/resolve-field/resolve-field';\nimport { WRAPPER_AUTO_ASSOCIATIONS } from '@ng-forge/dynamic-forms/internal';\nimport { DEFAULT_WRAPPERS } from '@ng-forge/dynamic-forms/internal';\nimport { createWrapperChainController } from '../../utils/wrapper-chain/wrapper-chain-controller';\nimport { isSameWrapperChain, resolveWrappers } from '../../utils/resolve-wrappers/resolve-wrappers';\nimport { READONLY_FIELD_TREE_CACHE } from '@ng-forge/dynamic-forms/internal';\nimport { buildFieldInputs } from '../../utils/build-field-inputs/build-field-inputs';\nimport { WrapperFieldInputs } from '@ng-forge/dynamic-forms/internal';\nimport { FieldComponentSlot } from './field-component-slot';\n\n/**\n * Structural directive that renders a `ResolvedField` with its effective\n * wrapper chain.\n */\n@Directive({\n selector: '[dfFieldOutlet]',\n})\nexport class DfFieldOutlet {\n // Named to match the structural directive microsyntax directly\n // (`*dfFieldOutlet=\"field; environmentInjector: env\"`) so no aliasing is needed.\n readonly dfFieldOutlet = input.required<ResolvedField>();\n readonly dfFieldOutletEnvironmentInjector = input<EnvironmentInjector | undefined>(undefined);\n\n private readonly vcrRef = inject(ViewContainerRef);\n private readonly vcr: Signal<ViewContainerRef> = signal(this.vcrRef).asReadonly();\n private readonly destroyRef = inject(DestroyRef);\n private readonly wrapperAutoAssociations = inject(WRAPPER_AUTO_ASSOCIATIONS);\n private readonly defaultWrappersSignal = inject(DEFAULT_WRAPPERS, { optional: true });\n private readonly readonlyFieldCache = inject(READONLY_FIELD_TREE_CACHE);\n\n /** Encapsulates Angular's imperative ComponentRef / ViewContainerRef lifecycle. */\n private readonly fieldComponent = new FieldComponentSlot();\n\n private readonly componentIdentity: Signal<Type<unknown>> = computed(() => this.dfFieldOutlet().component);\n /**\n * Gate for the wrapper chain controller: true only when required inputs are populated AND the\n * field isn't hidden. Combining the two prevents an initial-mount race where the chain mounts\n * during the brief window between `renderReady` flipping true and `hidden()` settling — that\n * window is exactly where Angular Signal Forms' `[formField]` directive emits NG01916.\n */\n private readonly renderReady: Signal<boolean> = computed(() => this.dfFieldOutlet().renderReady() && !this.dfFieldOutlet().hidden());\n private readonly rawInputs = computed(() => this.dfFieldOutlet().inputs());\n\n /**\n * Effective wrapper chain. Element-wise identity comparison keeps the signal\n * stable across `ResolvedField` reference changes that don't actually change\n * the chain — avoids rebuilds on reconciled fields.\n */\n private readonly wrappers = computed(\n () => resolveWrappers(this.dfFieldOutlet().fieldDef, this.defaultWrappersSignal?.(), this.wrapperAutoAssociations),\n { equal: isSameWrapperChain },\n );\n\n /**\n * `fieldInputs` bag handed to every wrapper in the chain AND forwarded to\n * the innermost field component (when it declares `fieldInputs` as an\n * input). Memoised on `rawInputs` identity so repeated emissions with the\n * same underlying object return the same view and don't cascade OnPush\n * re-evaluations.\n */\n private readonly fieldInputs = computed<WrapperFieldInputs>(() =>\n buildFieldInputs(this.rawInputs(), this.readonlyFieldCache, this.dfFieldOutlet().fieldDef.type),\n );\n\n private readonly defaultEnvInjector = inject(EnvironmentInjector);\n /** Environment injector for the innermost field component — `[environmentInjector]` input takes precedence over the directive's own DI. */\n private readonly fieldEnvInjector = computed(() => this.dfFieldOutletEnvironmentInjector() ?? this.defaultEnvInjector);\n /** Field-level injector (FIELD_SIGNAL_CONTEXT, ARRAY_CONTEXT, …). Threaded to the controller so wrappers can inject it too. */\n private readonly fieldInjector = computed(() => this.dfFieldOutlet().injector);\n\n constructor() {\n createWrapperChainController({\n vcr: this.vcr,\n wrappers: this.wrappers,\n gate: this.renderReady,\n rebuildKey: this.componentIdentity,\n fieldInputs: this.fieldInputs,\n fieldInjector: this.fieldInjector,\n beforeRebuild: () => this.fieldComponent.detach(),\n renderInnermost: (slot) => {\n const resolved = this.dfFieldOutlet();\n this.fieldComponent.mountOrReuse(\n slot,\n resolved.component,\n resolved.injector,\n this.fieldEnvInjector(),\n this.rawInputs(),\n this.fieldInputs(),\n );\n },\n });\n\n // Push rawInputs (plus the wrapper-style fieldInputs bag) to the innermost\n // field on every change. The slot dedupes per-key, so the initial render's\n // synchronous push from `renderInnermost` and the effect's first fire\n // converge cleanly with no double-pushing.\n explicitEffect([this.rawInputs, this.fieldInputs], ([rawInputs, fieldInputs]) =>\n this.fieldComponent.pushInputs(rawInputs, fieldInputs),\n );\n\n this.destroyRef.onDestroy(() => this.fieldComponent.destroyOnTeardown());\n }\n}\n","import { Signal } from '@angular/core';\nimport { FieldTree, submit } from '@angular/forms/signals';\nimport { catchError, EMPTY, exhaustMap, firstValueFrom, from, isObservable, Observable } from 'rxjs';\nimport { EventBus } from '@ng-forge/dynamic-forms/internal';\nimport { FormSubmitEvent } from '../../events/constants/submit.event';\nimport { FormConfig } from '@ng-forge/dynamic-forms/internal';\nimport { RegisteredFieldTypes } from '@ng-forge/dynamic-forms/internal';\nimport type { InferFormModel } from '@ng-forge/dynamic-forms/internal';\nimport type { Logger } from '@ng-forge/dynamic-forms/internal';\n\n/**\n * Options for creating a submission handler.\n *\n * @typeParam TFields - Array of registered field types available for this form\n * @typeParam TModel - The form value model type (inferred from TFields by default)\n */\nexport interface SubmissionHandlerOptions<\n TFields extends RegisteredFieldTypes[] = RegisteredFieldTypes[],\n TModel extends Record<string, unknown> = InferFormModel<TFields>,\n> {\n /** Event bus instance for listening to submit events */\n eventBus: EventBus;\n /** Signal containing the form configuration */\n configSignal: Signal<FormConfig<TFields>>;\n /** Signal containing the form instance */\n formSignal: Signal<FieldTree<TModel>>;\n /** Signal indicating whether the form is currently valid */\n validSignal: Signal<boolean>;\n /** Logger instance for consistent error reporting */\n logger: Logger;\n}\n\n/**\n * Wraps a submission action to handle both Promise and Observable returns.\n * Converts Observables to Promises for compatibility with Angular Signal Forms' submit().\n *\n * @param action - The submission action function\n * @returns A wrapped function that returns a Promise\n */\nfunction wrapSubmissionAction<TModel extends Record<string, unknown>>(\n action: (formTree: FieldTree<TModel>) => unknown,\n): (formTree: FieldTree<TModel>) => Promise<void> {\n return async (formTree: FieldTree<TModel>): Promise<void> => {\n const result = action(formTree);\n // If the action returns an Observable, convert it to a Promise\n if (isObservable(result)) {\n await firstValueFrom(result);\n return;\n }\n await Promise.resolve(result);\n };\n}\n\n/**\n * Creates an Observable that handles form submission with optional submission action.\n *\n * @param options - Configuration options for the submission handler\n * @returns Observable that processes submissions (emits when submission completes)\n */\nexport function createSubmissionHandler<\n TFields extends RegisteredFieldTypes[] = RegisteredFieldTypes[],\n TModel extends Record<string, unknown> = InferFormModel<TFields>,\n>(options: SubmissionHandlerOptions<TFields, TModel>): Observable<unknown> {\n const { eventBus, configSignal, formSignal, validSignal, logger } = options;\n\n // exhaustMap ensures first-submit-wins: a second submit event while the first\n // is in-flight is silently dropped rather than cancelling the running Promise.\n // switchMap would unsubscribe the Observable wrapper but cannot cancel the\n // underlying Promise, causing both side effects to execute.\n return eventBus.on<FormSubmitEvent>('submit').pipe(\n exhaustMap(() => {\n const submissionConfig = configSignal().submission;\n\n // If no submission action is configured, let the submitted output handle it\n // This maintains backward compatibility for users handling submission manually\n if (!submissionConfig?.action) {\n return EMPTY;\n }\n\n // Guard: match the (submitted) output's safety contract — reject submission\n // when the form is invalid or has pending async validators.\n if (!validSignal()) {\n logger.debug('Submission action skipped: form is not valid (invalid or pending async validators)');\n return EMPTY;\n }\n\n // Type assertion needed: submission.action accepts the form tree but its signature\n // is defined broadly in FormConfig. The actual runtime type is FieldTree<TModel>.\n const wrappedAction = wrapSubmissionAction<TModel>(submissionConfig.action as (formTree: FieldTree<TModel>) => unknown);\n\n // Use Angular Signal Forms' native submit() function\n // This automatically:\n // - Sets form.submitting() to true during execution\n // - Applies server errors to form fields on completion\n // - Sets form.submitting() to false when done\n // catchError keeps the exhaustMap stream alive after action failure —\n // without it, an unhandled error would terminate all future submissions.\n return from(submit(formSignal(), wrappedAction)).pipe(\n catchError((error: unknown) => {\n logger.error('Submission action failed:', error);\n return EMPTY;\n }),\n );\n }),\n );\n}\n","import { FormEvent } from '@ng-forge/dynamic-forms/internal';\n\n/** Event dispatched to submit the form (the action behind a `submit` button). */\nexport class FormSubmitEvent implements FormEvent {\n readonly type = 'submit' as const;\n}\n","import { FormEvent } from '@ng-forge/dynamic-forms/internal';\n\n/** Event dispatched to advance a paged form to the next page. */\nexport class NextPageEvent implements FormEvent {\n readonly type = 'next-page' as const;\n}\n","import { FormEvent } from '@ng-forge/dynamic-forms/internal';\n\n/** Event emitted when the active page of a paged form changes. */\nexport class PageChangeEvent implements FormEvent {\n readonly type = 'page-change' as const;\n\n constructor(\n /** The current page index (0-based) */\n public readonly currentPageIndex: number,\n /** Total number of pages */\n public readonly totalPages: number,\n /** Previous page index (0-based), undefined if first navigation */\n public readonly previousPageIndex?: number,\n ) {}\n}\n","import { FormEvent } from '@ng-forge/dynamic-forms/internal';\n\n/** Event dispatched to return a paged form to the previous page. */\nexport class PreviousPageEvent implements FormEvent {\n readonly type = 'previous-page' as const;\n}\n","import { FormEvent } from '@ng-forge/dynamic-forms/internal';\n\n/** Event dispatched when the form should be reset to its default values. */\nexport class FormResetEvent implements FormEvent {\n readonly type = 'form-reset' as const;\n}\n","import { FormEvent } from '@ng-forge/dynamic-forms/internal';\n\n/** Event dispatched when the form should be cleared. */\nexport class FormClearEvent implements FormEvent {\n readonly type = 'form-clear' as const;\n}\n","import { FormEvent } from '@ng-forge/dynamic-forms/internal';\nimport { ArrayItemDefinition, ArrayItemTemplate } from '@ng-forge/dynamic-forms/internal';\n\n/**\n * Template type for array items used in events.\n * Canonical definition lives in {@link ArrayItemDefinition} — this alias\n * preserves the events-specific naming convention.\n */\nexport type ArrayItemDefinitionTemplate = ArrayItemDefinition;\n\n// Re-export the canonical ArrayItemTemplate for backwards-compatible event API surface\nexport type { ArrayItemTemplate };\n\n/**\n * Event dispatched to append a new item at the END of an array field.\n *\n * @typeParam TTemplate - The type of the template (single field or array of fields)\n */\nexport class AppendArrayItemEvent<TTemplate extends ArrayItemDefinitionTemplate = ArrayItemDefinitionTemplate> implements FormEvent {\n readonly type = 'append-array-item' as const;\n\n constructor(\n /** The key of the array field to append an item to */\n public readonly arrayKey: string,\n /**\n * Template for the new array item. REQUIRED.\n * - Single field (FieldDef): Creates a primitive item (field's value is extracted directly)\n * - Array of fields (FieldDef[]): Creates an object item (fields merged into object)\n */\n public readonly template: TTemplate,\n ) {}\n}\n","import { FormEvent } from '@ng-forge/dynamic-forms/internal';\nimport { ArrayItemDefinitionTemplate } from './append-array-item.event';\n\n/**\n * Event dispatched to prepend a new item at the BEGINNING of an array field.\n *\n * @typeParam TTemplate - The type of the template (single field or array of fields)\n */\nexport class PrependArrayItemEvent<TTemplate extends ArrayItemDefinitionTemplate = ArrayItemDefinitionTemplate> implements FormEvent {\n readonly type = 'prepend-array-item' as const;\n\n constructor(\n /** The key of the array field to prepend an item to */\n public readonly arrayKey: string,\n /**\n * Template for the new array item. REQUIRED.\n * - Single field (FieldDef): Creates a primitive item (field's value is extracted directly)\n * - Array of fields (FieldDef[]): Creates an object item (fields merged into object)\n */\n public readonly template: TTemplate,\n ) {}\n}\n","import { FormEvent } from '@ng-forge/dynamic-forms/internal';\nimport { ArrayItemDefinitionTemplate } from './append-array-item.event';\n\n/**\n * Event dispatched to insert a new item at a SPECIFIC INDEX in an array field.\n *\n * @typeParam TTemplate - The type of the template (single field or array of fields)\n */\nexport class InsertArrayItemEvent<TTemplate extends ArrayItemDefinitionTemplate = ArrayItemDefinitionTemplate> implements FormEvent {\n readonly type = 'insert-array-item' as const;\n\n constructor(\n /** The key of the array field to insert an item into */\n public readonly arrayKey: string,\n /** The index at which to insert the new item */\n public readonly index: number,\n /**\n * Template for the new array item. REQUIRED.\n * - Single field (FieldDef): Creates a primitive item (field's value is extracted directly)\n * - Array of fields (FieldDef[]): Creates an object item (fields merged into object)\n */\n public readonly template: TTemplate,\n ) {}\n}\n","import { FormEvent } from '@ng-forge/dynamic-forms/internal';\n\n/** Event dispatched to remove the LAST item from an array field. */\nexport class PopArrayItemEvent implements FormEvent {\n readonly type = 'pop-array-item' as const;\n\n constructor(\n /** The key of the array field to remove the last item from */\n public readonly arrayKey: string,\n ) {}\n}\n","import { FormEvent } from '@ng-forge/dynamic-forms/internal';\n\n/** Event dispatched to remove the FIRST item from an array field. */\nexport class ShiftArrayItemEvent implements FormEvent {\n readonly type = 'shift-array-item' as const;\n\n constructor(\n /** The key of the array field to remove the first item from */\n public readonly arrayKey: string,\n ) {}\n}\n","import { FormEvent } from '@ng-forge/dynamic-forms/internal';\n\n/** Event dispatched to move an existing item from one index to another within an array field. */\nexport class MoveArrayItemEvent implements FormEvent {\n readonly type = 'move-array-item' as const;\n\n constructor(\n /** The key of the array field containing the item to move */\n public readonly arrayKey: string,\n /** The current index of the item to move */\n public readonly fromIndex: number,\n /** The target index to move the item to */\n public readonly toIndex: number,\n ) {}\n}\n","import { FormEvent } from '@ng-forge/dynamic-forms/internal';\n\n/** Event dispatched to remove an item at a SPECIFIC INDEX from an array field. */\nexport class RemoveAtIndexEvent implements FormEvent {\n readonly type = 'remove-at-index' as const;\n\n constructor(\n /** The key of the array field to remove an item from */\n public readonly arrayKey: string,\n /** The index of the item to remove */\n public readonly index: number,\n ) {}\n}\n","import { FormEvent } from '@ng-forge/dynamic-forms/internal';\nimport { PageOrchestratorState } from '../../core/page-orchestrator/page-orchestrator.interfaces';\n\nexport class PageNavigationStateChangeEvent implements FormEvent {\n type = 'page-navigation-state-change' as const;\n\n constructor(public state: PageOrchestratorState) {}\n}\n","import { ChangeDetectionStrategy, Component, computed, inject, input, linkedSignal } from '@angular/core';\nimport { takeUntilDestroyed } from '@angular/core/rxjs-interop';\nimport { EventBus } from '@ng-forge/dynamic-forms/internal';\nimport { NextPageEvent, PageChangeEvent, PreviousPageEvent } from '../../events/constants';\nimport { NavigationResult, PageOrchestratorState } from './page-orchestrator.interfaces';\nimport { PageField } from '@ng-forge/dynamic-forms/internal';\nimport { ContainerLogicConfig } from '@ng-forge/dynamic-forms/internal';\nimport { FieldSignalContext } from '@ng-forge/dynamic-forms/internal';\nimport PageFieldComponent from '../../fields/page/page-field.component';\nimport { explicitEffect } from 'ngxtension/explicit-effect';\nimport { PageNavigationStateChangeEvent } from '../../events/constants/page-navigation-state-change.event';\nimport { FieldTree } from '@angular/forms/signals';\nimport { FIELD_SIGNAL_CONTEXT, FORM_OPTIONS } from '@ng-forge/dynamic-forms/internal';\nimport { ConditionalExpression } from '@ng-forge/dynamic-forms/internal';\nimport { evaluateCondition } from '@ng-forge/dynamic-forms/internal';\nimport { FunctionRegistryService } from '@ng-forge/dynamic-forms/internal';\nimport { FieldContextRegistryService } from '@ng-forge/dynamic-forms/internal';\nimport { FieldDef } from '@ng-forge/dynamic-forms/internal';\nimport { isRowField } from '@ng-forge/dynamic-forms/internal';\n\n/**\n * PageOrchestrator manages page navigation and visibility for paged forms.\n * It acts as an intermediary between the DynamicForm component and PageField components,\n * handling page state management and navigation events without interfering with form data.\n */\n@Component({\n selector: 'div[page-orchestrator]',\n imports: [PageFieldComponent],\n template: `\n @for (pageField of pageFields(); track pageField.key; let i = $index) {\n <!--\n Skip pages hidden by 'hidden' logic conditions entirely. FormStateManager\n derives schema/value/validators from config (not from mounted components),\n so unmounting a hidden page has no effect on form state — only on render +\n CD cost. This is the page-level equivalent of the existing field-level\n \\`@if (!field.hidden())\\` gate in page-field.component.ts.\n -->\n @if (!pageHiddenStates()[i]) {\n @if (i === state().currentPageIndex || i === state().currentPageIndex + 1 || i === state().currentPageIndex - 1) {\n <!-- Current and adjacent pages (±1): render immediately for flicker-free navigation -->\n @defer (on immediate) {\n <section\n page-field\n [field]=\"pageField\"\n [key]=\"pageField.key\"\n [pageIndex]=\"i\"\n [isVisible]=\"i === state().currentPageIndex\"\n ></section>\n } @placeholder {\n <div class=\"df-page-placeholder\" [attr.data-page-index]=\"i\" [attr.data-page-key]=\"pageField.key\"></div>\n }\n } @else {\n <!-- Distant pages: defer until browser is idle for memory savings -->\n @defer (on idle) {\n <section page-field [field]=\"pageField\" [key]=\"pageField.key\" [pageIndex]=\"i\" [isVisible]=\"false\"></section>\n } @placeholder {\n <div class=\"df-page-placeholder\" [attr.data-page-index]=\"i\" [attr.data-page-key]=\"pageField.key\"></div>\n }\n }\n }\n }\n `,\n styleUrl: './page-orchestrator.component.scss',\n host: {\n class: 'df-page-orchestrator',\n '[attr.data-current-page]': 'state().currentPageIndex',\n '[attr.data-total-pages]': 'state().totalPages',\n },\n providers: [\n {\n provide: FIELD_SIGNAL_CONTEXT,\n useFactory: (orchestrator: PageOrchestratorComponent) => orchestrator.extendedFieldSignalContext(),\n deps: [PageOrchestratorComponent],\n },\n ],\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class PageOrchestratorComponent {\n private readonly eventBus = inject(EventBus);\n private readonly fieldContextRegistry = inject(FieldContextRegistryService);\n private readonly functionRegistry = inject(FunctionRegistryService);\n private readonly formOptions = inject(FORM_OPTIONS, { optional: true });\n\n /** Array of page field definitions to render */\n pageFields = input.required<PageField[]>();\n\n /**\n * Root form instance from parent DynamicForm.\n * Uses FieldTree<unknown> to accept any form type.\n */\n form = input.required<FieldTree<unknown>>();\n\n /** Field signal context for child fields */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- FieldSignalContext is contravariant in TModel, using any allows accepting any form type\n fieldSignalContext = input.required<FieldSignalContext<any>>();\n\n /**\n * Computed signal that tracks which pages are hidden.\n * Returns an array of booleans where true means the page is hidden.\n * This signal is reactive and will re-evaluate when form values change.\n */\n readonly pageHiddenStates = computed(() => {\n const pages = this.pageFields();\n\n return pages.map((page) => this.evaluatePageHidden(page));\n });\n\n /**\n * Computed signal that returns indices of visible (non-hidden) pages.\n * This is used for navigation to skip hidden pages.\n */\n readonly visiblePageIndices = computed(() => {\n const hiddenStates = this.pageHiddenStates();\n return hiddenStates\n .map((hidden, index) => ({ index, hidden }))\n .filter((item) => !item.hidden)\n .map((item) => item.index);\n });\n\n /**\n * Internal signal for current page index that tracks with page fields.\n * This tracks the actual page index, not the visible page index.\n */\n private readonly currentPageIndex = linkedSignal(() => {\n const totalPages = this.pageFields().length;\n\n if (totalPages === 0) return 0;\n\n // Start on page 0 (will be adjusted if page 0 is hidden by initial navigation)\n return 0;\n });\n\n /** Computed state for the orchestrator */\n readonly state = computed<PageOrchestratorState>(() => {\n const currentIndex = this.currentPageIndex();\n const totalPages = this.pageFields().length;\n const visibleIndices = this.visiblePageIndices();\n\n // Find where the current index is in the visible pages list\n const currentVisiblePosition = visibleIndices.indexOf(currentIndex);\n const isFirstVisiblePage = currentVisiblePosition === 0 || currentVisiblePosition === -1;\n const isLastVisiblePage = currentVisiblePosition >= visibleIndices.length - 1;\n\n return {\n currentPageIndex: currentIndex,\n totalPages,\n isFirstPage: isFirstVisiblePage,\n isLastPage: isLastVisiblePage,\n navigationDisabled: false,\n };\n });\n\n /**\n * Signal indicating whether all fields on the current page are valid.\n *\n * @returns `true` if current page is valid, `false` otherwise\n */\n readonly currentPageValid = computed(() => {\n const currentIndex = this.currentPageIndex();\n const pages = this.pageFields();\n const form = this.form();\n\n // No pages or invalid index\n if (pages.length === 0 || currentIndex >= pages.length) {\n return true;\n }\n\n const currentPage = pages[currentIndex];\n const pageFields = currentPage.fields || [];\n\n // Collect all leaf field keys, recursively traversing group/row containers\n const leafKeys = collectLeafFieldKeys(pageFields);\n\n // Check validity of each leaf field on the current page\n // Fields are stored at root level in the form (pages don't add nesting)\n for (const fieldKey of leafKeys) {\n const field = (form as Record<string, unknown>)[fieldKey];\n if (field && typeof field === 'function') {\n const fieldState = (field as () => { valid: () => boolean })();\n if (fieldState && typeof fieldState.valid === 'function' && !fieldState.valid()) {\n return false;\n }\n }\n }\n\n return true;\n });\n\n /** Extended field signal context that includes currentPageValid. */\n readonly extendedFieldSignalContext = computed(() => ({\n ...this.fieldSignalContext(),\n currentPageValid: this.currentPageValid,\n }));\n\n constructor() {\n // Setup event listeners for navigation\n this.setupEventListeners();\n\n // B15: Auto-navigate away when current page becomes hidden\n explicitEffect([this.state, this.visiblePageIndices], ([state, visibleIndices]) => {\n const currentVisiblePosition = visibleIndices.indexOf(state.currentPageIndex);\n if (currentVisiblePosition === -1 && visibleIndices.length > 0) {\n // Current page is hidden — navigate to the nearest visible page\n const nearest = this.findNearestVisiblePage(state.currentPageIndex, visibleIndices);\n if (nearest !== -1) {\n this.navigateToPage(nearest);\n }\n }\n });\n }\n\n /**\n * Navigate to the next visible page, skipping hidden pages.\n *\n * @returns Navigation result\n */\n navigateToNextPage(): NavigationResult {\n // Guard: do not advance if current page has invalid fields.\n // Respects disableWhenPageInvalid option (defaults to true).\n const disableWhenPageInvalid = this.formOptions?.()?.nextButton?.disableWhenPageInvalid ?? true;\n if (disableWhenPageInvalid && !this.currentPageValid()) {\n return {\n success: false,\n newPageIndex: this.state().currentPageIndex,\n error: 'Current page has invalid fields',\n };\n }\n\n const currentState = this.state();\n const visibleIndices = this.visiblePageIndices();\n\n if (currentState.isLastPage) {\n return {\n success: false,\n newPageIndex: currentState.currentPageIndex,\n error: 'Already on the last visible page',\n };\n }\n\n if (currentState.navigationDisabled) {\n return {\n success: false,\n newPageIndex: currentState.currentPageIndex,\n error: 'Navigation is currently disabled',\n };\n }\n\n // Find the next visible page after the current index\n const currentVisiblePosition = visibleIndices.indexOf(currentState.currentPageIndex);\n if (currentVisiblePosition === -1 || currentVisiblePosition >= visibleIndices.length - 1) {\n return {\n success: false,\n newPageIndex: currentState.currentPageIndex,\n error: 'No next visible page available',\n };\n }\n\n const nextVisiblePageIndex = visibleIndices[currentVisiblePosition + 1];\n return this.navigateToPage(nextVisiblePageIndex);\n }\n\n /**\n * Navigate to the previous visible page, skipping hidden pages.\n *\n * @returns Navigation result\n */\n navigateToPreviousPage(): NavigationResult {\n const currentState = this.state();\n const visibleIndices = this.visiblePageIndices();\n\n if (currentState.isFirstPage) {\n return {\n success: false,\n newPageIndex: currentState.currentPageIndex,\n error: 'Already on the first visible page',\n };\n }\n\n if (currentState.navigationDisabled) {\n return {\n success: false,\n newPageIndex: currentState.currentPageIndex,\n error: 'Navigation is currently disabled',\n };\n }\n\n // Find the previous visible page before the current index\n const currentVisiblePosition = visibleIndices.indexOf(currentState.currentPageIndex);\n if (currentVisiblePosition <= 0) {\n return {\n success: false,\n newPageIndex: currentState.currentPageIndex,\n error: 'No previous visible page available',\n };\n }\n\n const prevVisiblePageIndex = visibleIndices[currentVisiblePosition - 1];\n return this.navigateToPage(prevVisiblePageIndex);\n }\n\n /**\n * Navigate to a specific page index\n *\n * @param pageIndex The target page index (0-based)\n * @returns Navigation result\n */\n navigateToPage(pageIndex: number): NavigationResult {\n const currentState = this.state();\n const totalPages = currentState.totalPages;\n\n // Validate page index bounds\n if (pageIndex < 0 || pageIndex >= totalPages) {\n return {\n success: false,\n newPageIndex: currentState.currentPageIndex,\n error: `Invalid page index: ${pageIndex}. Valid range is 0 to ${totalPages - 1}`,\n };\n }\n\n // Validate target page is visible\n const visibleIndices = this.visiblePageIndices();\n if (!visibleIndices.includes(pageIndex)) {\n return {\n success: false,\n newPageIndex: currentState.currentPageIndex,\n error: `Cannot navigate to hidden page at index ${pageIndex}. Visible pages: [${visibleIndices.join(', ')}]`,\n };\n }\n\n // Check if already on target page\n if (pageIndex === currentState.currentPageIndex) {\n return {\n success: true,\n newPageIndex: pageIndex,\n };\n }\n\n // Perform navigation\n const previousIndex = currentState.currentPageIndex;\n this.currentPageIndex.set(pageIndex);\n\n // Emit page change event\n this.eventBus.dispatch(PageChangeEvent, pageIndex, totalPages, previousIndex);\n\n return {\n success: true,\n newPageIndex: pageIndex,\n };\n }\n\n /**\n * Finds the nearest visible page index to the given index.\n * Prefers the forward (higher index) page when equidistant.\n */\n private findNearestVisiblePage(currentIndex: number, visibleIndices: number[]): number {\n let nearest = -1;\n let minDistance = Infinity;\n\n for (const idx of visibleIndices) {\n const distance = Math.abs(idx - currentIndex);\n // Prefer forward (higher index) when tied\n if (distance < minDistance || (distance === minDistance && idx > nearest)) {\n minDistance = distance;\n nearest = idx;\n }\n }\n\n return nearest;\n }\n\n /** Set up event listeners for navigation events */\n private setupEventListeners(): void {\n // Listen for next page events\n this.eventBus\n .on<NextPageEvent>('next-page')\n .pipe(takeUntilDestroyed())\n .subscribe(() => {\n this.navigateToNextPage();\n });\n\n // Listen for previous page events\n this.eventBus\n .on<PreviousPageEvent>('previous-page')\n .pipe(takeUntilDestroyed())\n .subscribe(() => {\n this.navigateToPreviousPage();\n });\n\n explicitEffect([this.state], ([state]) => this.eventBus.dispatch(PageNavigationStateChangeEvent, state));\n }\n\n /**\n * Evaluates whether a page should be hidden based on its logic configuration.\n * A page is hidden if ANY of its hidden logic conditions evaluate to true.\n *\n * @param page The page field to evaluate\n * @returns true if the page should be hidden, false otherwise\n */\n private evaluatePageHidden(page: PageField): boolean {\n // If no logic defined, page is visible\n if (!page.logic || page.logic.length === 0) {\n return false;\n }\n\n // Filter to only hidden logic (pages only support hidden type)\n const hiddenLogic = page.logic.filter((l): l is ContainerLogicConfig => l.type === 'hidden');\n\n // If no hidden logic, page is visible\n if (hiddenLogic.length === 0) {\n return false;\n }\n\n // Check each hidden logic - if ANY condition is true, the page is hidden\n for (const logic of hiddenLogic) {\n // Handle static boolean conditions (fast path)\n if (typeof logic.condition === 'boolean') {\n if (logic.condition) {\n return true;\n }\n continue;\n }\n\n // Evaluate conditional expression using centralized context creation\n const condition = logic.condition as ConditionalExpression;\n const context = this.fieldContextRegistry.createDisplayOnlyContext(page.key || '', this.functionRegistry.getCustomFunctions());\n\n if (evaluateCondition(condition, context)) {\n return true;\n }\n }\n\n return false;\n }\n}\n\n/** Recursively collects form-tree keys for all value-bearing nodes on a page. */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- accepts any field shape for recursive traversal\nfunction collectLeafFieldKeys(fields: readonly FieldDef<any>[]): string[] {\n const keys: string[] = [];\n\n for (const field of fields) {\n if (isRowField(field)) {\n // Row children are at the same form-tree level as the row itself\n keys.push(...collectLeafFieldKeys(field.fields));\n } else if (field.key) {\n keys.push(field.key);\n }\n }\n\n return keys;\n}\n","import { FieldDef } from '@ng-forge/dynamic-forms/internal';\nimport { ArrayAllowedChildren } from '@ng-forge/dynamic-forms/internal';\n\n/** Metadata attached to normalized array fields during simplified array expansion. */\nexport interface NormalizedArrayMetadata {\n /**\n * Remove button FieldDef for primitive simplified arrays.\n * The array component renders this alongside each item without\n * wrapping in a row, preserving flat primitive form values.\n */\n readonly autoRemoveButton?: FieldDef<unknown>;\n /**\n * For primitive simplified arrays, the key of the value field template (e.g., 'value').\n * Stored during normalization so the array component doesn't need dynamic discovery.\n */\n readonly primitiveFieldKey?: string;\n /**\n * Copied from `SimplifiedArrayField.template` during normalization. Used as the\n * fallback template for any array item present in the form value that was not\n * added through the event handlers and is not covered by a positional entry in\n * `fields` — e.g., items introduced by external form-value updates.\n */\n readonly template?: ArrayAllowedChildren | readonly ArrayAllowedChildren[];\n}\n\n/**\n * Symbol key for storing normalization metadata on array field objects.\n * Using a Symbol ensures no collision with user-defined properties and\n * the property is excluded from JSON serialization and enumeration.\n */\nexport const NORMALIZED_ARRAY_METADATA = Symbol('normalizedArrayMetadata');\n\n/**\n * Associates normalization metadata with an array field object.\n * Called during simplified array expansion.\n */\nexport function setNormalizedArrayMetadata(arrayField: Record<string | symbol, unknown>, metadata: NormalizedArrayMetadata): void {\n arrayField[NORMALIZED_ARRAY_METADATA] = metadata;\n}\n\n/**\n * Retrieves normalization metadata for an array field, if any.\n * Returns undefined for full-API arrays that were not normalized from a simplified definition.\n */\nexport function getNormalizedArrayMetadata(arrayField: object): NormalizedArrayMetadata | undefined {\n return (arrayField as Record<symbol, unknown>)[NORMALIZED_ARRAY_METADATA] as NormalizedArrayMetadata | undefined;\n}\n","import { FieldDef } from '@ng-forge/dynamic-forms/internal';\nimport {\n isSimplifiedArrayField,\n SimplifiedArrayField,\n ArrayButtonConfig,\n ArrayField,\n isArrayField,\n} from '@ng-forge/dynamic-forms/internal';\nimport { isContainerField, hasChildFields } from '@ng-forge/dynamic-forms/internal';\nimport { ArrayAllowedChildren } from '@ng-forge/dynamic-forms/internal';\nimport { ArrayItemDefinition } from '@ng-forge/dynamic-forms/internal';\nimport { setNormalizedArrayMetadata } from './normalized-array-metadata';\nimport { DynamicFormError } from '@ng-forge/dynamic-forms/internal';\n\n/** Normalizes simplified array fields into full array field definitions. */\nexport function normalizeSimplifiedArrays(fields: FieldDef<unknown>[]): FieldDef<unknown>[] {\n return fields.flatMap((field): FieldDef<unknown>[] => {\n // Recurse into non-array containers (page, group, row) that have child fields\n if (isContainerField(field) && !isArrayField(field) && hasChildFields(field)) {\n return [{ ...field, fields: normalizeSimplifiedArrays(field.fields as FieldDef<unknown>[]) } as FieldDef<unknown>];\n }\n\n // Full-API arrays may have simplified arrays nested inside their item templates\n if (isArrayField(field) && !isSimplifiedArrayField(field)) {\n const normalizedItems = field.fields.map((item: ArrayItemDefinition) =>\n Array.isArray(item) ? normalizeSimplifiedArrays(item as FieldDef<unknown>[]) : item,\n );\n return [{ ...field, fields: normalizedItems } as ArrayField as FieldDef<unknown>];\n }\n\n // Expand simplified array fields\n if (isSimplifiedArrayField(field)) {\n const { arrayField, addButton } = expandSimplifiedArray(field);\n return addButton ? [arrayField, addButton] : [arrayField];\n }\n\n // Pass through all other fields unchanged\n return [field];\n });\n}\n\ninterface ExpandedArray {\n arrayField: FieldDef<unknown>;\n addButton?: FieldDef<unknown>;\n}\n\n/**\n * Invalid field types that are not allowed as array children.\n * TypeScript enforces this via ArrayAllowedChildren at compile time,\n * but runtime validation is needed for JavaScript users and dynamic configs.\n */\nconst INVALID_ARRAY_CHILD_TYPES = new Set(['page', 'array']);\n\n/**\n * Validates a simplified array field's template configuration.\n * Throws DynamicFormError for invalid configurations that would silently produce broken output.\n */\nfunction validateSimplifiedTemplate(field: SimplifiedArrayField): void {\n const { template, key } = field;\n const isObjectTemplate = Array.isArray(template);\n\n if (isObjectTemplate) {\n const templateFields = template as readonly ArrayAllowedChildren[];\n for (const tmpl of templateFields) {\n // Cast to FieldDef for runtime check — TypeScript prevents this statically,\n // but dynamic configs (e.g., from JSON) may violate the constraint.\n if (INVALID_ARRAY_CHILD_TYPES.has((tmpl as FieldDef<unknown>).type)) {\n throw new DynamicFormError(\n `Simplified array \"${key}\" template contains a '${tmpl.type}' field (key: '${tmpl.key}'). ` +\n `Only leaf fields, rows, and groups are allowed as array children.`,\n );\n }\n }\n } else {\n const singleTemplate = template as FieldDef<unknown>;\n if (INVALID_ARRAY_CHILD_TYPES.has(singleTemplate.type)) {\n throw new DynamicFormError(\n `Simplified array \"${key}\" template has type '${singleTemplate.type}'. ` +\n `Only leaf fields, rows, and groups are allowed as array children.`,\n );\n }\n }\n}\n\n/** Expands a simplified array field into a full ArrayField + optional add button. */\nfunction expandSimplifiedArray(field: SimplifiedArrayField): ExpandedArray {\n const {\n template,\n value = [],\n addButton,\n removeButton,\n key,\n logic,\n minLength,\n maxLength,\n wrappers,\n skipAutoWrappers,\n skipDefaultWrappers,\n } = field;\n const isObjectTemplate = Array.isArray(template);\n const values = value as unknown[];\n\n // Validate template before expansion\n validateSimplifiedTemplate(field);\n\n // Build items from values\n const items: ArrayItemDefinition[] = values.map((v) => {\n if (isObjectTemplate) {\n return buildObjectItem(template as readonly ArrayAllowedChildren[], v as Record<string, unknown>, removeButton);\n }\n return buildPrimitiveItem(template as ArrayAllowedChildren, v);\n });\n\n // Build the add button template (item structure without values)\n // For primitive arrays, the add template is a single field (not wrapped in array)\n // so that handleAddFromEvent treats it as a primitive item\n const addTemplate: ArrayAllowedChildren | readonly ArrayAllowedChildren[] = isObjectTemplate\n ? buildObjectItemTemplate(template as readonly ArrayAllowedChildren[], removeButton)\n : (template as ArrayAllowedChildren);\n\n // For primitive arrays with remove buttons, store the remove button config\n // via Symbol metadata. The array component renders remove buttons alongside\n // each item without wrapping in a row, preserving flat primitive form values.\n const hasAutoRemove = !isObjectTemplate && removeButton !== false;\n\n // Construct the full ArrayField\n const arrayFieldObj: Record<string, unknown> = {\n key,\n type: 'array' as const,\n fields: items,\n };\n if (logic) {\n arrayFieldObj['logic'] = logic;\n }\n if (minLength !== undefined) {\n arrayFieldObj['minLength'] = minLength;\n }\n if (maxLength !== undefined) {\n arrayFieldObj['maxLength'] = maxLength;\n }\n if (wrappers !== undefined) {\n arrayFieldObj['wrappers'] = wrappers;\n }\n if (skipAutoWrappers !== undefined) {\n arrayFieldObj['skipAutoWrappers'] = skipAutoWrappers;\n }\n if (skipDefaultWrappers !== undefined) {\n arrayFieldObj['skipDefaultWrappers'] = skipDefaultWrappers;\n }\n\n // Safe cast: we're constructing a valid ArrayField shape with key, type, and fields\n const arrayField = arrayFieldObj as unknown as FieldDef<unknown>;\n\n // Store normalization metadata via Symbol property instead of a runtime property.\n // `template` always gets populated so the array component can resolve untracked\n // items (e.g., from external form-value updates).\n //\n // For object templates, we must include the auto-generated remove button in the stored\n // template because object arrays embed the button inside each item's fields (via\n // buildObjectItem) rather than via the `autoRemoveButton` metadata. Without this,\n // items resolved from metadata would render without the remove button that\n // originally-declared items have. Primitive templates keep the button as separate\n // metadata and pass the raw template through — withAutoRemove() appends the button\n // at resolution time from autoRemoveButton.\n const primitiveFieldKey = !isObjectTemplate ? (template as ArrayAllowedChildren).key : undefined;\n const metadataTemplate = isObjectTemplate ? buildObjectItemTemplate(template as readonly ArrayAllowedChildren[], removeButton) : template;\n\n setNormalizedArrayMetadata(arrayFieldObj, {\n ...(hasAutoRemove && { autoRemoveButton: buildRemoveButton(removeButton) }),\n ...(primitiveFieldKey && { primitiveFieldKey }),\n template: metadataTemplate,\n });\n\n // Construct the add button (sibling, placed after the array)\n let addButtonField: FieldDef<unknown> | undefined;\n if (addButton !== false) {\n const buttonConfig = (typeof addButton === 'object' ? addButton : {}) as ArrayButtonConfig;\n // Safe cast: we're constructing a valid add-array-item field shape\n addButtonField = {\n key: `${key}__add`,\n type: 'add-array-item',\n label: buttonConfig.label ?? 'Add',\n arrayKey: key,\n template: addTemplate,\n ...(buttonConfig.props && { props: buttonConfig.props }),\n // Logic is intentionally shared with the array field so the add button\n // hides/shows together with the array (e.g., when a hidden condition applies).\n ...(logic && { logic }),\n } as unknown as FieldDef<unknown>;\n }\n\n return { arrayField, addButton: addButtonField };\n}\n\n/** Builds a primitive array item as a single FieldDef (not wrapped in array). */\nfunction buildPrimitiveItem(template: ArrayAllowedChildren, value: unknown): ArrayItemDefinition {\n return { ...template, value } as ArrayAllowedChildren;\n}\n\n/** Builds an object array item: template fields with values merged + optional remove button. */\nfunction buildObjectItem(\n template: readonly ArrayAllowedChildren[],\n valueObj: Record<string, unknown>,\n removeButton: ArrayButtonConfig | false | undefined,\n): ArrayItemDefinition {\n const fieldsWithValues = template.map(\n (templateField) =>\n ({\n ...templateField,\n ...(Object.hasOwn(valueObj, templateField.key) && { value: valueObj[templateField.key] }),\n }) as ArrayAllowedChildren,\n );\n\n if (removeButton === false) {\n return fieldsWithValues;\n }\n\n return [...fieldsWithValues, buildRemoveButton(removeButton)];\n}\n\n/** Builds the add button template for object items (template fields without values + remove button). */\nfunction buildObjectItemTemplate(\n template: readonly ArrayAllowedChildren[],\n removeButton: ArrayButtonConfig | false | undefined,\n): readonly ArrayAllowedChildren[] {\n if (removeButton === false) {\n return template;\n }\n\n return [...template, buildRemoveButton(removeButton)];\n}\n\n/** Builds a remove button field definition. */\nfunction buildRemoveButton(config: ArrayButtonConfig | undefined): ArrayAllowedChildren {\n const buttonConfig = (typeof config === 'object' ? config : {}) as ArrayButtonConfig;\n // Safe cast: remove-array-item fields are valid ArrayAllowedChildren but not in the static union\n return {\n key: '__remove',\n type: 'remove-array-item',\n label: buttonConfig.label ?? 'Remove',\n ...(buttonConfig.props && { props: buttonConfig.props }),\n } as unknown as ArrayAllowedChildren;\n}\n","import { Injectable, signal, WritableSignal } from '@angular/core';\nimport { FieldDef } from '@ng-forge/dynamic-forms/internal';\n\n/**\n * Per-array durable state. Survives `ArrayFieldComponent` destroy/recreate\n * cycles so a hidden array that's gated with `@if` doesn't lose its dynamically\n * added items when it becomes visible again.\n */\nexport interface ArraySlot {\n /** Templates keyed by item id. Set on resolution, deleted on item removal. */\n readonly templates: Map<string, FieldDef<unknown>[]>;\n\n /**\n * Item ids in form-value order. The new `ArrayFieldComponent` reads this on\n * mount to know \"the form has N items, with these ids in this order, here\n * are their templates\" — without needing the prior component instance.\n */\n readonly itemOrder: string[];\n\n /**\n * Key of the value field for primitive array items (e.g. 'value'). Discovered\n * lazily — from normalization metadata first, then from existing item\n * definitions, then from the first dynamic-add event. Survives recreate so\n * the discovered key isn't lost when the component dies.\n */\n readonly primitiveFieldKey: WritableSignal<string | undefined>;\n\n /**\n * Generates a unique item id scoped to this slot's lifetime. Counter is\n * monotonic — never resets except on `clear()` — so new ids never collide\n * with previously-stored templates even across hide↔show.\n */\n nextId(): string;\n\n /** Sets a template for an item id (used on initial resolve + dynamic add). */\n setTemplate(itemId: string, templates: FieldDef<unknown>[]): void;\n /** Returns the stored template for an item id, or undefined. */\n getTemplate(itemId: string): FieldDef<unknown>[] | undefined;\n /** Whether a template is stored for this id. */\n hasTemplate(itemId: string): boolean;\n\n /** Records the id at a position in `itemOrder` (append/insert/recreate). */\n setIdAt(index: number, itemId: string): void;\n /** Removes an id from `itemOrder` and deletes its template. */\n removeAt(index: number): void;\n /** Moves an id within `itemOrder` (preserves the same id at the new index). */\n moveItem(fromIndex: number, toIndex: number): void;\n /** Truncates `itemOrder` to the given length, deleting templates beyond it. */\n truncate(length: number): void;\n /** Empties the slot — templates cleared, itemOrder emptied, counter reset, primitiveFieldKey nulled. */\n clear(): void;\n}\n\nfunction createSlot(): ArraySlot {\n let counter = 0;\n const templates = new Map<string, FieldDef<unknown>[]>();\n const itemOrder: string[] = [];\n const primitiveFieldKey = signal<string | undefined>(undefined);\n\n return {\n templates,\n itemOrder,\n primitiveFieldKey,\n nextId: () => `item-${counter++}`,\n setTemplate(itemId, t) {\n templates.set(itemId, t);\n },\n getTemplate(itemId) {\n return templates.get(itemId);\n },\n hasTemplate(itemId) {\n return templates.has(itemId);\n },\n setIdAt(index, itemId) {\n itemOrder[index] = itemId;\n },\n removeAt(index) {\n const [removed] = itemOrder.splice(index, 1);\n if (removed !== undefined) templates.delete(removed);\n },\n moveItem(fromIndex, toIndex) {\n const [id] = itemOrder.splice(fromIndex, 1);\n if (id !== undefined) itemOrder.splice(toIndex, 0, id);\n },\n truncate(length) {\n const removed = itemOrder.splice(length);\n for (const id of removed) templates.delete(id);\n },\n clear() {\n templates.clear();\n itemOrder.length = 0;\n counter = 0;\n primitiveFieldKey.set(undefined);\n },\n };\n}\n\n/**\n * Form-scoped registry of per-array durable state. One slot per array path\n * (e.g. 'members', 'team.members', 'orders.lineItems'). Survives\n * `ArrayFieldComponent` lifecycle, dies with the `DynamicForm`. Cleared on\n * schema swap by the `formSetup` effect in `FormStateManager`.\n */\n@Injectable()\nexport class ArrayItemRegistryService {\n private readonly slots = new Map<string, ArraySlot>();\n\n /** Get-or-create a slot for the array at the given path. */\n slotFor(arrayPath: string): ArraySlot {\n let slot = this.slots.get(arrayPath);\n if (!slot) {\n slot = createSlot();\n this.slots.set(arrayPath, slot);\n }\n return slot;\n }\n\n /** Drop all slots — called from the `formSetup` effect on schema change. */\n clearAll(): void {\n for (const slot of this.slots.values()) slot.clear();\n this.slots.clear();\n }\n}\n","import { Injectable } from '@angular/core';\nimport { SchemaDefinition } from '@ng-forge/dynamic-forms/internal';\n\n@Injectable()\nexport class SchemaRegistryService {\n private readonly registeredSchemas = new Map<string, SchemaDefinition>();\n\n /** Register a reusable schema */\n registerSchema(schema: SchemaDefinition): void {\n this.registeredSchemas.set(schema.name, schema);\n }\n\n /** Get registered schema by name */\n getSchema(name: string): SchemaDefinition | undefined {\n return this.registeredSchemas.get(name);\n }\n\n /** Get all registered schemas */\n getAllSchemas(): Map<string, SchemaDefinition> {\n return new Map(this.registeredSchemas);\n }\n\n /** Clear all registered schemas */\n clearSchemas(): void {\n this.registeredSchemas.clear();\n }\n\n /** Resolve schema from string reference or direct definition */\n resolveSchema(schema: string | SchemaDefinition): SchemaDefinition | null {\n if (typeof schema === 'string') {\n return this.getSchema(schema) || null;\n }\n return schema;\n }\n}\n","declare const ngDevMode: boolean | undefined;\n\n/** Dev-mode guard for tree-shakeable diagnostics. */\nexport const DEV_MODE: boolean = typeof ngDevMode === 'undefined' || !!ngDevMode;\n","import { Injector, Signal } from '@angular/core';\nimport { toObservable, toSignal } from '@angular/core/rxjs-interop';\nimport { debounceTime, distinctUntilChanged, tap } from 'rxjs';\n\n/** Default debounce duration in milliseconds. */\nexport const DEFAULT_DEBOUNCE_MS = 500;\n\n/**\n * Creates a debounced effect that triggers a callback after a signal's value\n * has stabilized for the specified duration.\n *\n * @param source - The source signal to watch for changes\n * @param callback - Function to call with the debounced value\n * @param options - Configuration options\n * @param options.ms - Debounce duration in milliseconds (default: 500)\n * @param options.injector - Angular injector for the effect context\n * @returns A signal that emits the debounced value (can be ignored if only side effects are needed)\n */\nexport function createDebouncedEffect<T>(\n source: Signal<T>,\n callback: (value: T) => void,\n options: { ms?: number; injector: Injector },\n): Signal<T | undefined> {\n const { ms = DEFAULT_DEBOUNCE_MS, injector } = options;\n\n const source$ = toObservable(source, { injector }).pipe(\n debounceTime(ms),\n distinctUntilChanged(),\n tap((value) => callback(value)),\n );\n\n return toSignal(source$, { injector });\n}\n\n/**\n * Creates a debounced signal that emits values from the source signal\n * only after the value has stabilized for the specified duration.\n *\n * @param source - The source signal to debounce\n * @param options - Configuration options\n * @param options.ms - Debounce duration in milliseconds (default: 500)\n * @param options.injector - Angular injector for the effect context\n * @returns A signal that emits the debounced value\n */\nexport function createDebouncedSignal<T>(source: Signal<T>, options: { ms?: number; injector: Injector }): Signal<T | undefined> {\n const { ms = DEFAULT_DEBOUNCE_MS, injector } = options;\n\n const source$ = toObservable(source, { injector }).pipe(debounceTime(ms), distinctUntilChanged());\n\n return toSignal(source$, { injector });\n}\n","import { disabled, hidden, readonly, required, LogicFn } from '@angular/forms/signals';\nimport { DEV_MODE } from '../../utils/dev-mode';\nimport type { SchemaPath, SchemaPathTree } from '@angular/forms/signals';\nimport {\n LogicConfig,\n isStateLogicConfig,\n isDerivationLogicConfig,\n LogicTrigger,\n type StateLogicType,\n} from '@ng-forge/dynamic-forms/internal';\nimport { ConditionalExpression } from '@ng-forge/dynamic-forms/internal';\nimport { createLogicFunction, createDebouncedLogicFunction } from '@ng-forge/dynamic-forms/internal';\nimport { DEFAULT_DEBOUNCE_MS } from '../../utils/debounce/debounce';\nimport { DynamicFormError } from '@ng-forge/dynamic-forms/internal';\n\ntype AnyLogicFn<TValue> = LogicFn<TValue, boolean> | (() => boolean);\n\n/** Extracts the trigger from a StateLogicConfig, handling the discriminated union. */\nfunction getConfigTrigger(config: LogicConfig): LogicTrigger {\n if (!isStateLogicConfig(config)) {\n return 'onChange';\n }\n return config.trigger ?? 'onChange';\n}\n\n/** Extracts debounceMs from a StateLogicConfig if trigger is 'debounced'. */\nfunction getConfigDebounceMs(config: LogicConfig): number | undefined {\n if (!isStateLogicConfig(config)) {\n return undefined;\n }\n // Type narrowing: debounceMs only exists when trigger is 'debounced'\n if (config.trigger === 'debounced') {\n return config.debounceMs;\n }\n return undefined;\n}\n\nexport function applyLogic<TValue>(config: LogicConfig, fieldPath: SchemaPath<TValue> | SchemaPathTree<TValue>): void {\n // Value derivations (including property derivations via targetProperty) are handled by their orchestrators — skip them.\n if (isDerivationLogicConfig(config)) return;\n\n // Guard against unrecognized logic types that may be added in the future.\n if (!isStateLogicConfig(config)) {\n if (DEV_MODE) {\n console.warn(\n `[Dynamic Forms] Unrecognized logic config type '${(config as { type: string }).type}' in applyLogic. ` +\n 'This config will be ignored. If this is a new logic type, ensure it is handled explicitly.',\n );\n }\n return;\n }\n\n const path = fieldPath as SchemaPath<TValue>;\n const trigger = getConfigTrigger(config);\n const debounceMs = getConfigDebounceMs(config) ?? DEFAULT_DEBOUNCE_MS;\n\n if (typeof config.condition === 'boolean') {\n applyLogicFn(config.type, path, () => config.condition as boolean);\n return;\n }\n\n // Create appropriate logic function based on trigger\n const logicFn =\n trigger === 'debounced'\n ? createDebouncedLogicFunction(config.condition as ConditionalExpression, debounceMs)\n : createLogicFunction(config.condition as ConditionalExpression);\n\n applyLogicFn(config.type, path, logicFn);\n}\n\nfunction applyLogicFn<TValue>(type: StateLogicType, path: SchemaPath<TValue>, logicFn: AnyLogicFn<TValue>): void {\n switch (type) {\n case 'hidden':\n hidden(path, logicFn);\n break;\n case 'readonly':\n readonly(path, logicFn);\n break;\n case 'disabled':\n disabled(path, logicFn);\n break;\n case 'required':\n required(path, { when: logicFn });\n break;\n default: {\n const _exhaustive: never = type;\n throw new DynamicFormError(`Unhandled state logic type: ${_exhaustive}`);\n }\n }\n}\n\nexport function applyMultipleLogic<TValue>(configs: LogicConfig[], fieldPath: SchemaPath<TValue> | SchemaPathTree<TValue>): void {\n configs.forEach((config) => applyLogic(config, fieldPath));\n}\n","import type { Logger } from '@ng-forge/dynamic-forms/internal';\nimport { ExpressionParser } from '@ng-forge/dynamic-forms/internal';\n\n/**\n * Create a type predicate function from a predicate string.\n *\n * @param predicate - A JavaScript expression that evaluates to boolean. The value to check is available as `value`.\n * @param logger - Logger for error reporting\n * @returns A type predicate function\n */\nexport function createTypePredicateFunction<T = unknown>(predicate: string, logger: Logger): (value: unknown) => value is T {\n return (value: unknown): value is T => {\n try {\n // Evaluate using secure AST-based parser with value in scope\n const result = ExpressionParser.evaluate(predicate, { value });\n return Boolean(result);\n } catch (error) {\n logger.error('Error evaluating type predicate:', predicate, error);\n return false;\n }\n };\n}\n","import { inject } from '@angular/core';\nimport { apply, applyEach, applyWhen, applyWhenValue, SchemaOrSchemaFn } from '@angular/forms/signals';\nimport type { SchemaPath, SchemaPathTree } from '@angular/forms/signals';\nimport { SchemaApplicationConfig, SchemaDefinition } from '@ng-forge/dynamic-forms/internal';\nimport { SchemaRegistryService } from './registry/schema-registry.service';\nimport { createLogicFunction } from '@ng-forge/dynamic-forms/internal';\nimport { createTypePredicateFunction } from './values/type-predicate-factory';\nimport { applyValidator } from '@ng-forge/dynamic-forms/internal';\nimport { applyLogic } from './logic/logic-applicator';\nimport { DynamicFormLogger } from '@ng-forge/dynamic-forms/internal';\n\n/**\n * Apply schema configuration.\n * Accepts both SchemaPath and SchemaPathTree for flexibility.\n */\nexport function applySchema(config: SchemaApplicationConfig, fieldPath: SchemaPath<unknown> | SchemaPathTree<unknown>): void {\n const logger = inject(DynamicFormLogger);\n const schemaRegistry = inject(SchemaRegistryService);\n const schema = schemaRegistry.resolveSchema(config.schema);\n\n if (!schema) {\n const availableSchemas = Array.from(schemaRegistry.getAllSchemas().keys()).join(', ') || '<none>';\n logger.error(\n `Schema not found: '${config.schema}'. ` +\n `Available schemas: ${availableSchemas}. ` +\n `Ensure the schema is registered in your schema registry.`,\n );\n return;\n }\n\n const schemaFn = createSchemaFunction(schema);\n\n // Cast to suppress union type errors - safe because we only use signal forms (see function docs)\n const path = fieldPath as SchemaPath<unknown>;\n\n switch (config.type) {\n case 'apply':\n apply(path, schemaFn);\n break;\n\n case 'applyWhen':\n if (config.condition) {\n const conditionFn = createLogicFunction(config.condition);\n applyWhen(path, conditionFn, schemaFn);\n }\n break;\n\n case 'applyWhenValue':\n if (config.typePredicate) {\n const predicate = createTypePredicateFunction(config.typePredicate, logger);\n applyWhenValue(path, predicate, schemaFn);\n }\n break;\n\n case 'applyEach':\n applyEach(path as SchemaPath<unknown[]>, schemaFn);\n break;\n }\n}\n\n/** Create a schema function from schema definition. */\nexport function createSchemaFunction(schema: SchemaDefinition): SchemaOrSchemaFn<unknown> {\n return (path: SchemaPathTree<unknown>) => {\n // Apply validators - path is SchemaPathTree which is accepted by applyValidator\n schema.validators?.forEach((validatorConfig) => {\n applyValidator(validatorConfig, path);\n });\n\n // Apply logic - path is SchemaPathTree which is accepted by applyLogic\n schema.logic?.forEach((logicConfig) => {\n applyLogic(logicConfig, path);\n });\n\n // Apply sub-schemas - path is SchemaPathTree which is accepted by applySchema\n schema.subSchemas?.forEach((subSchemaConfig) => {\n applySchema(subSchemaConfig, path);\n });\n };\n}\n","import { FieldContext, LogicFn } from '@angular/forms/signals';\nimport { FieldDef } from '@ng-forge/dynamic-forms/internal';\nimport { FieldWithValidation } from '@ng-forge/dynamic-forms/internal';\nimport { isStateLogicConfig } from '@ng-forge/dynamic-forms/internal';\nimport { createLogicFunction } from '@ng-forge/dynamic-forms/internal';\n\n/**\n * Cascading configuration that flows from parent to child during schema mapping.\n *\n * @internal\n */\nexport interface FieldTreeMappingContext {\n /**\n * Whether validators on this field run while the field (or any ancestor) is\n * hidden. Defaults to `false` at the global level.\n */\n readonly validateWhenHidden: boolean;\n\n /**\n * `true` when an ancestor is unconditionally hidden (`hidden: true` or\n * `logic: [{type: 'hidden', condition: true}]`). When set together with\n * `validateWhenHidden=false`, the descendant skips validator application\n * entirely — there's no reactive case to gate on.\n */\n readonly ancestorAlwaysHidden: boolean;\n\n /**\n * Reactive logic functions for ancestor-level dynamic hidden conditions\n * (`logic: [{type: 'hidden', condition: ...}]`). Layout containers do not\n * have a schema path, so Angular Signal Forms cannot propagate their hidden\n * state to descendants — this list does it manually. Each function evaluates\n * against the current form value through the field context.\n */\n readonly ancestorHiddenLogics: readonly LogicFn<unknown, boolean>[];\n}\n\n/**\n * Combines the inherited \"is any ancestor hidden\" check into a single LogicFn.\n * Returns `undefined` when no ancestor has dynamic hidden logic.\n *\n * @internal\n */\nexport function combineAncestorHiddenLogics(logics: readonly LogicFn<unknown, boolean>[]): LogicFn<unknown, boolean> | undefined {\n if (logics.length === 0) return undefined;\n if (logics.length === 1) return logics[0];\n return (ctx: FieldContext<unknown>) => logics.some((fn) => fn(ctx));\n}\n\n/**\n * Returns the `FieldTreeMappingContext` to use **for the field itself** — i.e.\n * for gating its own validators. Only applies overrides that affect the field\n * (currently `validateWhenHidden`). The field's own hidden state is NOT folded\n * into `ancestorAlwaysHidden`/`ancestorHiddenLogics` here — Angular Signal\n * Forms already exposes that through `ctx.state.hidden()`, which the gate\n * checks separately.\n *\n * @internal\n */\nexport function resolveFieldOwnContext(\n fieldDef: Pick<FieldDef<unknown>, 'validateWhenHidden'>,\n parentContext: FieldTreeMappingContext,\n): FieldTreeMappingContext {\n if (fieldDef.validateWhenHidden === undefined) return parentContext;\n if (fieldDef.validateWhenHidden === parentContext.validateWhenHidden) return parentContext;\n return {\n ...parentContext,\n validateWhenHidden: fieldDef.validateWhenHidden,\n };\n}\n\n/**\n * Returns the `FieldTreeMappingContext` to use **for the field's descendants**.\n * Builds on top of `ownContext` by folding in the field's own hidden state so\n * children can correctly skip validation when this field hides them. Used for\n * groups, arrays, and layout containers that recurse into children.\n *\n * @internal\n */\nexport function resolveDescendantContext(\n fieldDef: FieldDef<unknown> & Partial<Pick<FieldWithValidation, 'logic'>>,\n ownContext: FieldTreeMappingContext,\n): FieldTreeMappingContext {\n let ancestorAlwaysHidden = ownContext.ancestorAlwaysHidden;\n let ancestorHiddenLogics = ownContext.ancestorHiddenLogics;\n let mutated = false;\n\n if (fieldDef.hidden === true && !ancestorAlwaysHidden) {\n ancestorAlwaysHidden = true;\n mutated = true;\n }\n\n if (fieldDef.logic) {\n for (const logic of fieldDef.logic) {\n if (!isStateLogicConfig(logic) || logic.type !== 'hidden') continue;\n\n const condition = logic.condition;\n\n if (condition === true) {\n if (!ancestorAlwaysHidden) {\n ancestorAlwaysHidden = true;\n mutated = true;\n }\n continue;\n }\n\n if (condition === false) continue;\n\n // FormStateCondition strings ('formInvalid', etc.) are intended for buttons —\n // they don't make sense as a \"field hidden\" trigger, so we skip them here.\n if (typeof condition === 'string') continue;\n\n // Dynamic — we need a reactive logic function. createLogicFunction\n // requires an injection context, which the schema build provides.\n const fn = createLogicFunction(condition) as LogicFn<unknown, boolean>;\n ancestorHiddenLogics = ancestorHiddenLogics.length === 0 ? [fn] : [...ancestorHiddenLogics, fn];\n mutated = true;\n }\n }\n\n if (!mutated) return ownContext;\n\n return {\n validateWhenHidden: ownContext.validateWhenHidden,\n ancestorAlwaysHidden,\n ancestorHiddenLogics,\n };\n}\n","import {\n applyEach,\n applyWhen,\n disabled,\n email,\n hidden,\n max,\n maxLength,\n min,\n minLength,\n pattern,\n readonly,\n required,\n schema,\n} from '@angular/forms/signals';\nimport type { FieldContext, SchemaPath, SchemaPathTree } from '@angular/forms/signals';\nimport { FieldDef } from '@ng-forge/dynamic-forms/internal';\nimport { FieldWithValidation } from '@ng-forge/dynamic-forms/internal';\nimport { applyValidator } from '@ng-forge/dynamic-forms/internal';\nimport { applyLogic } from './logic/logic-applicator';\nimport { applySchema } from './schema-application';\nimport { isGroupField } from '@ng-forge/dynamic-forms/internal';\nimport { isArrayField } from '@ng-forge/dynamic-forms/internal';\nimport { isPageField } from '@ng-forge/dynamic-forms/internal';\nimport { isRowField } from '@ng-forge/dynamic-forms/internal';\nimport { isGenericContainerField } from '@ng-forge/dynamic-forms/internal';\nimport { getNormalizedArrayMetadata } from '../utils/array-field/normalized-array-metadata';\nimport { DynamicFormError } from '@ng-forge/dynamic-forms/internal';\nimport { isStateLogicConfig, LogicConfig } from '@ng-forge/dynamic-forms/internal';\nimport {\n combineAncestorHiddenLogics,\n FieldTreeMappingContext,\n resolveDescendantContext,\n resolveFieldOwnContext,\n} from './field-tree-mapping-context';\n\n/**\n * Default {@link FieldTreeMappingContext} used when no inherited context is provided.\n * Mirrors the global token defaults so callers that bypass `createSchemaFromFields`\n * (e.g. nested group forms) still get sensible behavior.\n */\nconst DEFAULT_FIELD_TREE_MAPPING_CONTEXT: FieldTreeMappingContext = {\n validateWhenHidden: false,\n ancestorAlwaysHidden: false,\n ancestorHiddenLogics: [],\n};\n\n// Type alias for schema path parameters\ntype AnySchemaPath = SchemaPath<unknown> | SchemaPathTree<unknown>;\n\n// ============================================================================\n// Type-safe validator application functions\n// ============================================================================\n\ninterface StringValidationConfig {\n email?: boolean;\n minLength?: number;\n maxLength?: number;\n pattern?: string | RegExp;\n}\n\ninterface NumberValidationConfig {\n min?: number;\n max?: number;\n}\n\n/**\n * Applies string-specific validators to a schema path.\n * This function is properly typed - no casts needed internally.\n */\nfunction applyStringValidators(path: SchemaPath<string>, config: StringValidationConfig): void {\n if (config.email) {\n email(path);\n }\n if (config.minLength !== undefined) {\n minLength(path, config.minLength);\n }\n if (config.maxLength !== undefined) {\n maxLength(path, config.maxLength);\n }\n if (config.pattern) {\n let regex: RegExp;\n if (typeof config.pattern === 'string') {\n try {\n regex = new RegExp(config.pattern);\n } catch (e) {\n throw new DynamicFormError(`Invalid regex pattern: '${config.pattern}' — ${e instanceof Error ? e.message : String(e)}`);\n }\n } else {\n regex = config.pattern;\n }\n pattern(path, regex);\n }\n}\n\n/**\n * Applies number-specific validators to a schema path.\n * This function is properly typed - no casts needed internally.\n */\nfunction applyNumberValidators(path: SchemaPath<number>, config: NumberValidationConfig): void {\n if (config.min !== undefined) {\n min(path, config.min);\n }\n if (config.max !== undefined) {\n max(path, config.max);\n }\n}\n\n/**\n * Extracts string validation config from a field definition.\n * Returns undefined if no string validators are configured.\n */\nfunction getStringValidationConfig(fieldDef: FieldWithValidation): StringValidationConfig | undefined {\n if (!fieldDef.email && fieldDef.minLength === undefined && fieldDef.maxLength === undefined && !fieldDef.pattern) {\n return undefined;\n }\n return {\n email: fieldDef.email,\n minLength: fieldDef.minLength,\n maxLength: fieldDef.maxLength,\n pattern: fieldDef.pattern,\n };\n}\n\n/**\n * Extracts number validation config from a field definition.\n * Handles both 'min'/'max' (standard) and 'minValue'/'maxValue' (SliderField).\n * Returns undefined if no number validators are configured.\n */\nfunction getNumberValidationConfig(fieldDef: FieldWithValidation): NumberValidationConfig | undefined {\n const minVal = fieldDef.min ?? (fieldDef as { minValue?: number }).minValue;\n const maxVal = fieldDef.max ?? (fieldDef as { maxValue?: number }).maxValue;\n\n if (minVal === undefined && maxVal === undefined) {\n return undefined;\n }\n return { min: minVal, max: maxVal };\n}\n\n/**\n * Maps a field definition to the Angular Signal Forms schema.\n *\n * @param fieldDef Field definition to map\n * @param fieldPath Schema path for this field\n * @param inheritedContext Cascading mapping context from the parent. Field-level\n * overrides on `fieldDef` are merged on top and passed down to descendants. Defaults\n * to {@link DEFAULT_FIELD_TREE_MAPPING_CONTEXT} when omitted.\n */\nexport function mapFieldToForm(\n fieldDef: FieldDef<unknown>,\n fieldPath: AnySchemaPath,\n inheritedContext: FieldTreeMappingContext = DEFAULT_FIELD_TREE_MAPPING_CONTEXT,\n): void {\n const ownContext = resolveFieldOwnContext(fieldDef, inheritedContext);\n\n // Layout containers (page, row, container) - flatten children to current level.\n // Layout containers have no schema path of their own, so any hidden state on them\n // must be propagated to descendants through the cascade.\n if (isPageField(fieldDef) || isRowField(fieldDef) || isGenericContainerField(fieldDef)) {\n const descendantContext = resolveDescendantContext(fieldDef, ownContext);\n mapContainerChildren(fieldDef.fields as FieldDef<unknown>[] | undefined, fieldPath, descendantContext);\n return;\n }\n\n // Group fields - map children under the group's path. Groups have a schema path,\n // so Angular Signal Forms can propagate `hidden(path, ...)` calls to descendants —\n // but groups don't currently apply their own hidden state, so we still propagate\n // through the cascade as a belt-and-suspenders fallback.\n if (isGroupField(fieldDef)) {\n const descendantContext = resolveDescendantContext(fieldDef, ownContext);\n mapContainerChildren(fieldDef.fields, fieldPath, descendantContext);\n return;\n }\n\n // Array fields - use applyEach for item schema.\n if (isArrayField(fieldDef)) {\n const descendantContext = resolveDescendantContext(fieldDef, ownContext);\n mapArrayFieldToForm(fieldDef, fieldPath, descendantContext);\n return;\n }\n\n // Leaf field - apply validation, logic, and configuration. Use the field's OWN\n // context (no descendant propagation) because the leaf's own hidden state is\n // already exposed via `ctx.state.hidden()`.\n mapLeafField(fieldDef, fieldPath, ownContext);\n}\n\n/** Maps children of a container field (page, row, group) to the form schema. */\nfunction mapContainerChildren(\n fields: readonly FieldDef<unknown>[] | undefined,\n parentPath: AnySchemaPath,\n inheritedContext: FieldTreeMappingContext,\n): void {\n if (!fields) return;\n\n const pathRecord = parentPath as Record<string, AnySchemaPath>;\n\n for (const field of fields) {\n if (isPageField(field) || isRowField(field) || isGenericContainerField(field)) {\n // Layout containers can still carry their own cascading overrides that flow\n // down to their flattened descendants.\n const layoutOwn = resolveFieldOwnContext(field, inheritedContext);\n const layoutDescendant = resolveDescendantContext(field, layoutOwn);\n mapContainerChildren(field.fields as readonly FieldDef<unknown>[] | undefined, parentPath, layoutDescendant);\n continue;\n }\n\n if (!field.key) continue;\n\n const childPath = pathRecord[field.key];\n if (childPath) {\n mapFieldToForm(field, childPath, inheritedContext);\n }\n }\n}\n\n/**\n * Returns true for state logic that contributes validation (currently only `required`).\n * The other state logic types (`hidden`, `disabled`, `readonly`) must always apply,\n * even on hidden fields, so they're not gated by `validateWhenHidden`.\n */\nfunction isValidationStateLogic(config: LogicConfig): boolean {\n return isStateLogicConfig(config) && config.type === 'required';\n}\n\n/**\n * Maps a leaf field (value-bearing field) to the form schema.\n *\n * @param context Resolved cascading context for this field. When\n * `context.validateWhenHidden` is `false`, the field's validation block is wrapped\n * in `applyWhen` keyed off `!ctx.state.hidden()`.\n */\nfunction mapLeafField(fieldDef: FieldDef<unknown>, fieldPath: AnySchemaPath, context: FieldTreeMappingContext): void {\n const validationField = fieldDef as FieldDef<unknown> & FieldWithValidation;\n const path = fieldPath as SchemaPath<unknown>;\n\n // Always-applied: state logic (hidden/disabled/readonly), schemas, and value derivations.\n // These must run regardless of hidden state — particularly the `hidden` logic itself,\n // which establishes the very signal `applyWhen` reads below.\n applyFieldState(fieldDef, path);\n\n if (validationField.logic) {\n for (const config of validationField.logic) {\n if (!isValidationStateLogic(config)) {\n applyLogic(config, fieldPath);\n }\n }\n }\n\n if (validationField.schemas) {\n for (const config of validationField.schemas) {\n applySchema(config, fieldPath);\n }\n }\n\n // Validation block — gated by `validateWhenHidden` when hidden.\n const applyValidation = (validationPath: SchemaPath<unknown>): void => {\n applySimpleValidationRules(validationField, validationPath);\n\n if (validationField.validators) {\n for (const config of validationField.validators) {\n applyValidator(config, validationPath);\n }\n }\n\n if (validationField.logic) {\n for (const config of validationField.logic) {\n if (isValidationStateLogic(config)) {\n applyLogic(config, validationPath);\n }\n }\n }\n };\n\n if (context.validateWhenHidden) {\n applyValidation(path);\n return;\n }\n\n // An ancestor is unconditionally hidden — there is no reactive case for the\n // validators to ever fire, so we simply do not apply them.\n if (context.ancestorAlwaysHidden) {\n return;\n }\n\n // applyWhen conditionally applies a sub-schema. The gate combines:\n // - the field's own `state.hidden()` (set by Angular Signal Forms whenever\n // `hidden(path, ...)` was called for this field or any ancestor with a\n // path); and\n // - any ancestor dynamic hidden logic that does NOT live on a schema path\n // (layout containers — see field-tree-mapping-context for details).\n const ancestorHiddenLogic = combineAncestorHiddenLogics(context.ancestorHiddenLogics);\n applyWhen(\n path,\n (ctx: FieldContext<unknown>) => {\n if (ctx.state.hidden()) return false;\n if (ancestorHiddenLogic && ancestorHiddenLogic(ctx)) return false;\n return true;\n },\n schema<unknown>((subPath) => applyValidation(subPath as SchemaPath<unknown>)),\n );\n}\n\n/**\n * Applies simple validation rules from field properties.\n * Casts are isolated to the boundary between untyped field definitions and typed validator functions.\n */\nfunction applySimpleValidationRules(fieldDef: FieldWithValidation, path: SchemaPath<unknown>): void {\n if (fieldDef.required) {\n required(path);\n }\n\n // String validators - single cast at boundary\n const stringConfig = getStringValidationConfig(fieldDef);\n if (stringConfig) {\n applyStringValidators(path as SchemaPath<string>, stringConfig);\n }\n\n // Number validators - single cast at boundary\n const numberConfig = getNumberValidationConfig(fieldDef);\n if (numberConfig) {\n applyNumberValidators(path as SchemaPath<number>, numberConfig);\n }\n}\n\n/** Applies field state configuration (disabled, readonly, hidden). */\nfunction applyFieldState(fieldDef: FieldDef<unknown>, path: SchemaPath<unknown>): void {\n if (fieldDef.disabled) {\n disabled(path);\n }\n\n if (fieldDef.readonly) {\n readonly(path);\n }\n\n if (fieldDef.hidden) {\n hidden(path, () => true);\n }\n}\n\n/** Maps an array field to the form schema using applyEach. */\nfunction mapArrayFieldToForm(arrayField: FieldDef<unknown>, fieldPath: AnySchemaPath, context: FieldTreeMappingContext): void {\n if (!isArrayField(arrayField)) {\n return;\n }\n\n // Apply array-level length validation\n if (arrayField.minLength !== undefined) {\n minLength(fieldPath as SchemaPath<unknown[]>, arrayField.minLength);\n }\n if (arrayField.maxLength !== undefined) {\n maxLength(fieldPath as SchemaPath<unknown[]>, arrayField.maxLength);\n }\n\n // Fields can be either FieldDef (primitive) or FieldDef[] (object)\n let itemDefinitions = arrayField.fields as readonly (FieldDef<unknown> | readonly FieldDef<unknown>[])[];\n\n // Simplified arrays initialized without `value` have empty `fields`; their\n // item shape only lives in Symbol metadata. Fall back to that template so\n // validators and logic declared inside it still apply when items are\n // materialized from a programmatically-set form value.\n if (!itemDefinitions || itemDefinitions.length === 0) {\n const metadataTemplate = getNormalizedArrayMetadata(arrayField)?.template;\n if (metadataTemplate) {\n itemDefinitions = [\n Array.isArray(metadataTemplate) ? [...(metadataTemplate as readonly FieldDef<unknown>[])] : (metadataTemplate as FieldDef<unknown>),\n ];\n }\n }\n\n // Empty array with no template metadata - items will be added via buttons with\n // their own templates. Create a minimal schema that just accepts any value.\n if (!itemDefinitions || itemDefinitions.length === 0) {\n const emptyItemSchema = schema<unknown>(() => {\n // No fields to map - items will be added dynamically via buttons\n });\n applyEach(fieldPath as SchemaPath<unknown[]>, emptyItemSchema);\n return;\n }\n\n // Analyze item definitions to determine schema type\n let hasPrimitiveItems = false;\n const allObjectFields: FieldDef<unknown>[] = [];\n\n for (const itemDef of itemDefinitions) {\n if (Array.isArray(itemDef)) {\n // Object item: collect fields for superset schema\n collectFieldsFromObjectItem(itemDef as readonly FieldDef<unknown>[], allObjectFields);\n } else if (\n isPageField(itemDef as FieldDef<unknown>) ||\n isRowField(itemDef as FieldDef<unknown>) ||\n isGenericContainerField(itemDef as FieldDef<unknown>)\n ) {\n // Layout container as a single-template item: its descendants describe\n // the item's object shape. Hand it to the object-item collector wrapped\n // as a 1-element array so the structured-item mapping path runs.\n collectFieldsFromObjectItem([itemDef as FieldDef<unknown>], allObjectFields);\n } else {\n // Primitive item: single FieldDef\n hasPrimitiveItems = true;\n }\n }\n\n // For pure primitive arrays, use a simple schema\n if (hasPrimitiveItems && allObjectFields.length === 0) {\n const primitiveItemSchema = schema<unknown>(() => {\n // Primitive items don't need field mapping - just accept any value\n });\n applyEach(fieldPath as SchemaPath<unknown[]>, primitiveItemSchema);\n return;\n }\n\n // For object or mixed arrays, use object schema with optional fields\n // Mixed arrays use the superset of all object fields (primitive items are just values)\n const itemSchema = schema<Record<string, unknown>>((itemPath) => {\n const pathRecord = itemPath as Record<string, AnySchemaPath>;\n\n // Map ALL unique template fields from all object items\n for (const templateField of allObjectFields) {\n if (isRowField(templateField) || isPageField(templateField) || isGenericContainerField(templateField)) {\n // Row/page/container templates flatten their children\n const layoutOwn = resolveFieldOwnContext(templateField, context);\n const layoutDescendant = resolveDescendantContext(templateField, layoutOwn);\n mapContainerChildren(templateField.fields as FieldDef<unknown>[] | undefined, itemPath, layoutDescendant);\n } else if (isGroupField(templateField)) {\n // Group template - access group's path first\n const groupKey = templateField.key;\n const groupOwn = resolveFieldOwnContext(templateField, context);\n const groupDescendant = resolveDescendantContext(templateField, groupOwn);\n if (groupKey) {\n const groupPath = pathRecord[groupKey];\n if (groupPath) {\n mapContainerChildren(templateField.fields, groupPath, groupDescendant);\n }\n } else {\n // No group key - apply children directly (edge case)\n mapContainerChildren(templateField.fields, itemPath, groupDescendant);\n }\n } else {\n // Simple field template - get the specific field's path\n const fieldKey = templateField.key;\n if (fieldKey) {\n const fieldPathForKey = pathRecord[fieldKey];\n if (fieldPathForKey) {\n mapFieldToForm(templateField, fieldPathForKey, context);\n }\n }\n }\n }\n });\n\n applyEach(fieldPath as SchemaPath<Record<string, unknown>[]>, itemSchema);\n}\n\n/**\n * Collects unique field definitions from an object item template.\n * Uses field key as the uniqueness identifier.\n * When the same key appears in multiple templates, uses the first occurrence.\n */\nfunction collectFieldsFromObjectItem(itemFields: readonly FieldDef<unknown>[], allFields: FieldDef<unknown>[]): void {\n const seenKeys = new Set(allFields.map((f) => f.key).filter(Boolean));\n\n for (const field of itemFields) {\n // For row/page/container fields, we need to collect their children's keys\n if (isRowField(field) || isPageField(field) || isGenericContainerField(field)) {\n // Use a synthetic key for container fields to dedupe them\n const containerKey = `__container_${field.type}_${JSON.stringify(field.fields?.map((f) => f.key))}`;\n if (!seenKeys.has(containerKey)) {\n seenKeys.add(containerKey);\n allFields.push(field);\n }\n } else {\n const key = field.key;\n if (key && !seenKeys.has(key)) {\n seenKeys.add(key);\n allFields.push(field);\n }\n }\n }\n}\n","import { FieldDef } from '@ng-forge/dynamic-forms/internal';\nimport { FieldTypeDefinition, getFieldValueHandling } from '@ng-forge/dynamic-forms/internal';\n\n/**\n * Generates appropriate default values for different field types in dynamic forms.\n *\n * @param field - Field definition to generate default value for\n * @param registry - Field type registry for value handling configuration\n * @returns Appropriate default value based on field type and configuration\n */\nexport function getFieldDefaultValue(field: FieldDef<unknown>, registry: Map<string, FieldTypeDefinition>): unknown {\n const valueHandling = getFieldValueHandling(field.type, registry);\n\n // Fields with 'exclude' handling don't contribute values\n if (valueHandling === 'exclude') {\n return undefined;\n }\n\n // Flatten fields (row/page/container) return flattened object of children's values\n // This is used by array fields to get the template structure\n // Note: fieldsToDefaultValues will skip these at top-level since they have keys and return objects\n if (valueHandling === 'flatten' && 'fields' in field && field.fields) {\n const childFields = field.fields as readonly FieldDef<unknown>[];\n\n // Collect only fields that contribute values (exclude buttons, text, etc.)\n const flattenedValues: Record<string, unknown> = {};\n for (const childField of childFields) {\n const childValueHandling = getFieldValueHandling(childField.type, registry);\n if (childValueHandling === 'exclude') continue;\n\n const childValue = getFieldDefaultValue(childField, registry);\n if (childValue === undefined) continue;\n\n if (childValueHandling === 'flatten' && childValue && typeof childValue === 'object' && !Array.isArray(childValue)) {\n // Nested flatten field — its value is already flat; spread it up.\n Object.assign(flattenedValues, childValue as Record<string, unknown>);\n } else if ('key' in childField && childField.key) {\n flattenedValues[childField.key] = childValue;\n }\n }\n\n // Return flattened object if there are any values, otherwise undefined\n return Object.keys(flattenedValues).length > 0 ? flattenedValues : undefined;\n }\n\n // Group fields with 'include' handling create nested objects\n if (field.type === 'group' && 'fields' in field) {\n const fields = field.fields as readonly FieldDef<unknown>[];\n if (!fields || fields.length === 0) {\n return undefined;\n }\n\n const groupDefaults: Record<string, unknown> = {};\n for (const childField of fields) {\n const childValueHandling = getFieldValueHandling(childField.type, registry);\n if (childValueHandling === 'exclude') continue;\n\n const childValue = getFieldDefaultValue(childField, registry);\n\n if (childValueHandling === 'flatten') {\n // Flatten container/row/page values into the group (they are presentational containers).\n // The recursive call already returns a flat object — spread it directly.\n if (childValue && typeof childValue === 'object' && !Array.isArray(childValue)) {\n Object.assign(groupDefaults, childValue as Record<string, unknown>);\n }\n } else if ('key' in childField && childField.key) {\n if (childValue !== undefined) {\n groupDefaults[childField.key] = childValue;\n }\n }\n }\n return groupDefaults;\n }\n\n // Narrow once: `field` is FieldDef<unknown>, but value/nullable are defined on BaseValueField.\n // This cast avoids per-access `as` and documents what we're reading.\n const valueField = field as FieldDef<unknown> & { value?: unknown; nullable?: boolean };\n const isNullable = valueField.nullable === true;\n\n // Use explicit value if provided (including null when nullable)\n if ('value' in field) {\n if (valueField.value !== null && valueField.value !== undefined) {\n return valueField.value;\n }\n\n // Preserve explicit null for nullable fields\n if (valueField.value === null && isNullable) {\n return null;\n }\n\n // Non-nullable null and undefined both fall through to type-specific defaults below\n }\n\n // Nullable fields with no explicit value default to null\n if (isNullable) {\n return null;\n }\n\n // Type-specific defaults when no value is specified\n if (field.type === 'checkbox') {\n return false;\n }\n\n if (field.type === 'array') {\n // Array field supports two item formats:\n // - Primitive items: single FieldDef (not wrapped in array) → extracts field value directly\n // - Object items: FieldDef[] (array of fields) → merges fields into object\n const arrayField = field as { fields?: readonly (FieldDef<unknown> | readonly FieldDef<unknown>[])[] };\n const itemDefinitions = arrayField.fields;\n\n if (!itemDefinitions || itemDefinitions.length === 0) {\n return [];\n }\n\n // Process each item definition\n return itemDefinitions.map((itemDef) => {\n // Primitive item: single FieldDef (not wrapped in array)\n // Extract field value directly - key is for internal tracking only\n if (!Array.isArray(itemDef)) {\n return getFieldDefaultValue(itemDef as FieldDef<unknown>, registry);\n }\n\n // Object item: FieldDef[] - merge fields into object\n const itemFields = itemDef as readonly FieldDef<unknown>[];\n let itemValue: Record<string, unknown> = {};\n\n for (const templateField of itemFields) {\n const fieldValue = getFieldDefaultValue(templateField, registry);\n const fieldValueHandling = getFieldValueHandling(templateField.type, registry);\n\n if (templateField.type === 'group' && 'key' in templateField && templateField.key) {\n // Groups wrap their value under the group key\n itemValue[templateField.key] = fieldValue;\n } else if (templateField.type === 'row' || templateField.type === 'container') {\n // Rows and containers flatten their fields directly\n if (fieldValue && typeof fieldValue === 'object') {\n itemValue = { ...itemValue, ...(fieldValue as Record<string, unknown>) };\n }\n } else if (fieldValueHandling === 'include' && 'key' in templateField && templateField.key) {\n itemValue[templateField.key] = fieldValue;\n }\n }\n\n return itemValue;\n });\n }\n\n // Number inputs need a number type default for Angular signal forms\n // to use valueAsNumber for coercion. NaN is ideal because:\n // - typeof NaN === 'number' (triggers Angular's valueAsNumber path)\n // - NaN displays as empty in number inputs\n // - valueAsNumber returns NaN when input is cleared\n if (field.type === 'input' && 'props' in field) {\n const props = field.props as { type?: string } | undefined;\n if (props?.type === 'number') {\n return NaN;\n }\n }\n\n return '';\n}\n","import { schema, Schema, SchemaPathTree, validateStandardSchema } from '@angular/forms/signals';\n// Import from secondary entry point - this is correct for ng-packagr's entry point architecture\n// (different from importing internal barrel files which should be avoided)\nimport { isStandardSchemaMarker, type FormSchema } from '@ng-forge/dynamic-forms/schema';\n\n/**\n * Applies a form-level schema validation to a schema path.\n * Supports both Standard Schema (Zod, Valibot, ArkType) and raw Angular schema callbacks.\n *\n * @typeParam TModel - The form value type\n * @param path - The schema path to validate (from Angular's schema callback)\n * @param formLevelSchema - Form-level schema (Standard Schema marker or Angular callback)\n * @remarks\n * Type assertions are required due to Angular's complex generic constraints:\n * - `validateStandardSchema` uses `IgnoreUnknownProperties<TSchema>` to accommodate Zod's strict types\n * - `SchemaPathTree` includes `SchemaPath` via conditional types that TypeScript can't narrow\n * These assertions are safe because the path originates from Angular's schema() function.\n * @internal\n */\nexport function applyFormLevelSchema<TModel>(path: SchemaPathTree<TModel>, formLevelSchema: FormSchema<TModel>): void {\n if (isStandardSchemaMarker(formLevelSchema)) {\n // Standard Schema (Zod, Valibot, ArkType, etc.)\n // Cast via Parameters: validateStandardSchema's IgnoreUnknownProperties<TSchema> creates type incompatibility\n // that TypeScript cannot resolve. The cast is safe as path comes from Angular's schema() callback.\n (validateStandardSchema as (...args: unknown[]) => void)(path, formLevelSchema.schema);\n } else if (typeof formLevelSchema === 'function') {\n // Angular schema callback - execute directly with the path\n // Cast via Function: AngularSchemaCallback expects intersection type SchemaPath & SchemaPathTree.\n // SchemaPathTree structurally includes SchemaPath, so this is safe at runtime.\n (formLevelSchema as (path: unknown) => void)(path);\n }\n}\n\n/**\n * Creates a schema from only form-level schema (when no field-level schema exists).\n *\n * @typeParam TModel - The form value type\n * @param formLevelSchema - Form-level Standard Schema\n * @returns Schema that applies form-level validation\n */\nexport function createFormLevelSchema<TModel>(formLevelSchema: FormSchema<TModel>): Schema<TModel> {\n return schema<TModel>((path) => {\n applyFormLevelSchema(path, formLevelSchema);\n });\n}\n","import { InjectionToken } from '@angular/core';\nimport { ResolvedValidationExecutionConfig } from '@ng-forge/dynamic-forms/internal';\n\n/**\n * Injection token for global validation execution defaults.\n *\n * @internal\n */\nexport const VALIDATION_EXECUTION_DEFAULTS = new InjectionToken<ResolvedValidationExecutionConfig>('VALIDATION_EXECUTION_DEFAULTS', {\n providedIn: 'root',\n factory: () => ({\n validateWhenHidden: false,\n }),\n});\n","import { inject } from '@angular/core';\nimport { Schema, schema, validateTree, FieldContext, ValidationError, FieldTree } from '@angular/forms/signals';\nimport type { SchemaPath, SchemaPathTree } from '@angular/forms/signals';\nimport { FieldDef } from '@ng-forge/dynamic-forms/internal';\nimport { mapFieldToForm } from './form-mapping';\nimport { FieldTypeDefinition, getFieldValueHandling } from '@ng-forge/dynamic-forms/internal';\nimport { getFieldDefaultValue } from '../utils/default-value/default-value';\nimport { CrossFieldValidatorEntry } from './cross-field/cross-field-types';\nimport { FunctionRegistryService } from '@ng-forge/dynamic-forms/internal';\nimport { ExpressionParser } from '@ng-forge/dynamic-forms/internal';\nimport { evaluateCondition } from '@ng-forge/dynamic-forms/internal';\nimport { CustomValidatorConfig, ValidatorConfig } from '@ng-forge/dynamic-forms/internal';\nimport { hasChildFields } from '@ng-forge/dynamic-forms/internal';\nimport { DynamicFormLogger } from '@ng-forge/dynamic-forms/internal';\nimport type { Logger } from '@ng-forge/dynamic-forms/internal';\nimport { EvaluationContext } from '@ng-forge/dynamic-forms/internal';\nimport { normalizeFieldsArray } from '@ng-forge/dynamic-forms/internal';\nimport { getNestedValue } from '@ng-forge/dynamic-forms/internal';\nimport { applyFormLevelSchema } from './form-schema-merger';\nimport type { FormSchema } from '@ng-forge/dynamic-forms/schema';\nimport { VALIDATION_EXECUTION_DEFAULTS } from '../providers/features/validation-execution/validation-execution.token';\nimport { FieldTreeMappingContext, resolveDescendantContext, resolveFieldOwnContext } from './field-tree-mapping-context';\n\nconst emailPattern = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n\n/** Gets a FieldTree by key, supporting nested paths with dot notation. */\nfunction getFieldTreeByKey<TModel>(ctx: FieldContext<TModel>, key: string): FieldTree<unknown> | undefined {\n // Simple case - no nesting\n if (!key.includes('.')) {\n return (ctx.fieldTree as Record<string, FieldTree<unknown>>)[key];\n }\n\n // Nested path - traverse through the structure\n const parts = key.split('.');\n let current: unknown = ctx.fieldTree;\n\n for (const part of parts) {\n // FieldTree is callable (function), so typeof returns 'function' not 'object'.\n // Accept both to correctly traverse group sub-fields.\n if (!current || (typeof current !== 'object' && typeof current !== 'function')) {\n return undefined;\n }\n current = (current as Record<string, unknown>)[part];\n }\n\n return current as FieldTree<unknown> | undefined;\n}\n\n/** Options for creating a schema from field definitions. */\nexport interface CreateSchemaOptions<TModel = unknown> {\n /** Optional array of collected cross-field validators. */\n crossFieldValidators?: CrossFieldValidatorEntry[];\n\n /** Optional form-level Standard Schema for additional validation. */\n formLevelSchema?: FormSchema<TModel>;\n\n /** Form-level `validateWhenHidden` setting (from `FormConfig.options.validateWhenHidden`). */\n validateWhenHidden?: boolean;\n}\n\n/**\n * Creates an Angular signal forms schema from field definitions\n * This is the single entry point at dynamic form level that replaces createSchemaFromFields\n * Uses the new modular signal forms adapter structure\n *\n * @param fields Field definitions to create schema from\n * @param registry Field type registry\n * @param optionsOrValidators Optional configuration object or array of cross-field validators (for backwards compatibility)\n */\nexport function createSchemaFromFields<TModel = unknown>(\n fields: FieldDef<unknown>[],\n registry: Map<string, FieldTypeDefinition>,\n optionsOrValidators?: CrossFieldValidatorEntry[] | CreateSchemaOptions<TModel>,\n): Schema<TModel> {\n // Inject services for cross-field validation\n // These will be available because createSchemaFromFields is called within runInInjectionContext\n const functionRegistry = inject(FunctionRegistryService);\n const logger = inject(DynamicFormLogger);\n const validationExecutionDefaults = inject(VALIDATION_EXECUTION_DEFAULTS);\n\n // Normalize options - support both old array signature and new options object\n const options: CreateSchemaOptions<TModel> = Array.isArray(optionsOrValidators)\n ? { crossFieldValidators: optionsOrValidators }\n : (optionsOrValidators ?? {});\n\n const { crossFieldValidators, formLevelSchema } = options;\n const rootContext: FieldTreeMappingContext = {\n validateWhenHidden: options.validateWhenHidden ?? validationExecutionDefaults.validateWhenHidden,\n ancestorAlwaysHidden: false,\n ancestorHiddenLogics: [],\n };\n\n return schema<TModel>((path) => {\n for (const fieldDef of fields) {\n const valueHandling = getFieldValueHandling(fieldDef.type, registry);\n\n // Handle different value handling modes\n if (valueHandling === 'exclude') {\n // Skip fields that don't contribute to form values\n continue;\n }\n\n if (valueHandling === 'flatten' && hasChildFields(fieldDef)) {\n // A flatten container can still set its own cascading overrides that flow\n // down to its children — including its own hidden state which has no\n // schema path of its own.\n const flattenedOwn = resolveFieldOwnContext(fieldDef, rootContext);\n const flattenedContext = resolveDescendantContext(fieldDef, flattenedOwn);\n for (const childField of normalizeFieldsArray(fieldDef.fields)) {\n if (!childField.key) continue;\n\n const childPath = (path as Record<string, SchemaPath<unknown>>)[childField.key];\n if (childPath) {\n mapFieldToForm(childField, childPath, flattenedContext);\n }\n }\n continue;\n }\n\n // Regular field processing for 'include' fields\n const fieldPath = (path as Record<string, SchemaPath<unknown>>)[fieldDef.key];\n\n if (!fieldPath) {\n continue;\n }\n\n // Use the new modular form mapping function\n // This will progressively apply validators, logic, and schemas\n // Cross-field logic is handled automatically via RootFormRegistryService\n mapFieldToForm(fieldDef, fieldPath, rootContext);\n }\n\n // Apply cross-field validators using validateTree\n if (crossFieldValidators && crossFieldValidators.length > 0) {\n applyCrossFieldTreeValidator(path as SchemaPathTree<TModel>, crossFieldValidators, functionRegistry, logger);\n }\n\n // Apply form-level Standard Schema validation\n if (formLevelSchema) {\n applyFormLevelSchema(path as SchemaPathTree<TModel>, formLevelSchema);\n }\n });\n}\n\n/**\n * Applies cross-field validators using Angular's validateTree API.\n *\n * @param rootPath The root schema path tree\n * @param validators Array of collected cross-field validators\n * @param functionRegistry Registry containing custom functions for expression evaluation\n * @param logger Logger for error reporting\n */\nfunction applyCrossFieldTreeValidator<TModel>(\n rootPath: SchemaPathTree<TModel>,\n validators: CrossFieldValidatorEntry[],\n functionRegistry: FunctionRegistryService,\n logger: Logger,\n): void {\n // Get custom functions for expression evaluation\n const customFunctions = functionRegistry.getCustomFunctions();\n\n validateTree(rootPath as SchemaPath<TModel>, (ctx: FieldContext<TModel>) => {\n if (validators.length === 0) {\n return null; // ValidationSuccess - no errors\n }\n\n // Read form value reactively - don't wrap in untracked() so Angular can track dependencies\n const formValue = ctx.value() as Record<string, unknown>;\n const errors: ValidationError.WithOptionalField[] = [];\n\n for (const entry of validators) {\n const { sourceFieldKey, config } = entry;\n\n try {\n const error = evaluateCrossFieldValidator(entry, formValue, sourceFieldKey, ctx, customFunctions, logger);\n\n if (error) {\n errors.push(error);\n }\n } catch (err) {\n logger.error(`Error evaluating cross-field validator for ${sourceFieldKey}:`, err);\n\n // On error, add a validation error to indicate the failure\n const targetField = getFieldTreeByKey(ctx, sourceFieldKey);\n\n if (targetField) {\n const customConfig = config as CustomValidatorConfig;\n errors.push({\n kind: customConfig.kind || config.type || 'custom',\n fieldTree: targetField,\n });\n }\n }\n }\n\n return errors.length > 0 ? errors : null;\n });\n}\n\n/** Evaluates a single cross-field validator entry and returns an error if validation fails. */\nfunction evaluateCrossFieldValidator<TModel>(\n entry: CrossFieldValidatorEntry,\n formValue: Record<string, unknown>,\n sourceFieldKey: string,\n ctx: FieldContext<TModel>,\n customFunctions: EvaluationContext['customFunctions'],\n logger: Logger,\n): ValidationError.WithOptionalField | null {\n const { config } = entry;\n const fieldValue = getNestedValue(formValue, sourceFieldKey);\n\n // Create evaluation context for condition/expression evaluation\n const evaluationContext: EvaluationContext = {\n fieldValue,\n formValue,\n fieldPath: sourceFieldKey,\n customFunctions,\n logger,\n };\n\n // Check if this is a custom validator (with expression) or a built-in validator (with when condition)\n if (config.type === 'custom') {\n return evaluateCustomCrossFieldValidator(config as CustomValidatorConfig, evaluationContext, sourceFieldKey, ctx);\n } else {\n // Built-in validator with cross-field when condition\n return evaluateBuiltInCrossFieldValidator(config, evaluationContext, sourceFieldKey, ctx);\n }\n}\n\n/** Evaluates a custom cross-field validator with an expression. */\nfunction evaluateCustomCrossFieldValidator<TModel>(\n config: CustomValidatorConfig,\n evaluationContext: EvaluationContext,\n sourceFieldKey: string,\n ctx: FieldContext<TModel>,\n): ValidationError.WithOptionalField | null {\n if (!config.expression) {\n return null;\n }\n\n const { fieldValue, formValue, logger, customFunctions } = evaluationContext;\n\n // First, evaluate the when condition if present\n // If the condition is false, the validator doesn't apply (validation passes)\n if (config.when) {\n const conditionMet = evaluateCondition(config.when, {\n fieldValue,\n formValue,\n fieldPath: sourceFieldKey,\n customFunctions: customFunctions || {},\n logger,\n });\n\n if (!conditionMet) {\n return null; // Condition not met, skip validation\n }\n }\n\n // Evaluate expression using the secure AST parser\n const result = ExpressionParser.evaluate(config.expression, evaluationContext);\n\n // If expression returns truthy, validation passes (no error)\n if (result) {\n return null;\n }\n\n // Validation failed - create error targeting the source field\n const targetField = getFieldTreeByKey(ctx, sourceFieldKey);\n if (!targetField) {\n evaluationContext.logger.warn(`Cross-field validator references non-existent field \"${sourceFieldKey}\"`);\n return null;\n }\n\n const errorObj: Record<string, unknown> = {\n kind: config.kind || 'custom',\n fieldTree: targetField,\n };\n\n // Evaluate and include errorParams for message interpolation\n if (config.errorParams) {\n for (const [key, expression] of Object.entries(config.errorParams)) {\n try {\n errorObj[key] = ExpressionParser.evaluate(expression, evaluationContext);\n } catch (error) {\n evaluationContext.logger.warn(`Failed to evaluate error param expression \"${key}\": ${expression}`, error);\n }\n }\n }\n\n return errorObj as unknown as ValidationError.WithOptionalField;\n}\n\n/**\n * Evaluates a built-in validator with a cross-field when condition.\n * First checks the when condition, then applies the built-in validation logic.\n */\nfunction evaluateBuiltInCrossFieldValidator<TModel>(\n config: ValidatorConfig,\n evaluationContext: EvaluationContext,\n sourceFieldKey: string,\n ctx: FieldContext<TModel>,\n): ValidationError.WithOptionalField | null {\n const { fieldValue, formValue, logger, customFunctions } = evaluationContext;\n\n // First, evaluate the when condition\n // If the condition is false, the validator doesn't apply (validation passes)\n if (config.when) {\n const conditionMet = evaluateCondition(config.when, {\n fieldValue,\n formValue,\n fieldPath: sourceFieldKey,\n customFunctions: customFunctions || {},\n logger,\n });\n\n if (!conditionMet) {\n return null; // Condition not met, skip validation\n }\n }\n\n // Now apply the built-in validation logic based on the validator type\n const isValid = applyBuiltInValidationLogic(config, fieldValue);\n\n if (isValid) {\n return null; // Validation passed\n }\n\n // Validation failed - create error targeting the source field\n const targetField = getFieldTreeByKey(ctx, sourceFieldKey);\n if (!targetField) {\n logger.warn(`Cross-field validator references non-existent field \"${sourceFieldKey}\"`);\n return null;\n }\n\n const errorObj: Record<string, unknown> = {\n kind: config.type,\n fieldTree: targetField,\n };\n\n // Include the static constraint on the error, matching native built-in errors\n // (e.g. { maxLength: 5 }) so message templates can interpolate it\n if ('value' in config && config.value !== undefined) {\n errorObj[config.type] = config.value;\n }\n\n return errorObj as unknown as ValidationError.WithOptionalField;\n}\n\n/**\n * Applies built-in validation logic based on the validator type.\n * Returns true if validation passes, false if it fails.\n */\nfunction applyBuiltInValidationLogic(config: ValidatorConfig, fieldValue: unknown): boolean {\n switch (config.type) {\n case 'required':\n // Required: value must be non-null, non-undefined, and non-empty string\n if (fieldValue === null || fieldValue === undefined) {\n return false;\n }\n if (typeof fieldValue === 'string' && fieldValue.trim() === '') {\n return false;\n }\n return true;\n\n case 'min':\n // Min: numeric value must be >= min\n if (fieldValue === null || fieldValue === undefined) {\n return true; // Empty values pass min validation (use required for emptiness)\n }\n if ('value' in config && typeof config.value === 'number') {\n return (fieldValue as number) >= config.value;\n }\n return true;\n\n case 'max':\n // Max: numeric value must be <= max\n if (fieldValue === null || fieldValue === undefined) {\n return true;\n }\n if ('value' in config && typeof config.value === 'number') {\n return (fieldValue as number) <= config.value;\n }\n return true;\n\n case 'minLength':\n // MinLength: string length must be >= minLength\n if (fieldValue === null || fieldValue === undefined || fieldValue === '') {\n return true;\n }\n if ('value' in config && typeof config.value === 'number') {\n return String(fieldValue).length >= config.value;\n }\n return true;\n\n case 'maxLength':\n // MaxLength: string length must be <= maxLength\n if (fieldValue === null || fieldValue === undefined || fieldValue === '') {\n return true;\n }\n if ('value' in config && typeof config.value === 'number') {\n return String(fieldValue).length <= config.value;\n }\n return true;\n\n case 'email':\n // Email: must match email pattern\n if (fieldValue === null || fieldValue === undefined || fieldValue === '') {\n return true;\n }\n\n return emailPattern.test(String(fieldValue));\n\n case 'pattern':\n // Pattern: must match regex pattern\n if (fieldValue === null || fieldValue === undefined || fieldValue === '') {\n return true;\n }\n if ('value' in config && config.value) {\n const regex = config.value instanceof RegExp ? config.value : new RegExp(String(config.value));\n return regex.test(String(fieldValue));\n }\n return true;\n\n default:\n // Unknown validator type, consider valid\n return true;\n }\n}\n\n/** Utility to convert field definitions to default values object */\nexport function fieldsToDefaultValues<TModel = unknown>(fields: FieldDef<unknown>[], registry: Map<string, FieldTypeDefinition>): TModel {\n const defaultValues: Record<string, unknown> = {};\n\n for (const field of fields) {\n if (!field.key) continue;\n\n // Skip flatten fields (row/page) at top level - they are presentational containers\n const valueHandling = getFieldValueHandling(field.type, registry);\n if (valueHandling === 'flatten') {\n continue;\n }\n\n const value = getFieldDefaultValue(field, registry);\n if (value !== undefined) {\n defaultValues[field.key] = value;\n }\n }\n\n return defaultValues as TModel;\n}\n","import { FieldDef } from '@ng-forge/dynamic-forms/internal';\nimport { FieldWithValidation } from '@ng-forge/dynamic-forms/internal';\nimport { isStateLogicConfig, LogicConfig, StateLogicConfig } from '@ng-forge/dynamic-forms/internal';\nimport { ValidatorConfig, CustomValidatorConfig, BuiltInValidatorConfig } from '@ng-forge/dynamic-forms/internal';\nimport { ConditionalExpression } from '@ng-forge/dynamic-forms/internal';\nimport { SchemaApplicationConfig } from '@ng-forge/dynamic-forms/internal';\nimport { hasChildFields, isGroupField } from '@ng-forge/dynamic-forms/internal';\nimport { normalizeFieldsArray } from '@ng-forge/dynamic-forms/internal';\nimport { DynamicFormError } from '@ng-forge/dynamic-forms/internal';\nimport {\n isCrossFieldValidator,\n isCrossFieldBuiltInValidator,\n hasCrossFieldWhenCondition,\n isCrossFieldStateLogic,\n isCrossFieldSchema,\n extractExpressionDependencies,\n extractStringDependencies,\n} from '@ng-forge/dynamic-forms/internal';\nimport { CrossFieldValidatorEntry, CrossFieldLogicEntry, CrossFieldSchemaEntry, LogicType } from './cross-field-types';\n\n/** Collected cross-field entries from traversing field definitions. */\nexport interface CrossFieldCollection {\n validators: CrossFieldValidatorEntry[];\n logic: CrossFieldLogicEntry[];\n schemas: CrossFieldSchemaEntry[];\n}\n\n/** Creates an empty cross-field collection. */\nexport function createEmptyCollection(): CrossFieldCollection {\n return {\n validators: [],\n logic: [],\n schemas: [],\n };\n}\n\n/** Traverses field definitions and collects cross-field entries (validators, logic, schemas). */\nexport function collectCrossFieldEntries(fields: FieldDef<unknown>[]): CrossFieldCollection {\n const collection = createEmptyCollection();\n traverseFields(fields, collection, '');\n return collection;\n}\n\nfunction traverseFields(fields: FieldDef<unknown>[], collection: CrossFieldCollection, pathPrefix: string): void {\n for (const field of fields) {\n collectFromField(field, collection, pathPrefix);\n\n // Recursively process container fields (page, row, group, array)\n if (hasChildFields(field)) {\n // Groups add their key to the path prefix; page/row/array are transparent —\n // their children are still traversed, but those containers don't contribute a\n // key segment. Arrays are transparent rather than skipped because array items\n // use dynamic indices that aren't statically knowable, so no path prefix can\n // be built for them.\n const groupKey = isGroupField(field) ? field.key : undefined;\n const childPrefix = groupKey ? (pathPrefix ? `${pathPrefix}.${groupKey}` : groupKey) : pathPrefix;\n traverseFields(normalizeFieldsArray(field.fields) as FieldDef<unknown>[], collection, childPrefix);\n }\n }\n}\n\nfunction collectFromField(field: FieldDef<unknown>, collection: CrossFieldCollection, pathPrefix: string): void {\n const fieldKey = field.key;\n if (!fieldKey) return;\n\n const fullKey = pathPrefix ? `${pathPrefix}.${fieldKey}` : fieldKey;\n\n const validationField = field as FieldDef<unknown> & FieldWithValidation;\n\n // Collect cross-field validators\n if (validationField.validators) {\n for (const config of validationField.validators) {\n const entry = tryCreateValidatorEntry(fullKey, config);\n if (entry) {\n collection.validators.push(entry);\n }\n }\n }\n\n // Collect cross-field logic\n if (validationField.logic) {\n for (const config of validationField.logic) {\n const entry = tryCreateLogicEntry(fullKey, config);\n if (entry) {\n collection.logic.push(entry);\n }\n }\n }\n\n // Collect cross-field schemas\n if (validationField.schemas) {\n for (const config of validationField.schemas) {\n const entry = tryCreateSchemaEntry(fullKey, config);\n if (entry) {\n collection.schemas.push(entry);\n }\n }\n }\n}\n\n/** Returns a validator entry if cross-field, null otherwise. */\nfunction tryCreateValidatorEntry(fieldKey: string, config: ValidatorConfig): CrossFieldValidatorEntry | null {\n // Check for custom validators with cross-field expressions\n if (config.type === 'custom') {\n const customConfig = config as CustomValidatorConfig;\n if (isCrossFieldValidator(customConfig)) {\n return {\n sourceFieldKey: fieldKey,\n config,\n dependsOn: extractStringDependencies(customConfig.expression || ''),\n category: 'validator',\n };\n }\n }\n\n // Check for built-in validators with cross-field dynamic expressions\n if (isCrossFieldBuiltInValidator(config)) {\n const builtInConfig = config as BuiltInValidatorConfig;\n return {\n sourceFieldKey: fieldKey,\n config: convertBuiltInToCustomValidator(builtInConfig),\n validatorType: config.type,\n dependsOn: extractStringDependencies(builtInConfig.expression || ''),\n category: 'validator',\n };\n }\n\n // Check for validators with cross-field when conditions\n if (hasCrossFieldWhenCondition(config)) {\n const whenCondition = config.when as ConditionalExpression;\n return {\n sourceFieldKey: fieldKey,\n config,\n validatorType: config.type,\n dependsOn: extractExpressionDependencies(whenCondition),\n category: 'validator',\n };\n }\n\n return null;\n}\n\n/** Returns a logic entry if cross-field state logic, null otherwise. */\nfunction tryCreateLogicEntry(fieldKey: string, config: LogicConfig): CrossFieldLogicEntry | null {\n // Only process state logic (hidden, readonly, disabled, required)\n // Derivation logic is handled by the derivation collector\n if (!isStateLogicConfig(config)) {\n return null;\n }\n\n if (!isCrossFieldStateLogic(config)) {\n return null;\n }\n\n const condition = config.condition as ConditionalExpression;\n return {\n sourceFieldKey: fieldKey,\n logicType: config.type as LogicType,\n condition,\n config: config as StateLogicConfig,\n dependsOn: extractExpressionDependencies(condition),\n category: 'logic',\n };\n}\n\n/** Returns a schema entry if cross-field, null otherwise. */\nfunction tryCreateSchemaEntry(fieldKey: string, config: SchemaApplicationConfig): CrossFieldSchemaEntry | null {\n if (!isCrossFieldSchema(config)) {\n return null;\n }\n\n const condition = config.condition as ConditionalExpression;\n return {\n sourceFieldKey: fieldKey,\n config,\n condition,\n dependsOn: extractExpressionDependencies(condition),\n category: 'schema',\n };\n}\n\n/** Converts a built-in validator with cross-field expression to a custom validator. */\nfunction convertBuiltInToCustomValidator(config: BuiltInValidatorConfig): CustomValidatorConfig {\n const expression = config.expression;\n if (!expression) {\n throw new DynamicFormError(`Built-in validator ${config.type} missing required expression for cross-field conversion`);\n }\n let validationExpression: string;\n\n switch (config.type) {\n case 'min':\n validationExpression = `fieldValue == null || fieldValue >= (${expression})`;\n break;\n case 'max':\n validationExpression = `fieldValue == null || fieldValue <= (${expression})`;\n break;\n case 'minLength':\n validationExpression = `fieldValue == null || (typeof fieldValue !== 'string' && !Array.isArray(fieldValue)) || fieldValue.length >= (${expression})`;\n break;\n case 'maxLength':\n validationExpression = `fieldValue == null || (typeof fieldValue !== 'string' && !Array.isArray(fieldValue)) || fieldValue.length <= (${expression})`;\n break;\n case 'pattern':\n // Validate regex pattern before injection to prevent runtime errors\n try {\n new RegExp(expression);\n } catch (e) {\n throw new DynamicFormError(\n `Invalid regex pattern in cross-field validator: '${expression}' — ${e instanceof Error ? e.message : String(e)}`,\n );\n }\n validationExpression = `fieldValue == null || new RegExp(${JSON.stringify(expression)}).test(fieldValue)`;\n break;\n default:\n throw new DynamicFormError(`Cannot convert ${config.type} validator to custom validator`);\n }\n\n return {\n type: 'custom',\n expression: validationExpression,\n kind: config.type,\n when: config.when,\n errorParams: {\n [config.type]: expression,\n },\n };\n}\n","import { FieldDef } from '@ng-forge/dynamic-forms/internal';\nimport { isRowField } from '@ng-forge/dynamic-forms/internal';\nimport { isGroupField } from '@ng-forge/dynamic-forms/internal';\nimport { isArrayField } from '@ng-forge/dynamic-forms/internal';\nimport { isGenericContainerField } from '@ng-forge/dynamic-forms/internal';\nimport { FieldTypeDefinition, getFieldValueHandling } from '@ng-forge/dynamic-forms/internal';\nimport { normalizeFieldsArray } from '@ng-forge/dynamic-forms/internal';\n\n/** Represents a field definition that has been processed through the flattening algorithm. */\nexport interface FlattenedField extends FieldDef<unknown> {\n /** Guaranteed non-empty key for form binding and field identification */\n readonly key: string;\n\n /**\n * Static hidden/disabled/readonly state inherited from a discarded flatten\n * container (page/row) ancestor. Set only when a hoisted child's ancestor\n * declared the state; the child's own state still wins for everything else.\n *\n * Read exclusively by the value-exclusion path (`filterFormValue`) so a hidden\n * page drops its hoisted children's values — mirroring how a hidden group\n * drops its preserved subtree. NOT consumed by schema building or rendering,\n * which keeps this purely a value-exclusion concern with no validation side\n * effects.\n */\n readonly inheritedExclusionState?: {\n readonly hidden?: boolean;\n readonly disabled?: boolean;\n readonly readonly?: boolean;\n };\n}\n\n/**\n * Propagates a discarded flatten-container's (page/row) exclusion-relevant config\n * onto a hoisted child so the value-exclusion path can still drop the child's value\n * when the container was hidden/disabled/readonly.\n *\n * - Static state (`hidden`/`disabled`/`readonly`) is OR-accumulated into the child's\n * `inheritedExclusionState`, so nested flatten containers compound correctly.\n * - `excludeValueIf*` config is applied only where the child has not declared its own\n * (child override wins), matching the group-cascade resolution model.\n *\n * Returns the child unchanged when the container carries no exclusion-relevant config,\n * preserving object identity for the common case.\n */\nfunction inheritContainerExclusion(container: FieldDef<unknown>, child: FlattenedField): FlattenedField {\n const inheritedHidden = container.hidden === true || child.inheritedExclusionState?.hidden === true;\n const inheritedDisabled = container.disabled === true || child.inheritedExclusionState?.disabled === true;\n const inheritedReadonly = container.readonly === true || child.inheritedExclusionState?.readonly === true;\n\n const hasInheritedState = inheritedHidden || inheritedDisabled || inheritedReadonly;\n const containerExcludeConfig =\n container.excludeValueIfHidden !== undefined ||\n container.excludeValueIfDisabled !== undefined ||\n container.excludeValueIfReadonly !== undefined;\n\n if (!hasInheritedState && !containerExcludeConfig) {\n return child;\n }\n\n return {\n ...child,\n ...(hasInheritedState\n ? { inheritedExclusionState: { hidden: inheritedHidden, disabled: inheritedDisabled, readonly: inheritedReadonly } }\n : {}),\n // Child override wins; otherwise inherit the container's exclusion config.\n excludeValueIfHidden: child.excludeValueIfHidden ?? container.excludeValueIfHidden,\n excludeValueIfDisabled: child.excludeValueIfDisabled ?? container.excludeValueIfDisabled,\n excludeValueIfReadonly: child.excludeValueIfReadonly ?? container.excludeValueIfReadonly,\n };\n}\n\n/**\n * Flattens a hierarchical field structure into a linear array for form processing.\n *\n * @param fields - Array of field definitions that may contain nested structures\n * @param registry - Field type registry for determining value handling behavior\n * @param options - Configuration options for flattening behavior\n * @param options.preserveRows - When true, keep row fields in structure for DOM rendering (grid layout)\n * @returns Flattened array of field definitions with guaranteed keys\n */\nexport function flattenFields(\n fields: FieldDef<unknown>[],\n registry: Map<string, FieldTypeDefinition>,\n options: { preserveRows?: boolean } = {},\n): FlattenedField[] {\n const result: FlattenedField[] = [];\n let autoKeyCounter = 0;\n\n // Process each field using appropriate strategy based on field type and configuration\n for (const field of fields) {\n // Step 1: Determine how this field type should handle its value in the form\n // valueHandling can be: 'include', 'exclude', or 'flatten'\n const valueHandling = getFieldValueHandling(field.type, registry);\n\n // Step 2: Check if this is a row or container field that should be preserved for DOM rendering\n // Row fields need to render their container element for grid layouts to work\n // Container fields need to render their container for the wrapper chain\n if (options.preserveRows && (isRowField(field) || isGenericContainerField(field))) {\n if (field.fields) {\n // Recursively flatten children while preserving row structure\n const flattenedChildren = flattenFields(normalizeFieldsArray(field.fields as FieldDef<unknown>[]), registry, options);\n\n // Keep the row/container field in the result with its flattened children\n // This allows the container component to render while children are flattened\n const autoPrefix = isRowField(field) ? 'auto_row' : 'auto_container';\n result.push({\n ...field,\n fields: flattenedChildren,\n key: field.key || `${autoPrefix}_${autoKeyCounter++}`,\n } as unknown as FlattenedField);\n }\n } else if (valueHandling === 'flatten' && 'fields' in field) {\n // Step 3: Handle fields with 'flatten' value handling (typically page/row fields)\n // These fields are pure containers - merge their children directly into the parent level\n if (field.fields) {\n const fields = field.fields as FieldDef<unknown>[] | Record<string, FieldDef<unknown>>;\n const flattenedChildren = flattenFields(normalizeFieldsArray(fields), registry, options);\n\n // Spread children directly into result - the container field itself is discarded.\n // Because the container (page/row) is dropped, its exclusion-relevant state would\n // otherwise be lost. Propagate it onto each hoisted child so a hidden/disabled/\n // readonly page drops its children's values during value exclusion — mirroring how\n // a hidden group drops its preserved subtree. The child's own config still wins.\n // This is used for page fields (form structure) and row fields (form values).\n result.push(...flattenedChildren.map((child) => inheritContainerExclusion(field, child)));\n }\n } else if (isGroupField(field)) {\n // Step 4: Handle group fields - preserve structure for nested form values\n // Group fields create a nested object in the form value: { groupKey: { field1: value1, ... } }\n const childFieldsArray = Object.values(field.fields) as FieldDef<unknown>[];\n const flattenedChildren = flattenFields(childFieldsArray, registry, options);\n\n // Keep the group field with its recursively flattened children nested under its key\n result.push({\n ...field,\n fields: flattenedChildren,\n key: field.key || `auto_group_${autoKeyCounter++}`,\n } as FlattenedField);\n } else if (isArrayField(field)) {\n // Step 5: Handle array fields - preserve structure for array form values\n // Array fields create an array in the form value: { arrayKey: [item1, item2, ...] }\n // field.fields is ArrayItemDefinition[] — each item is either:\n // - ArrayAllowedChildren (primitive item) → preserved as single FlattenedField\n // - ArrayItemTemplate (readonly ArrayAllowedChildren[]) → flattened as FlattenedField[]\n //\n // IMPORTANT: Preserve the primitive/object distinction so getFieldDefaultValue\n // can produce flat values (['angular']) vs nested values ([{value: 'angular'}]).\n const flattenedItemTemplates = field.fields.map((itemDef) => {\n if (!Array.isArray(itemDef)) {\n // Primitive item: single ArrayAllowedChildren → flatten, preserve non-array structure\n const flattened = flattenFields([itemDef as FieldDef<unknown>], registry, options);\n return flattened[0];\n }\n // Object item: ArrayItemTemplate (field[]) → flatten all fields as array\n return flattenFields([...(itemDef as FieldDef<unknown>[])], registry, options);\n });\n\n // Keep the array field with its flattened item templates nested under its key\n result.push({\n ...field,\n fields: flattenedItemTemplates,\n key: field.key || `auto_array_${autoKeyCounter++}`,\n } as FlattenedField);\n } else {\n // Step 6: Handle all other fields (inputs, buttons, etc.) - pass through unchanged\n // These fields are leaf nodes that don't contain children and map directly to form controls\n const key = field.key || `auto_field_${autoKeyCounter++}`;\n result.push({\n ...field,\n key,\n } as FlattenedField);\n }\n }\n\n return result;\n}\n","import { InjectionToken } from '@angular/core';\nimport { FieldDef } from '@ng-forge/dynamic-forms/internal';\nimport { FieldTypeDefinition } from '@ng-forge/dynamic-forms/internal';\nimport { flattenFields } from '../flattener/field-flattener';\nimport { getFieldDefaultValue } from '../default-value/default-value';\nimport { keyBy, mapValues } from '@ng-forge/dynamic-forms/internal';\n\n/**\n * Creates memoized field processing functions shared across container components.\n *\n * @returns Object containing memoized processing functions\n */\nexport function createContainerFieldProcessors() {\n const flattenCache = new WeakMap<readonly FieldDef<unknown>[], FieldDef<unknown>[]>();\n const memoizedFlattenFields = (fields: readonly FieldDef<unknown>[], registry: Map<string, FieldTypeDefinition>): FieldDef<unknown>[] => {\n const cached = flattenCache.get(fields);\n if (cached !== undefined) {\n return cached;\n }\n const result = flattenFields([...fields], registry);\n flattenCache.set(fields, result);\n return result;\n };\n\n const flattenForRenderingCache = new WeakMap<readonly FieldDef<unknown>[], FieldDef<unknown>[]>();\n const memoizedFlattenFieldsForRendering = (\n fields: readonly FieldDef<unknown>[],\n registry: Map<string, FieldTypeDefinition>,\n ): FieldDef<unknown>[] => {\n const cached = flattenForRenderingCache.get(fields);\n if (cached !== undefined) {\n return cached;\n }\n const result = flattenFields([...fields], registry, { preserveRows: true });\n flattenForRenderingCache.set(fields, result);\n return result;\n };\n\n const keyByCache = new WeakMap<object[], Record<string, unknown>>();\n const memoizedKeyBy = <T extends { key: string }>(fields: T[]): Record<string, T> => {\n const cached = keyByCache.get(fields);\n if (cached !== undefined) {\n return cached as Record<string, T>;\n }\n const result = keyBy(fields, 'key');\n keyByCache.set(fields, result);\n return result;\n };\n\n const defaultValuesCache = new WeakMap<object, Record<string, unknown>>();\n const memoizedDefaultValues = <T extends FieldDef<unknown>>(\n fieldsById: Record<string, T>,\n registry: Map<string, FieldTypeDefinition>,\n ): Record<string, unknown> => {\n const cached = defaultValuesCache.get(fieldsById);\n if (cached !== undefined) {\n return cached;\n }\n const result = mapValues(fieldsById, (field) => getFieldDefaultValue(field, registry));\n defaultValuesCache.set(fieldsById, result);\n return result;\n };\n\n return { memoizedFlattenFields, memoizedFlattenFieldsForRendering, memoizedKeyBy, memoizedDefaultValues };\n}\n\n/**\n * Shared container field processors injection token.\n *\n * @internal\n */\nexport const CONTAINER_FIELD_PROCESSORS = new InjectionToken<ReturnType<typeof createContainerFieldProcessors>>(\n 'CONTAINER_FIELD_PROCESSORS',\n {\n providedIn: 'root',\n factory: () => createContainerFieldProcessors(),\n },\n);\n","import { InjectionToken, signal, Signal, WritableSignal } from '@angular/core';\nimport { isEqual } from '@ng-forge/dynamic-forms/internal';\n\n/** Per-field signal store for property overrides. */\nexport interface PropertyOverrideStore {\n /**\n * Gets the override signal for a field.\n *\n * @param fieldKey - The store key (e.g., 'endDate' or 'contacts.0.email')\n * @returns Signal containing the current overrides record\n */\n getOverrides(fieldKey: string): Signal<Record<string, unknown>>;\n\n /**\n * Sets a single property override for a field.\n *\n * @param fieldKey - The store key\n * @param targetProperty - The property name (e.g., 'minDate', 'options')\n * @param value - The override value, or undefined to remove\n */\n setOverride(fieldKey: string, targetProperty: string, value: unknown): void;\n\n /**\n * Registers a field as having property derivations.\n *\n * @param fieldKey - The store key to register\n */\n registerField(fieldKey: string): void;\n\n /**\n * Non-reactive check for whether a field has been registered.\n *\n * @param fieldKey - The store key (or prefix for array fields)\n * @returns true if the field has property derivation entries\n */\n hasField(fieldKey: string): boolean;\n\n /** Clears all overrides and registered fields. */\n clear(): void;\n}\n\nconst EMPTY_OVERRIDES: Record<string, unknown> = Object.freeze({});\n\n/**\n * Creates a new PropertyOverrideStore instance.\n *\n * @returns A fresh PropertyOverrideStore\n */\nexport function createPropertyOverrideStore(): PropertyOverrideStore {\n const store = new Map<string, WritableSignal<Record<string, unknown>>>();\n const registeredFields = new Set<string>();\n\n function getOrCreateSignal(fieldKey: string): WritableSignal<Record<string, unknown>> {\n let existing = store.get(fieldKey);\n if (!existing) {\n existing = signal(EMPTY_OVERRIDES);\n store.set(fieldKey, existing);\n }\n return existing;\n }\n\n return {\n getOverrides(fieldKey: string): Signal<Record<string, unknown>> {\n return getOrCreateSignal(fieldKey);\n },\n\n setOverride(fieldKey: string, targetProperty: string, value: unknown): void {\n const fieldSignal = getOrCreateSignal(fieldKey);\n const current = fieldSignal();\n\n if (value === undefined) {\n // Remove the property key\n if (!(targetProperty in current)) {\n return; // Already absent, no-op\n }\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n const { [targetProperty]: _, ...rest } = current;\n fieldSignal.set(Object.keys(rest).length === 0 ? EMPTY_OVERRIDES : rest);\n } else {\n // Set or update the property\n const currentValue = current[targetProperty];\n if (isEqual(currentValue, value)) {\n return; // Value unchanged, no-op\n }\n fieldSignal.set({ ...current, [targetProperty]: value });\n }\n },\n\n registerField(fieldKey: string): void {\n registeredFields.add(fieldKey);\n },\n\n hasField(fieldKey: string): boolean {\n return registeredFields.has(fieldKey);\n },\n\n clear(): void {\n store.clear();\n registeredFields.clear();\n },\n };\n}\n\n/** Injection token for the PropertyOverrideStore. */\nexport const PROPERTY_OVERRIDE_STORE = new InjectionToken<PropertyOverrideStore>('PROPERTY_OVERRIDE_STORE');\n","import { DEV_MODE } from '../../utils/dev-mode';\nimport { DynamicFormError } from '@ng-forge/dynamic-forms/internal';\nimport { BaseValueField } from '@ng-forge/dynamic-forms/internal';\nimport { FieldWithValidation } from '@ng-forge/dynamic-forms/internal';\n\n/** Properties defined by individual field components, not part of the shared base interfaces. */\ntype AdapterFieldKey = 'options' | 'hint' | 'tooltip' | 'prefix' | 'suffix' | 'step' | 'rows' | 'cols' | 'multiple' | 'appearance';\n\n/**\n * Known standard field properties where a missing key likely indicates a typo.\n * Override keys not in this set are assumed to be intentional dynamic properties\n * and will not trigger dev-mode warnings.\n */\nconst KNOWN_FIELD_PROPERTY_NAMES = [\n 'label',\n 'placeholder',\n 'disabled',\n 'readonly',\n 'required',\n 'className',\n 'tabIndex',\n 'validationMessages',\n 'options',\n 'props',\n 'hint',\n 'tooltip',\n 'prefix',\n 'suffix',\n 'min',\n 'max',\n 'minLength',\n 'maxLength',\n 'step',\n 'pattern',\n 'rows',\n 'cols',\n 'multiple',\n 'appearance',\n] as const satisfies ReadonlyArray<keyof BaseValueField<unknown, unknown> | keyof FieldWithValidation | AdapterFieldKey>;\n\nconst KNOWN_FIELD_PROPERTIES = new Set<string>(KNOWN_FIELD_PROPERTY_NAMES);\n\n/**\n * Applies property overrides to a field's input record.\n *\n * @param inputs - The current field inputs record\n * @param overrides - Record of property names to override values\n * @returns Updated inputs record with overrides applied\n */\nexport function applyPropertyOverrides(inputs: Record<string, unknown>, overrides: Record<string, unknown>): Record<string, unknown> {\n const keys = Object.keys(overrides);\n if (keys.length === 0) {\n return inputs;\n }\n\n const result = { ...inputs };\n\n for (const key of keys) {\n const value = overrides[key];\n const dotIndex = key.indexOf('.');\n\n // Dev-mode check: warn only when a known standard field property is missing from inputs,\n // which likely indicates a typo. Dynamic/custom keys are intentionally skipped to avoid\n // false positives for properties added by derivations that aren't in the initial config.\n if (DEV_MODE) {\n const topLevelKey = dotIndex === -1 ? key : key.substring(0, dotIndex);\n if (KNOWN_FIELD_PROPERTIES.has(topLevelKey) && !(topLevelKey in inputs)) {\n console.warn(\n `[Dynamic Forms] Property override '${key}' does not match any existing input property. ` +\n 'This may indicate a typo in the targetProperty configuration. ' +\n `Available properties: ${Object.keys(inputs).join(', ')}`,\n );\n }\n }\n\n if (dotIndex === -1) {\n // Simple property: direct assignment\n result[key] = value;\n } else {\n // Nested property — split at first dot only\n const parentKey = key.substring(0, dotIndex);\n const childKey = key.substring(dotIndex + 1);\n\n // Validate max 2 levels (1 dot)\n if (childKey.includes('.')) {\n throw new DynamicFormError(\n `Property override path '${key}' exceeds maximum depth of 2 levels. ` +\n `Only simple ('minDate') and single-nested ('props.appearance') paths are supported.`,\n );\n }\n\n // Shallow-clone the parent object and set the child property\n const parentValue = result[parentKey];\n const clonedParent = parentValue && typeof parentValue === 'object' ? { ...(parentValue as Record<string, unknown>) } : {};\n clonedParent[childKey] = value;\n result[parentKey] = clonedParent;\n }\n }\n\n return result;\n}\n","/** Sentinel value for the `$` placeholder index used in registration keys. */\nexport const PLACEHOLDER_INDEX = '$' as const;\n\n/**\n * Builds a property override store key for a field.\n *\n * @param arrayKey - The array field key (e.g., 'contacts'), or undefined for non-array fields\n * @param index - The array item index, `PLACEHOLDER_INDEX` for the wildcard format, or undefined for non-array fields\n * @param fieldKey - The leaf field key (e.g., 'email')\n * @param groupPath - Dot-joined group ancestors scoped to the current array item (or top-level), or undefined when not inside any group\n * @returns The store key (e.g., 'email', 'address.street', 'contacts.0.email', 'contacts.$.profile.email')\n */\nexport function buildPropertyOverrideKey(\n arrayKey: string | undefined,\n index: number | typeof PLACEHOLDER_INDEX | undefined,\n fieldKey: string,\n groupPath?: string,\n): string {\n const leafScope = groupPath ? `${groupPath}.${fieldKey}` : fieldKey;\n if (arrayKey != null && index != null) {\n return `${arrayKey}.${index}.${leafScope}`;\n }\n return leafScope;\n}\n","import { computed, inject, isSignal, Signal } from '@angular/core';\nimport { FieldDef } from '@ng-forge/dynamic-forms/internal';\nimport { FieldWithValidation } from '@ng-forge/dynamic-forms/internal';\nimport { FieldMeta } from '@ng-forge/dynamic-forms/internal';\nimport { FieldTypeDefinition } from '@ng-forge/dynamic-forms/internal';\nimport { ARRAY_CONTEXT, FORM_ID_PREFIX, GROUP_CONTEXT } from '@ng-forge/dynamic-forms/internal';\nimport { ArrayContext } from '@ng-forge/dynamic-forms/internal';\nimport { baseFieldMapper } from '@ng-forge/dynamic-forms/internal';\nimport { PROPERTY_OVERRIDE_STORE } from '../../core/property-derivation/property-override-store';\nimport { applyPropertyOverrides } from '../../core/property-derivation/apply-property-overrides';\nimport { buildPropertyOverrideKey } from '../../core/property-derivation/property-override-key';\nimport { hasTargetProperty, isDerivationLogicConfig } from '@ng-forge/dynamic-forms/internal';\n\n/**\n * Returns true if the field has any property-derivation logic entries\n * (type: 'derivation' with a non-empty targetProperty). Mirrors the inclusion\n * rule used by `collectPropertyDerivations`, so the mapper and orchestrator\n * agree on which fields participate in property overrides — without depending\n * on whether the orchestrator's registration effect has run yet (which it\n * hasn't on the resolveFieldSync path with a warm COMPONENT_CACHE).\n */\nfunction fieldHasPropertyDerivations(fieldDef: FieldDef<unknown>): boolean {\n // Match the collector's keyless guard (property-derivation-collector.ts):\n // keyless fields are never registered in the store, so the mapper must skip\n // them too — otherwise getOverrides() would be called with an undefined key.\n if (!fieldDef.key) return false;\n const logic = (fieldDef as FieldDef<unknown> & FieldWithValidation).logic;\n if (!logic || logic.length === 0) return false;\n for (const entry of logic) {\n if (isDerivationLogicConfig(entry) && hasTargetProperty(entry)) return true;\n }\n return false;\n}\n\n/**\n * Merges forwarded props into meta, with meta taking precedence.\n *\n * @param inputs The current inputs record\n * @param propsToMeta Array of prop keys to forward to meta\n * @returns Updated inputs record with merged meta\n */\nfunction mergeForwardedPropsToMeta(inputs: Record<string, unknown>, propsToMeta: string[]): Record<string, unknown> {\n const props = inputs['props'] as Record<string, unknown> | undefined;\n if (!props || propsToMeta.length === 0) {\n return inputs;\n }\n\n // Extract values to forward from props\n // Values are typed as FieldMeta-compatible since they're forwarded attributes (type, rows, cols, etc.)\n const forwardedValues: FieldMeta = {};\n for (const key of propsToMeta) {\n const value = props[key];\n if (value !== undefined && value !== null) {\n // Props being forwarded are known HTML attributes (string, number, boolean)\n forwardedValues[key] = value as string | number | boolean;\n }\n }\n\n // If nothing to forward, return inputs unchanged\n if (Object.keys(forwardedValues).length === 0) {\n return inputs;\n }\n\n // Merge: forwarded props first, then meta (meta wins)\n const existingMeta = inputs['meta'] as FieldMeta | undefined;\n const mergedMeta: FieldMeta = {\n ...forwardedValues,\n ...existingMeta,\n };\n\n return {\n ...inputs,\n meta: mergedMeta,\n };\n}\n\n/**\n * Applies index suffix to the key property for array items.\n * This ensures unique DOM IDs while keeping form schema keys clean.\n *\n * @param inputs The current inputs record\n * @param index The array item index\n * @returns Updated inputs record with suffixed key\n */\nfunction applyIndexSuffix(inputs: Record<string, unknown>, index: number): Record<string, unknown> {\n const key = inputs['key'];\n if (typeof key !== 'string') {\n return inputs;\n }\n\n return {\n ...inputs,\n key: `${key}_${index}`,\n };\n}\n\n/**\n * Prefixes the key with the underscored group ancestor path so that the same\n * leaf key inside different groups produces distinct DOM IDs (issue #401).\n * Form-schema keys remain clean — only the rendered `key` input is rewritten.\n */\nfunction applyGroupPrefix(inputs: Record<string, unknown>, groupPath: string): Record<string, unknown> {\n const key = inputs['key'];\n if (typeof key !== 'string' || groupPath.length === 0) {\n return inputs;\n }\n\n return {\n ...inputs,\n key: `${groupPath.replace(/\\./g, '_')}_${key}`,\n };\n}\n\n/**\n * Prepends the form-level id prefix as the outermost key segment so the same key\n * across two forms on one page yields distinct DOM IDs. Rewrites only the rendered\n * `key`; schema/override-store keys stay clean. No-op when prefix is empty.\n */\nfunction applyFormPrefix(inputs: Record<string, unknown>, prefix: string): Record<string, unknown> {\n const key = inputs['key'];\n if (typeof key !== 'string' || prefix.length === 0) {\n return inputs;\n }\n\n return {\n ...inputs,\n key: `${prefix}_${key}`,\n };\n}\n\n/**\n * Main field mapper function that uses the field registry to get the appropriate mapper\n * based on the field's type property.\n *\n * @param fieldDef The field definition to map\n * @param fieldRegistry The registry of field type definitions\n * @returns Signal containing Record of input names to values, or undefined for componentless fields\n */\nexport function mapFieldToInputs(\n fieldDef: FieldDef<unknown>,\n fieldRegistry: Map<string, FieldTypeDefinition>,\n): Signal<Record<string, unknown>> | undefined {\n // Get the field type definition from registry\n const fieldType = fieldRegistry.get(fieldDef.type);\n\n // Componentless field (no mapper and no loadComponent) - nothing to map\n if (fieldType && !fieldType.loadComponent && !fieldType.mapper) {\n return undefined;\n }\n\n // Check if we're inside an array item context - if so, we need to suffix keys\n // Optional because ARRAY_CONTEXT is only provided inside array item injectors\n const arrayContext = inject(ARRAY_CONTEXT, { optional: true });\n\n // Check if we're inside a group context - if so, prefix the key with the group path\n // so overlapping leaf keys across groups produce distinct DOM IDs (issue #401).\n // Optional because GROUP_CONTEXT is only provided by GroupFieldComponent.\n const groupContext = inject(GROUP_CONTEXT, { optional: true });\n\n // Form-level DOM-id prefix (scopes ids to one form instance). Optional: absent under mock injectors.\n const formPrefixSignal = inject(FORM_ID_PREFIX, { optional: true });\n\n // Inject the property override store for property derivation overrides\n // Always available — provided at the DynamicForm component level via provideDynamicFormDI\n const store = inject(PROPERTY_OVERRIDE_STORE);\n\n // Get the base mapper result\n const mapperResult: Signal<Record<string, unknown>> = fieldType?.mapper ? fieldType.mapper(fieldDef) : baseFieldMapper(fieldDef);\n\n const propsToMeta = fieldType?.propsToMeta;\n const hasPropsForwarding = propsToMeta && propsToMeta.length > 0;\n // Check that arrayContext exists and has a valid index signal (guards against mock injectors)\n const indexSignal = arrayContext?.index;\n const hasArrayContext = indexSignal !== undefined && isSignal(indexSignal);\n const hasGroupContext = groupContext != null;\n // Bypasses the fast path so fields stay subscribed to a prefix that can flip\n // when a sibling form mounts (gating on its current value would miss the flip).\n const hasFormPrefix = formPrefixSignal != null;\n\n // Fast-path check for property overrides: only fields whose definition declares\n // a property-derivation enter the computed() wrapper. We inspect the FieldDef\n // directly rather than asking the store, because the store's registration\n // effect may not have fired yet when the mapper runs in the resolveFieldSync\n // path (warm COMPONENT_CACHE). Both the mapper and the orchestrator's\n // collector apply the same inclusion rule, so the answers always agree.\n const hasOverrides = fieldHasPropertyDerivations(fieldDef);\n\n // Fast path: no transformations needed\n if (!hasPropsForwarding && !hasArrayContext && !hasGroupContext && !hasOverrides && !hasFormPrefix) {\n return mapperResult;\n }\n\n // Wrap in computed to apply transformations\n return computed(() => {\n let inputs = mapperResult();\n\n // Apply props forwarding if configured\n if (hasPropsForwarding) {\n inputs = mergeForwardedPropsToMeta(inputs, propsToMeta);\n }\n\n // Apply group prefix BEFORE array suffix so the resulting key reads as\n // `{groupPath}_{key}_{index}` (e.g. `address_street_0`) — matching the\n // form-value path order: group → leaf → array index.\n if (hasGroupContext) {\n // Non-null after the hasGroupContext guard.\n inputs = applyGroupPrefix(inputs, groupContext!.groupPath());\n }\n\n // Apply index suffix for array items\n if (hasArrayContext) {\n const index = indexSignal();\n inputs = applyIndexSuffix(inputs, index);\n }\n\n // Outermost key segment; reading the signal here re-renders ids if the prefix flips.\n if (hasFormPrefix) {\n inputs = applyFormPrefix(inputs, formPrefixSignal!());\n }\n\n // Apply property overrides from the store (AFTER all static transformations).\n //\n // INVARIANT: the store key MUST be built from `fieldDef.key` + the group/array\n // ancestry (via buildPropertyOverrideKey) — NEVER from the DOM-scoped\n // `inputs['key']` produced by applyGroupPrefix/applyIndexSuffix above. The\n // collector (property-derivation-collector.ts) registers using the same\n // shape; if these drift, overrides silently miss their target.\n if (hasOverrides) {\n // Build inside computed() so the index/group signals establish reactive deps.\n // GROUP_CONTEXT resets at array boundaries (see create-array-item-injector.ts),\n // so groupPath() represents ancestors inside the current array item (or\n // top-level groups when not in an array) — matching the collector.\n const groupPath = hasGroupContext ? groupContext!.groupPath() : undefined;\n const key = hasArrayContext\n ? buildPropertyOverrideKey(\n (arrayContext as ArrayContext).arrayKey,\n (indexSignal as Signal<number>)(),\n fieldDef.key,\n groupPath || undefined,\n )\n : buildPropertyOverrideKey(undefined, undefined, fieldDef.key, groupPath || undefined);\n const overrides = store.getOverrides(key)();\n inputs = applyPropertyOverrides(inputs, overrides);\n }\n\n return inputs;\n });\n}\n","import { computed, DestroyRef, Injector, runInInjectionContext, Signal, Type } from '@angular/core';\nimport type { FieldTree } from '@angular/forms/signals';\nimport { catchError, forkJoin, from, map, Observable, of, OperatorFunction, pipe, scan, switchMap } from 'rxjs';\nimport { FieldDef } from '@ng-forge/dynamic-forms/internal';\nimport { FieldTypeDefinition } from '@ng-forge/dynamic-forms/internal';\nimport { mapFieldToInputs } from '../field-mapper/field-mapper';\n\n/** Resolved field ready for rendering with ngComponentOutlet / DfFieldOutlet. */\nexport interface ResolvedField {\n key: string;\n /** The original field definition (used by DfFieldOutlet to resolve wrappers). */\n fieldDef: FieldDef<unknown>;\n component: Type<unknown>;\n injector: Injector;\n inputs: Signal<Record<string, unknown>>;\n renderReady: Signal<boolean>;\n /**\n * Whether the field is currently hidden. Sourced from the FieldTree's\n * `state.hidden()` for value-bearing fields (set by Angular Signal Forms in\n * response to `hidden(path, …)` calls), or from the mapper's explicit\n * `hidden` input for non-value fields (button, text, container, …). Consumers\n * must `@if (!field.hidden())` around `*dfFieldOutlet` so hidden fields are\n * removed from the DOM — Angular Signal Forms warns (NG01916) otherwise.\n */\n hidden: Signal<boolean>;\n}\n\nexport function createRenderReadySignal(\n inputs: Signal<Record<string, unknown>>,\n definition: FieldTypeDefinition | undefined,\n): Signal<boolean> {\n const explicitRenderReadyWhen = definition?.renderReadyWhen;\n const requiredInputs = definition?.mapper && explicitRenderReadyWhen === undefined ? ['field'] : (explicitRenderReadyWhen ?? []);\n\n if (requiredInputs.length === 0) {\n return computed(() => true);\n }\n\n return computed(() => {\n const currentInputs = inputs();\n return requiredInputs.every((inputName) => currentInputs[inputName] !== undefined);\n });\n}\n\n/**\n * Reads the current hidden state from mapper inputs. Uniform across all field\n * types — value-bearing leaves source from the `FieldTree`'s `state.hidden()`\n * (where Angular Signal Forms cascades `hidden(path, …)`), and non-field outputs\n * (button/text/containers) source from the mapper's explicit `inputs.hidden`.\n */\nexport function createHiddenSignal(inputs: Signal<Record<string, unknown>>): Signal<boolean> {\n return computed(() => {\n const currentInputs = inputs();\n const fieldCandidate = currentInputs['field'];\n // Value-bearing leaves expose `field` as a callable `FieldTree` whose invocation returns a\n // `FieldState` with a `hidden` accessor. `isSignal()` doesn't recognise the Proxy-wrapped\n // FieldTree (no SIGNAL symbol), so we duck-type the shape and call `state.hidden()` directly.\n if (typeof fieldCandidate === 'function') {\n const state = (fieldCandidate as FieldTree<unknown>)();\n if (state && typeof state.hidden === 'function') {\n return state.hidden() === true;\n }\n }\n // Non-value fields (button, text, containers) carry an explicit `hidden` input set by\n // `applyHiddenLogic` based on the mapper-side evaluation.\n return currentInputs['hidden'] === true;\n });\n}\n\n/** Context required to resolve a field. */\nexport interface ResolveFieldContext {\n /**\n * The field registry to load components from.\n * Returns undefined for componentless fields (e.g., hidden fields).\n */\n loadTypeComponent: (type: string) => Promise<Type<unknown> | undefined>;\n /** The raw field registry map for mappers */\n registry: Map<string, FieldTypeDefinition>;\n /** The injector to use for the resolved field */\n injector: Injector;\n /** DestroyRef to check if component is destroyed */\n destroyRef: DestroyRef;\n /** Optional error handler for logging */\n onError?: (fieldDef: FieldDef<unknown>, error: unknown) => void;\n}\n\n/**\n * Resolves a single field definition to a ResolvedField using RxJS.\n * Loads the component asynchronously and maps inputs in the injection context.\n *\n * @param fieldDef - The field definition to resolve\n * @param context - The context containing dependencies for resolution\n * @returns Observable that emits ResolvedField or undefined (for componentless fields or on error)\n */\nexport function resolveField(fieldDef: FieldDef<unknown>, context: ResolveFieldContext): Observable<ResolvedField | undefined> {\n return from(context.loadTypeComponent(fieldDef.type)).pipe(\n map((component) => {\n // Check if component is destroyed before proceeding\n if (context.destroyRef.destroyed) {\n return undefined;\n }\n\n // Componentless fields (e.g., hidden) return undefined - nothing to render\n if (!component) {\n return undefined;\n }\n\n // Run mapper in injection context\n const inputs = runInInjectionContext(context.injector, () => mapFieldToInputs(fieldDef, context.registry));\n const definition = context.registry.get(fieldDef.type);\n\n // Fields with components should always have inputs (componentless fields are handled above)\n if (!inputs) {\n return undefined;\n }\n\n return {\n key: fieldDef.key,\n fieldDef,\n component,\n injector: context.injector,\n inputs,\n renderReady: createRenderReadySignal(inputs, definition),\n hidden: createHiddenSignal(inputs),\n };\n }),\n catchError((error) => {\n // Only call onError if component is not destroyed to avoid accessing cleaned-up state\n if (!context.destroyRef.destroyed) {\n context.onError?.(fieldDef, error);\n }\n return of(undefined);\n }),\n );\n}\n\n/** Context for synchronous field resolution when components are already cached. */\nexport interface SyncResolveFieldContext {\n /** Returns a previously loaded component, or undefined if not cached */\n getLoadedComponent: (type: string) => Type<unknown> | undefined;\n /** The raw field registry map for mappers */\n registry: Map<string, FieldTypeDefinition>;\n /** The injector to use for the resolved field */\n injector: Injector;\n}\n\n/**\n * Synchronously resolves a field definition to a ResolvedField using cached components.\n *\n * @param fieldDef - The field definition to resolve\n * @param context - The context containing cached components and dependencies\n * @returns ResolvedField or undefined (for componentless fields)\n */\nexport function resolveFieldSync(fieldDef: FieldDef<unknown>, context: SyncResolveFieldContext): ResolvedField | undefined {\n const component = context.getLoadedComponent(fieldDef.type);\n\n if (!component) {\n return undefined;\n }\n\n const inputs = runInInjectionContext(context.injector, () => mapFieldToInputs(fieldDef, context.registry));\n const definition = context.registry.get(fieldDef.type);\n\n if (!inputs) {\n return undefined;\n }\n\n return {\n key: fieldDef.key,\n fieldDef,\n component,\n injector: context.injector,\n inputs,\n renderReady: createRenderReadySignal(inputs, definition),\n hidden: createHiddenSignal(inputs),\n };\n}\n\n/**\n * Reconciles previous and current resolved fields to preserve injector instances\n * for fields that haven't changed type, preventing unnecessary component recreation.\n *\n * @param prev - Previous resolved fields array\n * @param curr - Current resolved fields array\n * @returns Reconciled fields with preserved injectors where applicable\n */\nexport function reconcileFields(prev: ResolvedField[], curr: ResolvedField[]): ResolvedField[] {\n const prevMap = new Map(prev.map((f) => [f.key, f]));\n\n return curr.map((field) => {\n const existing = prevMap.get(field.key);\n\n if (existing && existing.component === field.component && existing.injector === field.injector) {\n if (existing.fieldDef === field.fieldDef) {\n // Truly unchanged - preserve object identity for signal stability\n return existing;\n }\n\n // Same component instance + injector, but the field definition changed.\n // Preserve component identity (so ngComponentOutlet keeps the same instance)\n // while swapping in the freshly-mapped inputs/renderReady/hidden signals\n // so prop updates (e.g. select options) reach the component.\n return {\n ...existing,\n fieldDef: field.fieldDef,\n inputs: field.inputs,\n renderReady: field.renderReady,\n hidden: field.hidden,\n };\n }\n\n // New field, type changed, or context changed (new injector) - use new field\n return field;\n });\n}\n\n/**\n * Creates an RxJS pipe for resolving field definitions to rendered components.\n * Used by container components (page, group) to resolve their child fields.\n */\nexport function createFieldResolutionPipe(getContext: () => ResolveFieldContext): OperatorFunction<FieldDef<unknown>[], ResolvedField[]> {\n return pipe(\n switchMap((fields) => {\n if (!fields || fields.length === 0) {\n return of([] as (ResolvedField | undefined)[]);\n }\n const context = getContext();\n return forkJoin(fields.map((f) => resolveField(f, context)));\n }),\n map((fields) => fields.filter((f): f is ResolvedField => f !== undefined)),\n scan(reconcileFields, [] as ResolvedField[]),\n );\n}\n","import { detectFormMode, FormModeDetectionResult, isPageField } from '@ng-forge/dynamic-forms/internal';\nimport { RegisteredFieldTypes } from '@ng-forge/dynamic-forms/internal';\nimport { validatePageNesting } from '@ng-forge/dynamic-forms/internal';\nimport { DynamicFormError } from '@ng-forge/dynamic-forms/internal';\n\n/**\n * Comprehensive form configuration validator that checks:\n * 1. Form mode consistency (paged vs non-paged)\n * 2. Page nesting rules\n * 3. Field placement constraints\n */\nexport class FormModeValidator {\n /**\n * Validates a form configuration and returns detailed validation results\n *\n * @param fields The form field definitions to validate\n * @returns Validation result with mode detection and error details\n */\n static validateFormConfiguration<TFields extends RegisteredFieldTypes[]>(fields: TFields): FormConfigurationValidationResult {\n const modeDetection = detectFormMode(fields);\n const additionalErrors: string[] = [];\n\n // Additional validation for paged forms\n if (modeDetection.mode === 'paged' && modeDetection.isValid) {\n // Validate each page field individually\n for (let i = 0; i < fields.length; i++) {\n const field = fields[i];\n if (isPageField(field)) {\n if (!validatePageNesting(field)) {\n additionalErrors.push(\n `Page field at index ${i} (key: \"${field.key || 'unknown'}\") contains nested page fields, which is not allowed.`,\n );\n }\n }\n }\n }\n\n const allErrors = [...modeDetection.errors, ...additionalErrors];\n const isFullyValid = modeDetection.isValid && additionalErrors.length === 0;\n\n return {\n mode: modeDetection.mode,\n isValid: isFullyValid,\n errors: allErrors,\n warnings: this.generateWarnings(fields, modeDetection),\n };\n }\n\n /**\n * Validates form configuration and throws an error if invalid\n *\n * @param fields The form field definitions to validate\n * @throws Error with detailed validation messages if form is invalid\n */\n static validateFormConfigurationOrThrow<TFields extends RegisteredFieldTypes[]>(fields: TFields): void {\n const result = this.validateFormConfiguration(fields);\n\n if (!result.isValid) {\n const errorMessage = [`Invalid form configuration (${result.mode} mode):`, ...result.errors.map((error) => ` - ${error}`)].join(\n '\\n',\n );\n\n throw new DynamicFormError(errorMessage);\n }\n }\n\n /**\n * Generates helpful warnings for form configurations\n *\n * @param fields The form field definitions\n * @param modeDetection The mode detection result\n * @returns Array of warning messages\n */\n private static generateWarnings<TFields extends RegisteredFieldTypes[]>(\n fields: TFields,\n modeDetection: FormModeDetectionResult,\n ): string[] {\n const warnings: string[] = [];\n\n // Warn about single page forms\n if (modeDetection.mode === 'paged' && fields.length === 1) {\n warnings.push('Single page form detected. Consider using non-paged mode for better performance if page navigation is not needed.');\n }\n\n // Warn about empty pages\n if (modeDetection.mode === 'paged') {\n for (let i = 0; i < fields.length; i++) {\n const field = fields[i];\n if (isPageField(field)) {\n if (!field.fields || field.fields.length === 0) {\n warnings.push(`Page field at index ${i} (key: \"${field.key || 'unknown'}\") contains no fields and will render as empty.`);\n }\n }\n }\n }\n\n return warnings;\n }\n}\n\n/** Extended validation result including warnings */\nexport interface FormConfigurationValidationResult extends FormModeDetectionResult {\n /** Array of warning messages that don't prevent form operation */\n warnings: string[];\n}\n\n/**\n * Convenience function for quick form mode validation\n *\n * @param fields The form field definitions to validate\n * @returns True if form configuration is valid, false otherwise\n */\nexport function isValidFormConfiguration<TFields extends RegisteredFieldTypes[]>(fields: TFields): boolean {\n return FormModeValidator.validateFormConfiguration(fields).isValid;\n}\n","import { inject, InjectionToken, Type } from '@angular/core';\nimport { DynamicFormError } from '@ng-forge/dynamic-forms/internal';\nimport { FIELD_REGISTRY, FieldTypeDefinition } from '@ng-forge/dynamic-forms/internal';\nimport { resolveDefaultExport } from '../wrapper-chain/wrapper-chain';\n\n/**\n * Cache for resolved field type components.\n *\n * @internal\n */\nexport const COMPONENT_CACHE = new InjectionToken<Map<string, Type<unknown>>>('COMPONENT_CACHE', {\n providedIn: 'root',\n factory: () => new Map(),\n});\n\n/**\n * Injection function for accessing the dynamic form field registry.\n *\n * @returns Object with methods for field registry interaction\n * @Component({...})\n * export class MyComponent {\n * private fieldRegistry = injectFieldRegistry();\n *\n * async loadCustomField() {\n * if (this.fieldRegistry.hasType('custom-input')) {\n * const component = await this.fieldRegistry.loadTypeComponent('custom-input');\n * // Use component...\n * }\n * }\n * }\n * ```\n * @Injectable()\n * export class FormBuilderService {\n * private fieldRegistry = injectFieldRegistry();\n *\n * getAvailableFieldTypes(): string[] {\n * return this.fieldRegistry.getTypes().map(type => type.name);\n * }\n * }\n * ```\n */\nexport function injectFieldRegistry() {\n const registry = inject(FIELD_REGISTRY);\n const componentCache = inject(COMPONENT_CACHE);\n\n return {\n /**\n * Retrieves a field type definition by its registered name.\n *\n * @param name - The registered name of the field type\n * @returns The field type definition if found, undefined otherwise\n */\n getType(name: string): FieldTypeDefinition | undefined {\n return registry.get(name);\n },\n\n /**\n * Checks if a field type is registered in the registry.\n *\n * @param name - The name of the field type to check\n * @returns True if the field type exists, false otherwise\n */\n hasType(name: string): boolean {\n return registry.has(name);\n },\n\n /**\n * Loads a field type component with support for lazy loading.\n *\n * @param name - The name of the field type to load\n * @returns Promise resolving to the component constructor, or undefined for componentless fields\n * @throws {Error} When field type is not registered or loading fails\n */\n async loadTypeComponent(name: string): Promise<Type<unknown> | undefined> {\n const fieldType = registry.get(name);\n\n if (!fieldType) {\n throw new DynamicFormError(`Field type \"${name}\" is not registered`);\n }\n\n // Componentless field types (e.g., hidden) don't have loadComponent\n if (!fieldType.loadComponent) {\n return undefined;\n }\n\n // Check cache first for instant resolution\n const cached = componentCache.get(name);\n if (cached) {\n return cached;\n }\n\n try {\n const component = resolveDefaultExport(await fieldType.loadComponent());\n\n if (component) {\n componentCache.set(name, component);\n }\n\n return component;\n } catch (error) {\n throw new DynamicFormError(`Failed to load component for field type \"${name}\": ${error}`);\n }\n },\n\n /**\n * Returns a previously loaded component from cache, or undefined if not yet loaded.\n *\n * @param name - The name of the field type\n * @returns The cached component constructor, or undefined\n */\n getLoadedComponent(name: string): Type<unknown> | undefined {\n return componentCache.get(name);\n },\n\n /**\n * Gets all registered field type definitions.\n *\n * @returns Array of all field type definitions in the registry\n */\n getTypes(): FieldTypeDefinition[] {\n return Array.from(registry.values());\n },\n\n /**\n * Registers multiple field types at once.\n *\n * @param types - Array of field type definitions to register\n */\n registerTypes(types: FieldTypeDefinition[]): void {\n types.forEach((type) => {\n registry.set(type.name, type);\n });\n },\n\n /**\n * Provides direct access to the underlying registry Map.\n *\n * @returns The raw Map containing field type registrations\n */\n get raw() {\n return registry;\n },\n };\n}\n","import { FieldDef } from '@ng-forge/dynamic-forms/internal';\nimport { FieldWithValidation } from '@ng-forge/dynamic-forms/internal';\nimport { FieldTypeDefinition, getFieldValueHandling } from '@ng-forge/dynamic-forms/internal';\nimport { hasChildFields, isGroupField } from '@ng-forge/dynamic-forms/internal';\nimport { normalizeFieldsArray } from '@ng-forge/dynamic-forms/internal';\nimport { DynamicFormError } from '@ng-forge/dynamic-forms/internal';\nimport { Logger } from '@ng-forge/dynamic-forms/internal';\n\n/** Data collected during a single config traversal. */\ninterface ConfigTraversalData {\n /**\n * Dot-scoped paths (e.g. `'address.street'`) — checked for form-value uniqueness:\n * two leaves landing at the same form-value path are a real conflict.\n */\n keys: string[];\n /**\n * Underscore-projected DOM-ID paths (e.g. `'address_street'`) — checked for\n * DOM `id` / `data-testid` uniqueness. Catches the otherwise-silent collision\n * where a top-level key like `'foo_bar'` would render the same DOM id as a\n * leaf `'bar'` inside group `'foo'`.\n */\n domIds: string[];\n types: Set<string>;\n regexErrors: string[];\n /** Own keys of value-bearing fields containing a dot (unsupported — nest via group fields). */\n dottedKeys: string[];\n}\n\n/**\n * Collects all field keys, types, and validates regex patterns from a field tree\n * by recursively traversing containers (page, row, group, array).\n *\n * @param collectKeys - When false, the entire subtree skips key collection (used\n * when descending into an array container).\n */\nfunction collectFieldData(\n fields: FieldDef<unknown>[],\n data: ConfigTraversalData,\n registry: Map<string, FieldTypeDefinition>,\n pathPrefix: string,\n collectKeys = true,\n): void {\n for (const field of fields) {\n // Defensively skip only known non-include modes — unexpected/missing\n // valueHandling defaults back to participating in duplicate detection so a\n // mis-registered field type doesn't silently drop from collision checks.\n const valueHandling = field.type ? getFieldValueHandling(field.type, registry) : 'include';\n const participatesInValue = valueHandling !== 'exclude' && valueHandling !== 'flatten';\n\n // Dotted keys never nest (checked regardless of collectKeys, to cover array templates).\n if (field.key && field.key.includes('.') && participatesInValue) {\n data.dottedKeys.push(field.key);\n }\n\n if (collectKeys && field.key && participatesInValue) {\n const scopedKey = pathPrefix ? `${pathPrefix}.${field.key}` : field.key;\n data.keys.push(scopedKey);\n // Project to the DOM-ID format (dots → underscores) so we can detect\n // collisions like top-level `'foo_bar'` vs group `'foo'` + leaf `'bar'`.\n data.domIds.push(scopedKey.replace(/\\./g, '_'));\n }\n if (field.type) {\n data.types.add(field.type);\n }\n\n const fieldWithValidation = field as FieldWithValidation;\n if ('pattern' in field && typeof fieldWithValidation.pattern === 'string') {\n validateRegexPattern(fieldWithValidation.pattern, field.key || '<unknown>', data.regexErrors);\n }\n\n if ('validators' in field && fieldWithValidation.validators) {\n for (const validator of fieldWithValidation.validators) {\n if (validator.type === 'pattern' && 'value' in validator && typeof validator.value === 'string') {\n validateRegexPattern(validator.value, field.key || '<unknown>', data.regexErrors);\n }\n }\n }\n\n if (hasChildFields(field)) {\n // Array item fields share template keys across items by design — stop collecting keys\n // once inside an array. Types and regex patterns are still validated.\n const childCollectKeys = collectKeys && field.type !== 'array';\n // Only group containers extend the scoping path — page/row flatten into the parent\n // scope, and array items use dynamic indices that aren't statically knowable.\n const childPrefix = isGroupField(field) && field.key ? (pathPrefix ? `${pathPrefix}.${field.key}` : field.key) : pathPrefix;\n const children = normalizeFieldsArray(field.fields) as (FieldDef<unknown> | FieldDef<unknown>[])[];\n for (const child of children) {\n if (Array.isArray(child)) {\n collectFieldData(child as FieldDef<unknown>[], data, registry, childPrefix, childCollectKeys);\n } else {\n collectFieldData([child], data, registry, childPrefix, childCollectKeys);\n }\n }\n }\n }\n}\n\n/** Validates a single regex pattern string, collecting errors rather than throwing. */\nfunction validateRegexPattern(pattern: string, fieldKey: string, errors: string[]): void {\n try {\n new RegExp(pattern);\n } catch (e) {\n errors.push(`Invalid regex pattern in validator for field '${fieldKey}': '${pattern}' — ${e instanceof Error ? e.message : String(e)}`);\n }\n}\n\n/**\n * Validates that no duplicate field keys exist in the config.\n * Throws a DynamicFormError listing all duplicates if any are found.\n *\n * @throws {DynamicFormError} When duplicate keys are detected\n */\nfunction validateNoDuplicateKeys(allKeys: string[]): void {\n const seen = new Set<string>();\n const duplicates = new Set<string>();\n\n for (const key of allKeys) {\n if (seen.has(key)) {\n duplicates.add(key);\n }\n seen.add(key);\n }\n\n if (duplicates.size > 0) {\n const duplicateList = Array.from(duplicates)\n .map((k) => `'${k}'`)\n .join(', ');\n throw new DynamicFormError(\n `Duplicate field keys detected: ${duplicateList}. Field keys must be unique within their group scope (the same key may appear inside different groups).`,\n );\n }\n}\n\n/**\n * Validates that the underscore-projected DOM-ID namespace has no collisions.\n *\n * @throws {DynamicFormError} When two scoped paths project to the same DOM ID\n */\nfunction validateNoDomIdCollisions(domIds: string[]): void {\n const seen = new Set<string>();\n const duplicates = new Set<string>();\n\n for (const id of domIds) {\n if (seen.has(id)) {\n duplicates.add(id);\n }\n seen.add(id);\n }\n\n if (duplicates.size > 0) {\n const duplicateList = Array.from(duplicates)\n .map((k) => `'${k}'`)\n .join(', ');\n throw new DynamicFormError(\n `DOM id collision detected for: ${duplicateList}. Group-nested keys are joined with '_' to form DOM ids ` +\n `(e.g. group 'foo' + leaf 'bar' renders as id='foo_bar'). A separate top-level key like 'foo_bar' or a different ` +\n `group/leaf pair that underscores to the same string will produce duplicate ids. Rename one of the colliding keys.`,\n );\n }\n}\n\n/**\n * Validates that every field type referenced in the config exists in the registry.\n * Logs a warning for unregistered types — unknown fields are skipped during rendering\n * rather than blocking the whole form (graceful degradation).\n */\nfunction validateFieldTypesRegistered(allTypes: Set<string>, registry: Map<string, FieldTypeDefinition>, logger: Logger): void {\n if (registry.size === 0) return;\n\n const unregistered: string[] = [];\n\n for (const type of allTypes) {\n if (!registry.has(type)) {\n unregistered.push(type);\n }\n }\n\n if (unregistered.length > 0) {\n const typeList = unregistered.map((t) => `'${t}'`).join(', ');\n logger.warn(\n `Unknown field type(s): ${typeList}. Register them via provideDynamicForm(...withXxxFields()) or a custom registry entry. These fields will be skipped during rendering.`,\n );\n }\n}\n\n/**\n * Validates a form config at bootstrap time, checking for:\n * - Duplicate field keys (throws — this is always a developer error)\n * - Unregistered field types (warns — form degrades gracefully, skipping unknown fields)\n * - Invalid regex patterns in validators (throws — invalid regex will cause runtime errors)\n *\n * @throws {DynamicFormError} When duplicate keys or invalid regex patterns are detected\n */\nexport function validateFormConfig(fields: FieldDef<unknown>[], registry: Map<string, FieldTypeDefinition>, logger: Logger): void {\n const data: ConfigTraversalData = {\n keys: [],\n domIds: [],\n types: new Set<string>(),\n regexErrors: [],\n dottedKeys: [],\n };\n\n collectFieldData(fields, data, registry, '');\n\n if (data.dottedKeys.length > 0) {\n const list = Array.from(new Set(data.dottedKeys))\n .map((k) => `'${k}'`)\n .join(', ');\n throw new DynamicFormError(\n `Field key(s) contain a dot: ${list}. Dotted keys are not supported — express nesting with 'group' fields ` +\n `(e.g. { type: 'group', key: 'address', fields: [{ type: 'input', key: 'city' }] }).`,\n );\n }\n\n validateNoDuplicateKeys(data.keys);\n validateNoDomIdCollisions(data.domIds);\n validateFieldTypesRegistered(data.types, registry, logger);\n\n if (data.regexErrors.length > 0) {\n throw new DynamicFormError(\n data.regexErrors.length === 1\n ? data.regexErrors[0]\n : `Invalid regex pattern(s) found:\\n${data.regexErrors.map((e) => ` - ${e}`).join('\\n')}`,\n );\n }\n}\n","import { AnyAddon } from '@ng-forge/dynamic-forms/internal';\nimport { AddonTypeDefinition } from '@ng-forge/dynamic-forms/internal';\nimport { FieldDef } from '@ng-forge/dynamic-forms/internal';\nimport { FieldTypeDefinition } from '@ng-forge/dynamic-forms/internal';\nimport { AddonWarning } from './addon-warning';\n\n/**\n * Walk a field's addons; return the survivors plus a list of warnings for\n * anything dropped.\n *\n * @param field The field to validate.\n * @param fieldRegistry Snapshot of `FIELD_REGISTRY` map.\n * @param typeRegistry Snapshot of `ADDON_TYPE_REGISTRY` map.\n * @param source Whether the config originated from JSON (lenient drop\n * of code-only types) or inline TypeScript.\n */\nexport function validateFieldAddons(\n field: FieldDef<unknown>,\n fieldRegistry: ReadonlyMap<string, FieldTypeDefinition>,\n typeRegistry: ReadonlyMap<string, AddonTypeDefinition>,\n source: 'json' | 'inline',\n): { addons: AnyAddon[]; warnings: AddonWarning[] } {\n const warnings: AddonWarning[] = [];\n const incoming = field.addons;\n if (!incoming?.length) {\n return { addons: [], warnings };\n }\n\n const fieldType = field.type;\n const fieldDef = fieldRegistry.get(fieldType);\n if (!fieldDef) {\n warnings.push({ type: 'unknown-field-type', fieldKey: field.key, fieldType });\n return { addons: [], warnings };\n }\n\n const addonSupport = fieldDef.addons;\n if (!addonSupport) {\n warnings.push({ type: 'field-type-no-addon-support', fieldKey: field.key, fieldType });\n return { addons: [], warnings };\n }\n\n const allowedSlots = addonSupport.slots;\n const allowedTypes = addonSupport.allowedTypes;\n const registeredTypes = Array.from(typeRegistry.keys());\n\n const survivors: AnyAddon[] = [];\n for (const addon of incoming) {\n // Slot allowed?\n if (!allowedSlots.includes(addon.slot)) {\n warnings.push({\n type: 'unknown-slot',\n fieldKey: field.key,\n fieldType,\n slot: String(addon.slot),\n allowedSlots,\n });\n continue;\n }\n\n // Type registered? `AnyAddon.type` is already a string-literal union via\n // DynamicFormAddonRegistry; no need to cast.\n const typeDef = typeRegistry.get(addon.type);\n if (!typeDef) {\n warnings.push({\n type: 'unknown-type',\n fieldKey: field.key,\n addonType: addon.type,\n registeredTypes,\n });\n continue;\n }\n\n // Type whitelisted (if `allowedTypes` was set)?\n if (allowedTypes && !allowedTypes.includes(typeDef.type)) {\n warnings.push({\n type: 'type-not-allowed',\n fieldKey: field.key,\n fieldType,\n addonType: typeDef.type,\n allowedTypes,\n });\n continue;\n }\n\n // Code-only type dropped from JSON-source configs. A type opts out of\n // JSON-safety via `AddonTypeDefinition.jsonSafe: false` (defaults to true).\n if (source === 'json' && typeDef.jsonSafe === false) {\n warnings.push({\n type: 'code-only-type-in-json',\n fieldKey: field.key,\n addonType: typeDef.type,\n });\n continue;\n }\n\n // Multi-set XOR + JSON-source function stripping. Both runs operate on\n // a working copy via `ensureCopy()` so the original config isn't\n // mutated — the survivor pushed at the bottom is either the original\n // addon (no strips) or the sanitized copy.\n let sanitized = addon as unknown as Record<string, unknown>;\n let stripped = false;\n const ensureCopy = () => {\n if (!stripped) sanitized = { ...sanitized };\n stripped = true;\n };\n\n // Snapshot which click variants the original config declared BEFORE\n // any stripping. Computing variants AFTER function-strip would\n // underreport the multi-variant warning (`action` would silently\n // vanish from the reported list).\n const declaredVariants = (['preset', 'actionRef', 'action'] as const).filter((k) => sanitized[k] !== undefined);\n\n // JSON-source function strip — inline `action`/`hidden`/`disabled`/\n // `loading` functions can't round-trip through serialization, so they\n // get dropped here with a warning. Inline-source configs keep them.\n if (source === 'json') {\n const dropReactiveFn = (key: 'hidden' | 'disabled' | 'loading') => {\n if (typeof sanitized[key] === 'function') {\n ensureCopy();\n delete sanitized[key];\n warnings.push({\n type: 'code-only-action-in-json',\n fieldKey: field.key,\n reason: `function '${key}' on type '${typeDef.type}' (use a signal/observable in code, or omit in JSON)`,\n });\n }\n };\n if (typeof sanitized.action === 'function') {\n ensureCopy();\n delete sanitized.action;\n warnings.push({\n type: 'code-only-action-in-json',\n fieldKey: field.key,\n reason: `inline 'action' function on type '${typeDef.type}'`,\n });\n }\n dropReactiveFn('hidden');\n dropReactiveFn('disabled');\n dropReactiveFn('loading');\n }\n\n // Multi-set XOR — applies to BOTH inline and JSON sources. TypeScript\n // discriminated unions already reject multi-set at compile time, but\n // configs that bypass type-checking (loose `any`, runtime composition,\n // JSON parsing) still need defence-in-depth. Keep the first variant,\n // strip the rest; mirrors the runtime precedence\n // (preset > actionRef > action) so the chosen handler matches what\n // the dispatcher would have picked.\n if (declaredVariants.length > 1) {\n ensureCopy();\n for (const k of declaredVariants.slice(1)) delete sanitized[k];\n warnings.push({\n type: 'code-only-action-in-json',\n fieldKey: field.key,\n reason: `multiple click variants on type '${typeDef.type}' (${declaredVariants.join(', ')}); kept '${declaredVariants[0]}'`,\n });\n }\n\n if (stripped) {\n survivors.push(sanitized as unknown as AnyAddon);\n continue;\n }\n\n // Per-type shape validator (throws on violation).\n if (typeDef.validate) {\n try {\n typeDef.validate(addon, field.key);\n } catch (error) {\n warnings.push({\n type: 'shape-violation',\n fieldKey: field.key,\n addonType: typeDef.type,\n reason: error instanceof Error ? error.message : String(error),\n });\n continue;\n }\n }\n\n survivors.push(addon);\n }\n\n return { addons: survivors, warnings };\n}\n\n/**\n * Recursively walk a field tree, validating each field's addons. Returns a\n * sanitized copy of the input field array plus accumulated warnings.\n */\nexport function walkAndValidateAddons(\n fields: readonly FieldDef<unknown>[],\n fieldRegistry: ReadonlyMap<string, FieldTypeDefinition>,\n typeRegistry: ReadonlyMap<string, AddonTypeDefinition>,\n source: 'json' | 'inline',\n): { fields: FieldDef<unknown>[]; warnings: AddonWarning[] } {\n const allWarnings: AddonWarning[] = [];\n const sanitized = fields.map((field) => {\n let next: FieldDef<unknown> = field;\n\n if (field.addons?.length) {\n const { addons, warnings } = validateFieldAddons(field, fieldRegistry, typeRegistry, source);\n allWarnings.push(...warnings);\n next = { ...next, addons };\n }\n\n // Recurse into containers — every container field type carries its\n // children on a `fields` array.\n const children = (next as { fields?: unknown }).fields;\n if (Array.isArray(children)) {\n const { fields: visited, warnings } = walkAndValidateAddons(\n children as readonly FieldDef<unknown>[],\n fieldRegistry,\n typeRegistry,\n source,\n );\n allWarnings.push(...warnings);\n next = { ...next, fields: visited } as FieldDef<unknown>;\n }\n\n return next;\n });\n\n return { fields: sanitized, warnings: allWarnings };\n}\n","/** Reason an addon was dropped during validation. */\nexport type AddonWarning =\n | { type: 'unknown-field-type'; fieldKey: string; fieldType: string }\n | { type: 'field-type-no-addon-support'; fieldKey: string; fieldType: string }\n | { type: 'unknown-slot'; fieldKey: string; fieldType: string; slot: string; allowedSlots: readonly string[] }\n | { type: 'unknown-type'; fieldKey: string; addonType: string; registeredTypes: readonly string[] }\n | { type: 'type-not-allowed'; fieldKey: string; fieldType: string; addonType: string; allowedTypes: readonly string[] }\n | { type: 'shape-violation'; fieldKey: string; addonType: string; reason: string }\n | { type: 'code-only-type-in-json'; fieldKey: string; addonType: string }\n | { type: 'code-only-action-in-json'; fieldKey: string; reason: string };\n\n/**\n * Stable structural fingerprint for an `AddonWarning`, used by the\n * FormStateManager dedup cache. Includes every distinguishing field\n * (`type`, `fieldKey`, `addonType`/`slot`/`reason` as relevant) so warnings\n * for different fields or different addon types never collapse to one log\n * line — independent of the human-readable format which may change.\n */\nexport function addonWarningKey(w: AddonWarning): string {\n switch (w.type) {\n case 'unknown-field-type':\n return `${w.type}|${w.fieldKey}|${w.fieldType}`;\n case 'field-type-no-addon-support':\n return `${w.type}|${w.fieldKey}|${w.fieldType}`;\n case 'unknown-slot':\n return `${w.type}|${w.fieldKey}|${w.fieldType}|${w.slot}`;\n case 'unknown-type':\n return `${w.type}|${w.fieldKey}|${w.addonType}`;\n case 'type-not-allowed':\n return `${w.type}|${w.fieldKey}|${w.fieldType}|${w.addonType}`;\n case 'shape-violation':\n return `${w.type}|${w.fieldKey}|${w.addonType}|${w.reason}`;\n case 'code-only-type-in-json':\n return `${w.type}|${w.fieldKey}|${w.addonType}`;\n case 'code-only-action-in-json':\n return `${w.type}|${w.fieldKey}|${w.reason}`;\n }\n}\n\n/** Render an `AddonWarning` to a developer-friendly message. */\nexport function formatAddonWarning(w: AddonWarning): string {\n switch (w.type) {\n case 'unknown-field-type':\n return `Field '${w.fieldKey}': unknown field type '${w.fieldType}'. Addons dropped.`;\n case 'field-type-no-addon-support':\n return (\n `Field '${w.fieldKey}' (type: '${w.fieldType}'): this field type does not declare addon support. Addons dropped. ` +\n `If this is incorrect, add 'addons: { slots: [...] }' to the field type registration.`\n );\n case 'unknown-slot':\n return (\n `Field '${w.fieldKey}' (type: '${w.fieldType}'): slot '${w.slot}' is not supported. ` +\n `Allowed: ${w.allowedSlots.join(', ')}. Addon dropped.`\n );\n case 'unknown-type':\n return (\n `Field '${w.fieldKey}': addon type '${w.addonType}' is not registered. ` +\n `Did you forget withCustomAddon({ type: '${w.addonType}', ... })? ` +\n `Currently registered: ${w.registeredTypes.join(', ') || '(none)'}. Addon dropped.`\n );\n case 'type-not-allowed':\n return (\n `Field '${w.fieldKey}' (type: '${w.fieldType}'): addon type '${w.addonType}' is not in the allowed list. ` +\n `Allowed: ${w.allowedTypes.join(', ')}. Addon dropped.`\n );\n case 'shape-violation':\n return `Field '${w.fieldKey}': addon type '${w.addonType}' shape invalid: ${w.reason}. Addon dropped.`;\n case 'code-only-type-in-json':\n return `Field '${w.fieldKey}': addon type '${w.addonType}' is code-only and cannot be used with JSON-derived configs. Addon dropped.`;\n case 'code-only-action-in-json':\n return `Field '${w.fieldKey}': ${w.reason}. Use 'preset' or 'actionRef' instead. Addon dropped.`;\n }\n}\n","import { InjectionToken } from '@angular/core';\nimport { ResolvedValueExclusionConfig } from '@ng-forge/dynamic-forms/internal';\n\n/**\n * Injection token for global value exclusion defaults.\n *\n * @internal\n */\nexport const VALUE_EXCLUSION_DEFAULTS = new InjectionToken<ResolvedValueExclusionConfig>('VALUE_EXCLUSION_DEFAULTS', {\n providedIn: 'root',\n factory: () => ({\n excludeValueIfHidden: true,\n excludeValueIfDisabled: true,\n excludeValueIfReadonly: true,\n }),\n});\n","import { FieldTree } from '@angular/forms/signals';\nimport { FieldDef } from '@ng-forge/dynamic-forms/internal';\nimport { isGroupField } from '@ng-forge/dynamic-forms/internal';\nimport { isArrayField } from '@ng-forge/dynamic-forms/internal';\nimport { FieldTypeDefinition, getFieldValueHandling } from '@ng-forge/dynamic-forms/internal';\nimport { ResolvedValueExclusionConfig, ValueExclusionConfig } from '@ng-forge/dynamic-forms/internal';\n\n/**\n * Resolves the effective exclusion config for a field using the 3-tier hierarchy:\n * Field > Form > Global. For each property, the most specific defined value wins.\n *\n * @param global - Global defaults from VALUE_EXCLUSION_DEFAULTS token\n * @param form - Form-level overrides from FormOptions (may be undefined per property)\n * @param field - Field-level overrides from FieldDef (may be undefined per property)\n * @returns Fully resolved config with all properties defined\n */\nexport function resolveExclusionConfig(\n global: ResolvedValueExclusionConfig,\n form: ValueExclusionConfig | undefined,\n field: ValueExclusionConfig | undefined,\n): ResolvedValueExclusionConfig {\n return {\n excludeValueIfHidden: field?.excludeValueIfHidden ?? form?.excludeValueIfHidden ?? global.excludeValueIfHidden,\n excludeValueIfDisabled: field?.excludeValueIfDisabled ?? form?.excludeValueIfDisabled ?? global.excludeValueIfDisabled,\n excludeValueIfReadonly: field?.excludeValueIfReadonly ?? form?.excludeValueIfReadonly ?? global.excludeValueIfReadonly,\n };\n}\n\n/**\n * Static exclusion state a field may have inherited from a discarded flatten\n * container (page/row) ancestor during flattening. See `FlattenedField`.\n */\ninterface FieldWithInheritedExclusion {\n readonly inheritedExclusionState?: {\n readonly hidden?: boolean;\n readonly disabled?: boolean;\n readonly readonly?: boolean;\n };\n}\n\n/**\n * Determines whether a field's value should be excluded based on its reactive state,\n * the field def's static state (for fields whose `state.hidden()` isn't wired — e.g. groups,\n * or state inherited from a hidden page/row container that was flattened away), and the\n * resolved exclusion config.\n */\nfunction shouldExcludeField(field: FieldDef<unknown>, fieldState: FieldTree<unknown>, config: ResolvedValueExclusionConfig): boolean {\n const state = fieldState();\n const inherited = (field as FieldWithInheritedExclusion).inheritedExclusionState;\n\n const isHidden = state.hidden() || field.hidden === true || inherited?.hidden === true;\n if (config.excludeValueIfHidden && isHidden) {\n return true;\n }\n\n const isDisabled = state.disabled() || field.disabled === true || inherited?.disabled === true;\n if (config.excludeValueIfDisabled && isDisabled) {\n return true;\n }\n\n const isReadonly = state.readonly() || field.readonly === true || inherited?.readonly === true;\n if (config.excludeValueIfReadonly && isReadonly) {\n return true;\n }\n\n return false;\n}\n\n/**\n * Filters form values based on field reactive state (hidden, disabled, readonly)\n * and the 3-tier exclusion config hierarchy.\n *\n * @param rawValue - The unfiltered form value object\n * @param schemaFields - Flattened schema fields from FormSetup (groups/arrays preserved, pages/rows unwrapped)\n * @param formTree - The Angular Signal Forms FieldTree, accessed as `formInstance[key]`\n * @param registry - Field type registry for valueHandling mode lookup\n * @param globalDefaults - Global exclusion defaults from VALUE_EXCLUSION_DEFAULTS\n * @param formOptions - Form-level exclusion overrides from FormOptions\n * @returns Filtered value with excluded field values omitted\n */\nexport function filterFormValue<T extends Record<string, unknown>>(\n rawValue: T,\n schemaFields: FieldDef<unknown>[],\n formTree: Record<string, FieldTree<unknown>>,\n registry: Map<string, FieldTypeDefinition>,\n globalDefaults: ResolvedValueExclusionConfig,\n formOptions: ValueExclusionConfig | undefined,\n): Partial<T> {\n const result: Record<string, unknown> = {};\n\n for (const field of schemaFields) {\n const key = field.key;\n if (!key) continue;\n\n // Skip fields that don't contribute to form values\n const valueHandling = getFieldValueHandling(field.type, registry);\n if (valueHandling === 'exclude' || valueHandling === 'flatten') continue;\n\n // If the key doesn't exist in the raw value, skip\n if (!(key in rawValue)) continue;\n\n // Resolve per-field exclusion config\n const fieldExclusion: ValueExclusionConfig = {\n excludeValueIfHidden: field.excludeValueIfHidden,\n excludeValueIfDisabled: field.excludeValueIfDisabled,\n excludeValueIfReadonly: field.excludeValueIfReadonly,\n };\n const resolvedConfig = resolveExclusionConfig(globalDefaults, formOptions, fieldExclusion);\n\n // Componentless field (no FieldTree) — decide from the static field def alone, then include if not excluded.\n const fieldState = formTree[key] as FieldTree<unknown> | undefined;\n if (!fieldState || typeof fieldState !== 'function') {\n const inherited = (field as FieldWithInheritedExclusion).inheritedExclusionState;\n const staticallyExcluded =\n (resolvedConfig.excludeValueIfHidden && (field.hidden === true || inherited?.hidden === true)) ||\n (resolvedConfig.excludeValueIfDisabled && (field.disabled === true || inherited?.disabled === true)) ||\n (resolvedConfig.excludeValueIfReadonly && (field.readonly === true || inherited?.readonly === true));\n if (!staticallyExcluded) {\n result[key] = rawValue[key];\n }\n continue;\n }\n\n // Check if this field should be excluded based on its state\n if (shouldExcludeField(field, fieldState, resolvedConfig)) {\n continue;\n }\n\n // Handle group fields: recurse into nested structure\n if (isGroupField(field) && field.fields) {\n const groupValue = rawValue[key] as Record<string, unknown> | undefined;\n if (groupValue && typeof groupValue === 'object') {\n const childFields = Object.values(field.fields) as FieldDef<unknown>[];\n const nestedTree = fieldState as unknown as Record<string, FieldTree<unknown>>;\n result[key] = filterFormValue(\n groupValue as Record<string, unknown>,\n childFields,\n nestedTree,\n registry,\n globalDefaults,\n formOptions,\n );\n } else {\n result[key] = groupValue;\n }\n continue;\n }\n\n // Handle array fields: if not excluded at array level, include entire array value\n // (v1: no per-item filtering)\n if (isArrayField(field)) {\n result[key] = rawValue[key];\n continue;\n }\n\n // Leaf field: include value\n result[key] = rawValue[key];\n }\n\n return result as Partial<T>;\n}\n","import { FieldDef } from '@ng-forge/dynamic-forms/internal';\nimport { FieldTypeDefinition } from '@ng-forge/dynamic-forms/internal';\nimport { FormConfig } from '@ng-forge/dynamic-forms/internal';\nimport { RegisteredFieldTypes } from '@ng-forge/dynamic-forms/internal';\nimport { FormMode } from '@ng-forge/dynamic-forms/internal';\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Discriminant constants\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Lifecycle state discriminants.\n *\n * @internal\n */\nexport const LifecycleState = {\n Uninitialized: 'uninitialized',\n Initializing: 'initializing',\n Ready: 'ready',\n Transitioning: 'transitioning',\n Destroyed: 'destroyed',\n} as const;\n\n/** Indexed access type for {@link LifecycleState} values. */\nexport type LifecycleStateType = (typeof LifecycleState)[keyof typeof LifecycleState];\n\n/**\n * Transition phase discriminants.\n *\n * @internal\n */\nexport const Phase = {\n Teardown: 'teardown',\n Applying: 'applying',\n Restoring: 'restoring',\n} as const;\n\n/** Indexed access type for {@link Phase} values. */\nexport type PhaseType = (typeof Phase)[keyof typeof Phase];\n\n/**\n * Action type discriminants.\n *\n * @internal\n */\nexport const Action = {\n Initialize: 'initialize',\n ConfigChange: 'config-change',\n SetupComplete: 'setup-complete',\n ValueCaptured: 'value-captured',\n TeardownComplete: 'teardown-complete',\n ApplyComplete: 'apply-complete',\n RestoreComplete: 'restore-complete',\n Destroy: 'destroy',\n} as const;\n\n/** Indexed access type for {@link Action} values. */\nexport type ActionType = (typeof Action)[keyof typeof Action];\n\n/**\n * Side effect type discriminants.\n *\n * @internal\n */\nexport const Effect = {\n CaptureValue: 'capture-value',\n WaitFrameBoundary: 'wait-frame-boundary',\n CreateForm: 'create-form',\n RestoreValues: 'restore-values',\n} as const;\n\n/** Indexed access type for {@link Effect} values. */\nexport type EffectType = (typeof Effect)[keyof typeof Effect];\n\n// ─────────────────────────────────────────────────────────────────────────────\n// State types\n// ─────────────────────────────────────────────────────────────────────────────\n\n/** @internal */\nexport type FormLifecycleState<TFields extends RegisteredFieldTypes[] = RegisteredFieldTypes[]> =\n | FormLifecycleUninitialized\n | FormLifecycleInitializing<TFields>\n | FormLifecycleReady<TFields>\n | FormLifecycleTransitioning<TFields>\n | FormLifecycleDestroyed;\n\nexport interface FormLifecycleUninitialized {\n readonly type: (typeof LifecycleState)['Uninitialized'];\n}\n\nexport interface FormLifecycleInitializing<TFields extends RegisteredFieldTypes[] = RegisteredFieldTypes[]> {\n readonly type: (typeof LifecycleState)['Initializing'];\n readonly config: FormConfig<TFields>;\n}\n\nexport interface FormLifecycleReady<TFields extends RegisteredFieldTypes[] = RegisteredFieldTypes[]> {\n readonly type: (typeof LifecycleState)['Ready'];\n readonly config: FormConfig<TFields>;\n readonly formSetup: FormSetup;\n}\n\nexport type TransitionPhase = PhaseType;\n\nexport interface FormLifecycleTransitioning<TFields extends RegisteredFieldTypes[] = RegisteredFieldTypes[]> {\n readonly type: (typeof LifecycleState)['Transitioning'];\n readonly phase: TransitionPhase;\n readonly currentConfig: FormConfig<TFields>;\n readonly pendingConfig: FormConfig<TFields>;\n readonly preservedValue?: Record<string, unknown>;\n /** FormSetup from the ready state that initiated this transition */\n readonly currentFormSetup: FormSetup;\n /** FormSetup computed for the pending config (set when apply-complete fires) */\n readonly pendingFormSetup?: FormSetup;\n}\n\nexport interface FormLifecycleDestroyed {\n readonly type: (typeof LifecycleState)['Destroyed'];\n}\n\n/** @internal */\nexport interface FormSetup {\n /** Fields to render (flattened for non-paged, empty for paged). */\n readonly fields: FieldDef<unknown>[];\n /** All schema fields for validation (flattened from all pages/containers). */\n readonly schemaFields: FieldDef<unknown>[];\n readonly originalFields?: FieldDef<unknown>[];\n readonly defaultValues: Record<string, unknown>;\n readonly mode: FormMode;\n readonly registry: Map<string, FieldTypeDefinition>;\n}\n\n/**\n * Actions that can be dispatched to the state machine.\n *\n * @internal\n */\nexport type FormStateAction<TFields extends RegisteredFieldTypes[] = RegisteredFieldTypes[]> =\n | InitializeAction<TFields>\n | ConfigChangeAction<TFields>\n | SetupCompleteAction\n | ValueCapturedAction\n | TeardownCompleteAction\n | ApplyCompleteAction\n | RestoreCompleteAction\n | DestroyAction;\n\nexport interface InitializeAction<TFields extends RegisteredFieldTypes[] = RegisteredFieldTypes[]> {\n readonly type: (typeof Action)['Initialize'];\n readonly config: FormConfig<TFields>;\n}\n\nexport interface ConfigChangeAction<TFields extends RegisteredFieldTypes[] = RegisteredFieldTypes[]> {\n readonly type: (typeof Action)['ConfigChange'];\n readonly config: FormConfig<TFields>;\n}\n\nexport interface SetupCompleteAction {\n readonly type: (typeof Action)['SetupComplete'];\n readonly formSetup: FormSetup;\n}\n\nexport interface ValueCapturedAction {\n readonly type: (typeof Action)['ValueCaptured'];\n readonly value: Record<string, unknown>;\n}\n\nexport interface TeardownCompleteAction {\n readonly type: (typeof Action)['TeardownComplete'];\n}\n\nexport interface ApplyCompleteAction {\n readonly type: (typeof Action)['ApplyComplete'];\n readonly formSetup: FormSetup;\n}\n\nexport interface RestoreCompleteAction {\n readonly type: (typeof Action)['RestoreComplete'];\n}\n\nexport interface DestroyAction {\n readonly type: (typeof Action)['Destroy'];\n}\n\n/**\n * State transition record for debugging and testing.\n *\n * @internal\n */\nexport interface StateTransition<TFields extends RegisteredFieldTypes[] = RegisteredFieldTypes[]> {\n readonly from: FormLifecycleState<TFields>;\n readonly to: FormLifecycleState<TFields>;\n readonly action: FormStateAction<TFields>;\n readonly timestamp: number;\n}\n\n/**\n * Result of a state transition.\n *\n * @internal\n */\nexport interface TransitionResult<TFields extends RegisteredFieldTypes[] = RegisteredFieldTypes[]> {\n readonly state: FormLifecycleState<TFields>;\n readonly sideEffects: SideEffect[];\n}\n\n/**\n * Side effects to execute after state transition.\n *\n * @internal\n */\nexport type SideEffect =\n | { readonly type: (typeof Effect)['CaptureValue'] }\n | { readonly type: (typeof Effect)['WaitFrameBoundary'] }\n | { readonly type: (typeof Effect)['CreateForm'] }\n | { readonly type: (typeof Effect)['RestoreValues']; readonly values: Record<string, unknown> };\n\n/** Error from async field component loading. */\nexport interface FieldLoadingError {\n readonly fieldType: string;\n readonly fieldKey: string;\n readonly error: Error;\n}\n\n/** @internal */\nexport function isReadyState<TFields extends RegisteredFieldTypes[]>(\n state: FormLifecycleState<TFields>,\n): state is FormLifecycleReady<TFields> {\n return state.type === LifecycleState.Ready;\n}\n\n/** @internal */\nexport function isTransitioningState<TFields extends RegisteredFieldTypes[]>(\n state: FormLifecycleState<TFields>,\n): state is FormLifecycleTransitioning<TFields> {\n return state.type === LifecycleState.Transitioning;\n}\n\n/** @internal */\nexport function createUninitializedState(): FormLifecycleUninitialized {\n return { type: LifecycleState.Uninitialized };\n}\n\n/** @internal */\nexport function createInitializingState<TFields extends RegisteredFieldTypes[]>(\n config: FormConfig<TFields>,\n): FormLifecycleInitializing<TFields> {\n return { type: LifecycleState.Initializing, config };\n}\n\n/** @internal */\nexport function createReadyState<TFields extends RegisteredFieldTypes[]>(\n config: FormConfig<TFields>,\n formSetup: FormSetup,\n): FormLifecycleReady<TFields> {\n return { type: LifecycleState.Ready, config, formSetup };\n}\n\n/** @internal */\nexport function createTransitioningState<TFields extends RegisteredFieldTypes[]>(\n phase: TransitionPhase,\n currentConfig: FormConfig<TFields>,\n pendingConfig: FormConfig<TFields>,\n currentFormSetup: FormSetup,\n preservedValue?: Record<string, unknown>,\n pendingFormSetup?: FormSetup,\n): FormLifecycleTransitioning<TFields> {\n return { type: LifecycleState.Transitioning, phase, currentConfig, pendingConfig, currentFormSetup, preservedValue, pendingFormSetup };\n}\n\n/** @internal */\nexport function createDestroyedState(): FormLifecycleDestroyed {\n return { type: LifecycleState.Destroyed };\n}\n","import { afterNextRender, DestroyRef, Injector } from '@angular/core';\nimport { Observable, Subscriber } from 'rxjs';\n\n/**\n * Options for side effect execution.\n *\n * @internal\n */\nexport interface SideEffectOptions {\n /** Injector for accessing Angular services */\n readonly injector: Injector;\n /** DestroyRef for cleanup on component destruction */\n readonly destroyRef: DestroyRef;\n}\n\n/**\n * Scheduler for managing side effect timing in the state machine.\n * Provides blocking, frame-boundary (rAF), and after-render timing.\n *\n * @internal\n */\nexport class SideEffectScheduler {\n private readonly injector: Injector;\n private readonly abortController = new AbortController();\n\n /** Aborted when the owning component is destroyed. */\n private get destroyed(): boolean {\n return this.abortController.signal.aborted;\n }\n\n constructor(options: SideEffectOptions) {\n this.injector = options.injector;\n\n options.destroyRef.onDestroy(() => {\n this.abortController.abort();\n });\n }\n\n /** Executes a synchronous blocking effect. */\n executeBlocking<T>(effect: () => T): Observable<T> {\n return new Observable((subscriber: Subscriber<T>) => {\n if (this.destroyed) {\n subscriber.complete();\n return;\n }\n\n try {\n const result = effect();\n subscriber.next(result);\n subscriber.complete();\n } catch (error) {\n subscriber.error(error);\n }\n });\n }\n\n /**\n * Defers effect to next rAF (~16ms), giving CD and the async field resolution\n * pipeline time to settle before the state machine continues.\n */\n executeAtFrameBoundary<T>(effect: () => T, options?: { skipIf?: () => boolean }): Observable<T> {\n if (options?.skipIf?.()) {\n return this.executeBlocking(effect);\n }\n\n return new Observable((subscriber: Subscriber<T>) => {\n if (this.destroyed) {\n subscriber.complete();\n return;\n }\n\n const frameId = requestAnimationFrame(() => {\n if (this.destroyed) {\n subscriber.complete();\n return;\n }\n\n try {\n const result = effect();\n subscriber.next(result);\n subscriber.complete();\n } catch (error) {\n subscriber.error(error);\n }\n });\n\n // Cleanup on unsubscribe\n return () => {\n cancelAnimationFrame(frameId);\n };\n });\n }\n\n /** Executes effect after Angular's next render cycle via `afterNextRender`. */\n executeAfterRender<T>(effect: () => T): Observable<T> {\n return new Observable((subscriber: Subscriber<T>) => {\n if (this.destroyed) {\n subscriber.complete();\n return;\n }\n\n const local = new AbortController();\n\n afterNextRender(\n () => {\n if (local.signal.aborted || this.destroyed) {\n subscriber.complete();\n return;\n }\n\n try {\n const result = effect();\n subscriber.next(result);\n subscriber.complete();\n } catch (error) {\n subscriber.error(error);\n }\n },\n { injector: this.injector },\n );\n\n // afterNextRender can't be deregistered — AbortSignal makes the callback no-op\n return () => {\n local.abort();\n };\n });\n }\n}\n\n/** @internal */\nexport function createSideEffectScheduler(injector: Injector): SideEffectScheduler {\n const destroyRef = injector.get(DestroyRef);\n return new SideEffectScheduler({ injector, destroyRef });\n}\n","import { DestroyRef, Injector, signal, Signal, untracked, WritableSignal } from '@angular/core';\nimport { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop';\nimport { catchError, concatMap, EMPTY, Observable, of, Subject } from 'rxjs';\nimport { FormConfig } from '@ng-forge/dynamic-forms/internal';\nimport { Logger } from '@ng-forge/dynamic-forms/internal';\nimport { RegisteredFieldTypes } from '@ng-forge/dynamic-forms/internal';\nimport {\n Action,\n createDestroyedState,\n createInitializingState,\n createReadyState,\n createTransitioningState,\n createUninitializedState,\n Effect,\n FormLifecycleState,\n FormSetup,\n FormStateAction,\n isReadyState,\n isTransitioningState,\n LifecycleState,\n Phase,\n SideEffect,\n StateTransition,\n TransitionResult,\n} from './state-types';\nimport { assertNever } from '@ng-forge/dynamic-forms/internal';\nimport { SideEffectScheduler } from './side-effect-scheduler';\n\n/**\n * Configuration for the form state machine.\n *\n * @internal\n */\nexport interface FormStateMachineConfig<TFields extends RegisteredFieldTypes[] = RegisteredFieldTypes[]> {\n /** Injector for Angular service access */\n readonly injector: Injector;\n /** DestroyRef for cleanup */\n readonly destroyRef: DestroyRef;\n /** Side effect scheduler for controlled timing */\n readonly scheduler: SideEffectScheduler;\n /** Callback to create form setup from config */\n readonly createFormSetup: (config: FormConfig<TFields>) => FormSetup;\n /** Callback to capture current form value */\n readonly captureValue: () => Record<string, unknown>;\n /** Callback to restore form value */\n readonly restoreValue: (values: Record<string, unknown>, validKeys: Set<string>) => void;\n /** Predicate returning true when the field pipeline has already settled (all components cached) */\n readonly isFieldPipelineSettled?: () => boolean;\n /** Signal indicating whether the form is currently submitting */\n readonly isSubmitting?: Signal<boolean>;\n /** Callback when form is created (for registration) */\n readonly onFormCreated?: (formSetup: FormSetup) => void;\n /** Logger instance for error reporting */\n readonly logger: Logger;\n /** Callback for logging/debugging state transitions */\n readonly onTransition?: (transition: StateTransition<TFields>) => void;\n /** Callback when an action fails and error recovery runs */\n readonly onError?: (error: unknown, action: FormStateAction<TFields>) => void;\n}\n\n/**\n * RxJS-based state machine for form lifecycle management.\n * Uses `concatMap` for sequential action processing.\n *\n * @internal\n */\nexport class FormStateMachine<TFields extends RegisteredFieldTypes[] = RegisteredFieldTypes[]> {\n private readonly config: FormStateMachineConfig<TFields>;\n private readonly actions$ = new Subject<FormStateAction<TFields>>();\n private readonly _state: WritableSignal<FormLifecycleState<TFields>>;\n\n /** Signal of current state - use this for deriving computed signals */\n readonly state: Signal<FormLifecycleState<TFields>>;\n\n /** Observable of current state - for RxJS interop */\n readonly state$: Observable<FormLifecycleState<TFields>>;\n\n /** Current state value (synchronous read) */\n get currentState(): FormLifecycleState<TFields> {\n return this._state();\n }\n\n constructor(config: FormStateMachineConfig<TFields>) {\n this.config = config;\n this._state = signal(createUninitializedState());\n this.state = this._state.asReadonly();\n this.state$ = toObservable(this._state, { injector: config.injector });\n\n this.setupActionProcessing();\n }\n\n /** Dispatches an action. Processed sequentially via `concatMap`. */\n dispatch(action: FormStateAction<TFields>): void {\n this.actions$.next(action);\n }\n\n /** Sets up sequential action processing via `concatMap`. */\n private setupActionProcessing(): void {\n this.actions$\n .pipe(\n concatMap((action) =>\n this.processAction(action).pipe(\n catchError((error) => {\n this.config.logger.error(`Action '${action.type}' failed:`, error);\n this.recoverFromError(error, action);\n return EMPTY;\n }),\n ),\n ),\n takeUntilDestroyed(this.config.destroyRef),\n )\n .subscribe();\n }\n\n /**\n * Processes a single action: computes next state (pure), updates `_state`,\n * then executes side effects. Effects may dispatch follow-up actions that\n * are queued via `concatMap` and go through the same transition pipeline.\n */\n private processAction(action: FormStateAction<TFields>): Observable<void> {\n const currentState = this._state();\n const result = this.computeTransition(currentState, action);\n\n this.config.onTransition?.({\n from: currentState,\n to: result.state,\n action,\n timestamp: Date.now(),\n });\n\n this._state.set(result.state);\n return this.executeSideEffects(result.sideEffects);\n }\n\n /**\n * Recovers from a failed action by reverting to the last stable state.\n * Initializing → Uninitialized, Transitioning → Ready (using current setup).\n */\n private recoverFromError(error: unknown, action: FormStateAction<TFields>): void {\n const state = this._state();\n\n if (state.type === LifecycleState.Initializing) {\n this._state.set(createUninitializedState());\n } else if (isTransitioningState(state)) {\n this._state.set(createReadyState(state.currentConfig, state.currentFormSetup));\n }\n\n this.config.onError?.(error, action);\n }\n\n /** Pure transition: current state + action → next state + side effects. */\n private computeTransition(state: FormLifecycleState<TFields>, action: FormStateAction<TFields>): TransitionResult<TFields> {\n switch (action.type) {\n case Action.Initialize:\n return this.handleInitialize(state, action.config);\n\n case Action.ConfigChange:\n return this.handleConfigChange(state, action.config);\n\n case Action.SetupComplete:\n return this.handleSetupComplete(state, action.formSetup as FormSetup);\n\n case Action.ValueCaptured:\n return this.handleValueCaptured(state, action.value);\n\n case Action.TeardownComplete:\n return this.handleTeardownComplete(state);\n\n case Action.ApplyComplete:\n return this.handleApplyComplete(state, action.formSetup as FormSetup);\n\n case Action.RestoreComplete:\n return this.handleRestoreComplete(state);\n\n case Action.Destroy:\n return this.handleDestroy();\n\n default:\n return assertNever(action);\n }\n }\n\n private handleInitialize(state: FormLifecycleState<TFields>, config: FormConfig<TFields>): TransitionResult<TFields> {\n if (state.type !== LifecycleState.Uninitialized) {\n return { state, sideEffects: [] };\n }\n\n return {\n state: createInitializingState(config),\n sideEffects: [{ type: Effect.CreateForm }],\n };\n }\n\n private handleConfigChange(state: FormLifecycleState<TFields>, config: FormConfig<TFields>): TransitionResult<TFields> {\n if (state.type === LifecycleState.Uninitialized) {\n return {\n state: createInitializingState(config),\n sideEffects: [{ type: Effect.CreateForm }],\n };\n }\n\n if (state.type === LifecycleState.Initializing) {\n return {\n state: createInitializingState(config),\n sideEffects: [{ type: Effect.CreateForm }],\n };\n }\n\n // Guard: reject config changes during active submission to prevent races\n // between the in-flight action Promise and form teardown/rebuild.\n if (this.config.isSubmitting?.()) {\n this.config.logger.warn('Config changed during active submission; ignoring. Apply config changes after submission completes.');\n return { state, sideEffects: [] };\n }\n\n if (isReadyState(state)) {\n return {\n state: createTransitioningState(Phase.Teardown, state.config, config, state.formSetup),\n sideEffects: [{ type: Effect.CaptureValue }, { type: Effect.WaitFrameBoundary }],\n };\n }\n\n if (isTransitioningState(state)) {\n // Already transitioning — update pending config (latest wins)\n return {\n state: createTransitioningState(\n state.phase,\n state.currentConfig,\n config,\n state.currentFormSetup,\n state.preservedValue,\n state.pendingFormSetup,\n ),\n sideEffects: [],\n };\n }\n\n return { state, sideEffects: [] };\n }\n\n private handleSetupComplete(state: FormLifecycleState<TFields>, formSetup: FormSetup): TransitionResult<TFields> {\n if (state.type === LifecycleState.Initializing) {\n return {\n state: createReadyState(state.config, formSetup),\n sideEffects: [],\n };\n }\n\n return { state, sideEffects: [] };\n }\n\n private handleValueCaptured(state: FormLifecycleState<TFields>, value: Record<string, unknown>): TransitionResult<TFields> {\n if (!isTransitioningState(state)) {\n return { state, sideEffects: [] };\n }\n\n return {\n state: createTransitioningState(\n state.phase,\n state.currentConfig,\n state.pendingConfig,\n state.currentFormSetup,\n value,\n state.pendingFormSetup,\n ),\n sideEffects: [],\n };\n }\n\n private handleTeardownComplete(state: FormLifecycleState<TFields>): TransitionResult<TFields> {\n if (!isTransitioningState(state) || state.phase !== Phase.Teardown) {\n return { state, sideEffects: [] };\n }\n\n return {\n state: createTransitioningState(\n Phase.Applying,\n state.currentConfig,\n state.pendingConfig,\n state.currentFormSetup,\n state.preservedValue,\n ),\n sideEffects: [{ type: Effect.CreateForm }],\n };\n }\n\n private handleApplyComplete(state: FormLifecycleState<TFields>, formSetup: FormSetup): TransitionResult<TFields> {\n if (!isTransitioningState(state) || state.phase !== Phase.Applying) {\n return { state, sideEffects: [] };\n }\n\n if (state.preservedValue && Object.keys(state.preservedValue).length > 0) {\n return {\n state: createTransitioningState(\n Phase.Restoring,\n state.currentConfig,\n state.pendingConfig,\n state.currentFormSetup,\n state.preservedValue,\n formSetup,\n ),\n sideEffects: [{ type: Effect.RestoreValues, values: state.preservedValue }],\n };\n }\n\n return {\n state: createReadyState(state.pendingConfig, formSetup),\n sideEffects: [],\n };\n }\n\n private handleRestoreComplete(state: FormLifecycleState<TFields>): TransitionResult<TFields> {\n if (!isTransitioningState(state) || state.phase !== Phase.Restoring) {\n return { state, sideEffects: [] };\n }\n\n if (!state.pendingFormSetup) {\n this.config.logger.warn(\n 'handleRestoreComplete: pendingFormSetup was not set — falling back to recomputing. ' +\n 'This indicates a bug in the transition flow.',\n );\n }\n const formSetup = state.pendingFormSetup ?? this.config.createFormSetup(state.pendingConfig);\n\n return {\n state: createReadyState(state.pendingConfig, formSetup),\n sideEffects: [],\n };\n }\n\n private handleDestroy(): TransitionResult<TFields> {\n return {\n state: createDestroyedState(),\n sideEffects: [],\n };\n }\n\n /**\n * Executes side effects in sequence. Effects may call `this.dispatch()` internally,\n * queuing follow-up actions into the same `concatMap` pipeline.\n */\n private executeSideEffects(effects: SideEffect[]): Observable<void> {\n if (effects.length === 0) {\n return of(undefined);\n }\n\n return effects.reduce(\n (chain$, effect) => chain$.pipe(concatMap(() => this.executeSideEffect(effect))),\n of(undefined) as Observable<void>,\n );\n }\n\n private executeSideEffect(effect: SideEffect): Observable<void> {\n const { scheduler } = this.config;\n\n switch (effect.type) {\n case Effect.CaptureValue: {\n return scheduler.executeBlocking(() => {\n const value = this.config.captureValue();\n this.dispatch({ type: Action.ValueCaptured, value });\n });\n }\n\n case Effect.WaitFrameBoundary: {\n return scheduler.executeAtFrameBoundary(\n () => {\n this.dispatch({ type: Action.TeardownComplete });\n },\n { skipIf: this.config.isFieldPipelineSettled },\n );\n }\n\n case Effect.CreateForm: {\n return scheduler.executeBlocking(() => {\n const state = untracked(() => this._state());\n\n let config: FormConfig<TFields> | undefined;\n if (state.type === LifecycleState.Initializing) {\n config = state.config;\n } else if (isTransitioningState(state) && state.phase === Phase.Applying) {\n config = state.pendingConfig;\n }\n\n if (!config) return;\n\n const formSetup = this.config.createFormSetup(config);\n this.config.onFormCreated?.(formSetup);\n\n if (state.type === LifecycleState.Initializing) {\n this.dispatch({ type: Action.SetupComplete, formSetup });\n } else if (isTransitioningState(state) && state.phase === Phase.Applying) {\n this.dispatch({ type: Action.ApplyComplete, formSetup });\n }\n });\n }\n\n case Effect.RestoreValues: {\n return scheduler.executeAfterRender(() => {\n const state = untracked(() => this._state());\n if (!isTransitioningState(state) || state.phase !== Phase.Restoring) return;\n\n if (!state.pendingFormSetup) {\n this.config.logger.warn('RestoreValues effect: pendingFormSetup was not set — falling back to recomputing.');\n }\n const formSetup = state.pendingFormSetup ?? this.config.createFormSetup(state.pendingConfig);\n\n // Restore a captured value only when the key survives the swap AND the field's type\n // is unchanged. A retyped key (e.g. input → select) must initialize to the new field's\n // declared default rather than inheriting the stale value from the old field type.\n // Keyed on top-level schemaFields, so a retype of a nested-group child is not tracked here.\n const oldTypeByKey = new Map<string, string>();\n for (const f of state.currentFormSetup.schemaFields) {\n if (f.key !== undefined) oldTypeByKey.set(f.key, f.type);\n }\n const validKeys = new Set<string>();\n for (const f of formSetup.schemaFields) {\n if (f.key !== undefined && oldTypeByKey.get(f.key) === f.type) validKeys.add(f.key);\n }\n\n this.config.restoreValue(effect.values, validKeys);\n this.dispatch({ type: Action.RestoreComplete });\n });\n }\n\n default:\n return assertNever(effect);\n }\n }\n}\n\n/** @internal */\nexport function createFormStateMachine<TFields extends RegisteredFieldTypes[]>(\n config: FormStateMachineConfig<TFields>,\n): FormStateMachine<TFields> {\n return new FormStateMachine(config);\n}\n","import {\n computed,\n DestroyRef,\n inject,\n Injectable,\n InjectionToken,\n Injector,\n linkedSignal,\n OnDestroy,\n runInInjectionContext,\n signal,\n Signal,\n untracked,\n WritableSignal,\n} from '@angular/core';\nimport { takeUntilDestroyed } from '@angular/core/rxjs-interop';\nimport { FieldTree, form, Schema } from '@angular/forms/signals';\nimport { EMPTY, filter, forkJoin, map, Observable, of, pipe, scan, switchMap, take } from 'rxjs';\nimport { explicitEffect } from 'ngxtension/explicit-effect';\n\nimport { FieldDef } from '@ng-forge/dynamic-forms/internal';\nimport { normalizeSimplifiedArrays } from '../utils/array-field/normalize-simplified-arrays';\nimport { DynamicFormError } from '@ng-forge/dynamic-forms/internal';\nimport { isGroupField } from '@ng-forge/dynamic-forms/internal';\nimport { isPageField, PageField } from '@ng-forge/dynamic-forms/internal';\nimport { EventBus } from '@ng-forge/dynamic-forms/internal';\nimport { FormClearEvent } from '../events/constants/form-clear.event';\nimport { FormResetEvent } from '../events/constants/form-reset.event';\nimport { FormSubmitEvent } from '../events/constants/submit.event';\nimport { FieldSignalContext } from '@ng-forge/dynamic-forms/internal';\nimport { FIELD_SIGNAL_CONTEXT } from '@ng-forge/dynamic-forms/internal';\nimport { FieldTypeDefinition } from '@ng-forge/dynamic-forms/internal';\nimport { FormConfig, FormOptions } from '@ng-forge/dynamic-forms/internal';\nimport { RegisteredFieldTypes } from '@ng-forge/dynamic-forms/internal';\nimport { detectFormMode, FormMode } from '@ng-forge/dynamic-forms/internal';\nimport { InferFormModel } from '@ng-forge/dynamic-forms/internal';\nimport { DynamicFormLogger } from '@ng-forge/dynamic-forms/internal';\nimport { ArrayItemRegistryService } from '../core/registry/array-item-registry.service';\nimport { FunctionRegistryService } from '@ng-forge/dynamic-forms/internal';\nimport { SchemaRegistryService } from '../core/registry/schema-registry.service';\nimport { createSchemaFromFields } from '../core/schema-builder';\nimport { createFormLevelSchema } from '../core/form-schema-merger';\nimport { collectCrossFieldEntries } from '../core/cross-field/cross-field-collector';\nimport { deepMergeDefaults, isEqual } from '@ng-forge/dynamic-forms/internal';\nimport { CONTAINER_FIELD_PROCESSORS } from '../utils/container-utils/container-field-processors';\nimport { derivedFromDeferred } from '@ng-forge/dynamic-forms/internal';\nimport { reconcileFields, ResolvedField, resolveField, resolveFieldSync } from '../utils/resolve-field/resolve-field';\nimport { FormModeValidator } from '../utils/form-validation/form-mode-validator';\nimport { injectFieldRegistry } from '../utils/inject-field-registry/inject-field-registry';\nimport { validateFormConfig } from '../utils/config-validation/config-validator';\nimport { walkAndValidateAddons } from '../utils/validate-form-config/validate-field-addons';\nimport { addonWarningKey, formatAddonWarning } from '../utils/validate-form-config/addon-warning';\nimport { ADDON_TYPE_REGISTRY } from '@ng-forge/dynamic-forms/internal';\nimport { VALUE_EXCLUSION_DEFAULTS } from '../providers/features/value-exclusion/value-exclusion.token';\nimport { filterFormValue } from '../utils/value-filter/value-filter';\nimport { DEV_MODE } from '../utils/dev-mode';\nimport { ValueExclusionConfig } from '@ng-forge/dynamic-forms/internal';\n\nimport { Action, FieldLoadingError, FormSetup, isReadyState, isTransitioningState, LifecycleState, Phase } from './state-types';\nimport { createSideEffectScheduler } from './side-effect-scheduler';\nimport { createFormStateMachine, FormStateMachine } from './form-state-machine';\n\n/**\n * Shared deps holder injected by both DynamicForm and FormStateManager.\n * DynamicForm assigns its input signals to the holder's properties via an IIFE\n * field initializer (runs before FormStateManager is injected in declaration order);\n * FormStateManager reads them.\n *\n * @internal\n */\nexport interface FormStateDeps {\n config: Signal<FormConfig<RegisteredFieldTypes[]>> | null;\n formOptions: Signal<FormOptions | undefined> | null;\n value: WritableSignal<Partial<unknown> | undefined> | null;\n /**\n * Source mode forwarded to the addon validator at bootstrap. `'inline'`\n * (default) keeps every kind; `'json'` drops code-only kinds. Optional —\n * defaults to `'inline'` when omitted.\n */\n source: Signal<'inline' | 'json'> | null;\n}\n\n/** @internal */\nexport const FORM_STATE_DEPS = new InjectionToken<FormStateDeps>('FORM_STATE_DEPS');\n\n/** Casts a FieldTree to a record of per-key sub-trees. */\nfunction asFieldTreeRecord(tree: FieldTree<unknown>): Record<string, FieldTree<unknown>> {\n return tree as unknown as Record<string, FieldTree<unknown>>;\n}\n\n/** All-false baseline for the outward two-way binding filter — see `boundFormValue`. */\nconst BOUND_VALUE_EXCLUSION_BASELINE = {\n excludeValueIfHidden: false,\n excludeValueIfDisabled: false,\n excludeValueIfReadonly: false,\n} as const;\n\n/** Per-field axes that can drive value exclusion. */\ninterface FieldExclusionAxes {\n readonly hidden: boolean;\n readonly disabled: boolean;\n readonly readonly: boolean;\n}\n\n/** True when any of the three exclusion axes is asserted (i.e. the field could be filtered out of the bound model). */\nfunction isFieldExcluded(axes: FieldExclusionAxes | undefined): boolean {\n return !!axes && (axes.hidden || axes.disabled || axes.readonly);\n}\n\n/**\n * Recursively populates `out` with `dottedPath → exclusion axes` for every keyed field reachable\n * from `fields`, walking into groups so nested-leaf transitions can be detected. Arrays are\n * treated as leaves (whole-array filtering only — see value-filter v1 limitation).\n */\nfunction collectFieldStateSnapshot(\n fields: readonly FieldDef<unknown>[],\n treeRecord: Record<string, FieldTree<unknown>>,\n pathParts: readonly string[],\n out: Record<string, FieldExclusionAxes>,\n): void {\n for (const field of fields) {\n const key = field.key;\n if (!key) continue;\n const subtree = treeRecord[key];\n if (!subtree || typeof subtree !== 'function') continue;\n\n const state = subtree();\n const path = [...pathParts, key];\n out[path.join('.')] = { hidden: state.hidden(), disabled: state.disabled(), readonly: state.readonly() };\n\n if (isGroupField(field) && field.fields) {\n collectFieldStateSnapshot(field.fields as readonly FieldDef<unknown>[], asFieldTreeRecord(subtree), path, out);\n }\n }\n}\n\n/**\n * Reads a value from a nested object by dotted-path segments. Returns undefined\n * if any intermediate segment is missing or non-object.\n */\nfunction getValueAtPath(source: Record<string, unknown>, segments: readonly string[]): unknown {\n let current: unknown = source;\n for (const segment of segments) {\n if (current === null || typeof current !== 'object') return undefined;\n current = (current as Record<string, unknown>)[segment];\n }\n return current;\n}\n\n/**\n * Whether a value is worth capturing into the saved-hidden-value store. `NaN` (the number-input\n * default) is excluded — restoring it would re-leak the default into the bound model when the\n * field becomes visible again, defeating the bug fix.\n */\nfunction isRestoreRelevant(value: unknown): boolean {\n return !(typeof value === 'number' && Number.isNaN(value));\n}\n\n/**\n * Returns a new object with `value` set at the path described by `segments`.\n * Intermediate objects are cloned shallowly to preserve immutability of `target`.\n */\nfunction setValueAtPath(target: Record<string, unknown>, segments: readonly string[], value: unknown): Record<string, unknown> {\n if (segments.length === 0) return target;\n const [head, ...rest] = segments;\n const next = { ...target };\n if (rest.length === 0) {\n next[head] = value;\n } else {\n const child = target[head];\n const childObj = child !== null && typeof child === 'object' ? (child as Record<string, unknown>) : {};\n next[head] = setValueAtPath(childObj, rest, value);\n }\n return next;\n}\n\n/**\n * Central service that manages all form state and coordinates the form lifecycle.\n * Single source of truth for lifecycle state, field resolution, form signals, and events.\n *\n * @internal\n */\n@Injectable()\nexport class FormStateManager<\n TFields extends RegisteredFieldTypes[] = RegisteredFieldTypes[],\n TModel extends Record<string, unknown> = InferFormModel<TFields>,\n> implements OnDestroy {\n // ─────────────────────────────────────────────────────────────────────────────\n // Dependencies (injected)\n // ─────────────────────────────────────────────────────────────────────────────\n\n private readonly injector = inject(Injector);\n private readonly destroyRef = inject(DestroyRef);\n private readonly logger = inject(DynamicFormLogger);\n private readonly eventBus = inject(EventBus);\n private readonly functionRegistry = inject(FunctionRegistryService);\n private readonly schemaRegistry = inject(SchemaRegistryService);\n // Optional so unit-test setups that drive FormStateManager directly without provideDynamicFormDI()\n // (e.g. mocked RootFormRegistryService specs) don't have to manually wire the registry. Real\n // DynamicForm usage always provides it via coreProviders; if you hit the dev warning below in\n // your own setup, you're using DynamicForm without provideDynamicFormDI() — fix the providers\n // chain rather than ignoring it.\n private readonly arrayItemRegistry = inject(ArrayItemRegistryService, { optional: true });\n\n /** Host component dependencies (config, formOptions, value). */\n private readonly deps = (() => {\n const raw = inject(FORM_STATE_DEPS);\n if (!raw.config || !raw.formOptions || !raw.value) {\n throw new DynamicFormError(\n 'FormStateDeps must be connected before FormStateManager is created. ' +\n 'Ensure DynamicForm assigns its input signals to FORM_STATE_DEPS.',\n );\n }\n // Safe cast: DynamicForm populates FormStateDeps with its concrete signals.\n // `source` is optional — `DynamicForm` always populates it, but other\n // callers of `FormStateManager` (e.g., specs) may omit it.\n return raw as {\n readonly config: Signal<FormConfig<TFields>>;\n readonly formOptions: Signal<FormOptions | undefined>;\n readonly value: WritableSignal<Partial<TModel> | undefined>;\n readonly source: Signal<'inline' | 'json'> | null;\n };\n })();\n\n /** Global value exclusion defaults. */\n private readonly valueExclusionDefaults = inject(VALUE_EXCLUSION_DEFAULTS);\n\n /** Field registry for loading components. */\n private readonly fieldRegistry = injectFieldRegistry();\n /** Addon-kind registry — feeds the addon validator at config-setup time. */\n private readonly addonKindRegistry = inject(ADDON_TYPE_REGISTRY);\n /**\n * Fingerprints of addon warnings emitted by the last config setup.\n * Used to skip re-logging warnings that already fired for the previous\n * config — avoids spam when the form swaps config repeatedly but the\n * addon issues haven't changed.\n */\n private lastAddonWarningKeys: Set<string> = new Set();\n\n // ─────────────────────────────────────────────────────────────────────────────\n // State Machine\n // ─────────────────────────────────────────────────────────────────────────────\n\n private readonly scheduler = createSideEffectScheduler(this.injector);\n private readonly machine: FormStateMachine<TFields>;\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Memoized Functions\n // ─────────────────────────────────────────────────────────────────────────────\n\n private readonly fieldProcessors = inject(CONTAINER_FIELD_PROCESSORS);\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Internal State Signals\n // ─────────────────────────────────────────────────────────────────────────────\n\n /** Field loading errors accumulated during resolution. */\n readonly fieldLoadingErrors = signal<FieldLoadingError[]>([]);\n\n /**\n * Class-level form cache for the \"hold until settled\" pattern.\n * MUST be class-level — a local variable in the computed would reset on re-evaluation.\n * Mutable ref avoids a reactive cycle (form → isSettled → resolvedFields → form).\n */\n private readonly _formCache: { current: ReturnType<typeof form<TModel>> | undefined } = { current: undefined };\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Computed Signals - Derived from State Machine\n // ─────────────────────────────────────────────────────────────────────────────\n\n /**\n * The currently active config used for form rendering.\n * Teardown/Applying → old config, Restoring → new config.\n * Returns deps.config() in uninitialized state so the form schema is\n * available before the state machine dispatches 'initialize'.\n */\n readonly activeConfig = computed(() => {\n const state = this.machine.state();\n\n if (state.type === LifecycleState.Uninitialized) {\n return this.deps.config();\n }\n\n if (isReadyState(state)) return state.config;\n if (isTransitioningState(state)) {\n if (state.phase === Phase.Teardown || state.phase === Phase.Applying) return state.currentConfig;\n return state.pendingConfig;\n }\n if (state.type === LifecycleState.Initializing) return state.config;\n return undefined;\n });\n\n /** Current render phase: 'render' = showing form, 'teardown' = hiding old components. */\n readonly renderPhase = computed(() => {\n const state = this.machine.state();\n if (isTransitioningState(state) && state.phase === Phase.Teardown) return Phase.Teardown;\n return 'render';\n });\n\n /** Whether to render the form template. */\n readonly shouldRender = computed(() => this.activeConfig() !== undefined);\n\n /** Computed form mode detection with validation. */\n readonly formModeDetection = computed(() => {\n const config = this.activeConfig();\n if (!config) {\n return { mode: 'non-paged' as const, isValid: true, errors: [] };\n }\n\n return detectFormMode(config.fields || []);\n });\n\n /** Validation result for the current form configuration. */\n private readonly formConfigValidation = computed(() => {\n const config = this.activeConfig();\n if (!config) return { isValid: true, errors: [] as string[], warnings: [] as string[] };\n return FormModeValidator.validateFormConfiguration(config.fields || []);\n });\n\n /** Effective form options (merged from config and input). */\n readonly effectiveFormOptions = computed(() => {\n const config = this.activeConfig();\n const configOptions = config?.options || {};\n const inputOptions = this.deps.formOptions() ?? undefined;\n return { ...configOptions, ...inputOptions };\n });\n\n /** Page field definitions (for paged forms). */\n readonly pageFieldDefinitions = computed(() => {\n const config = this.activeConfig();\n const mode = this.formModeDetection().mode;\n\n if (mode === 'paged' && config?.fields) {\n return config.fields.filter(isPageField);\n }\n\n return [] as PageField[];\n });\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Computed Signals - Form Setup\n // ─────────────────────────────────────────────────────────────────────────────\n\n private readonly rawFieldRegistry = computed(() => this.fieldRegistry.raw);\n\n /** Computed form setup — reads from the state machine, computed from config on bootstrap. */\n readonly formSetup = computed(() => {\n const registry = this.rawFieldRegistry();\n const state = this.machine.state();\n\n if (isReadyState(state)) {\n return state.formSetup;\n }\n\n if (isTransitioningState(state)) {\n if (state.phase === Phase.Restoring && state.pendingFormSetup) {\n return state.pendingFormSetup;\n }\n return state.currentFormSetup;\n }\n\n // Bootstrap path: compute form setup before the state machine dispatches 'initialize'.\n // Register validators/schemas here so they are available before the first form is built.\n // This is a controlled side effect: registerValidatorsFromConfig is idempotent and only\n // mutates external registries (SchemaRegistry, FunctionRegistry), not reactive state.\n // The state machine's createFormSetup callback handles registration for subsequent configs.\n const config = this.activeConfig();\n if (config) {\n this.registerValidatorsFromConfig(config as FormConfig<TFields>);\n }\n if (!config) {\n return this.createEmptyFormSetup(registry);\n }\n\n if (config.fields && config.fields.length > 0) {\n const modeDetection = this.formModeDetection();\n return this.createFormSetupFromConfig(config.fields as FieldDef<unknown>[], modeDetection.mode, registry);\n }\n\n return this.createEmptyFormSetup(registry);\n });\n\n /** Default values computed from field definitions. */\n readonly defaultValues = linkedSignal(() => this.formSetup().defaultValues as TModel);\n\n /** Valid field keys — memoized separately so the Set isn't rebuilt on every keystroke. */\n private readonly validKeys = computed(() => {\n const schemaFields = this.formSetup().schemaFields;\n if (!schemaFields || schemaFields.length === 0) return undefined;\n return new Set(schemaFields.map((f) => f.key).filter((key): key is string => key !== undefined));\n });\n\n /**\n * Per-field exclusion-axis state (hidden/disabled/readonly) keyed by dotted path. Drives the\n * save-on-exclude effect's transition detection. Walks recursively into groups so nested-leaf\n * transitions are captured. Arrays are treated as leaves (whole-array preservation only).\n */\n private readonly fieldStateSnapshot = computed((): Record<string, FieldExclusionAxes> => {\n const setup = this.formSetup();\n if (!setup.schemaFields || setup.schemaFields.length === 0) return {};\n\n const snapshot: Record<string, FieldExclusionAxes> = {};\n collectFieldStateSnapshot(setup.schemaFields, asFieldTreeRecord(this.form()), [], snapshot);\n return snapshot;\n });\n\n /**\n * Captured values for fields actively excluded from the bound model — either via\n * `excludeValueIfHidden`, `excludeValueIfDisabled`, or `excludeValueIfReadonly`. Restored when\n * the field becomes re-included (visible/enabled/editable). The save effect only writes here\n * when the field is being filtered out by `boundFormValue` — fields without explicit opt-in\n * never enter the store.\n */\n private readonly excludedValueStore = signal<Record<string, unknown>>({});\n\n /** Tracks per-path exclusion state across save-effect runs to detect newly-excluded fields. */\n private readonly prevFieldStateSnapshot = signal<Record<string, FieldExclusionAxes>>({});\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Computed Signals - Entity & Form\n // ─────────────────────────────────────────────────────────────────────────────\n\n /** Entity (form value merged with defaults). */\n readonly entity = linkedSignal<TModel>(\n () => {\n const inputValue = this.deps.value();\n const defaults = this.defaultValues();\n const keys = this.validKeys();\n const saved = this.excludedValueStore();\n\n // Deep-merge so a partial nested object in `inputValue` (e.g. a group\n // value missing one of its declared sub-field keys) does not orphan\n // the absent sub-field in the Signal Forms validation graph.\n // Layer order: defaults < saved (excluded-field restorations) < inputValue.\n const withSaved = deepMergeDefaults(defaults as Record<string, unknown>, saved);\n const combined = deepMergeDefaults(withSaved, inputValue as Record<string, unknown>);\n\n if (keys) {\n const filtered: Record<string, unknown> = {};\n for (const key of Object.keys(combined)) {\n if (keys.has(key)) {\n filtered[key] = (combined as Record<string, unknown>)[key];\n }\n }\n return filtered as TModel;\n }\n\n return combined as TModel;\n },\n {\n debugName: 'FormStateManager.entity',\n equal: isEqual,\n },\n );\n\n /**\n * Schema derived from the current form config and field setup.\n * Separated from `form` so schema construction is memoized independently.\n * Requires `runInInjectionContext` because `createSchemaFromFields` calls `inject()` internally.\n */\n private readonly formSchema = computed((): Schema<TModel> | undefined =>\n runInInjectionContext(this.injector, () => {\n const setup = this.formSetup();\n const config = this.activeConfig();\n\n if (!config) return undefined;\n\n if (setup.schemaFields?.length) {\n const crossFieldCollection = collectCrossFieldEntries(setup.schemaFields as FieldDef<unknown>[]);\n return createSchemaFromFields(setup.schemaFields, setup.registry, {\n crossFieldValidators: crossFieldCollection.validators,\n formLevelSchema: config.schema,\n validateWhenHidden: this.effectiveFormOptions().validateWhenHidden,\n }) as Schema<TModel>;\n }\n\n if (config.schema) {\n return createFormLevelSchema(config.schema) as Schema<TModel>;\n }\n\n return undefined;\n }),\n );\n\n /** The Angular Signal Form instance. */\n readonly form = computed(() => {\n const schema = this.formSchema();\n const injector = this.injector;\n return untracked(() => (schema ? form(this.entity, schema, { injector }) : form(this.entity, { injector })));\n });\n\n /** Whether resolvedFields has caught up with fieldsSource (set in constructor). */\n private isFieldPipelineSettled: Signal<boolean>;\n\n /**\n * Field signal context injected into child components.\n * The `form` getter uses a \"hold until settled\" pattern: returns the cached\n * (old) form while resolvedFields != fieldsSource, preventing stale components\n * from accessing missing FieldTree references.\n */\n readonly fieldSignalContext: Signal<FieldSignalContext<TModel>> = computed(() => {\n const formSignal = this.form;\n const isSettled = () => this.isFieldPipelineSettled();\n const formCache = this._formCache;\n\n return {\n injector: this.injector,\n value: this.deps.value,\n defaultValues: this.defaultValues,\n get form() {\n const currentForm = formSignal();\n const settled = isSettled();\n\n if (!settled && formCache.current) {\n return formCache.current;\n }\n\n formCache.current = currentForm;\n return currentForm;\n },\n };\n });\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Computed Signals - Form State\n // ─────────────────────────────────────────────────────────────────────────────\n\n /** Intermediate computed that unwraps the double-signal (form()()) once. */\n private readonly formInstance = computed(() => this.form()());\n\n /** Current form values (reactive). */\n readonly formValue = computed(() => this.formInstance().value());\n\n /** Form values filtered by value exclusion rules. */\n readonly filteredFormValue = computed(() => {\n const rawValue = this.formValue();\n const setup = this.formSetup();\n const options = this.effectiveFormOptions();\n\n if (!setup.schemaFields || setup.schemaFields.length === 0) {\n return rawValue;\n }\n\n // form() returns the FieldTree<TModel> — a callable with per-key sub-trees.\n // formInstance() returns form()() — the FieldState. We need the FieldTree for\n // per-field state access (e.g., formTree[key]() gives FieldState with hidden/disabled/readonly).\n const formTree = this.form();\n\n const formOptions: ValueExclusionConfig = {\n excludeValueIfHidden: options.excludeValueIfHidden,\n excludeValueIfDisabled: options.excludeValueIfDisabled,\n excludeValueIfReadonly: options.excludeValueIfReadonly,\n };\n\n // FieldTree<TModel> is structurally a callable that also exposes per-key sub-trees\n // via bracket access (e.g., formTree['fieldKey']). TypeScript's nominal typing for\n // FieldTree doesn't expose this index signature, so we use a helper cast.\n const fieldTreeRecord = asFieldTreeRecord(formTree);\n\n return filterFormValue(\n rawValue as Record<string, unknown>,\n setup.schemaFields,\n fieldTreeRecord,\n setup.registry,\n this.valueExclusionDefaults,\n formOptions,\n );\n });\n\n /** Form value used for the outward two-way binding sync. */\n private readonly boundFormValue = computed(() => {\n const rawValue = this.formValue();\n const setup = this.formSetup();\n const options = this.effectiveFormOptions();\n\n if (!setup.schemaFields || setup.schemaFields.length === 0) {\n return rawValue;\n }\n\n const formTree = this.form();\n const formOptions: ValueExclusionConfig = {\n excludeValueIfHidden: options.excludeValueIfHidden,\n excludeValueIfDisabled: options.excludeValueIfDisabled,\n excludeValueIfReadonly: options.excludeValueIfReadonly,\n };\n const fieldTreeRecord = asFieldTreeRecord(formTree);\n\n return filterFormValue(\n rawValue as Record<string, unknown>,\n setup.schemaFields,\n fieldTreeRecord,\n setup.registry,\n BOUND_VALUE_EXCLUSION_BASELINE,\n formOptions,\n );\n });\n\n /** Whether the form is currently valid. */\n readonly valid = computed(() => this.formInstance().valid());\n\n /** Whether the form is currently invalid. */\n readonly invalid = computed(() => this.formInstance().invalid());\n\n /** Whether any form field has been modified. */\n readonly dirty = computed(() => this.formInstance().dirty());\n\n /** Whether any form field has been touched (blurred). */\n readonly touched = computed(() => this.formInstance().touched());\n\n /** Current validation errors from all fields. */\n readonly errors = computed(() => this.formInstance().errors());\n\n /** Whether the form is disabled (from options or form state). */\n readonly disabled = computed(() => {\n const optionsDisabled = this.effectiveFormOptions().disabled;\n const formDisabled = this.formInstance().disabled();\n return optionsDisabled ?? formDisabled;\n });\n\n /** Whether the form is currently submitting. */\n readonly submitting = computed(() => this.formInstance().submitting());\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Field Resolution\n // ─────────────────────────────────────────────────────────────────────────────\n\n /**\n * Phase-aware field source that coordinates component lifecycle with form updates.\n * Teardown/Applying → intersection of old/new fields; Restoring → all new fields.\n */\n private readonly fieldsSource = computed(\n () => {\n const state = this.machine.state();\n\n if (isTransitioningState(state)) {\n const oldFields = state.currentConfig.fields ?? [];\n const newFields = state.pendingConfig.fields ?? [];\n const newKeys = new Set(newFields.map((f) => f.key));\n\n if (state.phase === Phase.Teardown || state.phase === Phase.Applying) {\n return oldFields.filter((f) => newKeys.has(f.key));\n }\n\n if (state.phase === Phase.Restoring) {\n return state.pendingFormSetup?.fields ?? this.formSetup().fields;\n }\n }\n\n return this.formSetup().fields;\n },\n {\n equal: (a, b) => {\n if (a === b) return true;\n if (a.length !== b.length) return false;\n return a.every((field, i) => field === b[i]);\n },\n },\n );\n\n /** Injector for field components with FIELD_SIGNAL_CONTEXT. */\n private readonly fieldInjector = computed(() =>\n Injector.create({\n parent: this.injector,\n providers: [{ provide: FIELD_SIGNAL_CONTEXT, useValue: this.fieldSignalContext() }],\n }),\n );\n\n /** Resolved fields ready for rendering. */\n readonly resolvedFields: Signal<ResolvedField[]>;\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Event Streams (for outputs)\n // ─────────────────────────────────────────────────────────────────────────────\n\n /**\n * Stream of submit events when form is valid.\n *\n * Typed as the full inferred model. The emitted payload is `filteredFormValue()`,\n * so when `excludeValueIfHidden` / `excludeValueIfDisabled` / `excludeValueIfReadonly`\n * are enabled a field that is hidden/disabled/readonly at submit time is omitted at\n * runtime while the type still lists it. Those options default off, so the type is\n * accurate for the common case; this matches the long-standing `as TModel` cast below.\n */\n readonly submitted$: Observable<TModel>;\n\n /** Stream of reset events. */\n readonly reset$: Observable<void>;\n\n /** Stream of clear events. */\n readonly cleared$: Observable<void>;\n\n /** Emits once when the state machine reaches 'ready'. Useful for tests. */\n get ready$(): Observable<void> {\n return this.machine.state$.pipe(\n filter(isReadyState),\n take(1),\n map(() => undefined),\n );\n }\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Constructor\n // ─────────────────────────────────────────────────────────────────────────────\n\n constructor() {\n this.resolvedFields = derivedFromDeferred(\n this.fieldsSource,\n pipe(\n switchMap((fields) => {\n if (!fields || fields.length === 0) {\n return of([] as (ResolvedField | undefined)[]);\n }\n\n const registry = this.rawFieldRegistry();\n const injector = this.fieldInjector();\n\n const syncContext = {\n getLoadedComponent: (type: string) => this.fieldRegistry.getLoadedComponent(type),\n registry,\n injector,\n };\n\n // Hybrid: resolve cached fields sync, only await uncached ones\n const results: (ResolvedField | undefined)[] = new Array(fields.length);\n const asyncIndexes: number[] = [];\n const asyncObs: Observable<ResolvedField | undefined>[] = [];\n\n const onError = (fieldDef: FieldDef<unknown>, error: unknown) => {\n const fieldKey = fieldDef.key || '<no key>';\n this.fieldLoadingErrors.update((errors) => [\n ...errors,\n {\n fieldType: fieldDef.type,\n fieldKey,\n error: error instanceof Error ? error : new Error(String(error)),\n },\n ]);\n this.logger.error(\n `Failed to load component for field type '${fieldDef.type}' (key: ${fieldKey}). ` +\n `Ensure the field type is registered in your field registry.`,\n error,\n );\n };\n\n for (let i = 0; i < fields.length; i++) {\n const f = fields[i];\n const def = this.fieldRegistry.getType(f.type);\n const isCached = !def?.loadComponent || this.fieldRegistry.getLoadedComponent(f.type);\n\n if (isCached) {\n results[i] = resolveFieldSync(f, syncContext);\n } else {\n asyncIndexes.push(i);\n asyncObs.push(\n resolveField(f, {\n loadTypeComponent: (type: string) => this.fieldRegistry.loadTypeComponent(type),\n registry,\n injector,\n destroyRef: this.destroyRef,\n onError,\n }),\n );\n }\n }\n\n if (asyncObs.length === 0) {\n return of(results);\n }\n\n return forkJoin(asyncObs).pipe(\n map((asyncResults) => {\n for (let j = 0; j < asyncResults.length; j++) {\n results[asyncIndexes[j]] = asyncResults[j];\n }\n return results;\n }),\n );\n }),\n map((fields) => fields.filter((f): f is ResolvedField => f !== undefined)),\n scan(reconcileFields, [] as ResolvedField[]),\n ),\n { initialValue: [] as ResolvedField[], injector: this.injector },\n ) as Signal<ResolvedField[]>;\n\n // Length comparison works because reconcileFields preserves ordering and\n // fieldsSource changes atomically during transitions (lengths won't match until settled).\n this.isFieldPipelineSettled = computed(() => {\n return this.fieldsSource().length === this.resolvedFields().length;\n });\n\n this.submitted$ = this.eventBus.on<FormSubmitEvent>('submit').pipe(\n filter(() => {\n if (!this.valid()) {\n this.logger.debug('Form submitted while invalid, not emitting to (submitted) output');\n return false;\n }\n return true;\n }),\n // switchMap is intentional here: mapping is synchronous (reads signals, no async Promise).\n // The exhaustMap concern in createSubmissionHandler does not apply to this path.\n switchMap(() => {\n const config = this.activeConfig();\n const submissionConfig = config?.submission;\n\n if (submissionConfig?.action) {\n this.logger.warn(\n 'Both `submission.action` and `(submitted)` output are configured. ' +\n 'When using `submission.action`, the `(submitted)` output will not emit. ' +\n 'Use either `submission.action` OR `(submitted)`, not both.',\n );\n return EMPTY;\n }\n\n return of(this.filteredFormValue() as TModel);\n }),\n takeUntilDestroyed(this.destroyRef),\n );\n\n this.reset$ = this.eventBus.on<FormResetEvent>('form-reset').pipe(\n map(() => undefined as void),\n takeUntilDestroyed(this.destroyRef),\n );\n\n this.cleared$ = this.eventBus.on<FormClearEvent>('form-clear').pipe(\n map(() => undefined as void),\n takeUntilDestroyed(this.destroyRef),\n );\n\n // Create the state machine eagerly\n this.machine = createFormStateMachine<TFields>({\n injector: this.injector,\n destroyRef: this.destroyRef,\n scheduler: this.scheduler,\n logger: this.logger,\n createFormSetup: (config) => {\n this.registerValidatorsFromConfig(config);\n return this.createFormSetupFromConfig(\n (config.fields as FieldDef<unknown>[]) ?? [],\n detectFormMode(config.fields ?? []).mode,\n this.rawFieldRegistry(),\n );\n },\n captureValue: () => this.formValue() as Record<string, unknown>,\n isSubmitting: this.submitting,\n isFieldPipelineSettled: () => this.isFieldPipelineSettled(),\n restoreValue: (values, validKeys) => {\n const filtered: Record<string, unknown> = {};\n for (const [key, val] of Object.entries(values)) {\n if (validKeys.has(key)) {\n filtered[key] = val;\n }\n }\n // Strip any captured key that did NOT survive the swap (dropped or retyped). The stale\n // value lingers in `deps.value` from before teardown, and `entity` layers it on top of\n // the new field's default — so a retyped key would otherwise inherit the old value\n // instead of initializing to its declared default. `values` is the full captured\n // snapshot, so any captured key absent from `validKeys` must be removed from the model.\n this.deps.value.update((current) => {\n const next = { ...current, ...filtered } as Record<string, unknown>;\n for (const key of Object.keys(values)) {\n if (!validKeys.has(key)) delete next[key];\n }\n return next as Partial<TModel>;\n });\n },\n onTransition: (transition) => {\n this.logger.debug('State transition:', transition.from.type, '→', transition.to.type, transition.action.type);\n },\n onError: (error, action) => {\n this.logger.error(`State machine error recovery triggered for action '${action.type}':`, error);\n },\n });\n\n runInInjectionContext(this.injector, () => {\n this.setupEffects();\n this.setupEventHandlers();\n });\n }\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Public Methods\n // ─────────────────────────────────────────────────────────────────────────────\n\n /** Updates the form value model. */\n updateValue(value: Partial<TModel>): void {\n this.deps.value.set(value);\n }\n\n /** Resets the form to default values. */\n reset(): void {\n const rootField = this.form()();\n this.excludedValueStore.set({});\n const defaults = this.defaultValues();\n (rootField.value as WritableSignal<TModel>).set(defaults);\n this.deps.value.set(defaults as Partial<TModel>);\n rootField.reset();\n }\n\n /** Clears the form to empty state. */\n clear(): void {\n const rootField = this.form()();\n this.excludedValueStore.set({});\n const emptyValue = {} as TModel;\n (rootField.value as WritableSignal<TModel>).set(emptyValue);\n this.deps.value.set(emptyValue);\n rootField.reset();\n }\n\n /** Triggers form submission. */\n submit(): void {\n this.eventBus.dispatch(FormSubmitEvent);\n }\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Private Methods - Setup\n // ─────────────────────────────────────────────────────────────────────────────\n\n private setupEffects(): void {\n explicitEffect([this.deps.config], ([config]) => {\n this.fieldLoadingErrors.set([]);\n\n const state = this.machine.currentState;\n if (state.type === LifecycleState.Uninitialized) {\n this.machine.dispatch({ type: Action.Initialize, config });\n } else {\n this.machine.dispatch({ type: Action.ConfigChange, config });\n }\n });\n\n // Save-on-exclude: capture entity values for fields that boundFormValue is actively\n // filtering out via any of the three axes (hidden / disabled / readonly), so they can be\n // restored when the field becomes re-included. The transition fires once per \"no axis\n // asserted → at least one axis asserted\" edge; subsequent axis changes while still excluded\n // are no-ops. Comparing entity vs bound at the path limits the store to fields that are\n // actually being filtered — fields without explicit opt-in never enter the store and never\n // participate in the entity merge. `NaN` values are skipped: they're the number-input default\n // and would re-leak via the merge once the field becomes re-included.\n explicitEffect([this.fieldStateSnapshot], ([snapshot]) => {\n const currentSaved = this.excludedValueStore();\n const currentEntity = this.entity() as Record<string, unknown>;\n const boundValue = this.boundFormValue() as Record<string, unknown>;\n const prev = this.prevFieldStateSnapshot();\n let newSaved = currentSaved;\n\n for (const [path, axes] of Object.entries(snapshot)) {\n if (isFieldExcluded(axes) && !isFieldExcluded(prev[path])) {\n const segments = path.split('.');\n const inEntity = getValueAtPath(currentEntity, segments);\n const inBound = getValueAtPath(boundValue, segments);\n if (inEntity !== undefined && inBound === undefined && isRestoreRelevant(inEntity)) {\n newSaved = setValueAtPath(newSaved, segments, inEntity);\n }\n }\n }\n this.prevFieldStateSnapshot.set({ ...snapshot });\n\n if (newSaved !== currentSaved && !isEqual(newSaved, currentSaved)) {\n this.excludedValueStore.set(newSaved);\n }\n });\n\n // Schema change resets the saved store + transition tracker so values from a stale schema\n // can't leak into the new one. Also drops every per-array slot in the registry so a fresh\n // schema starts with an empty template/itemOrder/id-counter state.\n let warnedAboutMissingArrayRegistry = false;\n explicitEffect([this.formSetup], () => {\n this.prevFieldStateSnapshot.set({});\n if (Object.keys(this.excludedValueStore()).length > 0) {\n this.excludedValueStore.set({});\n }\n if (this.arrayItemRegistry) {\n this.arrayItemRegistry.clearAll();\n } else if (DEV_MODE && !warnedAboutMissingArrayRegistry) {\n warnedAboutMissingArrayRegistry = true;\n this.logger.warn(\n 'ArrayItemRegistryService was not provided to FormStateManager. ' +\n 'Array fields will lose dynamically-added item templates across hide/show cycles. ' +\n 'This usually means DynamicForm was bootstrapped without provideDynamicFormDI() — ' +\n 'check your providers chain.',\n );\n }\n });\n\n // Outward sync uses boundFormValue (honors only explicit excludeValueIf* opt-ins) so\n // global defaults that target submission output don't reshape the host's bound model.\n explicitEffect([this.boundFormValue], ([currentBound]) => {\n const currentValue = this.deps.value();\n if (!isEqual(currentBound, currentValue)) {\n this.deps.value.set(currentBound as TModel);\n }\n });\n\n explicitEffect([this.formConfigValidation], ([validation]) => {\n if (!validation.isValid) {\n this.logger.error('Invalid form configuration:', validation.errors);\n }\n if (validation.warnings.length > 0) {\n this.logger.warn('Form configuration warnings:', validation.warnings);\n }\n });\n }\n\n private setupEventHandlers(): void {\n this.eventBus\n .on<FormResetEvent>('form-reset')\n .pipe(takeUntilDestroyed(this.destroyRef))\n .subscribe(() => this.reset());\n\n this.eventBus\n .on<FormClearEvent>('form-clear')\n .pipe(takeUntilDestroyed(this.destroyRef))\n .subscribe(() => this.clear());\n }\n\n private registerValidatorsFromConfig({ customFnConfig, schemas }: FormConfig<TFields>): void {\n if (schemas) {\n schemas.forEach((schema) => {\n this.schemaRegistry.registerSchema(schema);\n });\n }\n\n if (!customFnConfig) {\n return;\n }\n\n if (customFnConfig.customFunctions) {\n Object.entries(customFnConfig.customFunctions).forEach(([name, fn]) => {\n this.functionRegistry.registerCustomFunction(name, fn);\n });\n }\n\n if (customFnConfig.derivations) {\n this.functionRegistry.setDerivationFunctions(customFnConfig.derivations);\n }\n\n if (customFnConfig.asyncDerivations) {\n this.functionRegistry.setAsyncDerivationFunctions(customFnConfig.asyncDerivations);\n }\n if (customFnConfig.asyncConditions) {\n this.functionRegistry.setAsyncConditionFunctions(customFnConfig.asyncConditions);\n }\n\n this.functionRegistry.setValidators(customFnConfig.validators);\n this.functionRegistry.setAsyncValidators(customFnConfig.asyncValidators);\n this.functionRegistry.setHttpValidators(customFnConfig.httpValidators);\n }\n\n private createFormSetupFromConfig(fields: FieldDef<unknown>[], mode: FormMode, registry: Map<string, FieldTypeDefinition>): FormSetup {\n const normalizedFields = normalizeSimplifiedArrays(fields);\n // Two validation passes run after normalization so simplified array templates\n // are already expanded into full ArrayField.fields and reachable during traversal:\n // 1. Structural — duplicate keys, unknown field types, invalid regex patterns.\n // 2. Addon pass below — strips invalid/unsupported addons with lenient warnings.\n validateFormConfig(normalizedFields, registry, this.logger);\n\n // Addon pass — strip invalid / unsupported addon entries and log a warning\n // for each one dropped. Lenient by design: the form keeps rendering even\n // when a JSON-source config ships an addon the FE doesn't understand.\n //\n // Warnings are deduped across consecutive config setups: a warning that\n // already fired for the previous config is skipped, so swapping config\n // back and forth doesn't spam the console.\n const source = this.deps.source?.() ?? 'inline';\n const { fields: sanitizedFields, warnings: addonWarnings } = walkAndValidateAddons(\n normalizedFields,\n registry,\n this.addonKindRegistry,\n source,\n );\n const nextKeys = new Set<string>();\n for (const w of addonWarnings) {\n // Dedup uses a structural fingerprint so different fields with the\n // same warning category never collide; the rendered message is what\n // we log.\n const key = addonWarningKey(w);\n nextKeys.add(key);\n if (!this.lastAddonWarningKeys.has(key)) {\n this.logger.warn(formatAddonWarning(w));\n }\n }\n this.lastAddonWarningKeys = nextKeys;\n const validatedFields = sanitizedFields as FieldDef<unknown>[];\n\n const flattenedFields = this.fieldProcessors.memoizedFlattenFields(validatedFields, registry);\n const flattenedFieldsForRendering = this.fieldProcessors.memoizedFlattenFieldsForRendering(validatedFields, registry);\n const fieldsById = this.fieldProcessors.memoizedKeyBy(flattenedFields);\n const defaultValues = this.fieldProcessors.memoizedDefaultValues(fieldsById, registry);\n const fieldsToRender = mode === 'paged' ? [] : flattenedFieldsForRendering;\n\n return {\n fields: fieldsToRender,\n schemaFields: flattenedFields,\n originalFields: fields,\n defaultValues,\n mode,\n registry,\n };\n }\n\n private createEmptyFormSetup(registry: Map<string, FieldTypeDefinition>): FormSetup {\n return {\n fields: [],\n schemaFields: [],\n defaultValues: {} as Record<string, unknown>,\n mode: 'non-paged' as const,\n registry,\n };\n }\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Lifecycle\n // ─────────────────────────────────────────────────────────────────────────────\n\n ngOnDestroy(): void {\n this.machine.dispatch({ type: Action.Destroy });\n }\n}\n","import { FieldDef } from '@ng-forge/dynamic-forms/internal';\nimport { hasChildFields } from '@ng-forge/dynamic-forms/internal';\nimport { getNormalizedArrayMetadata } from '../../utils/array-field/normalized-array-metadata';\nimport { normalizeFieldsArray } from '@ng-forge/dynamic-forms/internal';\n\n/**\n * Visitor invoked for every field encountered during traversal.\n *\n * @internal\n */\nexport type FieldVisitor<TContext> = (field: FieldDef<unknown>, context: TContext) => void;\n\n/**\n * Hooks for mutating the traversal context when crossing container boundaries.\n *\n * @internal\n */\nexport interface FieldTraversalHooks<TContext> {\n /** Called when descending into an `array` field. */\n onArrayChild?: (parent: TContext, field: FieldDef<unknown>) => Partial<TContext>;\n /** Called when descending into a `group` field. */\n onGroupChild?: (parent: TContext, field: FieldDef<unknown>) => Partial<TContext>;\n /** Called when descending into a layout container (page, row, container). */\n onLayoutChild?: (parent: TContext, field: FieldDef<unknown>) => Partial<TContext>;\n}\n\n/**\n * Recursively walks a field-definition tree, invoking `visitor` for each field.\n *\n * @internal\n */\nexport function traverseFieldsWithContext<TContext>(\n fields: FieldDef<unknown>[],\n context: TContext,\n visitor: FieldVisitor<TContext>,\n hooks?: FieldTraversalHooks<TContext>,\n): void {\n for (const field of fields) {\n visitor(field, context);\n\n if (!hasChildFields(field)) continue;\n\n if (field.type === 'array') {\n const childContext = mergeContext(context, hooks?.onArrayChild?.(context, field));\n const arrayChildren = collectArrayChildren(field);\n traverseFieldsWithContext(arrayChildren, childContext, visitor, hooks);\n continue;\n }\n\n const overrides = field.type === 'group' ? hooks?.onGroupChild?.(context, field) : hooks?.onLayoutChild?.(context, field);\n const childContext = mergeContext(context, overrides);\n traverseFieldsWithContext(normalizeFieldsArray(field.fields) as FieldDef<unknown>[], childContext, visitor, hooks);\n }\n}\n\n/**\n * Returns `true` as soon as `predicate` holds for any field in the tree,\n * short-circuiting the walk. Resolves children exactly like\n * {@link traverseFieldsWithContext} (including array-item templates), so a\n * presence check stays in lockstep with the full traversal.\n *\n * @internal\n */\nexport function someField(fields: readonly FieldDef<unknown>[], predicate: (field: FieldDef<unknown>) => boolean): boolean {\n return fields.some((field) => {\n if (predicate(field)) return true;\n if (!hasChildFields(field)) return false;\n const children = field.type === 'array' ? collectArrayChildren(field) : (normalizeFieldsArray(field.fields) as FieldDef<unknown>[]);\n return someField(children, predicate);\n });\n}\n\n/**\n * Returns the flattened list of children for an array field, falling back\n * to the Symbol metadata template when `fields` is empty.\n *\n * @internal\n */\nfunction collectArrayChildren(\n field: FieldDef<unknown> & { fields: FieldDef<unknown>[] | Record<string, FieldDef<unknown>> },\n): FieldDef<unknown>[] {\n let arrayItems = normalizeFieldsArray(field.fields) as (FieldDef<unknown> | FieldDef<unknown>[])[];\n\n if (arrayItems.length === 0) {\n const metadataTemplate = getNormalizedArrayMetadata(field)?.template;\n if (metadataTemplate) {\n arrayItems = [\n Array.isArray(metadataTemplate) ? [...(metadataTemplate as readonly FieldDef<unknown>[])] : (metadataTemplate as FieldDef<unknown>),\n ];\n }\n }\n\n const flattened: FieldDef<unknown>[] = [];\n for (const item of arrayItems) {\n if (Array.isArray(item)) {\n flattened.push(...item);\n } else {\n flattened.push(item);\n }\n }\n return flattened;\n}\n\n/** @internal */\nfunction mergeContext<TContext>(base: TContext, overrides: Partial<TContext> | undefined): TContext {\n return overrides ? { ...base, ...overrides } : { ...base };\n}\n","import { FieldDef, FieldWithValidation, isDerivationLogicConfig } from '@ng-forge/dynamic-forms/internal';\nimport { someField } from './field-traversal';\n\n/**\n * Cheap, eager predicate: does any field in the config declare a value or\n * property derivation — the shorthand `derivation` string, or a `logic[]` entry\n * of `type: 'derivation'` (the latter covers both value derivations and\n * `targetProperty` property derivations)?\n *\n * Deliberately reuses the collector's child resolution ({@link someField} shares\n * it with `traverseFieldsWithContext`) and detection predicate\n * ({@link isDerivationLogicConfig}) so this gate can never diverge from what\n * `createDerivationOrchestrator` would actually wire — including derivations\n * nested in containers and array-item templates.\n *\n * Stays lightweight (no dependency on the heavy derivation-orchestrator module)\n * so it can run eagerly to decide whether the orchestrator needs to be\n * lazy-loaded at all. Forms without derivations never pull the engine.\n *\n * @internal\n */\nexport function configHasDerivations(fields: FieldDef<unknown>[]): boolean {\n return someField(fields, (field) => {\n // Match the collector's keyless guard (derivation-collector.ts): a keyless\n // field never produces a derivation entry, so it must not trip the gate.\n // `someField` still recurses into its children.\n if (!field.key) return false;\n const validationField = field as FieldDef<unknown> & FieldWithValidation;\n return !!validationField.derivation || !!validationField.logic?.some(isDerivationLogicConfig);\n });\n}\n","import { computed, inject, Injector, InjectionToken, PendingTasks, runInInjectionContext, signal, Signal } from '@angular/core';\nimport { explicitEffect } from 'ngxtension/explicit-effect';\nimport { DynamicFormLogger, FieldDef } from '@ng-forge/dynamic-forms/internal';\nimport { FormStateManager } from '../../state/form-state-manager';\nimport { configHasDerivations } from './config-has-derivations';\n\n/**\n * Form-scoped signal that is `false` only while a derivation-bearing config is\n * still waiting for the lazily-loaded derivation engine to wire up. `DynamicForm`\n * ANDs this into `shouldRender`, so a form that uses derivations does not render\n * its fields until the orchestrator has loaded and registered its\n * value/property overrides — fields render already-derived, with no flash.\n *\n * For configs without derivations the gate is always `true` (no waiting, no\n * lazy load), and the heavy orchestrator module is never imported.\n *\n * @internal\n */\nexport const DERIVATION_RENDER_GATE = new InjectionToken<Signal<boolean>>('DERIVATION_RENDER_GATE');\n\n/**\n * Factory for {@link DERIVATION_RENDER_GATE}. Watches the form setup and, the\n * first time it declares a derivation, dynamically imports the derivation engine\n * (the chunk boundary) and wires the orchestrator in the form's injection\n * context. Flips the gate open once that bootstrap has run.\n *\n * The lazy import runs inside an Angular `PendingTasks` task so server-side\n * rendering waits for the engine to wire before serializing — derivation-bearing\n * forms render server-side rather than as empty markup. If the chunk fails to\n * load the gate opens anyway (the form renders without derivations rather than\n * locking shut forever).\n *\n * Must be created within the form's injection context (it captures the form\n * `Injector` for the deferred `runInInjectionContext`).\n *\n * @internal\n */\nexport function createDerivationRenderGate(): Signal<boolean> {\n const stateManager = inject(FormStateManager);\n const injector = inject(Injector);\n const pendingTasks = inject(PendingTasks);\n const logger = inject(DynamicFormLogger);\n\n const hasDerivations = computed(() => {\n const setup = stateManager.formSetup();\n return !!setup && configHasDerivations(setup.schemaFields as FieldDef<unknown>[]);\n });\n\n // Single source of truth for the lazy engine's lifecycle. `idle` until a config\n // first needs it; `loading` once the import is in flight (guards against a\n // second orchestrator if derivations swap out and back in); `wired` once the\n // orchestrator is constructed; `failed` if the chunk could not be loaded.\n const engine = signal<'idle' | 'loading' | 'wired' | 'failed'>('idle');\n\n explicitEffect([hasDerivations], ([has]) => {\n if (!has || engine() !== 'idle') return;\n engine.set('loading');\n // Tracked as a pending task so SSR awaits the engine before serializing.\n pendingTasks.run(() =>\n import('./bootstrap-derivation-orchestrator')\n .then(({ bootstrapDerivationOrchestrator }) => {\n runInInjectionContext(injector, () => bootstrapDerivationOrchestrator());\n engine.set('wired');\n })\n .catch((error: unknown) => {\n // A failed chunk load must not lock the form shut — render it degraded\n // (derivations won't apply) and surface the failure.\n engine.set('failed');\n logger.error('[Dynamic Forms] Failed to load the derivation engine; derivations will not apply.', error);\n }),\n );\n });\n\n // Open when there are no derivations, once the engine is wired, or if it failed\n // to load (degraded render beats a permanently closed gate).\n return computed(() => {\n if (!hasDerivations()) return true;\n const state = engine();\n return state === 'wired' || state === 'failed';\n });\n}\n","import { InjectionToken } from '@angular/core';\n\n/**\n * Multi-provider token. Each entry is resolved at form bootstrap purely for its\n * construction side effect — useful for orchestrators that need to wire reactive\n * streams before first render but don't need to be referenced at use-sites.\n *\n * @internal\n */\nexport const FORM_INITIALIZER = new InjectionToken<readonly unknown[]>('FORM_INITIALIZER');\n","import { computed, Injectable, Signal, signal } from '@angular/core';\n\nconst AUTO_ID_PREFIX = 'df-';\n\n/**\n * Root-singleton tracking mounted DynamicForm instances, driving auto-prefixing.\n *\n * Each form registers a `visible` signal (true while its host is rendered). Only\n * visible forms count toward \"more than one present\", so a cached but hidden page\n * — e.g. ionic's `ion-router-outlet` keeps the previous page in the DOM for\n * swipe-back — doesn't inflate the count. Counting signals (not the DOM) keeps\n * the computed pure; `_membership` only invalidates it on register/unregister,\n * since Map membership itself isn't reactive.\n *\n * Slots are recycled (lowest free `df-N`) so ids stay compact across navigation.\n */\n@Injectable({ providedIn: 'root' })\nexport class DynamicFormInstanceRegistry {\n private readonly slots = new Map<number, Signal<boolean>>();\n private readonly _membership = signal(0);\n\n /** True while more than one VISIBLE form is mounted — the trigger for auto-prefixing. */\n readonly multiplePresent: Signal<boolean> = computed(() => {\n this._membership();\n let visible = 0;\n for (const isVisible of this.slots.values()) {\n if (isVisible()) {\n visible += 1;\n if (visible > 1) return true;\n }\n }\n return false;\n });\n\n /** Registers a form's visibility signal; returns a stable, recycled auto-id. */\n register(visible: Signal<boolean>): string {\n let slot = 1;\n while (this.slots.has(slot)) slot++;\n this.slots.set(slot, visible);\n this._membership.update((m) => m + 1);\n return `${AUTO_ID_PREFIX}${slot}`;\n }\n\n unregister(id: string): void {\n this.slots.delete(Number(id.slice(AUTO_ID_PREFIX.length)));\n this._membership.update((m) => m + 1);\n }\n}\n","import { computed, DestroyRef, ElementRef, inject, Injectable, isDevMode, PLATFORM_ID, signal, Signal } from '@angular/core';\nimport { isPlatformBrowser } from '@angular/common';\nimport { explicitEffect } from 'ngxtension/explicit-effect';\nimport { FORM_OPTIONS } from '@ng-forge/dynamic-forms/internal';\nimport { DynamicFormLogger } from '@ng-forge/dynamic-forms/internal';\nimport { DynamicFormInstanceRegistry } from './dynamic-form-instance-registry.service';\n\n// Whitespace breaks the space-separated aria-describedby list; punctuation breaks selectors.\nconst INVALID_ID_CHARS = /[^\\w-]+/g;\n\nfunction sanitizeIdPrefix(prefix: string): string {\n return prefix.replace(INVALID_ID_CHARS, '_');\n}\n\nfunction isRendered(host: HTMLElement): boolean {\n if (!host.isConnected) return false;\n if (typeof host.checkVisibility === 'function') return host.checkVisibility();\n const rect = host.getBoundingClientRect();\n return rect.width > 0 || rect.height > 0;\n}\n\n/**\n * Per-form owner of the DOM-id prefix, published through `FORM_ID_PREFIX`.\n * Resolution: explicit `options.idPrefix` (trimmed + sanitized) wins; else the\n * instance's auto-id while more than one form is *visibly* mounted; else `''`.\n *\n * Not latched: a form reverts to unprefixed once it's the only visible one, so\n * navigating away from a multi-form page (incl. ionic's cached `ion-router-outlet`\n * stack) doesn't leave it stuck-prefixed.\n */\n@Injectable()\nexport class FormIdPrefixService {\n private readonly registry = inject(DynamicFormInstanceRegistry);\n private readonly options = inject(FORM_OPTIONS, { optional: true });\n private readonly logger = inject(DynamicFormLogger, { optional: true });\n private readonly host = inject(ElementRef<HTMLElement>).nativeElement;\n\n // Optimistic: counts toward \"multiple present\" until the ResizeObserver confirms\n // whether it's actually rendered. Starting true keeps SSR and the client's first\n // render in agreement (both count-all), avoiding a hydration mismatch; the\n // observer corrects it after layout (a normal reactive update, not a hydration error).\n private readonly visible = signal(true);\n\n private readonly autoId = this.registry.register(this.visible);\n\n private readonly rawPrefix = computed(() => this.options?.()?.idPrefix?.trim() || undefined);\n\n readonly prefix: Signal<string> = computed(() => {\n const explicit = this.rawPrefix();\n if (explicit) return sanitizeIdPrefix(explicit);\n return this.registry.multiplePresent() ? this.autoId : '';\n });\n\n constructor() {\n const destroyRef = inject(DestroyRef);\n destroyRef.onDestroy(() => this.registry.unregister(this.autoId));\n\n // A host's box collapses to 0 on display:none and restores when shown. set() is\n // a no-op when the value is unchanged, so plain resizes don't churn field inputs.\n if (isPlatformBrowser(inject(PLATFORM_ID)) && typeof ResizeObserver !== 'undefined') {\n const ro = new ResizeObserver(() => this.visible.set(isRendered(this.host)));\n ro.observe(this.host);\n destroyRef.onDestroy(() => ro.disconnect());\n }\n\n if (isDevMode()) {\n explicitEffect([this.rawPrefix], ([raw]) => {\n const safe = raw ? sanitizeIdPrefix(raw) : raw;\n if (raw && safe !== raw) {\n this.logger?.warn(`idPrefix \"${raw}\" sanitized to \"${safe}\".`);\n }\n });\n }\n }\n}\n","import { InjectionToken } from '@angular/core';\nimport { createWarningTracker, type WarningTracker } from '@ng-forge/dynamic-forms/internal';\n\n/**\n * Injection token for the derivation warning tracker.\n * Provided at form component level for instance-scoped tracking of derivation warnings\n * (keyed by field path) so the same warning fires once per form rather than every render.\n *\n * @internal\n */\nexport const DERIVATION_WARNING_TRACKER = new InjectionToken<WarningTracker>('DerivationWarningTracker', {\n providedIn: null,\n factory: createWarningTracker,\n});\n","import { computed, Provider, Signal } from '@angular/core';\nimport { EventBus } from '@ng-forge/dynamic-forms/internal';\nimport { ArrayItemRegistryService } from '../core/registry/array-item-registry.service';\nimport { FieldContextRegistryService } from '@ng-forge/dynamic-forms/internal';\nimport { FunctionRegistryService } from '@ng-forge/dynamic-forms/internal';\nimport { RootFormRegistryService } from '@ng-forge/dynamic-forms/internal';\nimport { SchemaRegistryService } from '../core/registry/schema-registry.service';\nimport { FormIdPrefixService } from '../core/registry/form-id-prefix.service';\nimport { FormStateManager, FORM_STATE_DEPS, FormStateDeps } from '../state/form-state-manager';\nimport {\n DEFAULT_PROPS,\n DEFAULT_VALIDATION_MESSAGES,\n DEFAULT_WRAPPERS,\n EXTERNAL_DATA,\n FORM_ID_PREFIX,\n FORM_OPTIONS,\n} from '@ng-forge/dynamic-forms/internal';\nimport { DERIVATION_WARNING_TRACKER } from '../core/derivation/derivation-warning-tracker';\nimport { DEPRECATION_WARNING_TRACKER } from '@ng-forge/dynamic-forms/internal';\nimport { createWarningTracker } from '@ng-forge/dynamic-forms/internal';\nimport { createDerivationRenderGate, DERIVATION_RENDER_GATE } from '../core/derivation/derivation-render-gate';\nimport { CONTAINER_FIELD_PROCESSORS, createContainerFieldProcessors } from '../utils/container-utils/container-field-processors';\nimport { createPropertyOverrideStore, PROPERTY_OVERRIDE_STORE } from '../core/property-derivation/property-override-store';\nimport { HTTP_CONDITION_CACHE, HttpConditionCache } from '@ng-forge/dynamic-forms/internal';\nimport { LogicFunctionCacheService } from '@ng-forge/dynamic-forms/internal';\nimport { HttpConditionFunctionCacheService } from '@ng-forge/dynamic-forms/internal';\nimport { AsyncConditionFunctionCacheService } from '@ng-forge/dynamic-forms/internal';\nimport { DynamicValueFunctionCacheService } from '@ng-forge/dynamic-forms/internal';\nimport { ADDON_ACTION_REGISTRY, createAddonActionRegistry } from '@ng-forge/dynamic-forms/internal';\n\n/**\n * Always-on providers for any DynamicForm: state machine, registries, signal-context tokens.\n * Pure form bootstrap — no derivation, HTTP, or async machinery.\n *\n * @internal\n */\nfunction coreProviders(): Provider[] {\n return [\n EventBus,\n { provide: CONTAINER_FIELD_PROCESSORS, useFactory: createContainerFieldProcessors },\n SchemaRegistryService,\n FunctionRegistryService,\n {\n provide: FORM_STATE_DEPS,\n useFactory: (): FormStateDeps => ({ config: null, formOptions: null, value: null, source: null }),\n },\n FormStateManager,\n {\n provide: RootFormRegistryService,\n useFactory: (stateManager: FormStateManager) =>\n new RootFormRegistryService(\n stateManager.formValue as Signal<Record<string, unknown>>,\n computed(() => stateManager.form()),\n ),\n deps: [FormStateManager],\n },\n FieldContextRegistryService,\n ArrayItemRegistryService,\n {\n provide: DEFAULT_PROPS,\n useFactory: (stateManager: FormStateManager) => computed(() => stateManager.activeConfig()?.defaultProps),\n deps: [FormStateManager],\n },\n {\n provide: DEFAULT_WRAPPERS,\n useFactory: (stateManager: FormStateManager) => computed(() => stateManager.activeConfig()?.defaultWrappers),\n deps: [FormStateManager],\n },\n {\n provide: DEFAULT_VALIDATION_MESSAGES,\n useFactory: (stateManager: FormStateManager) => computed(() => stateManager.activeConfig()?.defaultValidationMessages),\n deps: [FormStateManager],\n },\n {\n provide: FORM_OPTIONS,\n useFactory: (stateManager: FormStateManager) => stateManager.effectiveFormOptions,\n deps: [FormStateManager],\n },\n {\n provide: EXTERNAL_DATA,\n useFactory: (stateManager: FormStateManager) => computed(() => stateManager.activeConfig()?.externalData),\n deps: [FormStateManager],\n },\n // Per-form DOM-id prefix: registers this instance with the root registry for\n // multi-form collision detection and publishes the resolved prefix signal.\n FormIdPrefixService,\n {\n provide: FORM_ID_PREFIX,\n useFactory: (svc: FormIdPrefixService) => svc.prefix,\n deps: [FormIdPrefixService],\n },\n { provide: DEPRECATION_WARNING_TRACKER, useFactory: createWarningTracker },\n // ADDON_ACTION_REGISTRY must live at form scope (not root) because the\n // multi-provider source `ADDON_ACTION_HANDLERS` is contributed by\n // `withAddonActions(...)` features passed to `provideDynamicForm(...)`\n // — those entries are only visible inside the form's own injector tree.\n { provide: ADDON_ACTION_REGISTRY, useFactory: createAddonActionRegistry },\n ];\n}\n\n/**\n * Providers for the value-derivation engine: warning tracker + a lazy bootstrap\n * hook. The heavy orchestrator module (collectors, applicators, cycle detection,\n * and the value/property/HTTP/async stream factories) is NOT statically imported\n * here — it is dynamically imported the first time a config declares a derivation,\n * so forms without derivations never pay for the engine.\n *\n * @internal\n */\nfunction derivationProviders(): Provider[] {\n return [\n { provide: DERIVATION_WARNING_TRACKER, useFactory: createWarningTracker },\n // The render gate lazy-loads + wires the orchestrator on the first config\n // that declares a derivation, and holds `shouldRender` closed until it does\n // (so derivation-bearing fields render already-derived, no flash). The\n // orchestrator is component-scoped (it can't be `providedIn: 'root'`), so the\n // gate loads it with a plain dynamic `import()` + `runInInjectionContext`,\n // not `injectAsync`. `DynamicForm` injects the gate, which wakes the load.\n { provide: DERIVATION_RENDER_GATE, useFactory: createDerivationRenderGate },\n ];\n}\n\n/**\n * Provider for the property-derivation override store. The store is consumed\n * eagerly by field rendering (to apply derived hidden/disabled/readonly state),\n * so it stays eager even though the orchestrator that writes into it is lazy.\n *\n * @internal\n */\nfunction propertyDerivationProviders(): Provider[] {\n return [{ provide: PROPERTY_OVERRIDE_STORE, useFactory: createPropertyOverrideStore }];\n}\n\n/**\n * Providers for HTTP-backed condition expressions and validators.\n * Future: opt-in via withHttpExpressions() at a separate entry point.\n *\n * @internal\n */\nfunction httpExpressionProviders(): Provider[] {\n return [{ provide: HTTP_CONDITION_CACHE, useFactory: () => new HttpConditionCache(100) }, HttpConditionFunctionCacheService];\n}\n\n/**\n * Providers for async (Promise/Observable-returning) condition expressions.\n * Future: opt-in via withAsyncExpressions() at a separate entry point.\n *\n * @internal\n */\nfunction asyncExpressionProviders(): Provider[] {\n return [AsyncConditionFunctionCacheService];\n}\n\n/**\n * Providers for dynamic-value functions (numeric/string validator parameters resolved\n * from formValue expressions, e.g. `min: { expression: 'formValue.minAge' }`).\n * Future: opt-in via withDynamicValues() at a separate entry point.\n *\n * @internal\n */\nfunction dynamicValueProviders(): Provider[] {\n return [DynamicValueFunctionCacheService];\n}\n\n/**\n * Providers for the logic function cache (memoizes compiled state-logic conditions\n * — `when`, `disabled`, `readonly`, `hidden` predicates). Used universally by\n * validator-factory, logic-applicator, schema-application; treated as core for now.\n *\n * @internal\n */\nfunction logicProviders(): Provider[] {\n return [LogicFunctionCacheService];\n}\n\n/**\n * Composes all DynamicForm-level providers. Today: every feature group is included so\n * behavior matches the prior monolithic provider list. The grouped composition exists\n * so future feature flags / secondary entry points can drop groups consumers don't use\n * without re-touching every provider.\n *\n * @internal\n */\nexport function provideDynamicFormDI(): Provider[] {\n return [\n ...coreProviders(),\n ...logicProviders(),\n ...dynamicValueProviders(),\n ...httpExpressionProviders(),\n ...asyncExpressionProviders(),\n ...propertyDerivationProviders(),\n ...derivationProviders(),\n ];\n}\n","import { Injectable } from '@angular/core';\nimport { EventBus } from '@ng-forge/dynamic-forms/internal';\nimport { FormEvent } from '@ng-forge/dynamic-forms/internal';\n\n/**\n * Injectable service for dispatching events into a DynamicForm from outside the form.\n *\n * @Component({\n * providers: [EventDispatcher],\n * template: `<form [dynamic-form]=\"config\" [(value)]=\"formValue\"></form>`\n * })\n * export class MyComponent {\n * readonly dispatcher = inject(EventDispatcher);\n * readonly formValue = signal<Record<string, unknown>>({});\n *\n * constructor() {\n * effect(() => {\n * const category = this.formValue()?.['category'] as string | undefined;\n * if (category) {\n * this.dispatcher.dispatch(\n * arrayEvent('tasks').append([{ key: 'name', type: 'input', label: 'Task', value: category }])\n * );\n * }\n * });\n * }\n * }\n * ```\n *\n * ## Multi-form note\n *\n * If multiple `DynamicForm` instances exist under the same provider scope, **all forms**\n * will receive dispatched events. To target a specific form, scope the provider to a\n * wrapper component that contains only that form.\n *\n * ## What events can be dispatched\n *\n * Any `FormEvent` instance is accepted — array events, reset/clear events, custom events, etc.\n * The `arrayEvent()` builder is the recommended way to construct array manipulation events:\n *\n * ```typescript\n * dispatcher.dispatch(arrayEvent('tasks').append(template));\n * dispatcher.dispatch(arrayEvent('tasks').removeAt(0));\n * dispatcher.dispatch(new FormResetEvent());\n * ```\n */\n@Injectable()\nexport class EventDispatcher {\n private bus: EventBus | undefined;\n\n /**\n * Dispatches a form event into the connected DynamicForm's event bus.\n * No-op if no form is currently connected.\n *\n * @param event - A FormEvent instance. Use the `arrayEvent()` builder for array operations.\n */\n dispatch(event: FormEvent): void {\n this.bus?.emitInstance(event);\n }\n\n /** @internal - Called by DynamicForm on init */\n connect(bus: EventBus): void {\n this.bus = bus;\n }\n\n /** @internal - Called by DynamicForm on destroy */\n disconnect(): void {\n this.bus = undefined;\n }\n}\n","import { Directive, Injectable, inject, input, Signal, signal, TemplateRef } from '@angular/core';\n\n/**\n * Marks an `<ng-template>` projected into `<df-dynamic-form>` content as\n * available to template addons by name.\n */\n@Directive({\n selector: 'ng-template[dfTemplate]',\n exportAs: 'dfTemplate',\n})\nexport class DfTemplate {\n /** Name this template registers under — referenced from `TemplateAddon.templateKey`. */\n readonly dfTemplate = input.required<string>();\n\n /** The wrapped template, captured for registration and `NgTemplateOutlet` rendering. */\n readonly templateRef: TemplateRef<unknown> = inject(TemplateRef);\n}\n\n/**\n * Holder service published at the `<df-dynamic-form>` component scope.\n * `DynamicForm` collects projected `<ng-template dfTemplate=\"...\">` via\n * `contentChildren(DfTemplate)` and pushes the resulting map through\n * `DfFieldTemplateRegistry.set(...)`. The {@link DF_FIELD_TEMPLATES}\n * injection token is provided as a factory that returns this service's\n * `map` signal, so template-addon components can resolve `templateKey`\n * without coupling to the host form's generic class.\n */\n@Injectable()\nexport class DfFieldTemplateRegistry {\n private readonly _map = signal<ReadonlyMap<string, TemplateRef<unknown>>>(new Map());\n\n /** Reactive view consumed by the `template` addon type via DI. */\n readonly map: Signal<ReadonlyMap<string, TemplateRef<unknown>>> = this._map.asReadonly();\n\n /** Replace the registry contents — called from `DynamicForm` when projected templates change. */\n set(value: ReadonlyMap<string, TemplateRef<unknown>>): void {\n this._map.set(value);\n }\n}\n","import {\n afterNextRender,\n ChangeDetectionStrategy,\n Component,\n computed,\n contentChildren,\n DestroyRef,\n EnvironmentInjector,\n inject,\n Injector,\n input,\n model,\n Signal,\n TemplateRef,\n WritableSignal,\n} from '@angular/core';\nimport { FieldDef } from '@ng-forge/dynamic-forms/internal';\nimport { DfFieldOutlet } from './directives/df-field-outlet/df-field-outlet.directive';\nimport { FieldTree } from '@angular/forms/signals';\nimport { outputFromObservable, takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop';\nimport { createSubmissionHandler } from './utils/submission-handler/submission-handler';\nimport { FormConfig, FormOptions } from '@ng-forge/dynamic-forms/internal';\nimport { RegisteredFieldTypes } from '@ng-forge/dynamic-forms/internal';\nimport { EventBus } from '@ng-forge/dynamic-forms/internal';\nimport { FormSubmitEvent } from './events/constants/submit.event';\nimport { ComponentInitializedEvent } from '@ng-forge/dynamic-forms/internal';\nimport { setupInitializationTracking } from '@ng-forge/dynamic-forms/internal';\nimport { InferFormModel } from '@ng-forge/dynamic-forms/internal';\nimport { hasChildFields, isContainerField } from '@ng-forge/dynamic-forms/internal';\nimport { explicitEffect } from 'ngxtension/explicit-effect';\nimport { PageOrchestratorComponent } from './core/page-orchestrator/page-orchestrator.component';\nimport { DERIVATION_RENDER_GATE } from './core/derivation/derivation-render-gate';\nimport { FORM_INITIALIZER } from './providers/form-initializer.token';\nimport { FormClearEvent } from './events/constants/form-clear.event';\nimport { FormResetEvent } from './events/constants/form-reset.event';\nimport { PageChangeEvent } from './events/constants/page-change.event';\nimport { PageNavigationStateChangeEvent } from './events/constants/page-navigation-state-change.event';\nimport { DynamicFormLogger } from '@ng-forge/dynamic-forms/internal';\nimport { FormStateManager, FORM_STATE_DEPS } from './state/form-state-manager';\nimport { provideDynamicFormDI } from './providers/dynamic-form-di';\nimport { FormIdPrefixService } from './core/registry/form-id-prefix.service';\nimport { EventDispatcher } from './events/event-dispatcher';\nimport { DfTemplate, DfFieldTemplateRegistry } from './directives/df-template.directive';\nimport { DF_FIELD_TEMPLATES } from '@ng-forge/dynamic-forms/internal';\n\n/**\n * Renders a form from a {@link FormConfig}. Attribute component on a native\n * `<form>` element (selector `form[dynamic-form]`); state management is\n * delegated to `FormStateManager`.\n *\n * Provide a UI adapter once via {@link provideDynamicForm} (for example\n * `provideDynamicForm(...withMaterialFields())`), then bind a config and\n * listen for `submitted`. Field-value types are inferred from the config when\n * it is authored with `as const satisfies FormConfig`.\n *\n * @example\n * ```ts\n * @Component({\n * imports: [DynamicForm],\n * template: `<form [dynamic-form]=\"config\" (submitted)=\"onSubmit($event)\"></form>`,\n * })\n * export class SignInComponent {\n * config = {\n * fields: [\n * { key: 'email', type: 'input', value: '', label: 'Email', required: true, email: true },\n * { type: 'submit', key: 'submit', label: 'Sign in' },\n * ],\n * } as const satisfies FormConfig;\n *\n * onSubmit(value: InferFormValue<typeof this.config.fields>) {\n * console.log(value); // inferred: { email: string }\n * }\n * }\n * ```\n */\n@Component({\n selector: 'form[dynamic-form]',\n imports: [DfFieldOutlet, PageOrchestratorComponent],\n template: `\n @if (shouldRender()) {\n @switch (formModeDetection().mode) {\n @case ('paged') {\n <div page-orchestrator [pageFields]=\"pageFieldDefinitions()\" [form]=\"form()\" [fieldSignalContext]=\"fieldSignalContext()\"></div>\n }\n @case ('non-paged') {\n @for (field of resolvedFields(); track field.key) {\n <!-- @if + DfFieldOutlet's own renderReady-&-!hidden gate together silence NG01916:\n the template @if removes the host from the DOM (Angular's prescribed pattern), and\n the directive's gate stops a same-CD-pass mount before [formField] can warn.\n Don't dedupe one without the other — see DfFieldOutlet.renderReady. -->\n @if (!field.hidden()) {\n <ng-container *dfFieldOutlet=\"field; environmentInjector: environmentInjector\" />\n }\n }\n }\n @default {\n never;\n }\n }\n }\n `,\n styleUrl: './dynamic-form.component.scss',\n providers: [\n provideDynamicFormDI(),\n DfFieldTemplateRegistry,\n { provide: DF_FIELD_TEMPLATES, useFactory: () => inject(DfFieldTemplateRegistry).map },\n ],\n host: {\n class: 'df-dynamic-form df-form',\n novalidate: '', // Disable browser validation - Angular Signal Forms handles validation\n '[class.disabled]': 'disabled()',\n '[class.df-form-paged]': 'formModeDetection().mode === \"paged\"',\n '[class.df-form-non-paged]': 'formModeDetection().mode === \"non-paged\"',\n '[attr.data-form-mode]': 'formModeDetection().mode',\n '[attr.id]': 'idPrefix() || null',\n '(submit)': 'onNativeSubmit($event)',\n },\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class DynamicForm<\n TFields extends RegisteredFieldTypes[] = RegisteredFieldTypes[],\n TModel extends Record<string, unknown> = InferFormModel<TFields>,\n> {\n // ─────────────────────────────────────────────────────────────────────────────\n // Inputs (must be declared BEFORE deps connection and stateManager injection)\n // ─────────────────────────────────────────────────────────────────────────────\n\n /** Form configuration defining the structure, validation, and behavior. */\n config = input.required<FormConfig<TFields>>({ alias: 'dynamic-form' });\n\n /** Runtime form options that override config options when provided. */\n formOptions = input<FormOptions | undefined>(undefined);\n\n /** Form values for two-way data binding. */\n value = model<Partial<TModel> | undefined>(undefined);\n\n /**\n * How lenient the addon validator is on `config.fields[...].addons`.\n * `'inline'` (default) keeps everything; `'json'` drops code-only types\n * with a warning. Read at form init and on every config swap — change\n * `source` AND the config to force re-validation mid-flight.\n */\n source = input<'inline' | 'json'>('inline');\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Dependencies\n // ─────────────────────────────────────────────────────────────────────────────\n\n private destroyRef = inject(DestroyRef);\n private injector = inject(Injector);\n protected environmentInjector = inject(EnvironmentInjector);\n private eventBus = inject(EventBus);\n private logger = inject(DynamicFormLogger);\n private dispatcher = inject(EventDispatcher, { optional: true });\n private templateRegistry = inject(DfFieldTemplateRegistry);\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Projected templates — collected for the `template` addon type via DI.\n // ─────────────────────────────────────────────────────────────────────────────\n\n /** All `<ng-template dfTemplate=\"...\">` instances projected into this form. */\n private readonly _projectedTemplates = contentChildren(DfTemplate);\n\n private readonly stateManager = this.connectDeps();\n\n /**\n * Owns this form's DOM-id prefix and registers the instance with the root\n * registry for multi-form collision detection. Injected eagerly (not lazily\n * via first field map) so the live-form count is accurate from construction.\n */\n private readonly formIdPrefix = inject(FormIdPrefixService);\n\n /** Active DOM-id prefix — empty unless explicitly set or multiple forms are mounted. */\n protected readonly idPrefix = this.formIdPrefix.prefix;\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Private State\n // ─────────────────────────────────────────────────────────────────────────────\n\n private componentId = 'dynamic-form';\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Signals - Direct pass-through from state manager\n // ─────────────────────────────────────────────────────────────────────────────\n\n /** The currently active config used for form rendering */\n activeConfig = this.stateManager.activeConfig;\n\n /** Current render phase: 'render' = showing form, 'teardown' = hiding old components */\n renderPhase = this.stateManager.renderPhase;\n\n /** Computed form mode detection with validation */\n formModeDetection = this.stateManager.formModeDetection;\n\n /** Page field definitions for paged forms */\n pageFieldDefinitions = this.stateManager.pageFieldDefinitions;\n\n /** Effective form options (merged from config and input) */\n effectiveFormOptions = this.stateManager.effectiveFormOptions;\n\n /** Field signal context for injection into child components */\n fieldSignalContext = this.stateManager.fieldSignalContext;\n\n /** Default values computed from field definitions */\n defaultValues = this.stateManager.defaultValues;\n\n /** The Angular Signal Form instance */\n form = this.stateManager.form;\n\n /** Current form values (reactive) */\n formValue = this.stateManager.formValue;\n\n /** Whether the form is currently valid */\n valid = this.stateManager.valid;\n\n /** Whether the form is currently invalid */\n invalid = this.stateManager.invalid;\n\n /** Whether any form field has been modified */\n dirty = this.stateManager.dirty;\n\n /** Whether any form field has been touched (blurred) */\n touched = this.stateManager.touched;\n\n /** Current validation errors from all fields */\n errors = this.stateManager.errors;\n\n /** Whether the form is disabled (from options or form state) */\n disabled = this.stateManager.disabled;\n\n /** Whether the form is currently submitting */\n submitting = this.stateManager.submitting;\n\n /** Collects errors from async field component loading for error boundary patterns */\n fieldLoadingErrors = this.stateManager.fieldLoadingErrors;\n\n /** Whether to render the form template */\n // `derivationReady` is false only while a derivation-bearing config waits for\n // the lazily-loaded engine to wire, so fields render already-derived (no flash).\n private derivationReady = inject(DERIVATION_RENDER_GATE);\n shouldRender = computed(() => this.stateManager.shouldRender() && this.derivationReady());\n\n /** Resolved fields ready for rendering */\n protected resolvedFields = this.stateManager.resolvedFields;\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Computed Signals - Internal\n // ─────────────────────────────────────────────────────────────────────────────\n\n /**\n * Recursively counts container components that will emit ComponentInitializedEvent.\n * Includes the dynamic-form component itself (+1).\n */\n private totalComponentsCount = computed(() => {\n const fields = this.stateManager.formSetup()?.fields ?? [];\n return countContainersRecursive(fields) + 1;\n });\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Initialization\n // ─────────────────────────────────────────────────────────────────────────────\n\n initialized$ = setupInitializationTracking({\n eventBus: this.eventBus,\n totalComponentsCount: this.totalComponentsCount,\n injector: this.injector,\n componentId: this.componentId,\n });\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Outputs\n // ─────────────────────────────────────────────────────────────────────────────\n\n /** Emits when form validity changes. */\n validityChange = outputFromObservable(toObservable(this.valid));\n\n /** Emits when form dirty state changes. */\n dirtyChange = outputFromObservable(toObservable(this.dirty));\n\n /** Emits form values when submitted (via FormSubmitEvent) and form is valid. */\n submitted = outputFromObservable(this.stateManager.submitted$);\n\n /** Emits when form is reset to default values. */\n reset = outputFromObservable(this.eventBus.on<FormResetEvent>('form-reset'));\n\n /** Emits when form is cleared to empty state. */\n cleared = outputFromObservable(this.eventBus.on<FormClearEvent>('form-clear'));\n\n /** Emits all form events for custom event handling. */\n events = outputFromObservable(this.eventBus.events$);\n\n /**\n * Emits when all form components are initialized and ready for interaction.\n * Useful for E2E testing to ensure the form is fully rendered before interaction.\n */\n initialized = outputFromObservable(this.initialized$);\n\n /** Emits when the current page changes in paged forms. */\n onPageChange = outputFromObservable(this.eventBus.on<PageChangeEvent>('page-change'));\n\n /** Emits when page navigation state changes (canGoNext, canGoPrevious, etc.). */\n onPageNavigationStateChange = outputFromObservable(this.eventBus.on<PageNavigationStateChangeEvent>('page-navigation-state-change'));\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Constructor\n // ─────────────────────────────────────────────────────────────────────────────\n\n constructor() {\n this.dispatcher?.connect(this.eventBus);\n this.destroyRef.onDestroy(() => this.dispatcher?.disconnect());\n\n // Wake feature-provider orchestrators (derivation, property-derivation, …) so their\n // reactive streams are wired before first render. Each feature provider registers\n // itself via FORM_INITIALIZER multi-provider; resolving the array forces DI to\n // construct each entry. Without a feature provider, the array stays empty and no\n // static reference to that feature's classes leaks into this component.\n // Runs after dispatcher.connect() to preserve the prior bootstrap order.\n inject(FORM_INITIALIZER, { optional: true });\n\n this.setupEffects();\n this.setupEventHandlers();\n }\n\n /**\n * Handles native form submission triggered by:\n * - Pressing Enter in an input field\n * - Clicking a button with type=\"submit\"\n * - Programmatic form.submit() calls\n *\n * @param event - The native submit event from the form element\n */\n protected onNativeSubmit(event: Event): void {\n event.preventDefault();\n this.eventBus.dispatch(FormSubmitEvent);\n }\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Private Methods\n // ─────────────────────────────────────────────────────────────────────────────\n\n private setupEffects(): void {\n // Emit initialization event for paged forms\n explicitEffect([this.formModeDetection, this.pageFieldDefinitions], ([{ mode }, pages]) => {\n if (mode === 'paged' && pages.length > 0) {\n this.eventBus.dispatch(ComponentInitializedEvent, 'dynamic-form', this.componentId);\n }\n });\n\n // Emit initialization event for non-paged forms\n explicitEffect([this.resolvedFields, this.formModeDetection], ([fields, { mode }]) => {\n if (mode === 'non-paged' && fields.length > 0) {\n afterNextRender(\n () => {\n this.eventBus.dispatch(ComponentInitializedEvent, 'dynamic-form', this.componentId);\n },\n { injector: this.injector },\n );\n }\n });\n\n // Mirror projected `<ng-template dfTemplate=\"...\">` into the template\n // registry so `template` addons can resolve `templateKey` via DI.\n explicitEffect([this._projectedTemplates], ([templates]) => {\n const map = new Map<string, TemplateRef<unknown>>();\n for (const t of templates) {\n map.set(t.dfTemplate(), t.templateRef);\n }\n this.templateRegistry.set(map);\n });\n }\n\n /**\n * Populates FORM_STATE_DEPS with this component's input signals, then injects\n * FormStateManager. Must be called as a field initializer (after the input signals\n * are declared) so that FormStateManager reads populated deps when it is constructed.\n * inject() is valid here because field initializers run inside the injection context.\n */\n private connectDeps(): FormStateManager<TFields, TModel> {\n const deps = inject(FORM_STATE_DEPS);\n deps.config = this.config as Signal<FormConfig<RegisteredFieldTypes[]>>;\n deps.formOptions = this.formOptions;\n deps.value = this.value as WritableSignal<Partial<unknown> | undefined>;\n deps.source = this.source;\n return inject(FormStateManager<TFields, TModel>);\n }\n\n private setupEventHandlers(): void {\n createSubmissionHandler<TFields, TModel>({\n eventBus: this.eventBus,\n configSignal: this.config,\n formSignal: this.form as Signal<FieldTree<TModel>>,\n validSignal: this.valid,\n logger: this.logger,\n })\n .pipe(takeUntilDestroyed(this.destroyRef))\n .subscribe({\n error: (err) => this.logger.error('Submission handler error', err),\n });\n }\n}\n\n/**\n * Recursively counts container fields (page, row, group, array, wrapper) in a field tree.\n * Descends into container children including array item templates to ensure\n * nested containers are counted for accurate initialization tracking.\n */\nfunction countContainersRecursive(fields: FieldDef<unknown>[]): number {\n let count = 0;\n for (const field of fields) {\n if (isContainerField(field)) {\n count += 1;\n if (hasChildFields(field)) {\n const children = field.fields;\n if (Array.isArray(children)) {\n for (const child of children) {\n if (Array.isArray(child)) {\n // Array item template: FieldDef[] (object items)\n count += countContainersRecursive(child as FieldDef<unknown>[]);\n } else if (child != null && isContainerField(child as FieldDef<unknown>)) {\n count += countContainersRecursive([child as FieldDef<unknown>]);\n }\n }\n }\n }\n }\n }\n return count;\n}\n","import { DynamicFormError } from '@ng-forge/dynamic-forms/internal';\nimport { AddonTypeDefinition } from '@ng-forge/dynamic-forms/internal';\n\n/** Built-in addon types shipped by core. */\nexport const BUILT_IN_ADDON_TYPES: readonly AddonTypeDefinition[] = [\n {\n type: 'text',\n loadComponent: () => import('../addons/text-addon.component').then((m) => m.TextAddonComponent),\n validate: (addon, fieldKey) => {\n const text = (addon as { text?: unknown }).text;\n // Validates structural presence: rejects missing / null / non-string-non-callable,\n // and rejects literal empty strings. DynamicText values that resolve to empty\n // at runtime (signal/observable yielding '') are NOT caught here — they're\n // legal as far as the config is concerned, just render an empty span.\n if (text === undefined || text === null) {\n throw new DynamicFormError(`Addon type 'text' requires a 'text' field (field: '${fieldKey}').`);\n }\n if (typeof text === 'string' && text.length === 0) {\n throw new DynamicFormError(`Addon type 'text' requires a non-empty 'text' string (field: '${fieldKey}').`);\n }\n },\n },\n {\n type: 'template',\n loadComponent: () => import('../addons/template-addon.component').then((m) => m.TemplateAddonComponent),\n validate: (addon, fieldKey) => {\n const key = (addon as { templateKey?: unknown }).templateKey;\n if (typeof key !== 'string' || key.length === 0) {\n throw new DynamicFormError(`Addon type 'template' requires a non-empty 'templateKey' string (field: '${fieldKey}').`);\n }\n },\n },\n {\n type: 'component',\n // The shape carries a function loader — not serialisable; dropped by the\n // validator when the form config originated from JSON.\n jsonSafe: false,\n loadComponent: () => import('../addons/component-addon.component').then((m) => m.ComponentAddonComponent),\n validate: (addon, fieldKey) => {\n const loader = (addon as { component?: unknown }).component;\n if (typeof loader !== 'function') {\n throw new DynamicFormError(`Addon type 'component' requires a 'component' loader function (field: '${fieldKey}').`);\n }\n },\n },\n] as const;\n","import { FieldTypeDefinition } from '@ng-forge/dynamic-forms/internal';\nimport { arrayFieldMapper } from '@ng-forge/dynamic-forms/internal';\nimport { groupFieldMapper } from '@ng-forge/dynamic-forms/internal';\nimport { rowFieldMapper } from '@ng-forge/dynamic-forms/internal';\nimport { pageFieldMapper } from '@ng-forge/dynamic-forms/internal';\nimport { textFieldMapper } from '@ng-forge/dynamic-forms/internal';\nimport { containerFieldMapper } from '@ng-forge/dynamic-forms/internal';\nimport { RowField } from '@ng-forge/dynamic-forms/internal';\nimport { GroupField } from '@ng-forge/dynamic-forms/internal';\nimport { HiddenField } from '@ng-forge/dynamic-forms/internal';\nimport { ArrayField } from '@ng-forge/dynamic-forms/internal';\nimport { PageField } from '@ng-forge/dynamic-forms/internal';\nimport { TextField } from '@ng-forge/dynamic-forms/internal';\nimport { ContainerField } from '@ng-forge/dynamic-forms/internal';\nimport { WrapperTypeDefinition } from '@ng-forge/dynamic-forms/internal';\nimport { CssWrapper } from '@ng-forge/dynamic-forms/internal';\nimport { RowWrapper } from '@ng-forge/dynamic-forms/internal';\n\n/** Built-in field types provided by the dynamic form library. */\n/**\n * Base for container field types that consume the mapper-emitted `field` input\n * (row, group, array, page, container).\n */\nconst CONTAINER_FIELD_TYPES_BASE = {\n renderReadyWhen: ['field'],\n} as const;\n\n/**\n * Base for display-only fields (`text`) that render immediately without\n * waiting for form value integration.\n */\nconst DISPLAY_FIELD_TYPES_BASE = {\n renderReadyWhen: [],\n} as const;\n\n/** Built-in field types provided by the dynamic form library. */\nexport const BUILT_IN_FIELDS: FieldTypeDefinition[] = [\n {\n name: 'row',\n // `row` is a virtual field type: it maps to ContainerFieldComponent, and\n // the rowFieldMapper injects a `{ type: 'row' }` wrapper so the container\n // renders the flex/grid layout via RowWrapperComponent.\n loadComponent: () => import('../fields/container/container-field.component'),\n mapper: rowFieldMapper,\n valueHandling: 'flatten',\n ...CONTAINER_FIELD_TYPES_BASE,\n } satisfies FieldTypeDefinition<RowField>,\n {\n name: 'group',\n loadComponent: () => import('../fields/group/group-field.component'),\n mapper: groupFieldMapper,\n valueHandling: 'include',\n ...CONTAINER_FIELD_TYPES_BASE,\n } satisfies FieldTypeDefinition<GroupField>,\n {\n name: 'array',\n loadComponent: () => import('../fields/array/array-field.component'),\n mapper: arrayFieldMapper,\n valueHandling: 'include',\n ...CONTAINER_FIELD_TYPES_BASE,\n } satisfies FieldTypeDefinition<ArrayField>,\n {\n name: 'page',\n loadComponent: () => import('../fields/page/page-field.component'),\n mapper: pageFieldMapper,\n valueHandling: 'flatten',\n ...CONTAINER_FIELD_TYPES_BASE,\n } satisfies FieldTypeDefinition<PageField>,\n {\n name: 'text',\n loadComponent: () => import('../fields/text/text-field.component'),\n mapper: textFieldMapper,\n valueHandling: 'exclude',\n ...DISPLAY_FIELD_TYPES_BASE,\n } satisfies FieldTypeDefinition<TextField>,\n {\n name: 'hidden',\n // Componentless field - no loadComponent or mapper needed\n valueHandling: 'include',\n } satisfies FieldTypeDefinition<HiddenField>,\n {\n name: 'container',\n loadComponent: () => import('../fields/container/container-field.component'),\n mapper: containerFieldMapper,\n valueHandling: 'flatten',\n ...CONTAINER_FIELD_TYPES_BASE,\n } satisfies FieldTypeDefinition<ContainerField>,\n];\n\nexport const BUILT_IN_WRAPPERS: WrapperTypeDefinition[] = [\n {\n wrapperName: 'css',\n loadComponent: () => import('../wrappers/css/css-wrapper.component'),\n } satisfies WrapperTypeDefinition<CssWrapper>,\n {\n wrapperName: 'row',\n loadComponent: () => import('../wrappers/row/row-wrapper.component'),\n } satisfies WrapperTypeDefinition<RowWrapper>,\n];\n","import { Provider } from '@angular/core';\n\n/**\n * Base interface for dynamic form features.\n * Features are configuration options that can be passed to provideDynamicForm\n * alongside field type definitions.\n */\nexport interface DynamicFormFeature<TKind extends string = string> {\n /** Internal marker to identify this as a feature, not a field type */\n ɵkind: TKind;\n /** Providers to register for this feature */\n ɵproviders: Provider[];\n}\n\n/**\n * Known feature kinds for type safety. Adapter feature modules (and the\n * addon system) contribute their own discriminants via module augmentation.\n */\nexport type DynamicFormFeatureKind =\n | 'logger'\n | 'event-form-value'\n | 'value-exclusion'\n | 'validation-execution'\n | 'legacy-status-classes'\n | 'addons'\n | 'addon-actions';\n\n/** Type guard to check if a value is a DynamicFormFeature */\nexport function isDynamicFormFeature(value: unknown): value is DynamicFormFeature {\n return (\n typeof value === 'object' &&\n value !== null &&\n 'ɵkind' in value &&\n typeof (value as DynamicFormFeature).ɵkind === 'string' &&\n 'ɵproviders' in value &&\n Array.isArray((value as DynamicFormFeature).ɵproviders)\n );\n}\n\n/** Helper to create a feature with proper typing. */\nexport function createFeature<TKind extends string>(kind: TKind, providers: Provider[]): DynamicFormFeature<TKind> {\n return {\n ɵkind: kind,\n ɵproviders: providers,\n };\n}\n","import type { Logger } from '@ng-forge/dynamic-forms/internal';\n\n/** Console-based logger implementation. */\nexport class ConsoleLogger implements Logger {\n private readonly prefix = '[Dynamic Forms]';\n\n debug(message: string, ...args: unknown[]): void {\n console.debug(this.prefix, message, ...args);\n }\n\n info(message: string, ...args: unknown[]): void {\n console.info(this.prefix, message, ...args);\n }\n\n warn(message: string, ...args: unknown[]): void {\n console.warn(this.prefix, message, ...args);\n }\n\n error(message: string, ...args: unknown[]): void {\n console.error(this.prefix, message, ...args);\n }\n}\n","import { LazyComponentLoader, WrapperTypeDefinition } from '@ng-forge/dynamic-forms/internal';\n\n/** User-facing wrapper registration shape used with `createWrappers(...)`. */\nexport interface WrapperRegistration<TName extends string = string, TConfig = unknown> {\n /** Unique identifier for the wrapper type */\n readonly wrapperName: TName;\n /** Lazy-loader for the wrapper component */\n readonly loadComponent: LazyComponentLoader;\n /** Field types this wrapper auto-applies to (merged lowest-priority during wrapper resolution) */\n readonly types?: readonly string[];\n /**\n * Type carrier only — runtime value is always `undefined`.\n * Use `wrapperProps<YourConfig>()` to thread the config type.\n */\n readonly props?: TConfig;\n}\n\n/** Branded bundle returned by `createWrappers(...)`. */\nexport interface WrappersBundle<T extends readonly WrapperRegistration[] = readonly WrapperRegistration[]> {\n readonly ɵkind: 'wrappers';\n readonly ɵregistrations: T;\n readonly ɵdefinitions: readonly WrapperTypeDefinition[];\n}\n\n/**\n * Bundle wrapper registrations into a single object that can be passed to\n * `provideDynamicForm(...)`.\n */\nexport function createWrappers<const T extends readonly WrapperRegistration[]>(...registrations: T): WrappersBundle<T> {\n return {\n ɵkind: 'wrappers',\n ɵregistrations: registrations,\n ɵdefinitions: registrations.map(({ wrapperName, loadComponent, types }) => ({\n wrapperName,\n loadComponent,\n types,\n })),\n };\n}\n\n/** Type guard for a `WrappersBundle`. */\nexport function isWrappersBundle(value: unknown): value is WrappersBundle {\n return (\n typeof value === 'object' &&\n value !== null &&\n 'ɵkind' in value &&\n (value as { ɵkind: unknown }).ɵkind === 'wrappers' &&\n 'ɵdefinitions' in value &&\n Array.isArray((value as { ɵdefinitions: unknown }).ɵdefinitions)\n );\n}\n\n/** Derive the `FieldRegistryWrappers` augmentation shape from a wrapper bundle. */\nexport type InferWrapperRegistry<T> =\n T extends WrappersBundle<infer R>\n ? {\n [Reg in R[number] as Reg['wrapperName']]: Reg extends { props: infer P }\n ? Exclude<P, undefined>\n : { readonly type: Reg['wrapperName'] };\n }\n : never;\n","import { EnvironmentProviders, inject, makeEnvironmentProviders, Provider, Type } from '@angular/core';\nimport { ADDON_TYPE_COMPONENT_CACHE, ADDON_TYPE_REGISTRY, AddonTypeDefinition } from '@ng-forge/dynamic-forms/internal';\nimport { FIELD_REGISTRY, FieldTypeDefinition } from '@ng-forge/dynamic-forms/internal';\nimport { BUILT_IN_ADDON_TYPES } from './built-in-addons';\nimport { BUILT_IN_FIELDS, BUILT_IN_WRAPPERS } from './built-in-fields';\nimport { FieldDef } from '@ng-forge/dynamic-forms/internal';\nimport { DynamicFormFeature, isDynamicFormFeature } from './features/dynamic-form-feature';\nimport { ADDON_TYPE_DEFINITIONS } from '@ng-forge/dynamic-forms/internal';\nimport { DynamicFormLogger } from '@ng-forge/dynamic-forms/internal';\nimport { ConsoleLogger } from './features/logger/console-logger';\nimport type { InferFormValue as RealInferFormValue } from '@ng-forge/dynamic-forms/internal';\nimport {\n isWrapperTypeDefinition,\n WrapperTypeDefinition,\n WRAPPER_AUTO_ASSOCIATIONS,\n WRAPPER_REGISTRY,\n WrapperConfig,\n} from '@ng-forge/dynamic-forms/internal';\nimport { isWrappersBundle, WrappersBundle } from '../wrappers/create-wrappers';\n\n// Re-export global types for module augmentation\nexport type { DynamicFormFieldRegistry, AvailableFieldTypes } from '@ng-forge/dynamic-forms/internal';\nexport type { RegisteredFieldTypes } from '@ng-forge/dynamic-forms/internal';\n\n/** Extract FieldDef type from FieldTypeDefinition */\ntype ExtractFieldDef<T> = T extends FieldTypeDefinition<infer F> ? F : never;\n\n/** Union of all FieldDef types from provided field types */\ntype FieldDefUnion<T extends FieldTypeDefinition[]> = ExtractFieldDef<T[number]>;\n\n/** Infer form value type from field definitions using the real inference type. */\ntype InferFormValue<TFieldDefs extends FieldDef<unknown>[]> = RealInferFormValue<TFieldDefs>;\n\n/** Provider result with type inference */\ntype ProvideDynamicFormResult<T extends FieldTypeDefinition[]> = EnvironmentProviders & {\n __fieldDefs?: FieldDefUnion<T>;\n __formValue?: InferFormValue<FieldDefUnion<T>[]>;\n};\n\n/** Union type for items that can be passed to provideDynamicForm */\ntype FieldTypeOrFeature = FieldTypeDefinition | WrapperTypeDefinition | WrappersBundle | DynamicFormFeature;\n\n/** Extract only FieldTypeDefinition items from a tuple type */\ntype ExtractFieldTypes<T extends FieldTypeOrFeature[]> = {\n [K in keyof T]: T[K] extends FieldTypeDefinition ? T[K] : never;\n}[number] extends infer U\n ? U extends FieldTypeDefinition\n ? U[]\n : never\n : never;\n\n/**\n * Provider function to configure the dynamic form system with field types and options.\n *\n * @param items - Field type definitions and/or features (like withLoggerConfig)\n * @returns Environment providers for dependency injection with type inference\n * @typeParam T - Array of field type definitions for type inference\n */\nexport function provideDynamicForm<const T extends FieldTypeOrFeature[]>(\n ...items: T\n): ProvideDynamicFormResult<ExtractFieldTypes<T> extends FieldTypeDefinition[] ? ExtractFieldTypes<T> : FieldTypeDefinition[]> {\n // Separate field types, wrapper types, wrapper bundles, and features\n const fieldTypes = items.filter(\n (item): item is FieldTypeDefinition => !isDynamicFormFeature(item) && !isWrapperTypeDefinition(item) && !isWrappersBundle(item),\n );\n const wrapperTypes = items.filter(isWrapperTypeDefinition);\n const wrapperBundles = items.filter(isWrappersBundle);\n const features = items.filter(isDynamicFormFeature);\n\n const fields = [...BUILT_IN_FIELDS, ...fieldTypes];\n const wrappers = [...BUILT_IN_WRAPPERS, ...wrapperTypes, ...wrapperBundles.flatMap((bundle) => bundle.ɵdefinitions)];\n\n // Extract providers from features (includes config features like material-config, bootstrap-config, etc.)\n const featureProviders: Provider[] = [];\n const hasLoggerFeature = features.some((feature) => feature.ɵkind === 'logger');\n features.forEach((feature) => {\n featureProviders.push(...feature.ɵproviders);\n });\n\n // Default logger provider (ConsoleLogger) if no logger feature was provided\n const defaultLoggerProvider: Provider[] = hasLoggerFeature ? [] : [{ provide: DynamicFormLogger, useValue: new ConsoleLogger() }];\n\n return makeEnvironmentProviders([\n ...defaultLoggerProvider,\n {\n provide: FIELD_REGISTRY,\n useFactory: () => {\n const logger = inject(DynamicFormLogger);\n const registry = new Map();\n // Add custom field types\n fields.forEach((fieldType) => {\n if (registry.has(fieldType.name)) {\n logger.warn(`Field type \"${fieldType.name}\" is already registered. Overwriting.`);\n }\n registry.set(fieldType.name, fieldType);\n });\n // Back-compat: legacy camelCase array-action discriminants resolve to their\n // kebab-case canonical definitions. kebab is canonical as of 1.0; the camelCase\n // spellings are deprecated aliases kept so older/serialized configs keep working.\n const legacyTypeAliases: Record<string, string> = {\n addArrayItem: 'add-array-item',\n prependArrayItem: 'prepend-array-item',\n insertArrayItem: 'insert-array-item',\n removeArrayItem: 'remove-array-item',\n popArrayItem: 'pop-array-item',\n shiftArrayItem: 'shift-array-item',\n };\n for (const [legacy, canonical] of Object.entries(legacyTypeAliases)) {\n const def = registry.get(canonical);\n if (def && !registry.has(legacy)) registry.set(legacy, def);\n }\n return registry;\n },\n },\n // Always provide default Wrapper classes\n {\n provide: WRAPPER_REGISTRY,\n useFactory: () => {\n const logger = inject(DynamicFormLogger);\n const registry = new Map();\n // Add custom wrapper types\n wrappers.forEach((wrapperType) => {\n if (registry.has(wrapperType.wrapperName)) {\n logger.warn(`Wrapper type \"${wrapperType.wrapperName}\" is already registered. Overwriting.`);\n }\n registry.set(wrapperType.wrapperName, wrapperType);\n });\n return registry;\n },\n },\n // Pre-computed reverse index for auto-association lookup — built once so\n // resolveWrappers is O(1) per field render instead of scanning every\n // registered wrapper.\n {\n provide: WRAPPER_AUTO_ASSOCIATIONS,\n useFactory: () => {\n const autoMap = new Map<string, WrapperConfig[]>();\n for (const wrapperType of wrappers) {\n if (!wrapperType.types) continue;\n for (const fieldType of wrapperType.types) {\n const existing = autoMap.get(fieldType) ?? [];\n existing.push({ type: wrapperType.wrapperName } as WrapperConfig);\n autoMap.set(fieldType, existing);\n }\n }\n return autoMap;\n },\n },\n // Form-scoped addon type registry. Built from BUILT_IN_ADDON_TYPES plus\n // every kind contributed by `withCustomAddon(...)` features (via the\n // ADDON_TYPE_DEFINITIONS multi-provider). Overrides the empty\n // `providedIn: 'root'` default so contributions from form-scoped\n // providers are visible.\n {\n provide: ADDON_TYPE_REGISTRY,\n useFactory: () => {\n const logger = inject(DynamicFormLogger);\n const contributed = inject(ADDON_TYPE_DEFINITIONS, { optional: true }) ?? [];\n const registry = new Map<string, AddonTypeDefinition>();\n for (const def of BUILT_IN_ADDON_TYPES) {\n registry.set(def.type, def);\n }\n for (const def of contributed) {\n if (registry.has(def.type)) {\n logger.warn(`Addon type \"${def.type}\" is already registered. Overwriting.`);\n }\n registry.set(def.type, def);\n }\n return registry;\n },\n },\n // Per-form addon-component cache (no root fallback — isolated by form scope).\n { provide: ADDON_TYPE_COMPONENT_CACHE, useFactory: () => new Map<string, Type<unknown>>() },\n ...featureProviders,\n ]) as ProvideDynamicFormResult<ExtractFieldTypes<T> extends FieldTypeDefinition[] ? ExtractFieldTypes<T> : FieldTypeDefinition[]>;\n}\n\n/** Extract FieldDef types from provider result */\nexport type ExtractFieldDefs<T> = T extends { __fieldDefs?: infer F } ? F : never;\n\n/** Extract form value type from provider result */\nexport type ExtractFormValue<T> = T extends { __formValue?: infer V } ? V : never;\n","import { InjectionToken } from '@angular/core';\nimport { createFeature, DynamicFormFeature } from '../dynamic-form-feature';\nimport { DynamicFormLogger } from '@ng-forge/dynamic-forms/internal';\nimport { ConsoleLogger } from './console-logger';\nimport { NoopLogger } from '@ng-forge/dynamic-forms/internal';\nimport { DerivationLogConfig, DerivationLogLevel, createDefaultDerivationLogConfig } from '@ng-forge/dynamic-forms/internal';\n\n/**\n * Injection token for derivation logging configuration.\n *\n * @internal\n */\nexport const DERIVATION_LOG_CONFIG = new InjectionToken<DerivationLogConfig>('DERIVATION_LOG_CONFIG', {\n providedIn: 'root',\n factory: createDefaultDerivationLogConfig,\n});\n\n/** Configuration options for the logger feature. */\nexport interface LoggerConfigOptions {\n /**\n * Whether general logging is enabled.\n *\n * @default true\n */\n enabled?: boolean;\n\n /**\n * Derivation logging level.\n *\n * @default 'none'\n */\n derivations?: DerivationLogLevel;\n}\n\n/**\n * Configure logging for dynamic forms.\n *\n * @param config - Boolean to enable/disable logging, a function returning boolean, or a config object\n * @returns A DynamicFormFeature that configures logging\n */\nexport function withLoggerConfig(config: boolean | (() => boolean) | LoggerConfigOptions = true): DynamicFormFeature<'logger'> {\n // Handle boolean or function\n if (typeof config === 'boolean' || typeof config === 'function') {\n const isEnabled = typeof config === 'function' ? config() : config;\n const logger = isEnabled ? new ConsoleLogger() : new NoopLogger();\n return createFeature('logger', [{ provide: DynamicFormLogger, useValue: logger }]);\n }\n\n // Handle config object\n const isEnabled = config.enabled !== false;\n const logger = isEnabled ? new ConsoleLogger() : new NoopLogger();\n\n const providers: { provide: InjectionToken<unknown>; useValue: unknown }[] = [{ provide: DynamicFormLogger, useValue: logger }];\n\n // Add derivation log config if specified\n if (config.derivations !== undefined) {\n providers.push({\n provide: DERIVATION_LOG_CONFIG,\n useValue: { level: config.derivations },\n });\n }\n\n return createFeature('logger', providers);\n}\n","import { createFeature, DynamicFormFeature } from '../dynamic-form-feature';\nimport { EMIT_FORM_VALUE_ON_EVENTS } from '@ng-forge/dynamic-forms/internal';\n\n/**\n * Enables automatic form value attachment to all events dispatched through the EventBus.\n *\n * @remarks\n * **Precedence rules:**\n * 1. Per-form `false` - Disables for this form, regardless of global setting\n * 2. Per-form `true` - Enables for this form, regardless of global setting\n * 3. Per-form `undefined` - Uses global setting\n * 4. Global `withEventFormValue()` - Enables globally\n * 5. No global feature - Disabled globally (default)\n * @returns A DynamicFormFeature that enables form value attachment to events\n */\nexport function withEventFormValue(): DynamicFormFeature<'event-form-value'> {\n return createFeature('event-form-value', [{ provide: EMIT_FORM_VALUE_ON_EVENTS, useValue: true }]);\n}\n","import { ValueExclusionConfig } from '@ng-forge/dynamic-forms/internal';\nimport { createFeature, DynamicFormFeature } from '../dynamic-form-feature';\nimport { VALUE_EXCLUSION_DEFAULTS } from './value-exclusion.token';\n\n/**\n * Configures global value exclusion defaults for form submission output.\n *\n * @remarks\n * **Precedence rules:**\n * 1. Per-field `excludeValueIf*` on `FieldDef` — wins for that field\n * 2. Per-form `excludeValueIf*` on `FormOptions` — wins for all fields in that form\n * 3. Global `withValueExclusionDefaults()` — baseline default\n * 4. No global feature — uses token default (all enabled)\n * @param config - Partial override of exclusion rules. Unspecified properties default to `true`.\n * @returns A DynamicFormFeature that configures value exclusion defaults\n */\nexport function withValueExclusionDefaults(config?: Partial<ValueExclusionConfig>): DynamicFormFeature<'value-exclusion'> {\n const resolved = {\n excludeValueIfHidden: config?.excludeValueIfHidden ?? true,\n excludeValueIfDisabled: config?.excludeValueIfDisabled ?? true,\n excludeValueIfReadonly: config?.excludeValueIfReadonly ?? true,\n };\n\n return createFeature('value-exclusion', [{ provide: VALUE_EXCLUSION_DEFAULTS, useValue: resolved }]);\n}\n","import { ResolvedValidationExecutionConfig, ValidationExecutionConfig } from '@ng-forge/dynamic-forms/internal';\nimport { createFeature, DynamicFormFeature } from '../dynamic-form-feature';\nimport { VALIDATION_EXECUTION_DEFAULTS } from './validation-execution.token';\n\n/**\n * Configures global validation execution defaults — when validators should run\n * relative to a field's reactive state.\n *\n * @remarks\n * **Precedence rules:**\n * 1. Per-field `FieldDef.validateWhenHidden` — wins for that field; inherits down to descendants\n * that don't override.\n * 2. Per-form `FormOptions.validateWhenHidden` — root inherited value for the form.\n * 3. Global `withValidationExecutionDefaults()` — fallback when no form/field setting.\n * 4. No feature configured — uses token default (`validateWhenHidden: false`).\n * @param config - Partial override. Unspecified properties keep their defaults.\n * @returns A `DynamicFormFeature` that configures validation execution defaults.\n */\nexport function withValidationExecutionDefaults(config?: Partial<ValidationExecutionConfig>): DynamicFormFeature<'validation-execution'> {\n const resolved: ResolvedValidationExecutionConfig = {\n validateWhenHidden: config?.validateWhenHidden ?? false,\n };\n\n return createFeature('validation-execution', [{ provide: VALIDATION_EXECUTION_DEFAULTS, useValue: resolved }]);\n}\n","import { provideSignalFormsConfig } from '@angular/forms/signals';\nimport { NG_STATUS_CLASSES } from '@angular/forms/signals/compat';\nimport { createFeature, DynamicFormFeature } from '../dynamic-form-feature';\n\n/**\n * Opt into Angular Signal Forms' legacy `.ng-touched` / `.ng-invalid` /\n * `.ng-dirty` / `.ng-pristine` / `.ng-untouched` / `.ng-pending` /\n * `.ng-valid` CSS classes on form-bound elements.\n *\n * @returns A DynamicFormFeature that enables the legacy `ng-*` class strategy\n */\nexport function withLegacyStatusClasses(): DynamicFormFeature<'legacy-status-classes'> {\n return createFeature('legacy-status-classes', [...provideSignalFormsConfig({ classes: NG_STATUS_CLASSES })]);\n}\n","/** Zero-cost type carrier used in wrapper registrations. */\nexport function wrapperProps<T>(): T {\n return undefined as unknown as T;\n}\n","import { AsyncPipe } from '@angular/common';\nimport { ChangeDetectionStrategy, Component, input } from '@angular/core';\nimport { TextAddon } from '@ng-forge/dynamic-forms/internal';\nimport { DynamicTextPipe } from '@ng-forge/dynamic-forms/internal';\nimport { WrapperFieldInputs } from '@ng-forge/dynamic-forms/internal';\n\n/** Renderer for the universal `text` addon type. */\n@Component({\n selector: 'df-text-addon',\n imports: [AsyncPipe, DynamicTextPipe],\n template: `<span>{{ addon().text | dynamicText | async }}</span>`,\n host: {\n '[attr.aria-hidden]': '\"true\"',\n },\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class TextAddonComponent {\n readonly addon = input.required<TextAddon>();\n /** Accepted for contract uniformity — `NgComponentOutlet` setInput is strict; every kind must declare it. */\n readonly fieldInputs = input<WrapperFieldInputs | undefined>();\n}\n","import { NgTemplateOutlet } from '@angular/common';\nimport { ChangeDetectionStrategy, Component, computed, inject, input } from '@angular/core';\nimport { explicitEffect } from 'ngxtension/explicit-effect';\nimport { TemplateAddon } from '@ng-forge/dynamic-forms/internal';\nimport { DF_FIELD_TEMPLATES } from '@ng-forge/dynamic-forms/internal';\nimport { DynamicFormLogger } from '@ng-forge/dynamic-forms/internal';\nimport { WrapperFieldInputs } from '@ng-forge/dynamic-forms/internal';\n\n/** Renderer for the universal `template` addon type. */\n@Component({\n selector: 'df-template-addon',\n imports: [NgTemplateOutlet],\n template: `\n @if (template(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl; context: outletContext()\" />\n }\n `,\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class TemplateAddonComponent {\n private readonly templates = inject(DF_FIELD_TEMPLATES, { optional: true });\n private readonly logger = inject(DynamicFormLogger);\n\n readonly addon = input.required<TemplateAddon>();\n /** Forwarded by `df-addon-slot`; surfaced to the template as its implicit `$implicit` context. */\n readonly fieldInputs = input<WrapperFieldInputs | undefined>();\n\n // Pure computed: pure lookup, no side effects. Unresolved → null; the\n // `@if` in the template skips the outlet and renders nothing.\n protected readonly template = computed(() => {\n const map = this.templates?.();\n const key = this.addon().templateKey;\n return map?.get(key) ?? null;\n });\n\n /** Implicit context payload — template authors bind via `let-fi`. */\n protected readonly outletContext = computed(() => ({ $implicit: this.fieldInputs() }));\n\n constructor() {\n // Logging side effect lives in an effect, not the computed — keeps\n // the computed pure so signal-graph evaluations don't duplicate warnings.\n explicitEffect([this.template, this.addon], ([tpl, addon]) => {\n if (!tpl) {\n this.logger.warn(\n `Template addon: no <ng-template dfTemplate=\"${addon.templateKey}\"> found. Did you project it as a child of <df-dynamic-form>?`,\n );\n }\n });\n }\n}\n","import { NgComponentOutlet } from '@angular/common';\nimport { ChangeDetectionStrategy, Component, computed, inject, input, signal, Type } from '@angular/core';\nimport { explicitEffect } from 'ngxtension/explicit-effect';\nimport type { ComponentAddon } from '@ng-forge/dynamic-forms/internal';\nimport { DynamicFormLogger } from '@ng-forge/dynamic-forms/internal';\nimport { resolveDefaultExport } from '../utils/wrapper-chain/wrapper-chain';\nimport type { WrapperFieldInputs } from '@ng-forge/dynamic-forms/internal';\n\n/**\n * Renderer for the universal `component` addon type.\n *\n * @codeOnly The `component` loader is a function; this kind is dropped in\n * lenient validation when the config originated from JSON.\n */\n@Component({\n selector: 'df-component-addon',\n imports: [NgComponentOutlet],\n template: `\n @if (resolvedComponent(); as cmp) {\n <ng-container *ngComponentOutlet=\"cmp; inputs: inputs()\" />\n }\n `,\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class ComponentAddonComponent {\n private readonly logger = inject(DynamicFormLogger);\n\n readonly addon = input.required<ComponentAddon>();\n /** Accepted for contract uniformity — `NgComponentOutlet` setInput is strict; every kind must declare it. */\n readonly fieldInputs = input<WrapperFieldInputs | undefined>();\n\n protected readonly inputs = computed(() => (this.addon().inputs ?? {}) as Record<string, unknown>);\n\n /**\n * Latest async-loaded `{ addon, component }` pair. Tagging the result with\n * its source addon lets `resolvedComponent` filter stale loads structurally\n * — same kind-tagging pattern `DfAddonSlot` uses. When `addon` swaps\n * mid-load, the prior loader still completes and writes here, but the\n * computed ignores it because the recorded addon no longer matches the\n * current one.\n */\n private readonly asyncLoaded = signal<{ addon: ComponentAddon; component: Type<unknown> } | undefined>(undefined);\n\n protected readonly resolvedComponent = computed<Type<unknown> | undefined>(() => {\n const current = this.addon();\n const loaded = this.asyncLoaded();\n return loaded?.addon === current ? loaded.component : undefined;\n });\n\n constructor() {\n explicitEffect([this.addon], ([addon]) => {\n Promise.resolve(addon.component())\n .then((mod) => {\n const component = resolveDefaultExport(mod);\n if (component) this.asyncLoaded.set({ addon, component });\n })\n .catch((error: unknown) => {\n this.logger.warn(\n `Failed to load component addon (slot: '${addon.slot}'): ${error instanceof Error ? error.message : String(error)}`,\n );\n });\n });\n }\n}\n","import { AddonTypeDefinition } from '@ng-forge/dynamic-forms/internal';\nimport { BaseAddon } from '@ng-forge/dynamic-forms/internal';\nimport { createFeature, DynamicFormFeature } from '../dynamic-form-feature';\nimport { ADDON_TYPE_DEFINITIONS } from '@ng-forge/dynamic-forms/internal';\n\n/** Register a custom addon type with the form. */\nexport function withCustomAddon<T extends BaseAddon>(definition: AddonTypeDefinition<T>): DynamicFormFeature<'addons'> {\n return createFeature('addons', [\n {\n provide: ADDON_TYPE_DEFINITIONS,\n useValue: definition as AddonTypeDefinition,\n multi: true,\n },\n ]);\n}\n","import { createFeature, DynamicFormFeature } from '../dynamic-form-feature';\nimport { AddonActionHandler, ADDON_ACTION_HANDLERS } from '@ng-forge/dynamic-forms/internal';\n\n/**\n * Typed feature returned by {@link withAddonActions}. Carries the\n * registered handler keys as a phantom type so consumers can derive the\n * `DynamicFormActionRegistry` augmentation in one line instead of hand-typing\n * every key.\n */\nexport type AddonActionsFeature<K extends string = string> = DynamicFormFeature<'addon-actions'> & {\n /** @internal Phantom field carrying the registered keys; never read at runtime. */\n readonly __handlerKeys?: K;\n};\n\n/** Register named action handlers reachable via JSON-driven addon configs. */\nexport function withAddonActions<const H extends Record<string, AddonActionHandler>>(handlers: H): AddonActionsFeature<keyof H & string> {\n return createFeature('addon-actions', [\n {\n provide: ADDON_ACTION_HANDLERS,\n useValue: handlers,\n multi: true,\n },\n ]) as AddonActionsFeature<keyof H & string>;\n}\n","import { inject } from '@angular/core';\nimport { FieldDef } from '@ng-forge/dynamic-forms/internal';\nimport { ADDON_TYPE_REGISTRY } from '@ng-forge/dynamic-forms/internal';\nimport { FormConfig } from '@ng-forge/dynamic-forms/internal';\nimport { FIELD_REGISTRY } from '@ng-forge/dynamic-forms/internal';\nimport { Logger } from '@ng-forge/dynamic-forms/internal';\nimport { AddonWarning, formatAddonWarning } from './addon-warning';\nimport { walkAndValidateAddons } from './validate-field-addons';\n\n/** Options accepted by {@link sanitizeFormConfig}. */\nexport interface SanitizeFormConfigOptions {\n /**\n * Whether the config originated from JSON (server-driven) or inline\n * TypeScript code.\n */\n source?: 'json' | 'inline';\n}\n\n/** Result of a `sanitizeFormConfig` call. */\nexport interface SanitizedFormConfig {\n /** Sanitized config — invalid addons stripped from the field tree. */\n sanitized: FormConfig;\n /** Structured list of every addon that was dropped, and why. */\n warnings: readonly AddonWarning[];\n}\n\n/**\n * Walk a `FormConfig`, validate every field's addons against the active\n * `FIELD_REGISTRY` and `ADDON_TYPE_REGISTRY`, and return a sanitized copy\n * plus a list of dropped-addon warnings.\n *\n * @Component({ ... })\n * class AdminFormBuilder {\n * private readonly check = (cfg: FormConfig) => {\n * const { sanitized, warnings } = sanitizeFormConfig(cfg, { source: 'json' });\n * if (warnings.length > 0) {\n * this.diagnostics.set(warnings);\n * }\n * return sanitized;\n * };\n * }\n * ```\n */\nexport function sanitizeFormConfig(config: FormConfig, options: SanitizeFormConfigOptions = {}): SanitizedFormConfig {\n const fieldRegistry = inject(FIELD_REGISTRY);\n const typeRegistry = inject(ADDON_TYPE_REGISTRY);\n const source = options.source ?? 'inline';\n\n const { fields, warnings } = walkAndValidateAddons(\n (config.fields ?? []) as readonly FieldDef<unknown>[],\n fieldRegistry,\n typeRegistry,\n source,\n );\n\n return {\n sanitized: { ...config, fields: fields as FormConfig['fields'] },\n warnings,\n };\n}\n\n/**\n * Default warning sink. Pass a `DynamicFormLogger` to route through the\n * form's configured logger (respects `NoopLogger` opt-out); omit it to\n * fall back to `console.warn` for build-time / admin-UI use cases that\n * run outside an Angular injection context.\n */\nexport function logAddonWarnings(warnings: readonly AddonWarning[], logger?: Logger): void {\n for (const w of warnings) {\n if (logger) {\n logger.warn(formatAddonWarning(w));\n } else {\n console.warn('[Dynamic Forms]', formatAddonWarning(w));\n }\n }\n}\n\n/**\n * DI-free variant of {@link sanitizeFormConfig} for tooling that lives\n * outside Angular's injector — build-time linters, MCP-side checks, CLI\n * scripts.\n */\nexport function sanitizeFormConfigPure(\n config: FormConfig,\n fieldRegistry: ReadonlyMap<string, import('@ng-forge/dynamic-forms/internal').FieldTypeDefinition>,\n typeRegistry: ReadonlyMap<string, import('@ng-forge/dynamic-forms/internal').AddonTypeDefinition>,\n options: SanitizeFormConfigOptions = {},\n): SanitizedFormConfig {\n const source = options.source ?? 'inline';\n const { fields, warnings } = walkAndValidateAddons(\n (config.fields ?? []) as readonly FieldDef<unknown>[],\n fieldRegistry,\n typeRegistry,\n source,\n );\n return { sanitized: { ...config, fields: fields as FormConfig['fields'] }, warnings };\n}\n\nexport { formatAddonWarning } from './addon-warning';\nexport type { AddonWarning } from './addon-warning';\nexport { validateFieldAddons, walkAndValidateAddons } from './validate-field-addons';\n","import { AppendArrayItemEvent, ArrayItemDefinitionTemplate } from './constants/append-array-item.event';\nimport { PrependArrayItemEvent } from './constants/prepend-array-item.event';\nimport { InsertArrayItemEvent } from './constants/insert-array-item.event';\nimport { MoveArrayItemEvent } from './constants/move-array-item.event';\nimport { PopArrayItemEvent } from './constants/pop-array-item.event';\nimport { ShiftArrayItemEvent } from './constants/shift-array-item.event';\nimport { RemoveAtIndexEvent } from './constants/remove-at-index.event';\n\n/**\n * Builder for array manipulation events.\n *\n * @param arrayKey - The key of the array field to operate on\n * @returns An object with methods for all 7 array operations\n */\nexport function arrayEvent(arrayKey: string) {\n return {\n /**\n * Append a new item at the END of the array.\n * This is the most common operation for adding items.\n *\n * @param template - Template for the new item (REQUIRED)\n * - Single field: Creates a primitive item (field's value is extracted directly)\n * - Array of fields: Creates an object item (fields merged into object)\n * @returns An AppendArrayItemEvent to dispatch\n */\n append: <T extends ArrayItemDefinitionTemplate>(template: T) => new AppendArrayItemEvent(arrayKey, template),\n\n /**\n * Prepend a new item at the BEGINNING of the array.\n * Use when new items should appear at the start.\n *\n * @param template - Template for the new item (REQUIRED)\n * - Single field: Creates a primitive item (field's value is extracted directly)\n * - Array of fields: Creates an object item (fields merged into object)\n * @returns A PrependArrayItemEvent to dispatch\n */\n prepend: <T extends ArrayItemDefinitionTemplate>(template: T) => new PrependArrayItemEvent(arrayKey, template),\n\n /**\n * Insert a new item at a SPECIFIC INDEX in the array.\n * Use when you need precise control over item placement.\n *\n * @param index - The position at which to insert the new item\n * @param template - Template for the new item (REQUIRED)\n * - Single field: Creates a primitive item (field's value is extracted directly)\n * - Array of fields: Creates an object item (fields merged into object)\n * @returns An InsertArrayItemEvent to dispatch\n */\n insertAt: <T extends ArrayItemDefinitionTemplate>(index: number, template: T) => new InsertArrayItemEvent(arrayKey, index, template),\n\n /**\n * Remove the LAST item from the array.\n * Equivalent to JavaScript's `Array.pop()`.\n *\n * @returns A PopArrayItemEvent to dispatch\n */\n pop: () => new PopArrayItemEvent(arrayKey),\n\n /**\n * Remove the FIRST item from the array.\n * Equivalent to JavaScript's `Array.shift()`.\n *\n * @returns A ShiftArrayItemEvent to dispatch\n */\n shift: () => new ShiftArrayItemEvent(arrayKey),\n\n /**\n * Remove an item at a SPECIFIC INDEX from the array.\n * Use when you need to remove a specific item by position.\n *\n * @param index - The position of the item to remove\n * @returns A RemoveAtIndexEvent to dispatch\n */\n removeAt: (index: number) => new RemoveAtIndexEvent(arrayKey, index),\n\n /**\n * Move an existing item from one position to another within the array.\n * This is an atomic reorder — the item is NOT destroyed and recreated.\n * The resolved component, form value, and stored template are preserved.\n *\n * @param from - The current index of the item to move\n * @param to - The target index to move the item to\n * @returns A MoveArrayItemEvent to dispatch\n */\n move: (from: number, to: number) => new MoveArrayItemEvent(arrayKey, from, to),\n };\n}\n"]}
|