@ng-forge/dynamic-forms 1.2.0-next.7 → 1.2.0-next.9
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/fesm2022/{ng-forge-dynamic-forms-array-field.component-DK7dAQv7.mjs → ng-forge-dynamic-forms-array-field.component-6yyDriVB.mjs} +2 -2
- package/fesm2022/{ng-forge-dynamic-forms-array-field.component-DK7dAQv7.mjs.map → ng-forge-dynamic-forms-array-field.component-6yyDriVB.mjs.map} +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-bootstrap-derivation-orchestrator-BfxYqAQA.mjs → ng-forge-dynamic-forms-bootstrap-derivation-orchestrator-BB8jAcY4.mjs} +2 -2
- package/fesm2022/{ng-forge-dynamic-forms-bootstrap-derivation-orchestrator-BfxYqAQA.mjs.map → ng-forge-dynamic-forms-bootstrap-derivation-orchestrator-BB8jAcY4.mjs.map} +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-container-field.component-Cj2msZp_.mjs → ng-forge-dynamic-forms-container-field.component-DfQ0y-77.mjs} +2 -2
- package/fesm2022/{ng-forge-dynamic-forms-container-field.component-Cj2msZp_.mjs.map → ng-forge-dynamic-forms-container-field.component-DfQ0y-77.mjs.map} +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-form-schema.service-C3cVSmft.mjs → ng-forge-dynamic-forms-form-schema.service-BzbBv-r0.mjs} +2 -2
- package/fesm2022/{ng-forge-dynamic-forms-form-schema.service-C3cVSmft.mjs.map → ng-forge-dynamic-forms-form-schema.service-BzbBv-r0.mjs.map} +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-group-field.component-DN6X506r.mjs → ng-forge-dynamic-forms-group-field.component-DxHD5I8E.mjs} +3 -3
- package/fesm2022/{ng-forge-dynamic-forms-group-field.component-DN6X506r.mjs.map → ng-forge-dynamic-forms-group-field.component-DxHD5I8E.mjs.map} +1 -1
- package/fesm2022/ng-forge-dynamic-forms-internal.mjs +5 -1
- package/fesm2022/ng-forge-dynamic-forms-internal.mjs.map +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-ng-forge-dynamic-forms-DCX7tvaB.mjs → ng-forge-dynamic-forms-ng-forge-dynamic-forms-tA8ZkP8i.mjs} +464 -163
- package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-tA8ZkP8i.mjs.map +1 -0
- package/fesm2022/{ng-forge-dynamic-forms-page-field.component-D16ey8r-.mjs → ng-forge-dynamic-forms-page-field.component-DCdBvJ-h.mjs} +2 -2
- package/fesm2022/{ng-forge-dynamic-forms-page-field.component-D16ey8r-.mjs.map → ng-forge-dynamic-forms-page-field.component-DCdBvJ-h.mjs.map} +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-schema-builder-DO-su9sF.mjs → ng-forge-dynamic-forms-schema-builder-IsjNBo0h.mjs} +2 -2
- package/fesm2022/{ng-forge-dynamic-forms-schema-builder-DO-su9sF.mjs.map → ng-forge-dynamic-forms-schema-builder-IsjNBo0h.mjs.map} +1 -1
- package/fesm2022/ng-forge-dynamic-forms.mjs +1 -1
- package/package.json +1 -1
- package/types/ng-forge-dynamic-forms-internal.d.ts +16 -2
- package/types/ng-forge-dynamic-forms.d.ts +48 -3
- package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-DCX7tvaB.mjs.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ng-forge-dynamic-forms-ng-forge-dynamic-forms-tA8ZkP8i.mjs","mappings":";;;;;;;;;;;;AA6CE;AACA;AACD;;ACvCK;;AACgD;AACpD;AACF;;ACXA;AACA;;AAQE;AACA;AAAyC;AACzC;AACF;AAEA;;AAEE;AACF;;ACaM;AACJ;AACE;;AAGF;;;;;;;AASE;;;AAGJ;AAQM;;AACuB;;;AAE3B;AACF;;ACtDA;;AAemB;AACA;AAEjB;AACE;;;AAEE;AACF;;;;AAIqB;AACrB;AAEA;AACE;;YAGA;AACA;AACF;;AAGM;;;;AAIJ;;;AAGF;AACA;;AAGM;;AAEN;;;AAEA;;AACA;AACA;;AAEA;AACA;AACA;;AACA;AACA;;AAGM;;AAEN;AAAc;AAEd;;AAGI;AACE;AAA0D;;AAE9D;AAGF;;AAEA;;;;;;;;;AC7EJ;AAGA;AAGA;AAaM;;AACS;AACb;AAA2B;AAC3B;;AACqB;;AAErB;AACF;AAGM;IAOJ;AACE;;;AAIF;AACA;AAEA;;AAKE;;AAGF;AACF;AAGA;AAGA;;AAM+C;;AACW;;AAG1D;;ACzEO;;ACkBP;AAEA;;;AAGI;;AAEA;;AAEF;AACF;;AAIE;AACE;;AAEJ;AAaM;AACJ;AACF;AAGM;AACJ;AACF;AAGO;;AAOL;AAAY;;AAGZ;AAAa;;AAGb;AAAiB;AAEjB;;AAEE;AAAe;AACf;AACF;AACA;AACA;AACF;AAQM;AAOJ;AAA0B;;;;;;AAYhB;;AAEF;AACF;AAIR;AAmCM;;AAEJ;;AAGE;AACA;AACG;;;AAGL;AACF;AAEA;AAME;AACE;;;;;;;AAcA;AACD;AACD;AAEA;gBAGE;;AACA;;AAGF;;;YAOA;AAEA;;;;;AAOQ;;;AAKc;;;;AAKxB;AASA;AACE;AACA;AACE;;AACA;AACA;;AAEJ;AAOM;;AACmC;AACvC;AACF;AAGA;AAEA;AAEqB;AACA;;AAEjB;;;;AAIO;AACP;;AACiC;AACjC;;AAEH;;AChLK;AACJ;AACA;AAEA;AACA;;YAMA;AACG;AACA;wBAIC;AACE;;;;AAGA;;AAEA;YAEA;AACA;;AAEJ;;AAGF;AACE;AACA;AACF;AACF;AAoCA;;AAEI;AACA;AACA;AACA;AACA;AACA;;AAEJ;AAEA;AACE;AAEI;AACA;AACA;AACD;AAGL;AAMA;;AAEA;AAEA;AACE;;;;gBAOE;;;AAIF;AAGF;AAQA;;AAEE;;;AAOE;AAAoB;AACpB;;IAIF;;AAEE;AACA;;;;gBAaA;AAAuD;AACvD;;AAEA;;AAGF;AAAoC;IAEpC;AAA4B;;AAG1B;AACA;;;AAGA;AACA;AACA;;AAED;AACD;AACA;AACF;AAEA;;AACwB;AACtB;AAA4C;;AAE9C;AAOA;;;AAEE;AACE;AAA6B;AAC/B;AACF;;AC9QM;AAMJ;;AAIA;;AAME;;;AAKA;AACA;AACA;;;;AAMJ;;ACXA;;;;AAWE;AASA;;;;IAeS;AAET;AAcE;AAEA;;AAEE;;;AAIoB;AACpB;AACA;AAAkC;;;gCAQpC;AAA+B;AAC/B;gBAGA;AACA;AACA;;;AAGC;;AAED;;AAGF;AAQE;AACA;;AACA;;;AAEA;AACE;;;;wCAQF;;;AAGA;;AAGF;AAOE;AACA;AAAiC;;AAEjC;;AACc;AACd;;;;AAOF;AAgBE;;;AAEA;AACA;;;AAKA;;;AAEA;;AAGF;AAME;;;AAEA;AACA;AACA;;AAGF;AAME;AACA;AAAkC;AAClC;AACA;AACA;;AAGM;AACN;AACA;AACA;AAAkE;;;AAGhE;AACA;AACA;;;AAII;;;AAEN;AACA;AACG;;;AAGN;;AC7NK;;;AAUA;;;;;;AAKF;AAGJ;;ACjBM;AACJ;AACF;AAGM;AACJ;AACF;AAGM;;AACoB;;AACC;;AACA;;AACgC;AACzD;AACF;;;AAWE;QAEE;;AAEA;;AAEA;;AAEA;;AACA;;AAEE;;;AAMJ;AACF;;;AAKA;AASM;AACJ;AACA;AACF;AAWM;AAOJ;AACE;;;;;;AAME;;;AAGJ;AACF;;ACtFA;AAEM;AACJ;AACA;AAAY;;AAEV;AACA;AACA;AACA;AACA;AACA;;AAEJ;AAEM;;;AAIA;AACA;AACA;AACA;AACA;AACA;;AAGN;;;;;;;ACAmB;;AAEA;AACA;AACA;;AAEA;;AAEA;AAEA;;;;;AAKA;;AACA;;IAGA;IAOA;;AASA;AACA;;;;IAGA;;;AAGA;;;;IAMA;;AAEf;;;;AAOI;;;;;AASW;;AACA;;AAGb;AACF;IAIe;AAKjB;AACE;;;;;;;;;AASE;AACE;AACA;;AASH;QAGD;QAKA;AACE;;AACA;AACA;;AAEE;;AAEJ;;AAIE;;;AAII;AAAgC;AAClC;AACF;AACA;;AAEE;AACA;AACF;AACF;;;;AAMI;AACA;AAAmB;;;AAEnB;;AAEJ;AAEA;;IAIM;AACN;AACA;AAAc;AACd;AAA8C;;;;;;;;AAjKvC;AACT;AACD;;;ACFD;AACE;AACA;AACF;AAYA;AAGE;AACE;;AAIA;AACF;AACF;AAQM;AAIJ;gBAMA;AAEI;gBAIA;AACE;;gBAKF;AACE;AACA;;;wDAcF;AAEI;AACA;;;AAKV;;;;AClHC;;ACKM;AACL;AACA;AACD;AAQM;AACL;AACA;AACD;;AA6BC;AACA;AACA;;AAGE,QAMA;AACE;;AAGF,QAMA;AACE;;AAGF;;;AAWI;;YAIF;AACE;;;;AAMA;;;;AAKA;;AAGF;;AAEK;;AAEA;;AAEG;;AAGF;AACF;AACC;;AAED;;AAGF;AACA;;;;;;AAMJ,QAMA;AACE;;AAGF;;;AASA,QAKA;AACE;;AAEA;;AAGF,QAKA;AACE;;;AAGN;;AC5IO;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;AACE;;;;;;;;AA4BE;AACF;;AAME;;;IASF;;AAEE;AACA;;;AAGA;;AAEF;AACE;;AAEF;AACE;;AAEF;AACE;;AAEF;AACE;;AAEF;AACE;;QAIF;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;;AC9OA;AACE;AACE;AACA;AACA;YAEE;AACE;AAA0B;;AACrB;;;AAIT;AACA;AACA;AAA6B;AACxB;AAAc;;;;AAGrB;AACF;;AAiCE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;;AAEI;AAAoC;AACpC;AACA;AACF;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AAEA;;AACgB;oCAId;AACE;;;;AAKJ;AACE;;;;AAGF;AACE;;;;QAMF;AACF;;AAGE;AACE;;AACA;AAEA;;;;;;;AASN;;;ACjIoB;AAEA;AAEA;;AANlB;;;;;;;;AAQD;;;ACAoB;;AAAnB;;;AACD;;ACDM;AACL;AACA;AACD;;ACNK;;AAGJ;;;;AAGS;AACL;;;AAIJ;AACF;;AC8BA;;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;;;;;;;;;ACYJ;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;;ACrKM;;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;;;ACnJE;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;;AC9EA;AAOE;;;QAQE;;;;AAKE;AACA;wBAGA;;AAEF;;;;;AAME;;;AAIA;AACE;AACE;;;;AAKN;;wBAME;;AAEA;AACE;;;;AAGE;;;;;AAKV;AAGA;AACE;AACE;;;;;AAMI;;AAGR;AAQA;AACE;AACA;AAEA;AACE;AACE;;AAEF;;AAGF;AACE;;;;;AAOI;;AAGR;AAOA;AACE;AACA;AAEA;AACE;AACE;;AAEF;;AAGF;AACE;;;;;;;AASI;;AAGR;AAOA;AACE;;;AAIA;;AAEI;;;AAIJ;;AAEE;;AAIJ;;AAWE;AACE;AACA;;AAEA;AACA;;;;AAMA;;;;;;AAQI;;AAIN;AACA;IAEA;;;;AAKM;;;AAIR;;ACjOM;;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;;AAQE;;AAEE;AACE;;AAGF;AAGF;AACF;AAeA;;AAOE;AACE;;;;AAIA;;AAEA;;AAEE;;;AAIF;;AAGF;AACF;AAQA;AACE;AACA;AAEA;AACA;AACE;;AAGF;AACA;AACE;;AAGF;AACA;AACE;;AAGF;AACF;AAeM;;;;;AAiBJ;AACE;AACA;;;AAIA;;QAGA;;QAGA;;;;;;QAQA;;AAEE;;AAGE;AACA;;;;;;;;;;AAcF;AACA;;;;;;AAaE;;;;gBAOJ;;;;;;AASF;AACF;;ACvMO;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;;;ACjEmB;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;;QAIF;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;;oBAGA;;;AAGA;gFAMA;;AAEE;;;AAEF;AACA;AACE;AAA+D;;;;AAKnE;;AAGF;AACE;;;AAGP;AAGK;AAGJ;AACF;;ACnWO;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;;;;;IAOK;AACP;AACA;;AAEA;;;AAGF;;IASS;AACP;;;;;AAOA;;;;;IAUO;AACP;;AAGA;;;AAIA;AACE;;;;;wCAWF;;AAEE;;;AAGA;;AAGF;AACE;AACA;;AAGF;;;IAIe;;;;AAIf;AACE;;;;;AAKF;;AACA;AAAiD;AACjD;;;IAIO;;;;IAgBQ;;AAEf;AAAgD;AAEhD;AACA;AACE;;AACA;;AAEF;;;IAIe;AACf;;AAC4D;AAE5D;AACA;;;;AAIC;;;IAIc;AACf;;AAC4D;AAE5D;;;;;AAKC;;;AAGH,IAKiB;;AAEf;AAAiH;;AAGjH;AACA;;;AAGF;;;;;IAgBiB;AAEjB,IAKiB;;AAEf;AACE;AACA;;AAEF;AACA;AACA;;;gBAQO;;AAUL;AACA;AACA;AACA;;;;;;AAWI;;;;AAIF;;AAGF;AACF;AAEE;AACA;AACD;;AAMH,IAKiB;AACf;AACA;AACA;AAAsE;;AAGpE;;;AAGA;;;;AAIJ;AAOE;AACE;oCAIA;AACF;AAGF,IAOiB;IAGA;AACf;;AAEE;AACA;;;AAGA;AACA;;AAEF;AACE;;AAEF;;;IAIO;;AAC6C;AACpD;AACA;AAAc;;;;IAKP;;AAC6C;AACpD;AAA+B;AAC/B;AAAkD;AAClD;;;;;IAOO;AACP;AACA;AACA;;;IAIM;AAER,IAMS;AACP;;AAEA;;;AAIE;;AAEA;AACE;AACA;AAEA;;;AAIA;AACA;;;;;;;IAaW;;;;IAMR;AACP;AACA;AACA;AACA;AAEA;AACE;;wBAMF;AAEA;;;;;wBASA;;;;IAce;AACf;AACA;AACA;AACA;AAEA;AACE;;AAGF;AACA;;;;;AAKA;;;;IAcF;;;;IAiBS;;IAGA;;;;IAMA;;AAEP;;;;IAKO;;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;gBAOA;;AAEG;;QAIH;;;;;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;;;;AAOA;AAEA;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;;;gEAOI;AACA;;;AAGJ;;AAKE;AACE;;AACA;AACA;;;;;;AAMF;;;;AAKF;;AAEG;;AAGH;;AAEG;;;AAIG;;AAEJ;AACE;AACF;;;;;AAOF;AACE;;AAEA;;AAGF;;;AAIA;;;AAGA;;;;;;;AASM;AACN;;;;;AAwBE;AACA;gDAIE;AACA;;;;;AAKF;;;AAIF;AACA;;AAEA;AACA;;AAGE;AACA;AACA;;;;;;AAOI;;AAEJ;AACA;AACA;AACA;;;;;AAUF;;;;;;;;;;AC9sCe;AACA;AACA;;AAEA;;IAIA;;IAGA;;;;;;AAQjB;;;;AAUA,IAKS;AACP;AAEA;;;AAGF,IAIS;AACP;AACA;AACG;;;;;IAMY;AASf;;;;;;;AAYI;AAEE;;AAEA;;AAEA;AAEA;;AAGF;;;AAIA;AACF;AACF;;;AAMM;;;;;AAON;;AACyC;;;;AAIzC;AAAe;AAEf;;;IAKe;;;AAGf;;;;;;;;IAWO;AACP;;AAEA;;;;;AAQE;;AAEA;AACA;;;;AAIJ;;AASA,IASQ;AACN;AACA;;AAGE;;QAIF;gBAIA;AACE;AACA;AACE;AACA;AACE;;;;AAKN;;IAIO;;;;;AAKT,IAKS;;;;;IAOA;AACP;AACA;AACA;AAEA;;;AAGE;;AAEA;AACD;;;AAGH;;wBAOE;AACA;AACE;AACF;gBAIA;AACE;AACA;;;AAEF;QAGA;;;gBAII;AACA;AACE;;;AAGN;;AAGF;gBAQE;;;AAGI;AACA;AACA;;;AAIJ;AACA;AAEA;;AAEI;;AAEA;;;;AAMJ;;AAEI;;AAEA;;;;AAKJ;;AAGF;AAME;AACA;AAEA;;AAEI;;AAEA;;;;AAMJ;;AAEI;;AAEA;;;;AAKJ;;AAGF;;AAkBE;gBAIA;AACE;;QAIF;AACE;;AAGF;;AAEE;;AAGF;AACA;AAEA;AACE;;QAIF;;AAGE;AACA;AACA;;;AAIJ,IASQ;AACN;AACA;;;AAKI;;AAEA;;;QAKJ;;;AAGI;;;;;QAOJ;;AAEI;AACA;;;QAKJ;AACA;QAGA;;AAGE;AACA;;;AAIJ;AAKE;;AAGA;;YAGE;;;;;AAMF;;;QAMA;;;;;AAKE;QAGF;;;;;AAKE;gCAMF;AACE;;;;AAGF;;;;AAQF,IAOQ;QAEN;AACE;;QAIF;QAGA;AACE;;QAIF;YAEE;AACE;AACE;;;;YAMJ;;AAGA;AACE;;;AAIJ;;;AAnhBS;AART;AACE;;;AAGD;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAZF;;;;;;AA7BS;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCR;AACA;AACA;;AAGA;AACE;;AAEA;AACD;;;;AC1CC;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;;ACwBD;;;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;;;AAOD;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;;;ACtJU;AAER,IAMA;AACE;;IAIF;AACE;;;AAKA;;;;;;;;;;;;ICnDO;;;;;;AATA;AACT;AACA;AACD;;;AAoBkB;;IAGR;IAGT;AACE;;;;;;;;;;;;;;;ACsHF;;YAYQ;AACA;AACE;AACF;AACA;;AAEA;AACA;;;AASS;AAEjB,IAKiB;IAGE;;gBAanB;IAGA;IAGA;IAGA;IAGA;IAGA;IAGA;IAGA;IAGA;IAGA;IAGA;IAGA;IAGA;IAGA;IAGA;IAGA;IAGA;YAKQ;;;IAIE;AAEV,IAKmB;;YAQF;;;;AAGA;AACf;AACA;AAAa;AAEb;AAA+C;AAE/C;AACA;AACE;AACA;;AAEF;;;;;;;IA4BF;IAGA;;AAKA,IAKA;AAEA,IAOA;IAGA;IAGA;YAMA;AACE;;AAEG;AACA;AACC;;AAEE;AAAsB;AACtB;AACA;AACA;AACF;AACF;AACF;;AAEG;AACA;;AAGH;;wCAeA;;;;;AAOF,IAQU;;AAER;;AAGF;AAOE;AACA;;IAIQ;;;;;;;AAaJ;;AAEJ;;AAIE;;AAGM;;;AAKR;gBAIA;AACE;AACA;AACE;;AAEF;AACF;;AAGF;AAOE;AACA;AACA;AACA;AACA;AACA;;;AAIA;;;;;;;AAOG;AACA;AACC;AACD;;;AA5VM;AAhBT;;AAEA;AACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AANA;;;;AAnCS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsCR;;AAEA;;AAGA;wCAEA;AACA;AACA;AACA;AACA;AACA;;;;ACjIG;AACL;AACE;AACA;AACA;AACE;;AAME;;;AAGA;;;AAGL;AACD;AACE;AACA;AACA;AACE;;AAEE;;;AAGL;AACD;AACE;gBAGA;AACA;AACA;AACE;AACA;AACE;;;AAGL;;;ACpBH;;;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;AAC7C;AACE;AACA;AACA;AACmD;;;ACzEjD;AACJ;AAEE;AACA;AACA;AACA;;AAGJ;AAGM;;AAEF;AACA;;AAEJ;;;;ACzCE;AACE;;AAGF;AACE;;AAGF;AACE;;AAGF;AACE;;AAEH;;ACOK;;AAEF;AACA;AACA;;;;AAIC;;AAEL;AAGM;AACJ;AAEE;AACA;;AAEA;;AAGJ;;ACSM;IAIJ;;;;;;;;AAaA;AACA;;AAEA;;AAKA;AACE;AACA;AACE;;AAEE;AACA;gBAEA;;;;;AAMA;gDAIA;AACE;AACA;AACA;AACA;AACA;AACA;;AAEF;;;AAEoC;;AAEpC;;AAEH;QAED;AACE;;AAEE;AACA;gEAKA;AACA;gBAGA;AACA;AACE;;AAEE;AACE;;AAEF;;AAEF;AACF;AACA;;AAEH;wBAID;AACE;;AAEE;AACA;;;AAEE;;;AAGE;;;AAGJ;;AAEH;wCAMD;AACE;;AAEE;AACA;AACA;AACA;;;AAGA;;;;;;AAOA;;AAEH;QAED;AACA;AACD;AACH;;;AClLE;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;;ACSM;;AAEJ;AACF;;ACOM;AAKJ;AACE;;AAEA;AACA;;AAGF;AACF;;;AC/CE;AACA;AACE;;AAEH;;ACKK;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;;;ACzFoB;AACA;;;;;;AAEnB;;;ACkBmB;AAEA;;AAJlB;;;;;;AAMD;;;;AC3BA;;;;ACAA;;;;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;;ACEK;;AAEF,QASA;AAEA,QASA;AAEA,QAUA;AAEA;AAQA;AAQA,QAOA;AAEA,QASA;;AAEJ","names":[],"ignoreList":[],"sources":["../../../../packages/dynamic-forms/src/lib/providers/features/field-windowing/field-windowing.token.ts","../../../../packages/dynamic-forms/src/lib/providers/features/clamp-window.ts","../../../../packages/dynamic-forms/src/lib/providers/features/field-windowing/field-parking-margin.ts","../../../../packages/dynamic-forms/src/lib/providers/features/field-windowing/resolve-field-windowing.ts","../../../../packages/dynamic-forms/src/lib/directives/df-field-outlet/field-viewport-observer.service.ts","../../../../packages/dynamic-forms/src/lib/utils/resolve-wrappers/resolve-wrappers.ts","../../../../packages/dynamic-forms/src/lib/utils/dev-mode.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/utils/emit-initialization/emit-initialization.ts","../../../../packages/dynamic-forms/src/lib/utils/container-utils/container-utils.ts","../../../../packages/dynamic-forms/src/lib/directives/df-field-outlet/field-parking-state.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/utils/inject-field-registry/inject-field-registry.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/utils/preload-form-components/preload-form-components.ts","../../../../packages/dynamic-forms/src/lib/events/constants/page-change.event.ts","../../../../packages/dynamic-forms/src/lib/events/constants/pager-state.event.ts","../../../../packages/dynamic-forms/src/lib/providers/features/page-preload/page-preload.token.ts","../../../../packages/dynamic-forms/src/lib/utils/page-utils/collect-leaf-field-keys.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/flattener/field-flattener.ts","../../../../packages/dynamic-forms/src/lib/utils/default-value/default-value.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/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/page-orchestrator/page-orchestrator.component.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/page-preload/with-page-preload.ts","../../../../packages/dynamic-forms/src/lib/providers/features/field-windowing/with-field-windowing.ts","../../../../packages/dynamic-forms/src/lib/providers/features/validation-execution/validation-execution.token.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/constants/active-page-initialized.event.ts","../../../../packages/dynamic-forms/src/lib/events/constants/go-to-page.event.ts","../../../../packages/dynamic-forms/src/lib/events/constants/next-page.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/array-event.ts"],"sourcesContent":["import { InjectionToken } from '@angular/core';\n\n/**\n * Global default configuration for progressive field mounting (\"field\n * windowing\"): fields beyond `eager` render as `@defer (on viewport)`\n * placeholders instead of mounting immediately, and mount only once scrolled\n * near/into view.\n *\n * @internal\n */\nexport interface FieldWindowingConfig {\n /** Whether field windowing is active. */\n readonly enabled: boolean;\n /** Number of leading fields mounted eagerly (not windowed). */\n readonly eager: number;\n /** CSS `min-height` reserved for each unmounted field's placeholder. */\n readonly placeholderHeight: string;\n /** Parking: hold scrolled-away fields out of change detection, DOM intact. */\n readonly park: FieldParkingConfig;\n}\n\n/**\n * Parking config. `@defer (on viewport)` only ever mounts — it never unmounts —\n * so without parking a form that has been scrolled through ends up with every\n * field live again, and the windowing win is handed back for the rest of the\n * session. Parking closes that: a field that scrolls away leaves change\n * detection but keeps its DOM, so it stays findable, autofillable and\n * reachable by assistive tech.\n *\n * Off in this default, on whenever `withFieldWindowing()` is used. Parking\n * suspends model → DOM updates for a scrolled-away field until it returns, so\n * it is a rendering behaviour change and is not something a form should get\n * without asking. It exists to bound what windowing leaves mounted, which is\n * why enabling windowing enables it too.\n *\n * @internal\n */\nexport interface FieldParkingConfig {\n /** Whether scrolled-away fields are held out of change detection. */\n readonly enabled: boolean;\n /** `IntersectionObserver` rootMargin — how far outside the viewport stays live. */\n readonly margin: string;\n}\n\nexport const FIELD_WINDOWING = new InjectionToken<FieldWindowingConfig>('FIELD_WINDOWING', {\n providedIn: 'root',\n factory: () => ({ enabled: false, eager: 12, placeholderHeight: '4rem', park: { enabled: false, margin: '100%' } }),\n});\n","/**\n * Normalizes a preload/eager window size to a non-negative integer.\n *\n * Guards the non-finite cases `Math.floor` lets through: `NaN` (which would make\n * every `<= window` comparison false, mounting nothing) and `Infinity` (which\n * would remove the bound, mounting everything). Both fall back to `fallback`, as\n * does `undefined`.\n */\nexport function clampWindowSize(value: number | undefined, fallback: number): number {\n if (value === undefined || !Number.isFinite(value)) return fallback;\n return Math.max(0, Math.floor(value));\n}\n","const DEFAULT_MARGIN = '100%';\nconst ROOT_MARGIN_PART = /^[+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:e[+-]?\\d+)?(?:px|%)$/i;\n\n/**\n * IntersectionObserver accepts one to four whitespace-separated pixel or\n * percentage lengths. Invalid public configuration must not make its\n * constructor throw and abort form rendering.\n */\nexport function normalizeFieldParkingMargin(value: string | undefined, fallback = DEFAULT_MARGIN): string {\n const normalizedFallback = isValidRootMargin(fallback) ? fallback.trim() : DEFAULT_MARGIN;\n if (!value || !isValidRootMargin(value)) return normalizedFallback;\n return value.trim().split(/\\s+/).join(' ');\n}\n\nfunction isValidRootMargin(value: string): boolean {\n const parts = value.trim().split(/\\s+/);\n return parts.length >= 1 && parts.length <= 4 && parts.every((part) => ROOT_MARGIN_PART.test(part));\n}\n","import { FieldParkingConfig, FieldWindowingConfig } from './field-windowing.token';\nimport { clampWindowSize } from '../clamp-window';\nimport { normalizeFieldParkingMargin } from './field-parking-margin';\n\n/** Per-form override shape for parking. `false` opts out; an object tunes the margin. */\nexport type FieldParkingOption =\n | boolean\n | {\n /** IntersectionObserver root margin. Supports one to four `px` or `%` values. */\n margin?: string;\n };\n\n/** Per-form override shape for `FormOptions.fieldWindowing`. */\nexport type FieldWindowingOption = boolean | { eager?: number; placeholderHeight?: string; park?: FieldParkingOption };\n\n/**\n * Resolves the effective field-windowing config for a form: per-form\n * `FormOptions.fieldWindowing` wins over the global `withFieldWindowing()`\n * default (`global`, the injected `FIELD_WINDOWING` value).\n *\n * - `undefined` — use `global` as-is\n * - `false` — force-disable for this form\n * - `true` — force-enable, using `global`'s `eager` / `placeholderHeight` / `park`\n * - object — force-enable; per-form keys win over `global`'s\n *\n * A `park`-only object is the one exception: parking and progressive mounting\n * are independent, so tuning parking must not silently switch a form over to\n * deferred mounting. `{ park: false }` means \"don't park\", not \"start\n * deferring everything\".\n */\nexport function resolveFieldWindowing(global: FieldWindowingConfig, perForm: FieldWindowingOption | undefined): FieldWindowingConfig {\n if (perForm === undefined) {\n return global;\n }\n\n if (typeof perForm === 'boolean') {\n return { ...global, enabled: perForm };\n }\n\n const configuresMounting = 'eager' in perForm || 'placeholderHeight' in perForm || Object.keys(perForm).length === 0;\n\n return {\n enabled: configuresMounting ? true : global.enabled,\n eager: clampWindowSize(perForm.eager, global.eager),\n placeholderHeight: perForm.placeholderHeight ?? global.placeholderHeight,\n park: resolveFieldParking(global.park, perForm.park),\n };\n}\n\n/**\n * A form inherits whatever parking the global config sets — off by default, on\n * when `withFieldWindowing()` is in play — and can override it either way.\n * `{ park: true }` on a form that isn't windowed is legal and does park; the two\n * are independent axes.\n */\nexport function resolveFieldParking(global: FieldParkingConfig, perForm: FieldParkingOption | undefined): FieldParkingConfig {\n if (perForm === undefined) return global;\n if (typeof perForm === 'boolean') return { ...global, enabled: perForm };\n return { enabled: true, margin: normalizeFieldParkingMargin(perForm.margin, global.margin) };\n}\n","import { DestroyRef, inject, Injectable, PLATFORM_ID } from '@angular/core';\nimport { isPlatformBrowser } from '@angular/common';\nimport { distinctUntilChanged, Observable, of } from 'rxjs';\nimport { normalizeFieldParkingMargin } from '../../providers/features/field-windowing/field-parking-margin';\n\nconst ALWAYS_VISIBLE = of(true);\n\ntype VisibilityListener = (visible: boolean) => void;\n\ninterface ObserverEntry {\n readonly observer: IntersectionObserver;\n readonly listeners: Map<Element, Set<VisibilityListener>>;\n}\n\n/**\n * Shares one native observer per margin. Subscriptions own element cleanup;\n * SSR and unsupported browsers always report visible.\n */\n@Injectable()\nexport class FieldViewportObserver {\n private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID)) && typeof IntersectionObserver !== 'undefined';\n private readonly observers = new Map<string, ObserverEntry>();\n\n constructor() {\n inject(DestroyRef).onDestroy(() => {\n for (const { observer } of this.observers.values()) observer.disconnect();\n this.observers.clear();\n });\n }\n\n observe(el: Element, rootMargin: string): Observable<boolean> {\n if (!this.isBrowser) return ALWAYS_VISIBLE;\n rootMargin = normalizeFieldParkingMargin(rootMargin);\n\n return new Observable<boolean>((subscriber) => {\n const listener: VisibilityListener = (visible) => subscriber.next(visible);\n this.startObserving(el, rootMargin, listener);\n // Native callbacks are asynchronous. Keep fields live until the first one.\n subscriber.next(true);\n return () => this.stopObserving(el, rootMargin, listener);\n }).pipe(distinctUntilChanged());\n }\n\n private startObserving(el: Element, rootMargin: string, listener: VisibilityListener): void {\n const entry = this.observerFor(rootMargin);\n const listeners = entry.listeners.get(el);\n if (listeners) {\n listeners.add(listener);\n return;\n }\n entry.listeners.set(el, new Set([listener]));\n entry.observer.observe(el);\n }\n\n private stopObserving(el: Element, rootMargin: string, listener: VisibilityListener): void {\n const entry = this.observers.get(rootMargin);\n if (!entry) return;\n const listeners = entry.listeners.get(el);\n if (!listeners) return;\n listeners.delete(listener);\n if (listeners.size > 0) return;\n\n entry.observer.unobserve(el);\n entry.listeners.delete(el);\n if (entry.listeners.size > 0) return;\n entry.observer.disconnect();\n this.observers.delete(rootMargin);\n }\n\n private observerFor(rootMargin: string): ObserverEntry {\n const existing = this.observers.get(rootMargin);\n if (existing) return existing;\n\n const listeners = new Map<Element, Set<VisibilityListener>>();\n const observer = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n for (const listener of listeners.get(entry.target) ?? []) listener(entry.isIntersecting);\n }\n },\n { rootMargin },\n );\n const entry = { observer, listeners };\n this.observers.set(rootMargin, entry);\n return entry;\n }\n}\n","import { FieldDef } from '@ng-forge/dynamic-forms/internal';\nimport { ContainerValidation, WrapperAutoAssociations, WrapperConfig, WrapperTypeDefinition } 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/** Wrapper type name — must match the `BUILT_IN_WRAPPERS` registration. */\nconst FIELD_ERRORS_WRAPPER = 'field-errors';\n\n/** Frozen so the appended entry keeps its identity and `isSameWrapperChain` still memoises. */\nconst FIELD_ERRORS_CONFIG = Object.freeze({ type: FIELD_ERRORS_WRAPPER }) as WrapperConfig;\n\n/** The subset of a field this resolver reads. */\ntype ResolvableField = Pick<FieldDef<unknown>, 'type' | 'wrappers' | 'skipAutoWrappers' | 'skipDefaultWrappers'> &\n Partial<ContainerValidation>;\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: ResolvableField,\n defaultWrappers: readonly WrapperConfig[] | undefined,\n autoAssociations: WrapperAutoAssociations,\n wrapperRegistry: ReadonlyMap<string, WrapperTypeDefinition>,\n): readonly WrapperConfig[] {\n // Absolute escape hatch: no wrappers at all, error message included.\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 const composed = [...autoWrappers, ...defaults, ...fieldLevel];\n\n // A container validator (#568) has no field component to render its message. Appended\n // after all layers compose, so a custom error wrapper counts from any of them.\n if (needsErrorWrapper(field, composed, wrapperRegistry)) {\n composed.push(FIELD_ERRORS_CONFIG);\n }\n\n return composed.length === 0 ? EMPTY_WRAPPERS : composed;\n}\n\n/** Field types that own a schema path and so can carry their own validators (#568). */\nconst VALIDATABLE_CONTAINERS: ReadonlySet<string> = new Set(['group', 'array']);\n\n/** True when the field declares container validators and nothing in the chain renders them. */\nfunction needsErrorWrapper(\n field: ResolvableField,\n composed: readonly WrapperConfig[],\n wrapperRegistry: ReadonlyMap<string, WrapperTypeDefinition>,\n): boolean {\n // Leaves render their own errors in the field component; only containers need the wrapper.\n if (!VALIDATABLE_CONTAINERS.has(field.type)) return false;\n if (!field.validators || field.validators.length === 0) return false;\n\n return !composed.some((w) => w.type === FIELD_ERRORS_WRAPPER || wrapperRegistry.get(w.type)?.rendersFieldErrors);\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 {\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';\nimport { DEV_MODE } from '../dev-mode';\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 loadCache?: Map<string, Promise<Type<unknown> | undefined>>,\n): Promise<Type<unknown> | undefined> {\n const cached = cache.get(type);\n if (cached) return cached;\n\n const pending = loadCache?.get(type);\n if (pending) return pending;\n\n const definition = registry.get(type);\n if (!definition) return undefined;\n\n const load = (async () => {\n const component = resolveDefaultExport(await definition.loadComponent());\n if (component) cache.set(type, component);\n return component;\n })().finally(() => loadCache?.delete(type));\n loadCache?.set(type, load);\n return load;\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 loadCache?: Map<string, Promise<Type<unknown> | undefined>>,\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, loadCache)).pipe(\n catchError(() => of(undefined)),\n map((component) => {\n if (!component) {\n // Diagnostic only — dropping the wrapper is what keeps the chain usable.\n if (DEV_MODE) {\n logger.error(`Wrapper type '${config.type}' could not be loaded. Ensure it is registered via provideDynamicForm().`);\n }\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 /**\n * Classes added imperatively to the outermost wrapper host (the row's direct\n * flex child, where grid CSS matches). Sampled at render time, not reactive.\n */\n readonly outermostHostClasses?: string;\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 // Empty chain = the field host itself is the flex child and already carries the class.\n if (options.outermostHostClasses && refs.length > 0) {\n const tokens = options.outermostHostClasses.split(/\\s+/).filter(Boolean);\n if (tokens.length > 0) {\n (refs[0].location.nativeElement as HTMLElement).classList.add(...tokens);\n }\n }\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 // Diagnostic only — the unwind below is what keeps the DOM consistent.\n if (DEV_MODE) {\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 }\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 {\n ComponentRef,\n computed,\n DestroyRef,\n EnvironmentInjector,\n inject,\n Injector,\n Signal,\n ViewContainerRef,\n ViewRef,\n} 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 {\n WRAPPER_COMPONENT_CACHE,\n WRAPPER_COMPONENT_LOAD_CACHE,\n WRAPPER_REGISTRY,\n WrapperConfig,\n WrapperTypeDefinition,\n} 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 * Grid classes (e.g. `df-col-6`) for the outermost wrapper host, sampled on\n * each structural rebuild. Containers omit this.\n */\n readonly outermostHostClasses?: Signal<string | undefined>;\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. Return true when the caller owns the\n * controller's already-detached outer view and will reinsert it itself. Not\n * invoked on the pre-first-render path.\n */\n readonly beforeRebuild?: () => boolean | 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 const detached = { value: null as ViewRef | 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, detached });\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 // Same reason as the teardown branch: clear() cannot reach a detached view.\n detached.value?.destroy();\n detached.value = null;\n }\n });\n\n pushFieldInputsOnChange(opts, () => refs);\n deps.destroyRef.onDestroy(() => {\n detached.value?.destroy();\n opts.vcr().clear();\n });\n}\n\ninterface ChainDeps {\n readonly registry: ReadonlyMap<string, WrapperTypeDefinition>;\n readonly cache: Map<string, import('@angular/core').Type<unknown>>;\n readonly loadCache: Map<string, Promise<import('@angular/core').Type<unknown> | undefined>>;\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 /** View detached while the gate is closed, re-inserted when it reopens. */\n readonly detached: { value: ViewRef | null };\n}\n\nfunction injectChainDeps(): ChainDeps {\n return {\n registry: inject(WRAPPER_REGISTRY),\n cache: inject(WRAPPER_COMPONENT_CACHE),\n loadCache: inject(WRAPPER_COMPONENT_LOAD_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, deps.loadCache).pipe(\n map((loaded) => ({ state, loaded })),\n );\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, and detach it so it leaves the DOM and stops\n // taking change detection. Rebuilding instead costs ~5x the teardown.\n if (!structurallyChanged && !state.open) {\n if (vcr.length > 0) ctx.detached.value = vcr.detach(0) ?? ctx.detached.value;\n return refs;\n }\n\n // Re-opening a chain we detached rather than destroyed: put the view back.\n if (state.open && !structurallyChanged && mounted.value !== null && ctx.detached.value) {\n vcr.insert(ctx.detached.value);\n ctx.detached.value = null;\n return refs;\n }\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 // Preserve the innermost field before destroying a detached outer chain. Destroying the\n // ancestor first also destroys the field ref and leaves the caller holding a dead view.\n const preservesDetachedOuterView = opts.beforeRebuild?.() === true;\n // A view detached by an earlier gate close is not in the container, so vcr.clear() cannot\n // reach it. Once the preservable inner view is detached, destroy the remaining chain.\n if (ctx.detached.value && !preservesDetachedOuterView) ctx.detached.value.destroy();\n ctx.detached.value = null;\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 outermostHostClasses: opts.outermostHostClasses?.(),\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 validationMessages?: Record<string, unknown>,\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 // Containers can't emit this as a mapper output (no such component input), so it rides\n // the wrapper-facing bag only — where the error wrapper reads messages from.\n ...(validationMessages !== undefined && { validationMessages }),\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 /**\n * Parking intent, not a record of what was done to the LView. Orthogonal to\n * `phase` — it's about whether the mounted ref's own view participates in\n * change detection, not about where the view lives — so it isn't folded into\n * the state discriminant. Held as intent because `ViewContainerRef.insert()`\n * re-attaches the view it inserts, so a reuse-path remount silently undoes\n * the detach and `mountOrReuse` has to re-apply it.\n */\n private readonly _parked = signal(false);\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 /** Whether this field's view is currently held out of change detection. */\n readonly parked = this._parked.asReadonly();\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 // `slot.insert` re-attaches the host view to change detection, so a\n // standing park intent has to be re-applied before anything can refresh.\n if (this._parked()) current.ref.changeDetectorRef.detach();\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 // A different component class is a different field shape — the old ref's\n // parked state says nothing about the new one, which mounts live.\n this._parked.set(false);\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. Returns true when the outer wrapper controller already detached this\n * exact unwrapped field view, so it can transfer ownership without destroying it.\n */\n detach(): boolean {\n const current = this.state();\n if (current.phase !== 'mounted') return false;\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 // When the outer chain is already detached and there are no wrappers, the field's\n // host view is that outer view. Tell the controller not to destroy its detached handle.\n return idx < 0;\n }\n\n /**\n * Detach the mounted component's view from change detection, leaving its DOM\n * in place. Angular's `[formField]` reads all 16 pieces of field state from\n * inside the view's update pass, and a detached view is skipped before its\n * producers are even polled — so parking takes the field out of the\n * per-keystroke cost entirely while keeping it findable, autofillable and\n * reachable by assistive tech.\n *\n * User input still writes through: Angular binds `input` / `blur` with plain\n * DOM listeners that set the model synchronously, and those are not\n * view-scoped. Only the model → DOM direction is suspended.\n *\n * Policy (never park the focused field, never park a field showing an error)\n * belongs to the caller; this is the mechanism only.\n */\n park(): void {\n const current = this.state();\n if (current.phase === 'empty' || this._parked()) return;\n current.ref.changeDetectorRef.detach();\n this._parked.set(true);\n }\n\n /** Refresh a parked view once without returning it to the application CD tree. */\n refresh(): void {\n const current = this.state();\n if (current.phase === 'empty' || !this._parked()) return;\n current.ref.changeDetectorRef.detectChanges();\n }\n\n /**\n * Re-attach a parked view and force one refresh, so the DOM catches up with\n * every model change it slept through. `markForCheck` is required as well as\n * `reattach` — reattaching alone leaves an `OnPush` view clean.\n */\n unpark(): void {\n const current = this.state();\n if (current.phase === 'empty' || !this._parked()) return;\n current.ref.changeDetectorRef.reattach();\n current.ref.changeDetectorRef.markForCheck();\n this._parked.set(false);\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._parked.set(false);\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 { afterNextRender, Injector } from '@angular/core';\nimport { EventBus } from '@ng-forge/dynamic-forms/internal';\nimport { ComponentInitializedEvent } from '@ng-forge/dynamic-forms/internal';\nimport { DynamicFormLogger } from '@ng-forge/dynamic-forms/internal';\n\n/** Component type for initialization events. */\nexport type InitializationComponentType = 'dynamic-form' | 'group' | 'page' | 'array' | 'container';\n\n/**\n * Emits a component initialization event after the next render cycle.\n *\n * @param eventBus - The EventBus instance to dispatch the event on\n * @param componentType - The type of component being initialized\n * @param componentKey - The unique key/id of the component\n * @param injector - The injector to use for afterNextRender scheduling\n */\nexport function emitComponentInitialized(\n eventBus: EventBus,\n componentType: InitializationComponentType,\n componentKey: string,\n injector: Injector,\n): void {\n const logger = injector.get(DynamicFormLogger);\n\n afterNextRender(\n () => {\n try {\n eventBus.dispatch(ComponentInitializedEvent, componentType, componentKey);\n } catch (error: unknown) {\n logger.error(`Failed to emit initialization event for ${componentType} '${componentKey}'`, error);\n }\n },\n { injector },\n );\n}\n","import { computed, Injector, Signal } from '@angular/core';\nimport { explicitEffect } from 'ngxtension/explicit-effect';\nimport { EventBus } from '@ng-forge/dynamic-forms/internal';\nimport { emitComponentInitialized, InitializationComponentType } from '../emit-initialization/emit-initialization';\nimport { ResolvedField } from '../resolve-field/resolve-field';\nimport {\n FieldDef,\n hasChildFields,\n isArrayField,\n isGenericContainerField,\n isGroupField,\n isPageField,\n isRowField,\n normalizeFieldsArray,\n} from '@ng-forge/dynamic-forms/internal';\n\n/** Event identity used by containers that participate in initialization readiness. */\nexport function initializationComponentKey(type: InitializationComponentType, key: string): string {\n return `${type}:${key}`;\n}\n\n/** Joins a container's local key to its group scope for initialization identity. */\nexport function initializationComponentPath(key: string, groupPath?: string): string {\n return groupPath ? `${groupPath}.${key}` : key;\n}\n\n/** Component-initialization identity emitted by a rendered container field. */\nexport function initializationComponentType(field: FieldDef<unknown>): InitializationComponentType | undefined {\n if (isPageField(field)) return 'page';\n if (isGroupField(field)) return 'group';\n if (isArrayField(field)) return 'array';\n if (isRowField(field) || isGenericContainerField(field)) return 'container';\n return undefined;\n}\n\nexport interface InitializationComponentIdentity {\n readonly type: InitializationComponentType;\n readonly path: string;\n}\n\n/** Collects the initialization identities expected from rendered container components. */\nexport function collectInitializingContainers(fields: readonly FieldDef<unknown>[], groupPath = ''): InitializationComponentIdentity[] {\n const identities: InitializationComponentIdentity[] = [];\n\n for (const field of fields) {\n // A statically hidden container never mounts, and neither can its descendants.\n if (field.hidden === true) continue;\n\n const type = initializationComponentType(field);\n const componentPath = initializationComponentPath(field.key, groupPath);\n if (type && field.key) identities.push({ type, path: componentPath });\n\n if (!hasChildFields(field)) continue;\n const childGroupPath = isGroupField(field) ? componentPath : groupPath;\n for (const child of normalizeFieldsArray(field.fields)) {\n identities.push(\n ...collectInitializingContainers((Array.isArray(child) ? child : [child]) as readonly FieldDef<unknown>[], childGroupPath),\n );\n }\n }\n\n return identities;\n}\n\n/** Collects the initialization events expected from rendered container components. */\nexport function collectInitializingContainerKeys(fields: readonly FieldDef<unknown>[], groupPath = ''): string[] {\n return collectInitializingContainers(fields, groupPath).map(({ type, path }) => initializationComponentKey(type, path));\n}\n\n/**\n * Computes the host class string for a container component.\n *\n * @param containerType - The CSS class suffix (e.g., 'group', 'row', 'page-field')\n * @param className - Optional custom class name to append\n * @returns The computed host class string\n */\nexport function computeContainerHostClasses(containerType: string, className: string | undefined): string {\n const base = `df-field df-${containerType}`;\n return className ? `${base} ${className}` : base;\n}\n\n/**\n * Sets up the initialization effect common to all container components.\n *\n * @param resolvedFields - Signal of resolved fields\n * @param eventBus - EventBus for dispatching initialization events\n * @param componentType - Static type, or a getter evaluated at emission time\n * @param fieldKeyFn - Function returning the field's key\n * @param injector - Injector for afterNextRender scheduling\n */\nexport function setupContainerInitEffect(\n resolvedFields: Signal<ResolvedField[]>,\n eventBus: EventBus,\n componentType: InitializationComponentType | (() => InitializationComponentType),\n fieldKeyFn: () => string,\n injector: Injector,\n): void {\n const allReady = computed(() => {\n const fields = resolvedFields();\n return fields.length > 0 && fields.every((field) => field.renderReady());\n });\n\n explicitEffect([allReady], ([ready]) => {\n if (ready) {\n const type = typeof componentType === 'function' ? componentType() : componentType;\n emitComponentInitialized(eventBus, type, fieldKeyFn(), injector);\n }\n });\n}\n","import type { Signal } from '@angular/core';\n\nexport interface ParkedFieldState {\n readonly disabled?: Signal<boolean>;\n readonly readonly?: Signal<boolean>;\n readonly required?: Signal<boolean>;\n readonly touched?: Signal<boolean>;\n readonly dirty?: Signal<boolean>;\n readonly errors?: Signal<readonly unknown[]>;\n}\n\nexport type ParkableFieldTree = () => ParkedFieldState;\n\nexport interface ParkedDomState {\n readonly disabled: boolean;\n readonly readonly: boolean;\n readonly required: boolean;\n readonly touched: boolean;\n readonly dirty: boolean;\n readonly errors: readonly unknown[];\n}\n\nconst EMPTY_ERRORS: readonly unknown[] = Object.freeze([]);\n\nexport function snapshotParkedDomState(fieldTree: ParkableFieldTree | undefined): ParkedDomState | null {\n const state = fieldTree?.();\n if (!state) return null;\n return {\n disabled: state.disabled?.() ?? false,\n readonly: state.readonly?.() ?? false,\n required: state.required?.() ?? false,\n touched: state.touched?.() ?? false,\n dirty: state.dirty?.() ?? false,\n errors: state.errors?.() ?? EMPTY_ERRORS,\n };\n}\n\nexport function isSameParkedDomState(a: ParkedDomState | null, b: ParkedDomState | null): boolean {\n return (\n a === b ||\n (a !== null &&\n b !== null &&\n a.disabled === b.disabled &&\n a.readonly === b.readonly &&\n a.required === b.required &&\n a.touched === b.touched &&\n a.dirty === b.dirty &&\n a.errors === b.errors)\n );\n}\n","import {\n computed,\n DestroyRef,\n Directive,\n EnvironmentInjector,\n inject,\n Injector,\n input,\n Signal,\n signal,\n Type,\n ViewContainerRef,\n} from '@angular/core';\nimport { explicitEffect } from 'ngxtension/explicit-effect';\nimport { derivedFrom } from 'ngxtension/derived-from';\nimport { of, pipe, switchMap } from 'rxjs';\nimport { FORM_OPTIONS } from '@ng-forge/dynamic-forms/internal';\nimport { FIELD_WINDOWING } from '../../providers/features/field-windowing/field-windowing.token';\nimport { resolveFieldWindowing } from '../../providers/features/field-windowing/resolve-field-windowing';\nimport { FieldViewportObserver } from './field-viewport-observer.service';\nimport { ResolvedField } from '../../utils/resolve-field/resolve-field';\nimport { WRAPPER_REGISTRY, 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 { getGridClassString } 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';\nimport { EventBus, GROUP_CONTEXT } from '@ng-forge/dynamic-forms/internal';\nimport { emitComponentInitialized } from '../../utils/emit-initialization/emit-initialization';\nimport { collectInitializingContainers } from '../../utils/container-utils/container-utils';\nimport { isSameParkedDomState, snapshotParkedDomState } from './field-parking-state';\nimport type { ParkableFieldTree, ParkedDomState } from './field-parking-state';\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 wrapperRegistry = inject(WRAPPER_REGISTRY);\n private readonly defaultWrappersSignal = inject(DEFAULT_WRAPPERS, { optional: true });\n private readonly readonlyFieldCache = inject(READONLY_FIELD_TREE_CACHE);\n private readonly eventBus = inject(EventBus, { optional: true });\n private readonly injector = inject(Injector);\n\n private readonly fieldComponent = new FieldComponentSlot();\n\n private readonly componentIdentity: Signal<Type<unknown>> = computed(() => this.dfFieldOutlet().component);\n /** Avoids the NG01916 window between `renderReady` and hidden state settling. */\n private readonly renderReady: Signal<boolean> = computed(() => this.dfFieldOutlet().renderReady() && !this.dfFieldOutlet().hidden());\n private readonly hidden = computed(() => this.dfFieldOutlet().hidden());\n private readonly rawInputs = computed(() => this.dfFieldOutlet().inputs());\n\n /** Preserve wrapper identity across reconciled field snapshots. */\n private readonly wrappers = computed(\n () =>\n resolveWrappers(this.dfFieldOutlet().fieldDef, this.defaultWrappersSignal?.(), this.wrapperAutoAssociations, this.wrapperRegistry),\n { equal: isSameWrapperChain },\n );\n\n /** Shared input view for wrappers and addon-aware leaf components. */\n private readonly fieldInputs = computed<WrapperFieldInputs>(() =>\n buildFieldInputs(\n this.rawInputs(),\n this.readonlyFieldCache,\n this.dfFieldOutlet().fieldDef.type,\n (this.dfFieldOutlet().fieldDef as { validationMessages?: Record<string, unknown> }).validationMessages,\n ),\n );\n\n private readonly defaultEnvInjector = inject(EnvironmentInjector);\n private readonly fieldEnvInjector = computed(() => this.dfFieldOutletEnvironmentInjector() ?? this.defaultEnvInjector);\n private readonly fieldInjector = computed(() => this.dfFieldOutlet().injector);\n /** The outer wrapper is the row's grid child when wrappers exist. */\n private readonly outermostHostClasses = computed(() => getGridClassString(this.dfFieldOutlet().fieldDef) || undefined);\n\n private readonly formOptions = inject(FORM_OPTIONS, { optional: true });\n private readonly globalFieldWindowing = inject(FIELD_WINDOWING);\n private readonly viewportObserver = inject(FieldViewportObserver, { optional: true });\n\n private readonly parking = computed(() => resolveFieldWindowing(this.globalFieldWindowing, this.formOptions?.()?.fieldWindowing).park);\n\n /** A structural directive observes the mounted component's host element. */\n private readonly observedElement = computed(() => {\n const state = this.fieldComponent.snapshot();\n return state.phase === 'empty' ? null : (state.ref.location.nativeElement as HTMLElement);\n });\n\n private readonly visibility = derivedFrom(\n [this.observedElement, this.parking],\n pipe(\n switchMap(([element, parking]) => {\n const observer = this.viewportObserver;\n return parking.enabled && element && observer ? observer.observe(element, parking.margin) : of(true);\n }),\n ),\n { initialValue: true },\n );\n\n private readonly focusEpoch = signal(0);\n\n private readonly parked = computed(() => this.parking().enabled && !this.visibility());\n private readonly fieldTree = computed(\n () => {\n const candidate = this.rawInputs()['field'];\n return typeof candidate === 'function' ? (candidate as ParkableFieldTree) : undefined;\n },\n { equal: Object.is },\n );\n /** Subscribe to field state only while its view is parked. */\n private readonly parkedDomState = computed<ParkedDomState | null>(\n () => (this.parked() ? snapshotParkedDomState(this.fieldTree()) : null),\n { equal: isSameParkedDomState },\n );\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 outermostHostClasses: this.outermostHostClasses,\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 // The slot deduplicates the initial render and this reactive push.\n explicitEffect([this.rawInputs, this.fieldInputs], ([rawInputs, fieldInputs]) =>\n this.fieldComponent.pushInputs(rawInputs, fieldInputs),\n );\n\n // Hidden containers still need to release ancestor initialization trackers.\n explicitEffect([this.hidden], ([hidden]) => {\n if (!hidden || !this.eventBus) return;\n const resolved = this.dfFieldOutlet();\n const groupContext = resolved.injector.get(GROUP_CONTEXT, null);\n for (const { type, path } of collectInitializingContainers([resolved.fieldDef], groupContext?.groupPath())) {\n emitComponentInitialized(this.eventBus, type, path, this.injector);\n }\n });\n\n // Focus changes can make an otherwise offscreen field safe to park.\n explicitEffect([this.observedElement, this.parking], ([element, parking], onCleanup) => {\n if (!parking.enabled || !element) return;\n // Reactive disabling can move focus during render; defer past NG0600.\n const bumpEpoch = () =>\n queueMicrotask(() => {\n if (!this.destroyRef.destroyed) this.focusEpoch.update((n) => n + 1);\n });\n element.addEventListener('focusin', bumpEpoch);\n element.addEventListener('focusout', bumpEpoch);\n onCleanup(() => {\n element.removeEventListener('focusin', bumpEpoch);\n element.removeEventListener('focusout', bumpEpoch);\n });\n });\n\n // Refresh safety state without returning a parked view to application CD.\n explicitEffect([this.parked, this.focusEpoch, this.parkedDomState], ([parked, , domState]) => {\n if (parked && this.canPark(domState)) {\n const alreadyParked = this.fieldComponent.parked();\n this.fieldComponent.park();\n if (alreadyParked) this.fieldComponent.refresh();\n } else {\n this.fieldComponent.unpark();\n }\n });\n\n this.destroyRef.onDestroy(() => this.fieldComponent.destroyOnTeardown());\n }\n\n /** Focused and invalid fields must keep their DOM live. */\n private canPark(domState: ParkedDomState | null): boolean {\n const element = this.observedElement();\n if (!element) return false;\n if (element.contains(document.activeElement)) return false;\n return (domState?.errors.length ?? 0) === 0;\n }\n}\n","import { Signal } from '@angular/core';\nimport { FieldTree, submit, TreeValidationResult } 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 * Whether a resolved action result is a `TreeValidationResult` carrying server errors.\n *\n * `SubmissionConfig.action` also permits arbitrary success payloads (an Observable\n * action is typically an HTTP call resolving to a response body), so anything that\n * is not shaped like one-or-many `ValidationError` is reported to `submit()` as\n * success rather than being misread as a validation failure.\n */\nfunction isTreeValidationErrors(result: unknown): result is TreeValidationResult {\n const isError = (v: unknown): boolean => typeof v === 'object' && v !== null && 'kind' in v;\n return Array.isArray(result) ? result.every(isError) && result.length > 0 : isError(result);\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 * The resolved value is handed back to `submit()` so that an action returning\n * `TreeValidationResult` has its server errors applied to the form.\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<TreeValidationResult> {\n return async (formTree: FieldTree<TModel>): Promise<TreeValidationResult> => {\n const result = action(formTree);\n // If the action returns an Observable, take its first emission\n const resolved = isObservable(result) ? await firstValueFrom(result) : await Promise.resolve(result);\n\n return isTreeValidationErrors(resolved) ? resolved : undefined;\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 { 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 * In-flight component loads, scoped through DI so concurrent fields share one\n * dynamic import without introducing module-level mutable state.\n *\n * @internal\n */\nexport const COMPONENT_LOAD_CACHE = new InjectionToken<Map<string, Promise<Type<unknown> | undefined>>>('COMPONENT_LOAD_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 const componentLoadCache = inject(COMPONENT_LOAD_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 const pending = componentLoadCache.get(name);\n if (pending) {\n return pending;\n }\n\n try {\n const load = fieldType\n .loadComponent()\n .then(resolveDefaultExport)\n .then((component) => {\n if (component) {\n componentCache.set(name, component);\n }\n\n return component;\n })\n .catch((error: unknown) => {\n throw new DynamicFormError(`Failed to load component for field type \"${name}\": ${error}`);\n })\n .finally(() => componentLoadCache.delete(name));\n\n componentLoadCache.set(name, load);\n return load;\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 { 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 validators,\n validationMessages,\n required,\n validateWhenHidden,\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 // Container validators belong to the array itself, not to its items — carry\n // them onto the expanded ArrayField so the schema mapping still sees them.\n if (validators !== undefined) {\n arrayFieldObj['validators'] = validators;\n }\n if (validationMessages !== undefined) {\n arrayFieldObj['validationMessages'] = validationMessages;\n }\n if (required !== undefined) {\n arrayFieldObj['required'] = required;\n }\n if (validateWhenHidden !== undefined) {\n arrayFieldObj['validateWhenHidden'] = validateWhenHidden;\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 { inject, Type } from '@angular/core';\nimport {\n DEFAULT_WRAPPERS,\n FieldDef,\n FormConfig,\n isPageField,\n WRAPPER_AUTO_ASSOCIATIONS,\n WRAPPER_COMPONENT_LOAD_CACHE,\n WRAPPER_REGISTRY,\n} from '@ng-forge/dynamic-forms/internal';\nimport { WRAPPER_COMPONENT_CACHE } from '@ng-forge/dynamic-forms/internal';\nimport { injectFieldRegistry } from '../inject-field-registry/inject-field-registry';\nimport { loadWrapperComponent } from '../wrapper-chain/wrapper-chain';\nimport { resolveWrappers } from '../resolve-wrappers/resolve-wrappers';\nimport { normalizeSimplifiedArrays } from '../array-field/normalize-simplified-arrays';\nimport { getNormalizedArrayMetadata } from '../array-field/normalized-array-metadata';\n\n/**\n * Collects every field type named anywhere in a config, including inside\n * containers. Containers hold their children under `fields`, and array item\n * templates under `fields` too, so one recursive walk covers both.\n */\nfunction collectFieldDefs(fields: readonly FieldDef<unknown>[] | undefined, out: FieldDef<unknown>[] = []): FieldDef<unknown>[] {\n for (const field of fields ?? []) {\n out.push(field);\n const children = (field as { fields?: unknown }).fields;\n if (Array.isArray(children)) {\n // Array item templates nest one level deeper: `fields: [[...template]]`.\n for (const child of children) {\n if (Array.isArray(child)) collectFieldDefs(child as FieldDef<unknown>[], out);\n else collectFieldDefs([child as FieldDef<unknown>], out);\n }\n }\n\n const metadata = getNormalizedArrayMetadata(field);\n const template = metadata?.template;\n if (Array.isArray(template)) collectFieldDefs(template as readonly FieldDef<unknown>[], out);\n else if (template) collectFieldDefs([template as FieldDef<unknown>], out);\n if (metadata?.autoRemoveButton) collectFieldDefs([metadata.autoRemoveButton], out);\n }\n return out;\n}\n\nexport interface FormComponentPreloader {\n /** Preload a complete non-paged form config. Paged configs are orchestrator-owned. */\n readonly preloadConfig: (config: FormConfig) => void;\n /** Preload the exact field subtree selected for rendering. */\n readonly preloadFields: (fields: readonly FieldDef<unknown>[]) => void;\n}\n\n/**\n * Warms the field- and wrapper-component caches for everything a config will\n * actually render, in parallel, as soon as the config is known.\n *\n * Without this the two sets load in sequence: a field's wrapper chain is\n * resolved inside `DfFieldOutlet`, which does not exist until that field's own\n * component has loaded. Measured on the 240-field fixture, that dependent\n * waterfall put the last wrapper chunk 30ms before first paint on localhost,\n * where a round trip costs ~1ms — on a real network each wave costs a full RTT.\n *\n * Only the current render scope is fetched: a flat form, or the active page\n * window selected by PageOrchestrator. Both loaders dedupe (field loads through\n * `COMPONENT_LOAD_CACHE`, wrapper loads through the module system's own\n * `import()` dedup), so racing the normal resolution path is safe.\n *\n * Warming the caches also lets the sync fast paths take over: `resolveFieldSync`\n * and the wrapper chain controller's \"every wrapper cached\" branch both skip\n * their async hop entirely once the cache is populated.\n *\n * Failures are swallowed on purpose. This is a head start, not a load-bearing\n * step — every one of these types is loaded again through the normal path,\n * which owns the real error reporting.\n */\nexport function injectFormComponentPreloader(): FormComponentPreloader {\n const fieldRegistry = injectFieldRegistry();\n const wrapperRegistry = inject(WRAPPER_REGISTRY);\n const wrapperCache = inject(WRAPPER_COMPONENT_CACHE);\n const wrapperLoadCache = inject(WRAPPER_COMPONENT_LOAD_CACHE);\n const autoAssociations = inject(WRAPPER_AUTO_ASSOCIATIONS);\n const defaultWrappers = inject(DEFAULT_WRAPPERS, { optional: true });\n const preloadedConfigs = new WeakSet<FormConfig>();\n const preloadedFieldRoots = new WeakSet<FieldDef<unknown>>();\n\n const preloadFields = (fields: readonly FieldDef<unknown>[]): void => {\n const unseenFields = fields.filter((field) => {\n if (preloadedFieldRoots.has(field)) return false;\n preloadedFieldRoots.add(field);\n return true;\n });\n if (unseenFields.length === 0) return;\n\n const normalizedFields = normalizeSimplifiedArrays(unseenFields);\n const defs = collectFieldDefs(normalizedFields);\n if (defs.length === 0) return;\n\n const fieldTypes = new Set<string>();\n const wrapperTypes = new Set<string>();\n\n for (const def of defs) {\n if (def.type) fieldTypes.add(def.type);\n // Pure function of the field def plus registry state, all of it known\n // here — which is what makes the wrapper set computable before any field\n // has rendered.\n for (const wrapper of resolveWrappers(def, defaultWrappers?.(), autoAssociations, wrapperRegistry)) {\n wrapperTypes.add(wrapper.type);\n }\n }\n\n const started: Promise<unknown>[] = [];\n for (const type of fieldTypes) {\n if (fieldRegistry.getLoadedComponent(type)) continue;\n started.push(Promise.resolve(fieldRegistry.loadTypeComponent(type)).catch(() => undefined));\n }\n for (const type of wrapperTypes) {\n if (wrapperCache.has(type)) continue;\n started.push(\n loadWrapperComponent(type, wrapperRegistry, wrapperCache, wrapperLoadCache).catch((): Type<unknown> | undefined => undefined),\n );\n }\n // Deliberately not awaited: rendering must not be gated on the head start.\n void Promise.allSettled(started);\n };\n\n return {\n preloadConfig: (config: FormConfig): void => {\n if (preloadedConfigs.has(config)) return;\n preloadedConfigs.add(config);\n\n const fields = config.fields as readonly FieldDef<unknown>[];\n // PageOrchestrator owns paged rendering and calls preloadFields for the\n // active page plus its configured neighbours. Walking the raw config here\n // would eagerly fetch chunks for every distant page.\n if (fields.length > 0 && fields.every(isPageField)) return;\n preloadFields(fields);\n },\n preloadFields,\n };\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';\nimport { PagerState } from '../../core/page-orchestrator/page-orchestrator.interfaces';\n\n/**\n * Event emitted whenever the pager state of a paged form changes.\n *\n * Unlike `PageChangeEvent`, which fires only when the active page changed, this\n * fires on any change to the derived state — including when `isFirstPage` /\n * `isLastPage` flip because a page's `hidden` logic toggled and the set of\n * visible pages shifted, with the active page never moving.\n */\nexport class PagerStateEvent implements FormEvent {\n readonly type = 'pager-state' as const;\n\n constructor(public state: PagerState) {}\n}\n","import { InjectionToken } from '@angular/core';\n\n/**\n * Global default for how many pages on each side of the current page a paged\n * form mounts during idle time (the \"preload window\").\n *\n * A window of `1` (the default) renders the current page directly and mounts its\n * immediate neighbours (±1) on idle, so sequential navigation is flicker-free while\n * distant pages stay unmounted. Larger windows pre-warm more pages (useful for\n * jump navigation) at the cost of more initial DOM + change detection; `0`\n * mounts only the current page.\n *\n * @internal\n */\nexport const PAGE_PRELOAD_WINDOW = new InjectionToken<number>('PAGE_PRELOAD_WINDOW', {\n providedIn: 'root',\n factory: () => 1,\n});\n","import { isGenericContainerField, isRowField } from '@ng-forge/dynamic-forms/internal';\nimport type { FieldDef } from '@ng-forge/dynamic-forms/internal';\n\n/**\n * Form-node keys owned by a page's fields.\n *\n * Layout containers (`row`, `container`) are flattened by `mapFieldToForm`, so no form node\n * exists under their own key — traverse their children instead. `group` and `array` own a node\n * whose `valid()` already aggregates descendants, so their key is used as-is.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- accepts any field shape for recursive traversal\nexport function collectLeafFieldKeys(fields: readonly FieldDef<any>[]): string[] {\n const keys: string[] = [];\n\n for (const field of fields) {\n if (isRowField(field) || isGenericContainerField(field)) {\n keys.push(...collectLeafFieldKeys(field.fields as readonly FieldDef<unknown>[]));\n } else if (field.key) {\n keys.push(field.key);\n }\n }\n\n return keys;\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","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 { 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 { 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 { 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';\nimport { DEV_MODE } from '../dev-mode';\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 // Detail (field key + engine message) is dev-only; the pattern alone identifies it in prod.\n errors.push(\n DEV_MODE\n ? `Invalid regex pattern in validator for field '${fieldKey}': '${pattern}' — ${e instanceof Error ? e.message : String(e)}`\n : `Invalid regex pattern: '${pattern}'`,\n );\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 is load-bearing; only the explanatory tail is dev-only.\n throw new DynamicFormError(\n DEV_MODE\n ? `Duplicate field keys detected: ${duplicateList}. Field keys must be unique within their group scope (the same key may appear inside different groups).`\n : `Duplicate field keys detected: ${duplicateList}.`,\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 is load-bearing; only the explanatory tail is dev-only.\n throw new DynamicFormError(\n DEV_MODE\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 : `DOM id collision detected for: ${duplicateList}.`,\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 is load-bearing; only the explanatory tail is dev-only.\n throw new DynamicFormError(\n DEV_MODE\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 : `Field key(s) contain a dot: ${list}.`,\n );\n }\n\n validateNoDuplicateKeys(data.keys);\n validateNoDomIdCollisions(data.domIds);\n // Warn-only diagnostic — unknown types already degrade gracefully at render time.\n if (DEV_MODE) 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/** Returns whether any field has at least one effective value-exclusion axis enabled. */\nexport function hasEnabledValueExclusion(\n fields: readonly FieldDef<unknown>[],\n global: ResolvedValueExclusionConfig,\n form: ValueExclusionConfig | undefined,\n): boolean {\n return fields.some((field) => {\n const resolved = resolveExclusionConfig(global, form, field);\n if (resolved.excludeValueIfHidden || resolved.excludeValueIfDisabled || resolved.excludeValueIfReadonly) {\n return true;\n }\n\n return (\n isGroupField(field) && !!field.fields && hasEnabledValueExclusion(Object.values(field.fields) as FieldDef<unknown>[], global, form)\n );\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/** Keeps only declared value-bearing fields without consulting reactive field state. */\nfunction filterDeclaredValueShape(\n rawValue: Record<string, unknown>,\n fields: readonly FieldDef<unknown>[],\n registry: Map<string, FieldTypeDefinition>,\n): Record<string, unknown> {\n const kept: Record<string, unknown> = {};\n\n for (const field of fields) {\n const key = field.key;\n if (!key || !(key in rawValue)) continue;\n\n const valueHandling = getFieldValueHandling(field.type, registry);\n if (valueHandling === 'exclude' || valueHandling === 'flatten') continue;\n\n const value = rawValue[key];\n if (isGroupField(field) && field.fields && value !== null && typeof value === 'object' && !Array.isArray(value)) {\n kept[key] = filterDeclaredValueShape(value as Record<string, unknown>, Object.values(field.fields) as FieldDef<unknown>[], registry);\n continue;\n }\n\n kept[key] = value;\n }\n\n return kept;\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 * @param exclusionEnabled - Precomputed exclusion capability for hot callers\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 exclusionEnabled = hasEnabledValueExclusion(schemaFields, globalDefaults, formOptions),\n): Partial<T> {\n // No exclusion axis enabled: skip the per-field state reads and config resolution, but still\n // strip non-value field types and undeclared keys, which the loop below does unconditionally.\n if (!exclusionEnabled) {\n return filterDeclaredValueShape(rawValue, schemaFields, registry) as Partial<T>;\n }\n\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 true,\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';\nimport { DEV_MODE } from '../utils/dev-mode';\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 // Diagnostic only — the `??` fallback below is what keeps the transition correct.\n if (DEV_MODE && !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 // Diagnostic only — the `??` fallback below is what keeps the transition correct.\n if (DEV_MODE && !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 injectAsync,\n Injectable,\n InjectionToken,\n Injector,\n linkedSignal,\n OnDestroy,\n resource,\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 { GoToPageEvent, PageNavigationOptions } from '../events/constants/go-to-page.event';\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 { 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, hasEnabledValueExclusion } 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 /** 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 /**\n * A page the form has been asked to navigate to but that no pager has applied yet.\n * Held here because the orchestrator mounts behind the render gate: a request made on\n * `initialized` reaches the bus before that subscription exists, and would otherwise be\n * dropped. The orchestrator drains this once it can resolve visibility and validity.\n */\n readonly pendingPageRequest = signal<{ index: number; options?: PageNavigationOptions } | null>(null);\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 /** Config generation that owns the current form setup. */\n private readonly formSetupConfig = computed(() => {\n const state = this.machine.state();\n\n if (isReadyState(state)) return state.config;\n if (isTransitioningState(state)) {\n if (state.phase === Phase.Restoring && state.pendingFormSetup) {\n return state.pendingConfig;\n }\n return state.currentConfig;\n }\n if (state.type === LifecycleState.Initializing) return state.config;\n if (state.type === LifecycleState.Uninitialized) return this.activeConfig();\n return undefined;\n });\n\n /** Active page state, retained across pager remounts and scoped to its form config. */\n readonly activePageState = linkedSignal<\n object | undefined,\n {\n config: object | undefined;\n initialized: boolean;\n index: number;\n }\n >({\n source: this.activeConfig,\n computation: (config) => ({ config, initialized: false, index: 0 }),\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\n const keys = new Set<string>();\n for (const field of schemaFields) {\n if (field.key === undefined) continue;\n keys.add(field.key);\n }\n return keys;\n });\n\n /** Whether outward value binding needs the reactive exclusion pipeline. */\n private readonly boundValueExclusionEnabled = computed(() => {\n const setup = this.formSetup();\n if (!setup.schemaFields || setup.schemaFields.length === 0) return false;\n\n const options = this.effectiveFormOptions();\n return hasEnabledValueExclusion(setup.schemaFields, BOUND_VALUE_EXCLUSION_BASELINE, {\n excludeValueIfHidden: options.excludeValueIfHidden,\n excludeValueIfDisabled: options.excludeValueIfDisabled,\n excludeValueIfReadonly: options.excludeValueIfReadonly,\n });\n });\n\n /** Whether filtered values need reactive hidden/disabled/readonly state reads. */\n private readonly valueExclusionEnabled = computed(() => {\n const setup = this.formSetup();\n if (!setup.schemaFields || setup.schemaFields.length === 0) return false;\n\n const options = this.effectiveFormOptions();\n return hasEnabledValueExclusion(setup.schemaFields, this.valueExclusionDefaults, {\n excludeValueIfHidden: options.excludeValueIfHidden,\n excludeValueIfDisabled: options.excludeValueIfDisabled,\n excludeValueIfReadonly: options.excludeValueIfReadonly,\n });\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 schemaFields = this.formSetup().schemaFields;\n if (!this.formSchemaReady() || !this.boundValueExclusionEnabled() || !schemaFields || schemaFields.length === 0) return {};\n\n const snapshot: Record<string, FieldExclusionAxes> = {};\n collectFieldStateSnapshot(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 /** Pending reference pushed to `deps.value` by the outward-sync effect. */\n private readonly lastOutwardValue: { current: unknown; pending: boolean } = { current: undefined, pending: false };\n\n /** Last `deps.value` accepted as a genuine host change (memo for {@link externalValue}). */\n private readonly lastExternalValue: { current: unknown } = { current: undefined };\n\n /**\n * `deps.value` with our own outward sync filtered out. Without this the form reacts to the\n * value it just published: `entity` re-derives, which moves `boundFormValue`, which writes\n * `deps.value` again — a full whole-form merge per keystroke.\n */\n private readonly externalValue = computed<TModel>(() => {\n const incoming = this.deps.value();\n if (this.lastOutwardValue.pending && incoming === this.lastOutwardValue.current) {\n this.lastOutwardValue.pending = false;\n return this.lastExternalValue.current as TModel;\n }\n this.lastOutwardValue.pending = false;\n this.lastExternalValue.current = incoming;\n return incoming as TModel;\n });\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Computed Signals - Entity & Form\n // ─────────────────────────────────────────────────────────────────────────────\n\n /** Entity (form value merged with defaults). */\n readonly entity = linkedSignal<TModel>(\n () => {\n // Read for the dependency only: `externalValue` filters our own echo so this does not\n // re-derive on the value it just published. Content comes from the live host signal,\n // because the filter's memo is stale by exactly the amount it suppressed.\n //\n // The narrowing this buys: a host write that `isEqual` considers identical to our last\n // outward push no longer re-derives the entity. That is correct here (the value is the\n // same) but it does mean host writes are deduplicated by value, not by reference.\n this.externalValue();\n const inputValue = untracked(() => this.deps.value()) as TModel;\n const defaults = this.defaultValues();\n const keys = this.validKeys();\n const saved = this.excludedValueStore();\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 /** Loads once, on the first non-empty schema request. Angular memoizes the import promise. */\n private readonly loadFormSchemaService = injectAsync(() => import('../core/form-schema.service'));\n\n /**\n * The actual schema request. There is deliberately no `requiresSchema` capability predicate:\n * the first value-bearing field requests the lazy compiler, including a plain field with no\n * optional rules. An empty form remains the only no-request case.\n */\n private readonly formSchemaRequest = computed(() => {\n const setup = this.formSetup();\n const config = this.activeConfig();\n if (!config || (!(setup.schemaFields?.length > 0) && !config.schema)) return undefined;\n\n return {\n fields: setup.schemaFields ?? [],\n registry: setup.registry,\n formLevelSchema: config.schema,\n validateWhenHidden: this.effectiveFormOptions().validateWhenHidden,\n };\n });\n\n /**\n * Declarative async bridge between `injectAsync()` and Signal Forms' synchronous schema API.\n * The service is resolved first; schema construction then runs synchronously in this form's\n * injection context. `resource()` also participates in Angular application stability and SSR.\n */\n private readonly formSchemaResource = resource({\n params: () => this.formSchemaRequest(),\n loader: async ({ params }) => {\n const schemaService = await this.loadFormSchemaService();\n // FormConfig stores Standard Schema input as `unknown`; TModel is inferred independently\n // from the same field definitions. The service stays model-agnostic and this boundary\n // restores the state manager's inferred type after compilation.\n return runInInjectionContext(this.injector, () => schemaService.create(params)) as Schema<TModel>;\n },\n });\n\n /**\n * Last schema the resource produced, with the request it was built from.\n *\n * A params change clears the resource's value, not just its status, so without this the form\n * would rebuild schemaless and then rebuild again. Instance-scoped, never module-scoped, so it\n * stays SSR-safe.\n */\n private readonly schemaCache: { current: { schema: Schema<TModel>; request: unknown } | undefined } = { current: undefined };\n\n /** Keeps {@link schemaCache} in step with the resource, and drops it on failure. */\n private readonly trackSchemaCache = computed(() => {\n const request = this.formSchemaRequest();\n if (!request) {\n this.schemaCache.current = undefined;\n return undefined;\n }\n if (this.formSchemaResource.status() === 'error') {\n this.schemaCache.current = undefined;\n return undefined;\n }\n if (this.formSchemaResource.hasValue()) {\n this.schemaCache.current = { schema: this.formSchemaResource.value(), request };\n }\n return this.schemaCache.current;\n });\n\n /** Whether the schema in hand was built from the current request. */\n readonly isSchemaCurrent = computed(() => {\n if (this.formSetupConfig() !== this.activeConfig()) return false;\n const request = this.formSchemaRequest();\n if (!request) return true;\n return this.trackSchemaCache()?.request === request;\n });\n\n /** True once a schema is in hand, or the compiler failed and the degraded path opened. */\n readonly formSchemaReady = computed(() => {\n if (this.formSetupConfig() !== this.activeConfig()) return false;\n if (!this.formSchemaRequest()) return true;\n if (this.formSchemaResource.status() === 'error') return true;\n return this.trackSchemaCache() !== undefined;\n });\n\n /** Schema derived from the current form config and field setup. */\n private readonly formSchema = computed((): Schema<TModel> | undefined => this.trackSchemaCache()?.schema);\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 /** Value of the model Signal Forms owns. */\n private readonly modelValue = computed(() => this.formInstance().value());\n\n /** Current form values (reactive). */\n readonly formValue = computed(() => this.modelValue() as TModel);\n\n /** Form values filtered by value exclusion rules. */\n readonly filteredFormValue = computed(() => {\n const rawValue = this.modelValue();\n const setup = this.formSetup();\n const schemaFields = setup.schemaFields;\n const options = this.effectiveFormOptions();\n\n if (!this.formSchemaReady() || !schemaFields || 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 schemaFields,\n fieldTreeRecord,\n setup.registry,\n this.valueExclusionDefaults,\n formOptions,\n this.valueExclusionEnabled(),\n );\n });\n\n /** Form value used for the outward two-way binding sync. */\n private readonly boundFormValue = computed(() => {\n const rawValue = this.modelValue();\n const setup = this.formSetup();\n const schemaFields = setup.schemaFields;\n const options = this.effectiveFormOptions();\n\n if (!this.formSchemaReady() || !schemaFields || 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 schemaFields,\n fieldTreeRecord,\n setup.registry,\n BOUND_VALUE_EXCLUSION_BASELINE,\n formOptions,\n this.boundValueExclusionEnabled(),\n );\n });\n\n /** Whether the form is currently valid. */\n /**\n * Whether the form is currently valid.\n *\n * Schema compilation is lazy, so there is a window on first load, and after a config swap,\n * where no schema is in hand yet. Validity is unknown then rather than true: this reports\n * `false` and {@link invalid} reports `true` for that window, so a guard cannot let an\n * unvalidated form through. {@link dirty}, {@link touched} and {@link submitting} likewise\n * report `false` until the schema lands.\n */\n readonly valid = computed(() => this.formSchemaReady() && this.formSchemaResource.status() !== 'error' && this.formInstance().valid());\n\n /** Whether the form is currently invalid. True while the schema loads — see {@link valid}. */\n readonly invalid = computed(\n () => !this.formSchemaReady() || this.formSchemaResource.status() === 'error' || this.formInstance().invalid(),\n );\n\n /** Whether any form field has been modified. False while the schema loads — see {@link valid}. */\n readonly dirty = computed(() => this.formSchemaReady() && this.formInstance().dirty());\n\n /** Whether any form field has been touched (blurred). */\n readonly touched = computed(() => this.formSchemaReady() && this.formInstance().touched());\n\n /** Current validation errors from all fields. */\n readonly errors = computed(() => (this.formSchemaReady() ? 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.formSchemaReady() && this.formInstance().disabled();\n return optionsDisabled ?? formDisabled;\n });\n\n /** Whether the form is currently submitting. */\n readonly submitting = computed(() => this.formSchemaReady() && 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 // Record page requests here rather than only in the pager, so one made before the\n // orchestrator mounts survives until it can be applied.\n this.eventBus\n .on<GoToPageEvent>('go-to-page')\n .pipe(takeUntilDestroyed(this.destroyRef))\n .subscribe((event) => this.pendingPageRequest.set({ index: event.pageIndex, options: event.options }));\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.formSchemaResource.error], ([error]) => {\n if (error) {\n this.logger.error(\n '[Dynamic Forms] Failed to compile the form schema; rendering without validation and blocking submission.',\n error,\n );\n }\n });\n\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 // Record before writing so `externalValue` can recognise the echo. Only\n // `lastOutwardValue` — writing `lastExternalValue` here turned its memo into a\n // poisoned cache that returned a fresh object every keystroke, which notified\n // `entity` anyway. It is now written solely inside `externalValue`.\n this.lastOutwardValue.current = currentBound;\n this.lastOutwardValue.pending = true;\n this.deps.value.set(currentBound as TModel);\n }\n });\n\n // Reporting only — nothing downstream reads the result, so the whole validator\n // (FormModeValidator + page-nesting checks) tree-shakes out of production.\n if (DEV_MODE) {\n explicitEffect([this.activeConfig], ([config]) => {\n if (!config) return;\n const validation = FormModeValidator.validateFormConfiguration(config.fields || []);\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\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 // Diagnostics only — the addon *filtering* above is what's load-bearing.\n // Gated so `addonWarningKey`/`formatAddonWarning` tree-shake out of prod builds.\n if (DEV_MODE) {\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 }\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 { ChangeDetectionStrategy, Component, computed, inject, input, linkedSignal, untracked } from '@angular/core';\nimport { takeUntilDestroyed } from '@angular/core/rxjs-interop';\nimport { EventBus } from '@ng-forge/dynamic-forms/internal';\nimport { PageNavigationOptions } from '../../events/constants/go-to-page.event';\nimport { NextPageEvent } from '../../events/constants/next-page.event';\nimport { PageChangeEvent } from '../../events/constants/page-change.event';\nimport { PreviousPageEvent } from '../../events/constants/previous-page.event';\nimport { NavigationResult, PagerState } 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 { PagerStateEvent } from '../../events/constants/pager-state.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 { PAGE_PRELOAD_WINDOW } from '../../providers/features/page-preload/page-preload.token';\nimport { clampWindowSize } from '../../providers/features/clamp-window';\nimport { collectLeafFieldKeys } from '../../utils/page-utils/collect-leaf-field-keys';\nimport { FormStateManager } from '../../state/form-state-manager';\nimport { injectFormComponentPreloader } from '../../utils/preload-form-components/preload-form-components';\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 (page of pageRenderStates(); track page.field.key) {\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 (!page.hidden) {\n @if (page.active || page.preload) {\n @defer (when page.active; on idle) {\n <section page-field [field]=\"page.field\" [key]=\"page.field.key\" [pageIndex]=\"page.index\" [isVisible]=\"page.active\"></section>\n } @placeholder {\n <div class=\"df-page-placeholder\" [attr.data-page-index]=\"page.index\" [attr.data-page-key]=\"page.field.key\"></div>\n }\n } @else {\n <!--\n Pages outside the preload window remain lightweight placeholders.\n Form state derives from config, so leaving their components unmounted\n does not affect validity, derivations, or navigation. The active page\n renders directly; configured neighbours preload declaratively on idle.\n -->\n <div class=\"df-page-placeholder\" [attr.data-page-index]=\"page.index\" [attr.data-page-key]=\"page.field.key\"></div>\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 private readonly globalPreloadWindow = inject(PAGE_PRELOAD_WINDOW);\n private readonly stateManager = inject(FormStateManager, { optional: true });\n\n /** Pending programmatic page request, owned by FormStateManager. */\n private readonly pendingPageRequest = computed(() => this.stateManager?.pendingPageRequest() ?? null);\n\n /** Whether validation belongs to the page definitions currently being rendered. */\n private readonly schemaCurrent = computed(() => this.stateManager?.isSchemaCurrent() ?? true);\n\n /** Stable ownership scope; page arrays can be recreated during one config transition. */\n private readonly configScope = computed(() => this.stateManager?.activeConfig());\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 /** Current page ownership, reset atomically when the active config changes. */\n private readonly activePageOwnership = linkedSignal<\n object | undefined,\n {\n config: object | undefined;\n initialized: boolean;\n index: number;\n }\n >({\n source: this.configScope,\n computation: (config, previous) => {\n const totalPages = this.pageFields().length;\n\n if (totalPages === 0) return { config, initialized: true, index: 0 };\n\n // Untracked: re-land on a config swap, but never on a later validity or visibility change.\n return untracked(() => {\n // A newly mounted pager may restore ownership for the same config. An existing pager must\n // ignore ownership when the config changes because its outgoing effect can still publish\n // the previous index during that transition.\n const owned = this.stateManager?.activePageState();\n const mayRestore = previous === undefined || previous.source === config;\n if (\n mayRestore &&\n owned !== undefined &&\n owned.config === config &&\n owned.initialized &&\n owned.index >= 0 &&\n owned.index < totalPages\n ) {\n return { config, initialized: true, index: owned.index };\n }\n\n if (this.initialPage().validate && !this.schemaCurrent()) {\n return { config, initialized: false, index: 0 };\n }\n\n return { config, initialized: true, index: this.resolveInitialLanding() };\n });\n },\n });\n\n /** Actual page index, excluding hidden pages from its numbering. */\n private readonly currentPageIndex = computed(() => this.activePageOwnership().index);\n\n private setCurrentPageIndex(index: number): void {\n this.activePageOwnership.update((ownership) => ({ ...ownership, initialized: true, index }));\n }\n\n /** Where `initialPage` actually lands: hidden targets resolve forward, gated ones stop on the first invalid page. */\n private resolveInitialLanding(): number {\n const { index, validate } = this.initialPage();\n const visible = this.visiblePageIndices();\n if (index === 0 || visible.length === 0) return 0;\n\n const target = visible.includes(index) ? index : this.findNearestVisiblePage(index, visible);\n if (target <= 0) return Math.max(target, 0);\n if (!validate) return target;\n\n const firstInvalid = visible.filter((i) => i < target).find((i) => !this.isPageValid(i));\n return firstInvalid ?? target;\n }\n\n /** Resolved `FormOptions.initialPage`: out-of-range clamps to the last page, invalid values fall back to 0. */\n private readonly initialPage = computed<{ index: number; validate: boolean }>(() => {\n const raw = this.formOptions?.()?.initialPage;\n const totalPages = this.pageFields().length;\n const requested = typeof raw === 'number' ? raw : raw?.index;\n const validate = typeof raw === 'number' ? false : (raw?.validate ?? false);\n\n if (typeof requested !== 'number' || !Number.isFinite(requested) || requested < 0 || totalPages === 0) {\n return { index: 0, validate: false };\n }\n\n return { index: Math.min(Math.trunc(requested), totalPages - 1), validate };\n });\n\n /** Computed state for the orchestrator */\n readonly state = computed<PagerState>(() => {\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 };\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 // Reports invalid while the schema is stale: the incoming page's fields are mounted but not\n // yet validated, so trusting them would let a second click skip a page.\n readonly currentPageValid = computed(() => (this.stateManager?.isSchemaCurrent() ?? true) && this.isPageValid(this.currentPageIndex()));\n\n /**\n * Whether all fields on the given page are currently valid.\n *\n * Form state derives from config, not from mounted components, so any page can\n * be checked without having been visited.\n *\n * @param pageIndex The page index (0-based)\n * @returns `true` if the page is valid or the index is out of range\n */\n private isPageValid(pageIndex: number): boolean {\n const pages = this.pageFields();\n const form = this.form();\n\n if (pageIndex < 0 || pageIndex >= pages.length) {\n return true;\n }\n\n // Collect all leaf field keys, recursively traversing group/row containers\n const leafKeys = collectLeafFieldKeys(pages[pageIndex].fields || []);\n\n // Check validity of each leaf field on the 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 /**\n * Effective preload window (pages preloaded on idle on each side of the current page).\n * Per-form `FormOptions.pagePreloadWindow` wins over the global\n * `withPagePreload(n)` default; both fall back to `1`. Clamped to `>= 0`.\n */\n readonly preloadWindow = computed(() => {\n const perForm = this.formOptions?.()?.pagePreloadWindow;\n // The global is already normalized; a non-finite per-form value falls back to it.\n return clampWindowSize(perForm, this.globalPreloadWindow);\n });\n\n /** Declarative render mode for every configured page. */\n readonly pageRenderStates = computed(() => {\n const activeIndex = this.currentPageIndex();\n const hiddenStates = this.pageHiddenStates();\n const preloadWindow = this.preloadWindow();\n\n return this.pageFields().map((field, index) => ({\n field,\n index,\n hidden: hiddenStates[index],\n active: index === activeIndex,\n preload: index !== activeIndex && Math.abs(index - activeIndex) <= preloadWindow,\n }));\n });\n\n constructor() {\n // Setup event listeners for navigation\n this.setupEventListeners();\n\n // Warm only the pages that can currently render. When navigation moves the\n // window this effect preloads the newly selected subtree before its deferred\n // page component discovers those chunks field by field.\n const preloader = injectFormComponentPreloader();\n explicitEffect([this.pageRenderStates], ([states]) => {\n preloader.preloadFields(states.filter((page) => !page.hidden && (page.active || page.preload)).map((page) => page.field));\n });\n\n // A config swap exposes its page definitions before their schema is current. Defer a gated\n // landing until those fields can be validated, then resolve it exactly once for this page set.\n explicitEffect([this.schemaCurrent, this.pageFields], ([schemaCurrent]) => {\n const ownership = this.activePageOwnership();\n if (!schemaCurrent || ownership.initialized) return;\n this.activePageOwnership.set({ ...ownership, initialized: true, index: this.resolveInitialLanding() });\n });\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.executePageChange(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 // 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.executePageChange(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 // 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.executePageChange(prevVisiblePageIndex);\n }\n\n /**\n * Navigate to an arbitrary page, applying the jump validation semantics.\n *\n * Backward jumps are unconditional, matching `previous` having no validity gate.\n * Forward jumps validate every visible page crossed by the jump — from the\n * current page up to, but excluding, the target — because conditions may have\n * changed since those pages were last visited. On failure, navigation lands on\n * the first invalid page rather than staying put, so the user ends up where\n * work is required. Hidden pages are never validated.\n *\n * Respects `nextButton.disableWhenPageInvalid`, the same option that gates\n * next-page navigation.\n *\n * @param pageIndex The target page index (0-based)\n * @returns Navigation result\n */\n navigateToPage(pageIndex: number, options?: PageNavigationOptions): NavigationResult {\n const currentIndex = this.state().currentPageIndex;\n const visibleIndices = this.visiblePageIndices();\n\n // Backward jumps and no-ops need no gate. Out-of-bounds and hidden targets\n // fall through too — executePageChange reports those without navigating.\n if (pageIndex <= currentIndex || !visibleIndices.includes(pageIndex)) {\n return this.executePageChange(pageIndex);\n }\n\n // Per-dispatch opt-out, for restoring a saved session onto its exact page.\n if (options?.validate === false) {\n return this.executePageChange(pageIndex);\n }\n\n const disableWhenPageInvalid = this.formOptions?.()?.nextButton?.disableWhenPageInvalid ?? true;\n if (!disableWhenPageInvalid) {\n return this.executePageChange(pageIndex);\n }\n\n const crossedPages = visibleIndices.filter((index) => index >= currentIndex && index < pageIndex);\n const firstInvalidPage = crossedPages.find((index) => !this.isPageValid(index));\n\n if (firstInvalidPage === undefined) {\n return this.executePageChange(pageIndex);\n }\n\n // Partial jump: land on the first invalid page and report the failure.\n this.executePageChange(firstInvalidPage);\n\n return {\n success: false,\n newPageIndex: firstInvalidPage,\n error: `Cannot navigate to page ${pageIndex}: page ${firstInvalidPage} has invalid fields`,\n };\n }\n\n /**\n * Performs the actual page change, with no validity gating.\n *\n * Validates bounds and target visibility, updates the active index, and emits\n * `PageChangeEvent`. All gated entry points funnel through here.\n *\n * @param pageIndex The target page index (0-based)\n * @returns Navigation result\n */\n private executePageChange(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.setCurrentPageIndex(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 // Programmatic jumps arrive as pending intent on FormStateManager rather than as a live\n // subscription here: this component mounts behind the render gate, so a request made on\n // `initialized` would otherwise reach the bus before this listener existed. Draining it\n // here keeps visibility and validity resolution in the one place that knows about them.\n explicitEffect([this.pendingPageRequest], ([request]) => {\n if (!request) return;\n this.stateManager?.pendingPageRequest.set(null);\n this.navigateToPage(request.index, request.options);\n });\n\n explicitEffect([this.state], ([state]) => this.eventBus.dispatch(PagerStateEvent, state));\n\n // Publish upward so navigation survives this pager being temporarily unmounted.\n explicitEffect([this.activePageOwnership], ([ownership]) => this.stateManager?.activePageState.set(ownership));\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","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 { FieldViewportObserver } from '../directives/df-field-outlet/field-viewport-observer.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 // Form-scoped, never root: it holds a mutable observer map, and\n // module-scoped mutable state is shared across requests under SSR.\n FieldViewportObserver,\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 signal,\n} from '@angular/core';\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 { filter, shareReplay, take } from 'rxjs';\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 { InferFormModel } from '@ng-forge/dynamic-forms/internal';\nimport { isContainerField } from '@ng-forge/dynamic-forms/internal';\nimport { explicitEffect } from 'ngxtension/explicit-effect';\nimport { injectFormComponentPreloader } from './utils/preload-form-components/preload-form-components';\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 { PagerStateEvent } from './events/constants/pager-state.event';\nimport { ActivePageInitializedEvent } from './events/constants/active-page-initialized.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';\nimport { getGridClassString } from '@ng-forge/dynamic-forms/internal';\nimport { ResolvedField } from './utils/resolve-field/resolve-field';\nimport { FIELD_WINDOWING } from './providers/features/field-windowing/field-windowing.token';\nimport { resolveFieldWindowing } from './providers/features/field-windowing/resolve-field-windowing';\nimport { collectInitializingContainerKeys, initializationComponentKey } from './utils/container-utils/container-utils';\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; let i = $index) {\n @if (windowsField(field, i)) {\n @if (!field.hidden()) {\n @defer (on viewport) {\n <ng-container *dfFieldOutlet=\"field; environmentInjector: environmentInjector\" />\n } @placeholder {\n <div\n [class]=\"placeholderGridClass(field)\"\n [style.min-height]=\"fieldWindowing().placeholderHeight\"\n [attr.data-field-key]=\"field.key\"\n aria-hidden=\"true\"\n ></div>\n }\n }\n } @else {\n <!-- DfFieldOutlet owns the hidden gate so an already-mounted field can be detached\n and restored without recreating its component and wrapper chain. -->\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 private globalFieldWindowing = inject(FIELD_WINDOWING);\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.stateManager.formSchemaReady() && this.derivationReady());\n\n /** Resolved fields ready for rendering */\n protected resolvedFields = this.stateManager.resolvedFields;\n\n /**\n * Effective field-windowing config for non-paged forms. Per-form\n * `FormOptions.fieldWindowing` wins over the global `withFieldWindowing()`\n * default; both fall back to disabled (fully eager).\n */\n protected readonly fieldWindowing = computed(() =>\n resolveFieldWindowing(this.globalFieldWindowing, this.effectiveFormOptions()?.fieldWindowing),\n );\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Computed Signals - Internal\n // ─────────────────────────────────────────────────────────────────────────────\n\n private readonly initializedComponents = signal<ReadonlySet<string>>(new Set());\n private readonly initialPageReady = signal(false);\n\n private readonly initializationReady = computed(() => {\n const config = this.activeConfig();\n if (!config) return false;\n\n if (this.formModeDetection().mode === 'paged') return this.initialPageReady();\n\n const initialized = this.initializedComponents();\n const expected = [\n initializationComponentKey('dynamic-form', this.componentId),\n ...collectInitializingContainerKeys(config.fields ?? []),\n ];\n return expected.every((key) => initialized.has(key));\n });\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Initialization\n // ─────────────────────────────────────────────────────────────────────────────\n\n /** Replayable readiness so late subscribers cannot miss hot EventBus announcements. */\n initialized$ = toObservable(this.initializationReady, { injector: this.injector }).pipe(\n filter((ready) => ready),\n take(1),\n shareReplay({ bufferSize: 1, refCount: false }),\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 a non-paged form is fully initialized, or when the initially active page of a\n * paged form is ready for interaction. Virtualized inactive pages are not part of initial\n * readiness because they intentionally remain unmounted.\n */\n initialized = outputFromObservable(this.initialized$);\n\n /**\n * EXPERIMENTAL. Added alongside page preloading; the emission contract may change.\n *\n * Emits after the currently visible page and all of its visible fields render. Announced\n * once per visit to a page, and again when that page is returned to. Use this when work must\n * run after every page transition; `initialized` emits only for the initial page.\n */\n activePageInitialized = outputFromObservable(this.eventBus.on<ActivePageInitializedEvent>('active-page-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 pager state changes (currentPageIndex, totalPages, isFirstPage, isLastPage). */\n onPageNavigationStateChange = outputFromObservable(this.eventBus.on<PagerStateEvent>('pager-state'));\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Constructor\n // ─────────────────────────────────────────────────────────────────────────────\n\n constructor() {\n this.eventBus\n .on<ComponentInitializedEvent>('component-initialized')\n .pipe(takeUntilDestroyed(this.destroyRef))\n .subscribe((event) => {\n const key = initializationComponentKey(event.componentType, event.componentId);\n this.initializedComponents.update((current) => {\n if (current.has(key)) return current;\n const next = new Set(current);\n next.add(key);\n return next;\n });\n });\n this.eventBus\n .on<ActivePageInitializedEvent>('active-page-initialized')\n .pipe(takeUntilDestroyed(this.destroyRef), take(1))\n .subscribe(() => this.initialPageReady.set(true));\n\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 // Start every field and wrapper chunk this config needs, in parallel, before\n // rendering asks for any of them. Wrapper chains are otherwise discovered\n // per-field inside DfFieldOutlet, which cannot exist until that field's own\n // component has loaded — a dependent waterfall that costs a round trip per\n // wave. Fire-and-forget: nothing here gates render.\n const preload = injectFormComponentPreloader();\n explicitEffect([this.config], ([config]) => preload.preloadConfig(config as FormConfig));\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 * Whether a field should be deferred by field windowing. Containers are never\n * windowed: they emit `ComponentInitializedEvent`, which the form's init\n * tracker counts, so deferring one would stall `initialized` until it scrolls\n * into view. Leaf fields carry no such count, so they defer freely.\n */\n protected windowsField(field: ResolvedField, index: number): boolean {\n const w = this.fieldWindowing();\n return w.enabled && index >= w.eager && !isContainerField(field.fieldDef);\n }\n\n /** Class string for a windowed field's placeholder — matches the real field's grid column. */\n protected placeholderGridClass(field: ResolvedField): string {\n const grid = getGridClassString(field.fieldDef);\n return grid ? `df-field-placeholder ${grid}` : 'df-field-placeholder';\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, this.activeConfig], ([, { mode }, config]) => {\n if (config && mode === 'non-paged') {\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","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';\nimport { FieldErrorsWrapper } 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 wrapperName: 'field-errors',\n loadComponent: () => import('../wrappers/field-errors/field-errors-wrapper.component'),\n rendersFieldErrors: true,\n } satisfies WrapperTypeDefinition<FieldErrorsWrapper>,\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 | 'page-preload'\n | 'field-windowing'\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';\nimport { DEV_MODE } from '../utils/dev-mode';\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 customWrappers = [...wrapperTypes, ...wrapperBundles.flatMap((bundle) => bundle.ɵdefinitions)];\n const wrappers = [...BUILT_IN_WRAPPERS, ...customWrappers];\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 // Collision report only — last registration wins either way.\n if (DEV_MODE && 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 // Seed the built-ins, then let later registrations replace them. Overriding a\n // built-in name is supported — an adapter restyles `field-errors` that way,\n // and an app may then override the adapter's — so those never warn. Only two\n // custom registrations colliding on a name of their own is a mistake.\n const builtInNames = new Set(BUILT_IN_WRAPPERS.map((w) => w.wrapperName));\n BUILT_IN_WRAPPERS.forEach((wrapperType) => registry.set(wrapperType.wrapperName, wrapperType));\n\n // `seenCustom` exists only to report duplicates — last registration wins either way.\n const seenCustom = DEV_MODE ? new Set<string>() : null;\n customWrappers.forEach((wrapperType) => {\n const name = wrapperType.wrapperName;\n if (seenCustom) {\n if (seenCustom.has(name) && !builtInNames.has(name)) {\n logger.warn(`Wrapper type \"${name}\" is already registered. Overwriting.`);\n }\n seenCustom.add(name);\n }\n registry.set(name, 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 // Collision report only — last registration wins either way.\n if (DEV_MODE && 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 { createFeature, DynamicFormFeature } from '../dynamic-form-feature';\nimport { clampWindowSize } from '../clamp-window';\nimport { PAGE_PRELOAD_WINDOW } from './page-preload.token';\n\n/**\n * Configures the global page preload window for paged forms — how many pages on\n * each side of the current page are mounted during browser idle time.\n *\n * @remarks\n * Paged forms only ever display one page at a time, so mounting every page (the\n * fields, containers, and change-detection of pages the user may never reach)\n * is wasted work: it inflates initial-render time and makes every keystroke pay\n * change detection for off-screen pages. The preload window bounds how much is\n * mounted around the current page. The active page always renders directly;\n * eligible neighbours render on idle. Pages outside the window remain lightweight\n * placeholders until navigation or a window change makes them eligible.\n *\n * **Precedence:**\n * 1. Per-form `pagePreloadWindow` on `FormOptions` — wins for that form\n * 2. Global `withPagePreload(n)` — baseline default for all forms\n * 3. No feature — token default (`1`, i.e. current page ±1)\n *\n * @param window - Pages to preload on idle on each side of the current page. `0`\n * mounts only the current page; `1` (default) mounts ±1 for flicker-free sequential\n * navigation; higher values pre-warm more pages for jump navigation. Negative\n * values are clamped to `0`.\n * @returns A DynamicFormFeature that configures the global preload window\n *\n * @example\n * ```typescript\n * provideDynamicForm(...withMaterialFields(), withPagePreload(2));\n * ```\n */\nexport function withPagePreload(window: number): DynamicFormFeature<'page-preload'> {\n const resolved = clampWindowSize(window, 1);\n return createFeature('page-preload', [{ provide: PAGE_PRELOAD_WINDOW, useValue: resolved }]);\n}\n","import { createFeature, DynamicFormFeature } from '../dynamic-form-feature';\nimport { clampWindowSize } from '../clamp-window';\nimport { FIELD_WINDOWING, FieldWindowingConfig } from './field-windowing.token';\nimport { FieldParkingOption, resolveFieldParking } from './resolve-field-windowing';\n\n/**\n * Configures global progressive field mounting (\"field windowing\") for large\n * flat (single-page) forms.\n *\n * @remarks\n * A flat form mounts every field eagerly, so a large form pays the full\n * initial-render cost up front and every keystroke walks all N live views —\n * even for fields far off-screen. Field windowing bounds how much is mounted:\n * the first `eager` fields render immediately, and the rest render as a\n * lightweight placeholder that mounts the moment it scrolls near/into view.\n * Validators, derivations, and values live at the Signal Forms schema level\n * (not the DOM), so unmounted fields keep full form semantics.\n *\n * Mounting is one-way — `@defer (on viewport)` never unmounts — so on its own\n * that bound is temporary: scroll a long form once and every field is live\n * again for the rest of the session. **Parking** closes the gap and comes on\n * with this feature (it is off when the feature is not used, since it changes\n * rendering behaviour). A field scrolled outside `park.margin` leaves change detection but\n * keeps its DOM, so it costs nothing per keystroke while staying findable by\n * browser search, fillable by autofill, and reachable by assistive tech. Its\n * own edits still reach the model: Angular binds `input` / `blur` with plain\n * DOM listeners, which keep firing on a detached view.\n *\n * **Precedence:**\n * 1. Per-form `fieldWindowing` on `FormOptions` — wins for that form\n * 2. Global `withFieldWindowing(...)` — baseline default for all forms\n * 3. No feature — token default (disabled, i.e. fully eager)\n *\n * @param config - `eager` (default `12`), `placeholderHeight` (default `'4rem'`)\n * and `park` (default on, margin `'100%'`) overrides. `eager` is clamped to a\n * non-negative integer.\n * @returns A DynamicFormFeature that enables the global field windowing default\n *\n * @example\n * ```typescript\n * provideDynamicForm(...withMaterialFields(), withFieldWindowing({ eager: 20 }));\n * ```\n */\nexport function withFieldWindowing(config?: {\n eager?: number;\n placeholderHeight?: string;\n park?: FieldParkingOption;\n}): DynamicFormFeature<'field-windowing'> {\n const resolved: FieldWindowingConfig = {\n enabled: true,\n eager: clampWindowSize(config?.eager, 12),\n placeholderHeight: config?.placeholderHeight ?? '4rem',\n park: resolveFieldParking({ enabled: true, margin: '100%' }, config?.park),\n };\n\n return createFeature('field-windowing', [{ provide: FIELD_WINDOWING, useValue: resolved }]);\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 { 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 { FormEvent } from '@ng-forge/dynamic-forms/internal';\n\n/** Emitted after the active page and all of its visible fields have rendered. */\nexport class ActivePageInitializedEvent implements FormEvent {\n readonly type = 'active-page-initialized' as const;\n\n constructor(\n public readonly pageIndex: number,\n public readonly pageKey: string,\n ) {}\n}\n","import { FormEvent } from '@ng-forge/dynamic-forms/internal';\n\n/** Per-dispatch overrides for a page jump. */\nexport interface PageNavigationOptions {\n /**\n * Whether to apply the forward-jump validity gate.\n *\n * Set `false` to land on the target regardless of earlier pages, which is what\n * restoring a saved session wants. Bounds and hidden-page checks still apply.\n *\n * @default true\n */\n validate?: boolean;\n}\n\n/**\n * Event dispatched to move a paged form to a specific page.\n *\n * Backward jumps are unconditional. Forward jumps validate every visible page\n * between the current one and the target (the target itself is not validated);\n * if one is invalid, navigation stops on the first invalid page. Pass\n * `{ validate: false }` to skip that gate and land on the target.\n */\nexport class GoToPageEvent implements FormEvent {\n readonly type = 'go-to-page' as const;\n\n constructor(\n /** The target page index (0-based) */\n public readonly pageIndex: number,\n /** Per-dispatch overrides; omit for the default gated behavior. */\n public readonly options?: PageNavigationOptions,\n ) {}\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 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 { 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"]}
|