@ng-forge/dynamic-forms 1.1.0-next.1 → 1.1.0-next.2
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-CIheZh11.mjs → ng-forge-dynamic-forms-array-field.component-CftX-bCB.mjs} +2 -2
- package/fesm2022/{ng-forge-dynamic-forms-array-field.component-CIheZh11.mjs.map → ng-forge-dynamic-forms-array-field.component-CftX-bCB.mjs.map} +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-bootstrap-derivation-orchestrator-C9aVachF.mjs → ng-forge-dynamic-forms-bootstrap-derivation-orchestrator-CFSJLZPZ.mjs} +2 -2
- package/fesm2022/{ng-forge-dynamic-forms-bootstrap-derivation-orchestrator-C9aVachF.mjs.map → ng-forge-dynamic-forms-bootstrap-derivation-orchestrator-CFSJLZPZ.mjs.map} +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-container-field.component-DNafCSO8.mjs → ng-forge-dynamic-forms-container-field.component-B_bQ-f2g.mjs} +2 -2
- package/fesm2022/{ng-forge-dynamic-forms-container-field.component-DNafCSO8.mjs.map → ng-forge-dynamic-forms-container-field.component-B_bQ-f2g.mjs.map} +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-css-wrapper.component-BmHGlmES.mjs → ng-forge-dynamic-forms-css-wrapper.component-D_yRuLpu.mjs} +4 -3
- package/fesm2022/ng-forge-dynamic-forms-css-wrapper.component-D_yRuLpu.mjs.map +1 -0
- package/fesm2022/{ng-forge-dynamic-forms-group-field.component-CKVIw2K5.mjs → ng-forge-dynamic-forms-group-field.component-Bl9WRPvF.mjs} +2 -2
- package/fesm2022/{ng-forge-dynamic-forms-group-field.component-CKVIw2K5.mjs.map → ng-forge-dynamic-forms-group-field.component-Bl9WRPvF.mjs.map} +1 -1
- package/fesm2022/ng-forge-dynamic-forms-integration.mjs +12 -1
- package/fesm2022/ng-forge-dynamic-forms-integration.mjs.map +1 -1
- package/fesm2022/ng-forge-dynamic-forms-internal.mjs +40 -24
- package/fesm2022/ng-forge-dynamic-forms-internal.mjs.map +1 -1
- package/fesm2022/{ng-forge-dynamic-forms-ng-forge-dynamic-forms-CBawLbtp.mjs → ng-forge-dynamic-forms-ng-forge-dynamic-forms-fMB33l2f.mjs} +22 -14
- package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-fMB33l2f.mjs.map +1 -0
- package/fesm2022/{ng-forge-dynamic-forms-page-field.component-BRixkgyS.mjs → ng-forge-dynamic-forms-page-field.component-C9bQkVjQ.mjs} +2 -2
- package/fesm2022/{ng-forge-dynamic-forms-page-field.component-BRixkgyS.mjs.map → ng-forge-dynamic-forms-page-field.component-C9bQkVjQ.mjs.map} +1 -1
- package/fesm2022/ng-forge-dynamic-forms.mjs +1 -1
- package/package.json +1 -1
- package/fesm2022/ng-forge-dynamic-forms-css-wrapper.component-BmHGlmES.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-CBawLbtp.mjs.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ng-forge-dynamic-forms-internal.mjs","mappings":";;;;;;;;;;;;;;;AAOE;AACE;;AAEF;AACE;;AAEF;AACE;;IAGF;AACF;;ACjBM;AACI;AAER;;AAEE;;;;;ACgFF;AACA;AACD;;;ACnFD;;AAEA;;AAkBE;AACA;;QAIE;AACE;;QAIF;AACE;;;;;AAQF;;;AAQI;;;;;AAMA;;AAGF;;;AAGI;AACA;;AAEF;;;;AAEuC;;;;AAO3C,QAMA;AACE;AAAyB;AACzB;;QAIF;AACE;;;AAGN;;;;AC9DmB;AACA;;;AAGjB,IAMS;;AACT;;;;AAWmB;;AAEnB,IASiB;;;AAGjB,IAMmB;AACjB;AACA;;AACA;;;;AAIF;;AASA,IAOmB;;;AAGjB;AAAY;AACZ;AACA;;;IAIiB;AACjB;AACA;;;AAGF;gCAKE;AACE;AACA;AACA;;AAEE;AACA;;;AAEF;AACF;gBAIA;AACE;AACA;AAAgD,wBAChD;;AAEG;AACA;;AAGK;AAEN;AACJ;;;AA/GS;;;;AARV;;;;AAPQ;AACT;;AAEA;;;;AAIC;AACD;AACE;AACA;AACA;AACD;;AAEF;;;;ACwBC;AACA;AACD;AASK;;AAEJ;AACF;;;ACvCE;;AAEA;AACE;;;;AAGF;AACF;;ACMO;4BAcL;AACE;;;;AAQA;;;;;oCASE;AACA;;;;AAKA;AACA;;;;;AAME;;;AAGF;;AAEE;;;;;;;;;AASA;;;AAGF;gCAGE;;;;;;;;;AASF;;;;;wEAWA;;;;;AAOA;AACA;;;AAIF;AACE;;AAEN;;;;ACoPI;;AAEJ;AASM;AACJ;AAA6B;;;AAE7B;AACF;AAQM;AAGJ;AACF;AAQM;;AAEN;AAQM;AACJ;AACF;AAQM;AACJ;AACF;;AC7aA;AAGA;AAoBM;IAKJ;AACE;;AAGF;AACE;YAEE;AAEF;;;wBAOE;AACA;;AAEE;oBAEE;;;YAMJ;;AAGF;wBAGE;AAEF;AACA;;AAIA;AACE;;AAEN;AAQM;;AAEF;;AAGF;AAEA;AACE;AACE;gBAEE;;;AAIJ;;;AAKA;YAEE;;AAGF;;AAIA;AACA;;AAGI;;;;AAKN;AACF;AAMM;AACJ;AACA;AACA;AACF;AAGA;;AAEI;AACA;;;AAGA;QAEA;AACE;;;AAGN;AAYM;AACJ;AACE;;;AAGJ;AAQM;AACJ;;;;;AASA;AACF;AAGM;;AAEN;AASM;AACJ;AACE;;;AAGJ;AAaM;;AAEN;AAaM;;AAEF;;;AAGJ;;ACzOA;AASA;AACE;AACA;AAAsC;AACtC;AACF;AAOA;AAIE;AAAkB;IAGlB;AACE;;IAIF;AACE;;AAGF;AACF;AAWM;;AAKJ;AAAyC;AAEzC;AAEA;;AAEI;;;AAGA;;;AAGA;;;AAGA;;AAEH;AACH;AAWM;AACJ;AAEA;;AAEI;AACE;;;;AAKF;AACA;;;;;AAKH;AACH;AAOA;AACE;AAAc;;;AAOd;;AAEE;AAAW;;;;AAKf;;;IC9HW;AACV;;;;ACIG;;AAEA;;AAEA;;AAEA;;AAEA;;AAEA;;AAEA;AACE;AACF;AACE;AACF;AACE;AACF;AACE;AACE;;AACA;AACA;;AAEJ;AACE;;AAEN;AAGA;;;AAGA;AASM;AACJ;AACE;;AAEJ;AASM;;;AAIJ;AACE;AAA6C;;;AAI/C;AAA6C;;AAE/C;;;ACzEA;IAEE;AACA;AACA;AACA;AACA;AACA;IAGA;IAGA;AACA;AACA;AACA;AACA;IAGA;AACA;AACA;AACA;AACA;AACA;IAGA;AACA;AACA;IAGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;IAGA;AACF;AA+FM;AAGK;AACA;AAHT;;;;AAME;;AAEH;;;;ACtJkB;AAEjB;AACE;;;;;;;;;AAcE;AACA;;;AAIF;;;;;;;;;;AAcE;;QAIF;AACE;;QAIF;AACE;;QAIF;;AAGM;AACN;AACA;;;AAME;AACE,iCACA;;AAGF;;;;AAKI;;;AAGA;;;AAGA;;;AAGA;;;AAGA;;;AAGA;;;;;;;;;;;;;AAcN;;;;;;;AASE;;AAGF;;;AAIA;;;;;;;;;AAaM;;AAEJ;;AAEA;;AAEA;;AAEA;;AAEA;;;;;AAMF;;AAEA;AACA;QAGA;AACA;AACE;AACA;;AAEF;AACE;AACA;;QAIF;;AAEE;AACE;AACA;AACF;AACE;AACA;AACF;AACE;AACA;AACF;AACE;AACA;AACF;AACE;AACA;AACF;AACE;AACA;AACF;AACE;AACA;AACF;AACE;AACA;;;;AAMF;AACE;AACF;AACE;AACF;AACE;AACF;AACE;AACF;AACE;AACF;AACE;AACF;AACE;AACF;AACE;AACF;AACE;AACF;AACE;AACF;AACE;AACF;AACE;AACF;AACE;AACF;AACE;AACF;AACE;AACF;AACE;AACF;AACE;AACF;AACE;AACF;AACE;;;AAGP;;;;;ACpOkB;AAEjB;AACE;;;;AAMA;AACA;AAEA;;;AAIA;AAEA;AACE;;;AAQF;;;gBAMA;AACA;AAAW;AACX;;AAGF;AAKE;;AACqC;AAErC;;AAEA;;;AAIF;AAKE;;;AAKE,4BACA;AACA;;;AAKA;;AAIE;;;;AAIE;AAEE;AACA;;;;;AAKF;AACA;;AAEF;;AAEA;AACA;;;AAIA;AACA;;AAEF,4BAEA;;;AAIF;;;;AAI8C;;;;AAK9C;;;AAIE;AACA;;AAGF;;;AAIA;;;AAIE;AACA;;AAGF;;;AAIA;AAEA;;AAEE;AACA;;AAGF;;;AAIA;;;AAIE;AACA;;AAGF;;;AAIA;AAEA;;AAEE;AACA;;AAGF;;;AAIA;AAEA;;AAEE;AACA;;AAGF;;;AAIA;;AAEE;;;AAIF;;;AAIA;;;;AAMM;;;AAGF;;;;AAIE;;AAEA;;;AAEA;;AAEA;;;;AAGA;;;AAEA;;;;gBAIF;;AAEA;;;gBAGA;;AAEA;;;;;;AAMJ;;;;;;;;;;;;;;;;AAsBE;;;AAIA;;;AAKA;;AAEA;;;AAKA;;AAEA;;;AAKA;;;AAKA;;AAEA;;;;;;;AAUA;;;;AAKF;;;;;AAOE;;;;AAKF;;;;;AAOE;;AAGF;YAEE;;AAEA;AACA;AACE;;;;;;AAUF;;;AAIF;;;AAIA;AACE;;AAEE;;;AAGJ;;AAGM;;AACc;;;;AAKpB;;AACA;;;;;;;;;;;;AAgBA;AAAsB;AAEtB;;AAEH;;AC9XD;;;;;;;;;;;;;;;;;;;;;;;;;AA0BA;AAEA;;;;;;;;;;;;;;;;;;;;;;AAuBA;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BA;AACE;AACA;AACA;AACA;;AAOF;;;;;;;;AAQC;;AAIkB;AACA;;AAGf;AACA;;IAIF;AACE;AACE;;AAGA;;AAGA;AACE;AAEF;AACE;AAEF;AACE;AAEF;AACE;AAEF;AACE;AAEF;AACE;AAEF;AACE;AAEF;AACE;AAEF;AACE;AAEF;AACE;;;AAIE;AACN;AACE;;AAEF;;AAGM;;;AAIJ;;;AAKA;;QAIF;AACE;;AAGF;;AAGM;;;wBAMJ;;;AAIF;AAA6C;AAC7C;gBAIA;AACE;;AAGF;AACE;;AAGF;;AAGM;QAEN;;AAEE;;AAEF;;AAEE;;;;AAOF;YAEE;;AAEA;;AAEA;;AAEA;;AACmB;;AAEnB;;AACmB;;YAInB;;AAEA;;AAEA;;AAEA;;AAEA;;AAEA;;AAEA;;AAEA;;AAGA;AACE;;;AAIE;;AAGN;AACE;;AAEA;;AAEA;;AAEA;AACE;;;AAIE;;;;AAON;;AASE;;AAGF;QAGA;AACE;;;AAKA;;;QAOF;AACA;;;AAKA;;AAGM;AACN;;AAGM;;;AAGJ;AACE;;;;AAIJ;;AAGM;AACN;AACA;AACA;AACE;AACA;AACE;;;;AAIJ;;;;AAKE;;;AAKF;;AAEE;;AAGF;;AAEH;;ACjXD;AAMA;AACU;AACA;AAER;AACE;;AAGF;;AAEE;YAEE;;;AAGF;;;;AAME;;;;AAQA;AACA;AACE;;;;;AAMJ;;AAGF;AACE;;AAEH;AAGD;;AAIE;;;AAQI;;QAIF;AACA;QAGA;AAEA;;IAIF;AACE;;;AAGE;;;AAEA;AACE;;;;;IAYN;;;IAKA;;;AAGI;;;AAGL;;ACrGD;AACE;AAAiD;;AAEjD;AACF;AAMM;AACJ;AACE;AACE;AAEF;AACE;;;AAOA;AACA;AAEA;AACE;;;;;AAQA;;AAGF;AACE;AACA;;AAGM;AAEJ;;;;;AAIF;AACA;;;AAIJ;AACE;AAEF;AACE;AAEF;AACE;AAEI;AAEJ;AAEF;AACE;AAEI;AAEJ;AAEF;AACE;;AAEN;AAEA;;AAE6B;;;;;;AAY3B;AACF;AAEA;AACE;QAEE;;;;AAGA;AACA;;AAEJ;AAEA;AACE;AAAwC;IAExC;AACF;AAEA;AACE;AAAwC;IAExC;AACF;;;IC5HW;AACV;;ACDK;;AAEF;;AAGF;AACE;;AAGF;AACE;;;;AAKF;;AAEF;;;;;ACPE;;AAEE;;AAEF;AACF;;;;;;;;;;ACda;AACA;;;;;AAEZ;;;;ACFK;AACJ;AACE;;;AAGJ;;ACAA;;AAEA;AAGA;;;AAEqD;;AAEnD;AACF;AAGA;IAEE;AAAyB;YAKzB;;;;AAIM;AACA;;;;;AAMN;AACF;;AAQU;AACA;;;AAIR;gBAaE;wBAKA;;AAEA;AACA;;AAGE;;gCAOF;;;AAGE;AACA;;AAEA;;AAEA;AACA;;;AAGA;;;;;IAOI;AACN;AACE;AACE;;;;;;QAOJ;;;;;AAgBA;AAEA;AACE;;;;;wBASF;;;;;;AASI;AACA;;AAEA;;AAEA;AACA;;;AAGA;;;;;;;AAQF;;AAEA;AACA;AACA;;AAEA;;AAEA;AACA;;;AAGA;;;;;AAMJ,IAOQ;AACN;AACA;AAAyB;AAEzB;;AAGE;;;AAIF;AACE;;;;;AAMF;;;AAQA;;AAEA;AACA;;AAGE;;;AAIF;;;AAIE;AACA;;AAEA;;AAEA;AACA;;;AAGA;AACE;;;;AAKN,IASA;;;AAQI;;AAGF;;AAGE;AACA;;;AAGA;;AAEA;AACA;AACE;;;;;;;;;;;;AClRW;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEjB,IAOA;;QAQE;;AAGF,IAMA;;;AAIA,IAMA;;;;;;;AAWE;AACA;;YAOF;;;IAWA;;;;;;AASA,IAMA;;;;AAQE;;YAOF;;;IAWA;;;;;;AASA,IAMA;;;;AAQE;;YAOF;;;IAWA;;;;;;AASA,IAMA;;;;AAQE;;AAGF;;;IAWA;;;;AAME;;AAGF;;;IAWA;;;;AAME;;AAGF;;;IAWA;;;;AAME;;AAGF;AAOE;;;;;;;;AAUF,IAMA;;;AAIA,IAMA;;;AAIA,IAMA;;;;;;;;;;;;;;;;;;;;AC3RiB;AACA;;;;AAMjB;;AAEE;AAAY;;AAGV;AACA;;;;AAMJ;;AAGI;AACA;AACE;;;;;;AAQJ;;AAEH;;;AClCK;AACJ;AAEA;AACE;;AAEE;AACE;;AAIA;;AAEF;;;AAIJ;AACE;AACA;;;;AAIA;;AAEE;;;AAIJ;;;;AAKA;AACE;;AAEE;AACE;AACE;;;AAEA;;;AAGJ;;;AAEA;;;AAIJ;AACE;;AAGF;AACF;;ACvDA;;AAEA;AAOA;AACE;AACF;;AAqDE;QAEE;AACE;;QAIF;;AAEE;;QAIF;;;;;;;AAQI;AACA;AACF;;QAIF;AACF;AAEA;AACF;;AChGM;;AAGF;wBAGE;AAEE;AACA;AAEA;;AAEF;AACF;AAGF;AACF;;ACkDM;AAGJ;;;;;AAOI;;gDAMA;AACE;gCAMI;;AAMF;oBAEA;;;AAGD;AAED;AACF;;AAGA;;;AAIF;;;;AAIA;AACE;;;AAIF;;AAEE;;AAGE;;AAEJ;AAEA;AACF;AACF;;ACpHA;;AAEI;AACE;AACA;;;;;;;AAUA;;;;AAIN;AAGM;AACJ;;AAEE;;AAEF;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;IAGA;;;AAGE;;;;;;AAOA;AACE;;wBAIA;AAAe;AAEf;;;;AAIA;;AAEF;AACE;;AAEA;AAAgC;;AACX;AAErB;AAGI;;AAEF;AAEE;AACA;;;AAIP;;AAGD;AACF;;AC9FM;AACJ;AACA;AACA;AACA;AACA;AAEA;AACA;;AAGE;;YAQF;AAEA;;;AAGI;;;;;;;AAQF;oCAIE;;;;;AAKC;;;AAGH;wBAGE;AAEE;;;AAKA;;AAGF;;AAOI;AACA;;;AAIP;AAED;;;AAGA;AACF;;;IC5EW;IAGA;IAGA;AACV;;ACCD;AACE;AACE;AACE;AACE;;;;;;;AASR;AAQM;IAEJ;AACE;;IAIF;AACE;;;;;IASF;AACA;AACA;IAGA;;;AAKE;;AAGF;gBAGE;AAEA;AACF;;AAIA;AACF;AASM;IAEJ;AACE;;IAIF;AACE;;;;;IASF;AACA;AACA;AACA;;;;AAQE;;AAGF;;;;YAMI;;oCAKA;;AAOE;AACF;AAEA;;;QAKF;;gBAOA;;AAEE;AACE;;AAEJ;QAGA;AACF;;AAIA;AACF;;AC1JM;AACJ;AACA;AACF;AAqBM;AACJ;;;;;;;;;;;;AAYF;;;AASE;AACE;AACA;;AAEF;AACA;AACF;;AAaI;AACA;AACD;AAWG;;;AAMJ;AAAY;AAEZ;AACA;AACA;AACF;;ACxFA;IAEE;IAEA;IAEA;IAEA;;AA8EF;AACE;AACA;;AAIF;AACE;AACA;;AAUF;AACE;;AAGE;AACE;AAEF;AACE;AAEF;AACE;AAEF;AACE;;AAEN;AASA;IAEE;AACE;;IAIF;AACE;;oBAQF;;;gBAQA;AACA;AACE;;AAEA;AACA;;AAGF;AACF;AASA;;AAEI;;AAGF;AACF;AASA;;AAEI;;AAGF;AACF;AAUA;;AAMI;;AAGF;;;;;;AAOA;AACF;AAQM;;QAGF;AACE;;;;;QASF;AACE;AACA;;AAGF;;AAGE;;;AAIA;;AAGF;AACF;AACF;AAQM;;QAGF;AACE;;;;;QASF;AACE;AACA;;AAGF;AAEA;AACE;;;AAIA;;AAGF;AACF;AACF;AAQM;IAEJ;AACE;;;AAKA;;;;;AAKC;;IAIH;AACF;AAQM;;AAEN;AAQM;IAEJ;AACE;;;AAKA;;;;;AAKC;;IAIH;AACF;AAQM;;AAEN;;AAaE;AACA;;AAKA;;;;AAIE;AACF;AACF;;AC/QM;;;AAKN;;;ACnIE;AAEA;AACE;AAEA;AACE;;AAGF;AACE;;;AAKF;AACE;AACE;AACE;;;;;;;AAON;;;;AAGA;;AAEJ;;;ICpCW;AACV;;ACQK;QAGJ;AACA;AACA;;;AAKE;;AAGF;;AAIE;;;;;AAKE;;AAEJ;;AAIA;AACF;;ACDA;;AAEA;AAEA;;;;;AAII;;AAEJ;AAEA;;;;;AAII;;AAEJ;AAEA;;;;;AAII;;AAEJ;AAEA;;;;;AAII;;AAEJ;AAEA;;;;;AAOI;;AAEJ;AAEA;AACE;AACF;AAGM;;AAGJ;;;AAIA;AACE;AACE;AACE;;;;;;AAKJ;;;AAGA;AACE;;;;AAIF;AACE;;;;AAIF;AACE;;;;AAIF;AACE;;;;AAIF;AACE;AACE;AACA;AACE;;;;;;;;AAQA;;;;;AAKN;AACE;;AAEF;AACE;;AAEF;AACE;;;AAGN;AAEA;AACE;;;AAIA;AAEA;AACE;;;AAEA;;;AAEA;;;AAIF;AACE;AAAkC;AAClC;AACF;AACF;AAEA;AAGE;;AAGE;;AAGF;AACE;AACA;;AAGF;;AAEE;;AAGF;;;AAIE;;AAGF;AACF;AAEA;AAGE;AACA;;AAEE;;AAGF;AACA;;AAGE;AACE;;;AAIE;;AAGF;AACA;AAEA;AACE;AACE;AACE;;;;;AAIJ;;AAGF;;;;;;AAOJ;AACF;AAGA;;AAEI;AACA;;AAGF;AACA;AACE;;AACK;AACL;;;;;;;QAOA;;;AAIF;AACE;AACE;AAAkC;;;;;;;AAQtC;AACF;AAMA;AAIE;AACE;;;;;AAQA;;AAIF;AACF;AAGA;;AAEI;AACA;;AAGF;AACA;AACE;;AACK;AACL;;;;;;;QAOA;;;AAIF;AACE;AACE;AAAkC;;;;;;AAOtC;AACF;AAGA;AACE;AACA;AACA;;;AAIE;AACE;AAAkC;oCAIlC;;;AAIF;;;AAGA;AACE;;;AAGmC;AACzC;AAGM;AACJ;AACF;;AChWM;;AAIN;;ACEM;YAMJ;AACF;;ACsBM;AACJ;AACF;AAuEM;;AAEN;;ACnGM;AACJ;AAEE;AACA;AACA;;AAGJ;;ACjBM;;AAEN;;ACMM;AACJ;AACF;;ACRM;AACJ;AACF;;ACCM;AACJ;AACF;AAQM;AACJ;AACF;AAGA;AACE;AAEE;;AAGJ;AAQA;AACE;AACE;AACE;;QAIF;AACE;AACE;;;;AAIN;AACF;;;ACvEW;AACA;;;;;;AAEV;;;ACDC;AACA;AACD;;;;;;;;;;;ACYA;;ACPD;AACE;AACE;;AACA;AACA;;AAEJ;AAMA;AACE;AACE;;AACA;AACA;;AAEJ;AAMA;;AAEE;AACF;AAQA;;AAImB;AACA;AACA;AACA;AACA;AAEjB;AAgBA;;AAEI;;;;;;AAMJ,IAMA;AACE;;AAGF,IAIQ;AACN;;wBAIE;;;;;AAMF;;AAGF,IAIQ;AACN;AACE;;;;;;;;AASF;;AAiBF,IAMA;AACE;;;;;;;;;;;;ACtIE;;AAEN;;ACEM;AAIJ;QAEE;AACE;;;AAKA;;AAEA;;AAEA;;AAEA;;AAEA;;AAEA;gBAEE;;AAEN;AACF;;ACxCM;AACJ;;;;AAIA;AACF;AAQM;AACJ;AACA;;;AAKE;;;AAIA;;AAGF;AACF;;AChBM;AAMJ;AACA;gBAGE;AACE;;;AAGA;;AAED;;AAEL;;AC1BM;AACJ;AACA;AACA;;AAGE;;AAEE;;;;AAMF;AACF;AACF;;AClBM;AACJ;;oBAQA;AAEA;AACE;;IAIF;;;AAIE;;;AAIA;;AAGF;;;AAIA;AACE;;;AAMF;AAGA;AACE;;AAGF;AACE;;gBAOF;AACE;;AAGF;AACF;AAQM;;AAIN;;ACpEM;AACJ;AACA;AACA;;gDASE;AAEA;;AAEE;;;;AAMF;AACF;AACF;;ACxBM;AACJ;AACA;AACA;;AAGE;;AAEE;;;;AAMF;AACF;AACF;;ACtBA;AAGM;AACJ;AACA;AACA;;gDASE;AAEA;;AAEE;;;;AAMF;AACF;AACF;;ACxBM;AACJ;IAGA;;AAEE;;AAED;AACH;;ACLM;AACJ;AACA;AACA;;;AAKE;;AAIA;AACF;AACF;;ACqBM;AACJ;AACF;;;;;AC7BE;AACA;AACA;AACE;AACE;;;AAGJ;AACF;;;;ACUM;AACJ;AACF;;AA0BE;AACA;AACD;;AAIC;AACA;AACD;;AAUC;AACA;AACD;;AC3ED;;AAGI;;AAGJ;AAmBM;;AAEF;;AAGF;AACF;AAMM;;;AAKJ;AACF;AAQM;;;AAGA;AACA;AACA;;;AAIJ;AACA;AACA;;;;AAQI;;;AAKA;;;AAIA;AACA;;;;;;QAQF;AACE;;;AAIA;AACA;;;;AAIN;AAQA;AACE;AACE;AACE;;;AAKA;AACE;;;;AAIN;AACF;AASA;AACE;;AAGI;AACE;;;;AAMF;AACE;;;;AAIN;AACF;AAGM;AACJ;;AAEF;AAGM;AACJ;;AAEF;;AC/JM;;AAIN;AAGA;AAaM;;AAEN;AAMM;AACJ;AACF;AAOM;;AAEN;AAMM;;AAEN;;;;AC9CmB;AAEjB,IAMA;AACE;AACE;;AAGF;AACE;;AAGF;;;;;;;AArBE;AACJ;AACD;;;;ACAC;IAGA;AACE;AACE;;;AAIJ;AAAW;IAGX;AACE;;;AAGA;;AAGF;AACF;;AClBM;AACJ;AACE;;AAGF;AACE;;AAGF;;;AAIA;AACF;;;ACbE;AACA;AACD;AAGK;;AAMN;AAeM;;;;;AASA;AAEI,cACE;AAKF;QAGJ;AAGI;AACE;AAEI;;AAEA;gBAGJ;;AAEF;;AAGN;AAGJ;;ACtEM;;AAEJ;AAEA;;AAEE;AACF;AAEA;AACF;AAOA;;AAEI;;IAIF;AACE;;IAIF;AACE;;IAIF;AACE;;IAIF;AACE;;IAIF;QAEE;AACE;;QAGF;;AACA;QAEA;;AAEJ;AAOA;;;AAIsB;;AACA;;AACI;;AACD;;AACE;gBAMzB;AACE;;AACK;QAEL;AACA;;AACK;QAEL;AACA;;;AAE2B;;;;;AAQ7B;AAEA;AACF;;AC5FM;AACJ;AACF;AAOA;;AAEe;;;;IAQb;AAA4B;IAG5B;AAA2B;IAG3B;;;;;IASA;QAEE;;IAIF;AACA;IAGA;AAA2C;;;;;AASzC;;;AAKA;AAAuB;;;AAGnB;;;AAGJ;;;AAKA;AAAuB;AACvB;;;;AAMM;;;;;;AAMA;AAAY;;;AAEZ;;;;AAIN;;IAIF;AACE;AAA2B;;;;;IAS7B;AACA;AAEA;AAAmC;IAGnC;AAEA;AACF;AAUM;AACJ;AACA;AACA;AACF;AAUM;;;AAIA;;AAIN;AAUM;AACJ;;AAGI;;AAIN;AAQM;;AACe;AAEnB;;;;AAKE;AACA;;;;AAIO;;;;AAGL;;;AAIJ;AACF;AAgBA;AACE;AAAsC;;AAK2C;;AAG/E;;AAEE;;AAEF;AACF;AACF;AAQA;AACE;AAAiD;;;AAGnD;AAIA;AAiBM;;;AAQJ;AAEA;;;AAIE;AACE;AACA;AACA;;AAGF;AAEA;;AAEE;AACE;;;AAIJ;AACA;AACF;AACF;AASM;;AAEN;AASM;AAIJ;IAGA;AAA2B;;AAEzB;;;;;;IAUF;AACE;AACA;QAGA;AACE;;;;AAMA;;;AAIJ;AACF;AAGM;AACJ;AACF;;;ACzVE;AACF","names":[],"ignoreList":[],"sources":["../../../../packages/dynamic-forms/internal/src/lib/utils/dynamic-value/resolve-dynamic-value.ts","../../../../packages/dynamic-forms/internal/src/lib/errors/dynamic-form-error.ts","../../../../packages/dynamic-forms/internal/src/lib/models/addon/addon-type.ts","../../../../packages/dynamic-forms/internal/src/lib/addons/inject-addon-type-registry.ts","../../../../packages/dynamic-forms/internal/src/lib/addons/df-addon-slot.component.ts","../../../../packages/dynamic-forms/internal/src/lib/models/field-type.ts","../../../../packages/dynamic-forms/internal/src/lib/addons/inject-fields-supporting-addons.ts","../../../../packages/dynamic-forms/internal/src/lib/addons/run-preset-action.ts","../../../../packages/dynamic-forms/internal/src/lib/models/logic/logic-config.ts","../../../../packages/dynamic-forms/internal/src/lib/core/cross-field/cross-field-detector.ts","../../../../packages/dynamic-forms/internal/src/lib/core/derivation/field-state-extractor.ts","../../../../packages/dynamic-forms/internal/src/lib/core/expressions/async-condition-function-cache.service.ts","../../../../packages/dynamic-forms/internal/src/lib/core/expressions/value-utils.ts","../../../../packages/dynamic-forms/internal/src/lib/core/expressions/parser/types.ts","../../../../packages/dynamic-forms/internal/src/lib/core/expressions/parser/tokenizer.ts","../../../../packages/dynamic-forms/internal/src/lib/core/expressions/parser/parser.ts","../../../../packages/dynamic-forms/internal/src/lib/core/expressions/parser/evaluator.ts","../../../../packages/dynamic-forms/internal/src/lib/core/expressions/parser/expression-parser.ts","../../../../packages/dynamic-forms/internal/src/lib/core/expressions/condition-evaluator.ts","../../../../packages/dynamic-forms/internal/src/lib/core/expressions/http-condition-function-cache.service.ts","../../../../packages/dynamic-forms/internal/src/lib/utils/stable-stringify.ts","../../../../packages/dynamic-forms/internal/src/lib/models/field-signal-context.token.ts","../../../../packages/dynamic-forms/internal/src/lib/core/registry/root-form-registry.service.ts","../../../../packages/dynamic-forms/internal/src/lib/utils/safe-read-path-keys.ts","../../../../packages/dynamic-forms/internal/src/lib/core/registry/field-context-registry.service.ts","../../../../packages/dynamic-forms/internal/src/lib/core/registry/function-registry.service.ts","../../../../packages/dynamic-forms/internal/src/lib/core/http/http-condition-cache.ts","../../../../packages/dynamic-forms/internal/src/lib/core/http/http-request-resolver.ts","../../../../packages/dynamic-forms/internal/src/lib/utils/derived-from-deferred/derived-from-deferred.ts","../../../../packages/dynamic-forms/internal/src/lib/utils/resource-composition/with-previous-value.ts","../../../../packages/dynamic-forms/internal/src/lib/core/expressions/debounced-resource-logic-fn.ts","../../../../packages/dynamic-forms/internal/src/lib/core/expressions/http-condition-logic-function.ts","../../../../packages/dynamic-forms/internal/src/lib/core/expressions/async-condition-logic-function.ts","../../../../packages/dynamic-forms/internal/src/lib/core/expressions/logic-function-cache.service.ts","../../../../packages/dynamic-forms/internal/src/lib/core/expressions/logic-function-factory.ts","../../../../packages/dynamic-forms/internal/src/lib/core/field-tree-utils.ts","../../../../packages/dynamic-forms/internal/src/lib/core/logic/non-field-logic-resolver.ts","../../../../packages/dynamic-forms/internal/src/lib/models/validation/validator-config.ts","../../../../packages/dynamic-forms/internal/src/lib/core/http/http-response-evaluator.ts","../../../../packages/dynamic-forms/internal/src/lib/core/values/dynamic-value-function-cache.service.ts","../../../../packages/dynamic-forms/internal/src/lib/core/values/dynamic-value-factory.ts","../../../../packages/dynamic-forms/internal/src/lib/core/validation/validator-factory.ts","../../../../packages/dynamic-forms/internal/src/lib/definitions/base/base-checked-field.ts","../../../../packages/dynamic-forms/internal/src/lib/definitions/base/base-value-field.ts","../../../../packages/dynamic-forms/internal/src/lib/definitions/default/array-field.ts","../../../../packages/dynamic-forms/internal/src/lib/definitions/default/container-field.ts","../../../../packages/dynamic-forms/internal/src/lib/definitions/default/group-field.ts","../../../../packages/dynamic-forms/internal/src/lib/definitions/default/hidden-field.ts","../../../../packages/dynamic-forms/internal/src/lib/definitions/default/row-field.ts","../../../../packages/dynamic-forms/internal/src/lib/definitions/default/page-field.ts","../../../../packages/dynamic-forms/internal/src/lib/events/constants/component-initialized.event.ts","../../../../packages/dynamic-forms/internal/src/lib/providers/features/event-form-value/emit-form-value.token.ts","../../../../packages/dynamic-forms/internal/src/lib/providers/features/logger/noop-logger.ts","../../../../packages/dynamic-forms/internal/src/lib/events/event.bus.ts","../../../../packages/dynamic-forms/internal/src/lib/events/interfaces/form-event.ts","../../../../packages/dynamic-forms/internal/src/lib/events/utils/token-resolver.ts","../../../../packages/dynamic-forms/internal/src/lib/utils/grid-classes/grid-classes.ts","../../../../packages/dynamic-forms/internal/src/lib/mappers/apply-hidden-logic.ts","../../../../packages/dynamic-forms/internal/src/lib/mappers/array/array-field-mapper.ts","../../../../packages/dynamic-forms/internal/src/lib/mappers/base/base-field-mapper.ts","../../../../packages/dynamic-forms/internal/src/lib/mappers/container/container-field-mapper.ts","../../../../packages/dynamic-forms/internal/src/lib/mappers/group/group-field-mapper.ts","../../../../packages/dynamic-forms/internal/src/lib/mappers/row/row-field-mapper.ts","../../../../packages/dynamic-forms/internal/src/lib/mappers/page/page-field-mapper.ts","../../../../packages/dynamic-forms/internal/src/lib/mappers/text/text-field-mapper.ts","../../../../packages/dynamic-forms/internal/src/lib/models/addon/addon-action.ts","../../../../packages/dynamic-forms/internal/src/lib/models/addon/addon-action-registry.ts","../../../../packages/dynamic-forms/internal/src/lib/models/wrapper-type.ts","../../../../packages/dynamic-forms/internal/src/lib/models/types/form-mode.ts","../../../../packages/dynamic-forms/internal/src/lib/models/types/type-guards.ts","../../../../packages/dynamic-forms/internal/src/lib/pipes/dynamic-text/dynamic-text.pipe.ts","../../../../packages/dynamic-forms/internal/src/lib/utils/apply-meta.ts","../../../../packages/dynamic-forms/internal/src/lib/utils/dynamic-text-to-observable.ts","../../../../packages/dynamic-forms/internal/src/lib/utils/initialization-tracker/initialization-tracker.ts","../../../../packages/dynamic-forms/internal/src/lib/utils/interpolate-params.ts","../../../../packages/dynamic-forms/internal/src/lib/utils/object-utils.ts","../../../../packages/dynamic-forms/internal/src/lib/utils/warning-tracker.ts"],"sourcesContent":["import { computed, Injector, isSignal, Signal } from '@angular/core';\nimport { toSignal } from '@angular/core/rxjs-interop';\nimport { isObservable } from 'rxjs';\nimport { DynamicValue } from '../../models/types/dynamic-value';\n\n/** Normalises a {@link DynamicValue} into a `Signal<T>`. */\nexport function resolveDynamicValue<T>(value: DynamicValue<T> | undefined, fallback: T, injector?: Injector): Signal<T> {\n if (value === undefined) {\n return computed(() => fallback);\n }\n if (isSignal(value)) {\n return value;\n }\n if (isObservable(value)) {\n return toSignal(value, { initialValue: fallback, injector });\n }\n // Static value\n return computed(() => value);\n}\n","/** Base error class for all Dynamic Forms errors. */\nexport class DynamicFormError extends Error {\n private static readonly PREFIX = '[Dynamic Forms]';\n\n constructor(message: string) {\n super(`${DynamicFormError.PREFIX} ${message}`);\n this.name = 'DynamicFormError';\n }\n}\n","import { InjectionToken, Type } from '@angular/core';\nimport { LazyComponentLoader } from '../wrapper-type';\nimport { BaseAddon } from './addon-def';\nimport { AddonSlot } from './addon-slot';\n\n/**\n * Minimal JSON Schema fragment used by the MCP server to surface an addon\n * type's shape to AI form generators. Covers the common subset\n * (`type`/`properties`/`required`/`enum`/etc.) without pulling in a full JSON\n * Schema dependency. Extra keys are allowed via the `[key: string]` index so\n * vendor-specific annotations (`x-foo`, `markdownDescription`) still typecheck.\n */\nexport interface AddonTypeSchema {\n readonly $ref?: string;\n readonly $schema?: string;\n readonly title?: string;\n readonly description?: string;\n readonly type?:\n | 'object'\n | 'array'\n | 'string'\n | 'number'\n | 'integer'\n | 'boolean'\n | 'null'\n | ReadonlyArray<'object' | 'array' | 'string' | 'number' | 'integer' | 'boolean' | 'null'>;\n readonly enum?: ReadonlyArray<unknown>;\n readonly const?: unknown;\n readonly properties?: Readonly<Record<string, AddonTypeSchema>>;\n readonly required?: ReadonlyArray<string>;\n readonly additionalProperties?: boolean | AddonTypeSchema;\n readonly items?: AddonTypeSchema | ReadonlyArray<AddonTypeSchema>;\n readonly oneOf?: ReadonlyArray<AddonTypeSchema>;\n readonly anyOf?: ReadonlyArray<AddonTypeSchema>;\n readonly allOf?: ReadonlyArray<AddonTypeSchema>;\n readonly not?: AddonTypeSchema;\n readonly default?: unknown;\n readonly examples?: ReadonlyArray<unknown>;\n readonly minimum?: number;\n readonly maximum?: number;\n readonly minLength?: number;\n readonly maxLength?: number;\n readonly pattern?: string;\n readonly format?: string;\n /** Vendor extensions and unrecognised keywords pass through untyped. */\n readonly [key: string]: unknown;\n}\n\n/**\n * Optional shape validator a type ships to enforce its required fields at\n * runtime. Throws are caught by the validator and converted into lenient\n * warnings; the offending addon is dropped and the form keeps rendering.\n */\nexport type AddonShapeValidator<T extends BaseAddon = BaseAddon> = (addon: T, fieldKey: string) => void;\n\n/** Per-type registration metadata. */\nexport interface AddonTypeDefinition<T extends BaseAddon = BaseAddon> {\n /** Discriminant matching `addon.type`. */\n readonly type: string;\n /** Lazy loader for the type's renderer component. */\n readonly loadComponent: LazyComponentLoader;\n /** Optional shape validator — throw `DynamicFormError` to drop the addon with a warning. */\n readonly validate?: AddonShapeValidator<T>;\n /** Optional JSON Schema fragment for MCP / pre-flight tooling. */\n readonly schema?: AddonTypeSchema;\n /**\n * Whether the type survives `JSON.stringify` / `JSON.parse`. Types whose\n * shape includes a function payload (e.g., the built-in `'component'`\n * loader, inline `prime-button` `action` handlers) declare `jsonSafe: false`\n * so the validator drops them when the config was loaded from a JSON\n * source (`sanitizeFormConfig(config, { source: 'json' })`). Defaults to\n * `true` — types are JSON-safe unless explicitly opted out.\n */\n readonly jsonSafe?: boolean;\n}\n\n/** Per-field-type addon-support metadata declared at field registration. */\nexport interface FieldAddonSupport {\n /** Slots a field of this type accepts. */\n readonly slots: readonly AddonSlot[];\n /** Optional whitelist of allowed addon types. Omitted = any registered type. */\n readonly allowedTypes?: readonly string[];\n}\n\n/** Global registry of addon types. */\nexport const ADDON_TYPE_REGISTRY = new InjectionToken<Map<string, AddonTypeDefinition>>('ADDON_TYPE_REGISTRY', {\n providedIn: 'root',\n factory: () => new Map<string, AddonTypeDefinition>(),\n});\n\n/**\n * Cache for resolved addon-type components. Provided at form scope by\n * `provideDynamicForm`.\n *\n * @internal\n */\nexport const ADDON_TYPE_COMPONENT_CACHE = new InjectionToken<Map<string, Type<unknown>>>('ADDON_TYPE_COMPONENT_CACHE');\n","import { inject, Type } from '@angular/core';\nimport { DynamicFormError } from '../errors/dynamic-form-error';\nimport { ADDON_TYPE_COMPONENT_CACHE, ADDON_TYPE_REGISTRY, AddonTypeDefinition } from '../models/addon/addon-type';\n\n/** Pick the component class out of whatever a lazy loader returned. */\nfunction resolveDefaultExport<T>(result: Type<T> | { default: Type<T> }): Type<T> {\n return typeof result === 'object' && result !== null && 'default' in result ? result.default : result;\n}\n\n/**\n * Public shape returned by {@link injectAddonTypeRegistry}. Pinned explicitly\n * so the API extractor doesn't churn between versions when the inner factory\n * gets refactored.\n */\nexport interface AddonTypeRegistryRef {\n getType(type: string): AddonTypeDefinition | undefined;\n hasType(type: string): boolean;\n getTypeNames(): string[];\n loadTypeComponent(type: string): Promise<Type<unknown>>;\n getLoadedTypeComponent(type: string): Type<unknown> | undefined;\n readonly raw: ReadonlyMap<string, AddonTypeDefinition>;\n}\n\n/** Injection function for accessing the addon type registry. */\nexport function injectAddonTypeRegistry(): AddonTypeRegistryRef {\n const registry = inject(ADDON_TYPE_REGISTRY);\n const componentCache = inject(ADDON_TYPE_COMPONENT_CACHE);\n\n return {\n /** Look up a type definition by its discriminant. */\n getType(type: string): AddonTypeDefinition | undefined {\n return registry.get(type);\n },\n\n /** Whether a type is currently registered. */\n hasType(type: string): boolean {\n return registry.has(type);\n },\n\n /** All registered type names — useful for actionable validator warnings. */\n getTypeNames(): string[] {\n return Array.from(registry.keys());\n },\n\n /**\n * Resolve a type to its renderer component. Caches on first resolution.\n *\n * @throws DynamicFormError when the type is not registered.\n */\n async loadTypeComponent(type: string): Promise<Type<unknown>> {\n const definition = registry.get(type);\n if (!definition) {\n const known = Array.from(registry.keys()).join(', ') || '(none)';\n throw new DynamicFormError(`Addon type '${type}' is not registered. Currently registered types: ${known}.`);\n }\n\n const cached = componentCache.get(type);\n if (cached) {\n return cached;\n }\n\n try {\n const component = resolveDefaultExport(await definition.loadComponent());\n if (component) {\n componentCache.set(type, component);\n return component;\n }\n throw new DynamicFormError(`Addon type '${type}' loadComponent resolved to null.`);\n } catch (error) {\n if (error instanceof DynamicFormError) throw error;\n throw new DynamicFormError(\n `Failed to load component for addon type '${type}': ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n },\n\n /**\n * Synchronous lookup of an already-loaded type component, or undefined.\n * Returns `undefined` when the type isn't registered in this scope even\n * if a prior scope cached a component under the same name — prevents\n * cross-form cache hits when registrations diverge.\n */\n getLoadedTypeComponent(type: string): Type<unknown> | undefined {\n if (!registry.has(type)) return undefined;\n return componentCache.get(type);\n },\n\n /** Direct read-only access to the registry map. */\n get raw(): ReadonlyMap<string, AddonTypeDefinition> {\n return registry;\n },\n };\n}\n","import { NgComponentOutlet } from '@angular/common';\nimport { ChangeDetectionStrategy, Component, computed, inject, Injector, input, Signal, signal, Type } from '@angular/core';\nimport { explicitEffect } from 'ngxtension/explicit-effect';\nimport { AnyAddon } from '../models/addon/addon-def';\nimport { DynamicFormLogger } from '../providers/features/logger/logger.token';\nimport { resolveDynamicValue } from '../utils/dynamic-value/resolve-dynamic-value';\nimport { injectAddonTypeRegistry } from './inject-addon-type-registry';\nimport { WrapperFieldInputs } from '../wrappers/wrapper-field-inputs';\n\n/**\n * Dispatcher component that renders an addon by looking up its `type` in\n * `ADDON_TYPE_REGISTRY` and instantiating the registered component.\n */\n@Component({\n selector: 'df-addon-slot',\n imports: [NgComponentOutlet],\n template: `\n @if (resolvedComponent(); as cmp) {\n <ng-container *ngComponentOutlet=\"cmp; inputs: typeInputs()\" />\n }\n `,\n host: {\n '[attr.slot]': 'slotAttr()',\n '[class]': 'className()',\n '[style.display]': 'isHidden() ? \"none\" : null',\n },\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class DfAddonSlot {\n private readonly registry = injectAddonTypeRegistry();\n private readonly logger = inject(DynamicFormLogger);\n private readonly hostInjector = inject(Injector);\n\n readonly addon = input.required<AnyAddon>();\n /**\n * Optional read-only view of the host field's mapped inputs — same shape\n * wrappers receive (`field?: ReadonlyFieldTree`, `key`, `props`, etc.).\n * Forwarded to type components so they can read field state without\n * re-injecting `FIELD_SIGNAL_CONTEXT` and rebuilding context themselves.\n */\n readonly fieldInputs = input<WrapperFieldInputs | undefined>();\n /**\n * Pre-resolved `hidden` signal supplied by the host field. When provided,\n * the dispatcher reads this instead of re-resolving `addon.hidden` —\n * lets the host (e.g., `prime-input`) share a single subscription per\n * Observable-typed `hidden` between its own visibility filter and the\n * slot's `[style.display]` binding.\n */\n readonly hidden = input<Signal<boolean> | undefined>(undefined);\n\n /** `slot` HTML attribute forwarded to host — needed for Ionic shadow projection. */\n protected readonly slotAttr = computed(() => this.addon().slot);\n protected readonly className = computed(() => this.addon().className ?? null);\n\n /**\n * Locally-resolved `hidden` signal — always reflects `addon.hidden`\n * regardless of whether the host supplied a pre-resolved signal.\n * Populated via `explicitEffect` so `resolveDynamicValue` (which may call\n * `toSignal` for Observable-typed values) does NOT run inside a reactive\n * context (NG0602). The WeakMap caches per `hidden`-value identity, so\n * swapping `addon` inputs that share the same Observable reuses the\n * subscription instead of leaking a fresh one per swap.\n */\n private readonly _hiddenByValue = new WeakMap<object, Signal<boolean>>();\n private readonly _resolvedHidden = signal<Signal<boolean> | undefined>(undefined);\n\n /**\n * `hidden` resolved from `DynamicValue<boolean>` to a flat `Signal<boolean>`.\n * Prefers the host-supplied signal when present (avoids duplicate `toSignal`\n * subscriptions for Observable-typed `hidden`); falls back to the locally-\n * resolved signal, which is always populated by the addon-tracking effect.\n */\n protected readonly isHidden = computed(() => {\n const pre = this.hidden();\n if (pre) return pre();\n const local = this._resolvedHidden();\n return local ? local() : false;\n });\n\n /**\n * Latest async-loaded `{ type, component }` pair. Tagging the result with\n * its type lets `resolvedComponent` filter stale loads structurally — no\n * monotonic token needed. When `addon.type` changes mid-load, the prior\n * load still completes and writes here, but the computed ignores it\n * because the recorded type no longer matches the current one.\n */\n private readonly asyncLoaded = signal<{ type: string; component: Type<unknown> } | undefined>(undefined);\n\n /**\n * Resolved type component — composed declaratively from two sources of\n * truth: the registry's synchronous cache (warm hits, e.g., a second\n * `<df-addon-slot>` for an already-loaded type) and the type-tagged async\n * loader result. Reading `addon().type` first makes the computed react to\n * addon swaps and naturally invalidate stale async-load results.\n */\n protected readonly resolvedComponent = computed<Type<unknown> | undefined>(() => {\n const type = this.addon().type;\n const cached = this.registry.getLoadedTypeComponent(type);\n if (cached) return cached;\n const loaded = this.asyncLoaded();\n return loaded?.type === type ? loaded.component : undefined;\n });\n\n /** Inputs passed to the type component — addon plus the wrapper-style host bag. */\n protected readonly typeInputs = computed(() => ({\n addon: this.addon(),\n fieldInputs: this.fieldInputs(),\n }));\n\n constructor() {\n // Resolve `addon.hidden` outside any reactive context AND unconditionally —\n // the host-supplied signal is a perf hint, not a precondition. Keeping\n // `_resolvedHidden` always-fresh closes a race where `isHidden` could\n // return `false` between a host-`hidden` clear and the effect re-running.\n explicitEffect([this.addon], ([addon]) => {\n const h = addon.hidden;\n const valueKey = typeof h === 'object' && h !== null ? h : null;\n let resolved = valueKey ? this._hiddenByValue.get(valueKey) : undefined;\n if (!resolved) {\n resolved = resolveDynamicValue(addon.hidden, false, this.hostInjector);\n if (valueKey) this._hiddenByValue.set(valueKey, resolved);\n }\n this._resolvedHidden.set(resolved);\n });\n\n // Kick off async loads on cache miss. `resolvedComponent` filters stale\n // results by type identity, so this effect is a pure side-effect dispatcher.\n explicitEffect([this.addon], ([addon]) => {\n const type = addon.type;\n if (this.registry.getLoadedTypeComponent(type)) return; // sync path; computed picks it up\n this.registry\n .loadTypeComponent(type)\n .then((component) => this.asyncLoaded.set({ type, component }))\n .catch((error: unknown) => {\n this.logger.warn(\n `Failed to load addon type '${type}': ${error instanceof Error ? error.message : String(error)}. ` +\n `Registered types: ${this.registry.getTypeNames().join(', ') || '(none)'}.`,\n );\n });\n });\n }\n}\n","import { InjectionToken } from '@angular/core';\nimport { FieldDef } from '../definitions/base/field-def';\nimport { MapperFn } from '../mappers/types';\nimport { FieldAddonSupport } from './addon/addon-type';\nimport { LazyComponentLoader } from './wrapper-type';\n\n/** Defines how a field type handles form values and data collection. */\nexport type ValueHandlingMode = 'include' | 'exclude' | 'flatten';\n\n/** Semantic grouping of interchangeable field UI alternatives. */\nexport type FieldScope = 'boolean' | 'single-select' | 'multi-select' | 'text-input' | 'numeric' | 'date';\n\n/** Mapped component inputs that must be available before a field component is instantiated. */\nexport type RenderReadyInput = 'field' | (string & {});\n\n/**\n * Configuration interface for registering custom field types.\n *\n * @typeParam T - The field definition type this field type handles\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Intentionally using `any` for maximum flexibility in field type registration\nexport interface FieldTypeDefinition<T extends FieldDef<any> = any> {\n /** Unique identifier for the field type */\n name: string;\n /** Field definition type marker (internal use) */\n _fieldDef?: T;\n /**\n * Function to load the component (supports lazy loading).\n * Returns a Promise that resolves to the component class or module with default export.\n */\n loadComponent?: LazyComponentLoader;\n /** Mapper function that converts field definition to component bindings. */\n mapper?: MapperFn<T>;\n /** How this field type handles form values and data collection (defaults to 'include') */\n valueHandling?: ValueHandlingMode;\n /** List of prop keys to forward to the meta object. */\n propsToMeta?: string[];\n /** Semantic scope for tooling to discover interchangeable field alternatives */\n scope?: FieldScope | FieldScope[];\n /** Mapped component inputs that must exist before the renderer instantiates the component */\n renderReadyWhen?: readonly RenderReadyInput[];\n\n /**\n * Declares which addon slots this field accepts and (optionally) the\n * subset of kinds allowed.\n */\n addons?: FieldAddonSupport;\n}\n\n/** Injection token for the global field type registry. */\nexport const FIELD_REGISTRY = new InjectionToken<Map<string, FieldTypeDefinition>>('FIELD_REGISTRY', {\n providedIn: 'root',\n factory: () => new Map(),\n});\n\n/**\n * Gets the value handling mode for a specific field type from the registry.\n *\n * @param fieldType - The field type identifier\n * @param registry - The field type registry\n * @returns The value handling mode ('include' is the default if not specified)\n */\nexport function getFieldValueHandling(fieldType: string, registry: Map<string, FieldTypeDefinition>): ValueHandlingMode {\n const definition = registry.get(fieldType);\n return definition?.valueHandling ?? 'include';\n}\n","import { inject } from '@angular/core';\nimport { FieldAddonSupport } from '../models/addon/addon-type';\nimport { FIELD_REGISTRY } from '../models/field-type';\n\n/** One entry per field type that declares addon support. */\nexport interface FieldAddonSupportEntry {\n readonly name: string;\n readonly slots: FieldAddonSupport['slots'];\n readonly allowedTypes: FieldAddonSupport['allowedTypes'];\n}\n\n/**\n * Walk `FIELD_REGISTRY` and return every field type that opted into addons\n * (i.e., declared `addons` on its `FieldTypeDefinition`).\n *\n * @Component({ ... })\n * class FormBuilder {\n * protected readonly addonable = injectFieldsSupportingAddons();\n *\n * constructor() {\n * // -> [{ name: 'prime-input', slots: ['prefix', 'suffix'], allowedTypes: [...] }, ...]\n * }\n * }\n * ```\n */\nexport function injectFieldsSupportingAddons(): FieldAddonSupportEntry[] {\n const registry = inject(FIELD_REGISTRY);\n const entries: FieldAddonSupportEntry[] = [];\n for (const [name, def] of registry.entries()) {\n if (def.addons === undefined) continue;\n entries.push({ name, slots: def.addons.slots, allowedTypes: def.addons.allowedTypes });\n }\n return entries;\n}\n","import { WritableSignal } from '@angular/core';\nimport { AddonActionContext, AddonActionPreset } from '../models/addon/addon-action';\nimport type { Logger } from '../providers/features/logger/logger.interface';\n\n/**\n * Optional collaborators a preset may need beyond the base context. Passed\n * by the button kind component which has access to its own DI scope.\n */\nexport interface PresetCollaborators {\n /** Per-field input-type override signal — populated only inside an input field. */\n readonly typeOverride?: WritableSignal<string | undefined>;\n /** Writer that pushes a value into the host field's tree. */\n readonly fieldValueSetter?: (next: unknown) => void;\n /**\n * Read the host field's configured default value (from the form's\n * `defaultValues` map keyed by `field.key`). Used by the `'reset'`\n * preset; absent when the addon is rendered outside an active field\n * context, in which case `'reset'` falls back to clearing.\n */\n readonly fieldDefaultValueGetter?: () => unknown;\n /**\n * Baseline configured input type (from `props().type`), read at preset-run\n * time. Used by `toggle-password-visibility` to refuse to corrupt\n * `'email'`/`'number'`/etc. inputs into `'password'` when no override is\n * yet set.\n */\n readonly baselineType?: () => string | undefined;\n /** Logger for warnings about presets that can't be fulfilled in this context. */\n readonly logger: Logger;\n}\n\n/**\n * Apply a built-in preset action against the addon's host field. Shared\n * across all adapters — each adapter's `ADDON_PRESET_HANDLER` provider\n * delegates here.\n *\n * @param adapterLabel Used in warnings — `'Material'` / `'Bootstrap'` / etc.\n * @param fieldLabel Used in warnings — `'mat-input'` / `'bs-input'` / etc.\n */\nexport async function runPresetAction(\n preset: AddonActionPreset,\n ctx: AddonActionContext,\n collaborators: PresetCollaborators,\n adapterLabel: string,\n fieldLabel: string,\n): Promise<void> {\n // Guard against orphan dispatches — none of the built-in presets do\n // anything useful without a host field, except `copy` and `paste` which\n // only need the clipboard API. The canonical discriminant per the\n // `AddonActionContext` discriminated union is `ctx.form` — non-null on\n // the field-bound variant, null on the orphan variant. `field.key` was\n // unreliable for nested-array scenarios where keys can legitimately be\n // empty strings.\n if (ctx.form === null && preset !== 'copy' && preset !== 'paste') {\n collaborators.logger.warn(`preset '${String(preset)}' fired without a host field context — ignoring.`);\n return;\n }\n\n // A disabled field is non-editable; value-mutating presets must not change it, consistent with\n // the rest of the library treating disabled fields as non-interactive. `readonly` is not exposed\n // on the field-bound `ReadonlyFieldTree`, so the guard is scoped to `disabled`.\n if (ctx.form !== null && (preset === 'clear' || preset === 'reset' || preset === 'paste') && ctx.form.disabled()) {\n collaborators.logger.warn(`preset '${String(preset)}' skipped — host '${fieldLabel}' is disabled.`);\n return;\n }\n\n switch (preset) {\n case 'clear': {\n // Preserve type semantics: strings clear to `''` (browser-friendly for\n // input.value), everything else clears to `undefined` (numeric / object\n // / date fields would otherwise become a string and break their type).\n const next = typeof ctx.value === 'string' ? '' : undefined;\n collaborators.fieldValueSetter?.(next);\n return;\n }\n\n case 'reset': {\n const defaultValue = collaborators.fieldDefaultValueGetter?.();\n collaborators.fieldValueSetter?.(defaultValue !== undefined ? defaultValue : typeof ctx.value === 'string' ? '' : undefined);\n return;\n }\n\n case 'paste': {\n if (typeof navigator === 'undefined' || !navigator.clipboard) {\n collaborators.logger.warn(`preset 'paste' requires a browser clipboard API (not available in this environment).`);\n return;\n }\n try {\n const text = await navigator.clipboard.readText();\n collaborators.fieldValueSetter?.(text);\n } catch (error) {\n collaborators.logger.warn(`preset 'paste' clipboard read failed: ${error instanceof Error ? error.message : String(error)}`);\n }\n return;\n }\n\n case 'copy': {\n if (typeof navigator === 'undefined' || !navigator.clipboard) {\n collaborators.logger.warn(`preset 'copy' requires a browser clipboard API (not available in this environment).`);\n return;\n }\n try {\n // Explicit null check — `String(null) === 'null'` would write the\n // literal string \"null\" to the clipboard.\n const v = ctx.value;\n await navigator.clipboard.writeText(v == null ? '' : String(v));\n } catch (error) {\n collaborators.logger.warn(`preset 'copy' clipboard write failed: ${error instanceof Error ? error.message : String(error)}`);\n }\n return;\n }\n\n case 'toggle-password-visibility': {\n const override = collaborators.typeOverride;\n if (!override) {\n collaborators.logger.warn(`preset 'toggle-password-visibility' has no effect outside a '${fieldLabel}' field.`);\n return;\n }\n // Refuse to flip non-password inputs. Re-check on EVERY dispatch (not\n // just when override is undefined) because `props().type` can change\n // reactively after the first toggle — e.g., 'password' → 'email'. Once\n // the override is touched, only the override is canonical for the\n // current visible/hidden state; the baseline still gates whether\n // toggling makes semantic sense for this field.\n const baseline = collaborators.baselineType?.();\n if (baseline !== undefined && baseline !== 'password') {\n collaborators.logger.warn(\n `preset 'toggle-password-visibility' refused: host '${fieldLabel}' has type '${baseline}', not 'password'.`,\n );\n return;\n }\n const current = override();\n override.set(current === 'text' ? 'password' : 'text');\n return;\n }\n\n default:\n collaborators.logger.warn(`preset '${String(preset)}' is not implemented in the ${adapterLabel} adapter.`);\n }\n}\n","import type { AsyncDerivationFunction } from '../../core/expressions/async-custom-function-types';\nimport type { CustomFunction } from '../../core/expressions/custom-function-types';\nimport { ConditionalExpression } from '../expressions/conditional-expression';\nimport { HttpRequestConfig } from '../http/http-request-config';\n\n/** Special form-state conditions for button disabled logic. */\nexport type FormStateCondition =\n /** True when form.valid() === false */\n | 'formInvalid'\n /** True when form.submitting() === true */\n | 'formSubmitting'\n /** True when fields on the current page are invalid (for paged forms) */\n | 'pageInvalid';\n\n/** Logic type for controlling field state (hidden, readonly, disabled, required). */\nexport type StateLogicType = 'hidden' | 'readonly' | 'disabled' | 'required';\n\n/**\n * Base configuration for conditional field state logic.\n *\n * @internal\n */\ninterface BaseStateLogicConfig {\n /** Logic type identifier for field state. */\n type: StateLogicType;\n\n /** Condition that determines when this logic applies. */\n condition: ConditionalExpression | boolean | FormStateCondition;\n}\n\n/**\n * State logic that evaluates immediately on change (default).\n *\n * @internal\n */\ninterface ImmediateStateLogicConfig extends BaseStateLogicConfig {\n /**\n * Trigger for immediate evaluation.\n *\n * @default 'onChange'\n */\n trigger?: 'onChange';\n /** Not allowed for onChange trigger */\n debounceMs?: never;\n}\n\n/**\n * State logic that evaluates after a debounce period.\n *\n * @internal\n */\ninterface DebouncedStateLogicConfig extends BaseStateLogicConfig {\n /**\n * Trigger for debounced evaluation.\n * Evaluates after the value has stabilized for the debounce duration.\n */\n trigger: 'debounced';\n /**\n * Debounce duration in milliseconds.\n *\n * @default 500\n */\n debounceMs?: number;\n}\n\n/** Configuration for conditional field state logic. */\nexport type StateLogicConfig = ImmediateStateLogicConfig | DebouncedStateLogicConfig;\n\n/** Trigger timing for when logic is evaluated. */\nexport type LogicTrigger = 'onChange' | 'debounced';\n\n/**\n * Shared fields that appear on all derivation logic config variants.\n *\n * @internal\n */\ninterface SharedDerivationFields {\n /** Logic type identifier for value derivation. */\n type: 'derivation';\n\n /** Target property name for property derivation. */\n targetProperty?: string;\n\n /** Optional name for this derivation for debugging purposes. */\n debugName?: string;\n\n /** Condition that determines when this derivation applies. */\n condition?: ConditionalExpression | boolean;\n\n /**\n * When true, the derivation stops running after the user manually\n * edits the target field.\n */\n stopOnUserOverride?: boolean;\n\n /**\n * When true (and `stopOnUserOverride` is also true), clears the\n * user-override flag when any dependency of this derivation changes,\n * allowing the derivation to run again.\n */\n reEngageOnDependencyChange?: boolean;\n}\n\n/**\n * Trigger variants for derivation timing.\n *\n * @internal\n */\ntype ImmediateDerivationTrigger = { trigger?: 'onChange'; debounceMs?: never };\ntype DebouncedDerivationTrigger = { trigger: 'debounced'; debounceMs?: number };\n\n// ============================================================================\n// HTTP derivation mode (source: 'http' — required; dependsOn/responseExpression — required)\n// ============================================================================\n\n/**\n * Base for HTTP derivations. `source: 'http'` is required.\n * TypeScript enforces `dependsOn` and `responseExpression` at the type level.\n *\n * @internal\n */\ninterface HttpDerivationBase extends SharedDerivationFields {\n /** Identifies this derivation as HTTP-driven. */\n source: 'http';\n /** Forbidden on HTTP source */\n fn?: never;\n /** Forbidden on HTTP source */\n asyncFn?: never;\n /** HTTP request configuration for server-driven derivations. */\n http: HttpRequestConfig;\n /**\n * Explicit field dependencies. Required for HTTP derivations to prevent\n * wildcard triggering on every keystroke.\n */\n dependsOn: string[];\n /** Expression to extract the derived value from the HTTP response. */\n responseExpression: string;\n // Mutual exclusivity: other sources are not allowed\n value?: never;\n expression?: never;\n functionName?: never;\n asyncFunctionName?: never;\n}\n\n// ============================================================================\n// Async function derivation mode (source: 'asyncFunction' — required; dependsOn — required)\n// ============================================================================\n\n/**\n * Shared fields for both branches of {@link AsyncFunctionDerivationBase}.\n *\n * @internal\n */\ninterface AsyncFunctionDerivationShared extends SharedDerivationFields {\n /** Identifies this derivation as async-function-driven. */\n source: 'asyncFunction';\n /** Forbidden on asyncFunction source */\n fn?: never;\n /**\n * Explicit field dependencies. Required for async derivations to prevent\n * wildcard triggering on every form change.\n */\n dependsOn: string[];\n // Mutual exclusivity: other sources are not allowed\n value?: never;\n expression?: never;\n functionName?: never;\n http?: never;\n responseExpression?: never;\n}\n\n/**\n * Base for async function derivations. `source: 'asyncFunction'` is required.\n * TypeScript enforces `dependsOn` at the type level, and `asyncFunctionName` /\n * `asyncFn` are mutually exclusive (XOR).\n *\n * @internal\n */\ntype AsyncFunctionDerivationBase =\n | (AsyncFunctionDerivationShared & {\n /** Name of a registered async derivation function. */\n asyncFunctionName: string;\n /** Inline form is forbidden when `asyncFunctionName` is set */\n asyncFn?: never;\n })\n | (AsyncFunctionDerivationShared & {\n /** Inline async derivation function. Mutually exclusive with `asyncFunctionName`. */\n asyncFn: AsyncDerivationFunction;\n /** Registered form is forbidden when `asyncFn` is set */\n asyncFunctionName?: never;\n });\n\n// ============================================================================\n// Sync derivation modes\n// ============================================================================\n\n/**\n * Base for expression derivations.\n *\n * @internal\n */\ninterface ExpressionDerivationBase extends SharedDerivationFields {\n source?: never;\n /** Forbidden on expression mode */\n fn?: never;\n /** Forbidden on expression mode */\n asyncFn?: never;\n /** JavaScript expression to evaluate for the derived value. */\n expression: string;\n /** Explicit field dependencies for expressions. */\n dependsOn?: string[];\n // Mutual exclusivity: other mode payload fields are not allowed\n value?: never;\n functionName?: never;\n http?: never;\n asyncFunctionName?: never;\n responseExpression?: never;\n}\n\n/**\n * Base for value derivations.\n *\n * @internal\n */\ninterface ValueDerivationBase extends SharedDerivationFields {\n source?: never;\n /** Forbidden on value mode */\n fn?: never;\n /** Forbidden on value mode */\n asyncFn?: never;\n /** Static value to set on this field. */\n value: unknown;\n /** Explicit field dependencies for value derivations. */\n dependsOn?: string[];\n // Mutual exclusivity: other mode payload fields are not allowed\n expression?: never;\n functionName?: never;\n http?: never;\n asyncFunctionName?: never;\n responseExpression?: never;\n}\n\n/**\n * Shared fields for both branches of {@link FunctionDerivationBase}.\n *\n * @internal\n */\ninterface FunctionDerivationShared extends SharedDerivationFields {\n source?: never;\n /** Forbidden on function-derivation mode */\n asyncFn?: never;\n /** Explicit field dependencies for function derivations. */\n dependsOn?: string[];\n // Mutual exclusivity: other mode payload fields are not allowed\n value?: never;\n expression?: never;\n http?: never;\n asyncFunctionName?: never;\n responseExpression?: never;\n}\n\n/**\n * Base for custom sync function derivations. `functionName` and `fn` are\n * mutually exclusive (XOR).\n *\n * @internal\n */\ntype FunctionDerivationBase =\n | (FunctionDerivationShared & {\n /** Name of a registered custom derivation function. */\n functionName: string;\n /** Inline form is forbidden when `functionName` is set */\n fn?: never;\n })\n | (FunctionDerivationShared & {\n /** Inline custom derivation function. Mutually exclusive with `functionName`. */\n fn: CustomFunction;\n /** Registered form is forbidden when `fn` is set */\n functionName?: never;\n });\n\n/**\n * HTTP derivation that evaluates immediately on change (default).\n *\n * @internal\n */\ntype OnChangeHttpDerivationLogicConfig = HttpDerivationBase & ImmediateDerivationTrigger;\n\n/**\n * HTTP derivation that evaluates after a debounce period.\n *\n * @internal\n */\ntype DebouncedHttpDerivationLogicConfig = HttpDerivationBase & DebouncedDerivationTrigger;\n\n/**\n * Async function derivation that evaluates immediately on change (default).\n *\n * @internal\n */\ntype OnChangeAsyncFunctionDerivationLogicConfig = AsyncFunctionDerivationBase & ImmediateDerivationTrigger;\n\n/**\n * Async function derivation that evaluates after a debounce period.\n *\n * @internal\n */\ntype DebouncedAsyncFunctionDerivationLogicConfig = AsyncFunctionDerivationBase & DebouncedDerivationTrigger;\n\n/**\n * Expression derivation that evaluates immediately on change (default).\n *\n * @internal\n */\ntype OnChangeExpressionDerivationLogicConfig = ExpressionDerivationBase & ImmediateDerivationTrigger;\n\n/**\n * Expression derivation that evaluates after a debounce period.\n *\n * @internal\n */\ntype DebouncedExpressionDerivationLogicConfig = ExpressionDerivationBase & DebouncedDerivationTrigger;\n\n/**\n * Value derivation that evaluates immediately on change (default).\n *\n * @internal\n */\ntype OnChangeValueDerivationLogicConfig = ValueDerivationBase & ImmediateDerivationTrigger;\n\n/**\n * Value derivation that evaluates after a debounce period.\n *\n * @internal\n */\ntype DebouncedValueDerivationLogicConfig = ValueDerivationBase & DebouncedDerivationTrigger;\n\n/**\n * Function derivation that evaluates immediately on change (default).\n *\n * @internal\n */\ntype OnChangeFunctionDerivationLogicConfig = FunctionDerivationBase & ImmediateDerivationTrigger;\n\n/**\n * Function derivation that evaluates after a debounce period.\n *\n * @internal\n */\ntype DebouncedFunctionDerivationLogicConfig = FunctionDerivationBase & DebouncedDerivationTrigger;\n\n/** Configuration for value derivation logic. */\nexport type DerivationLogicConfig =\n | OnChangeHttpDerivationLogicConfig\n | DebouncedHttpDerivationLogicConfig\n | OnChangeAsyncFunctionDerivationLogicConfig\n | DebouncedAsyncFunctionDerivationLogicConfig\n | OnChangeExpressionDerivationLogicConfig\n | DebouncedExpressionDerivationLogicConfig\n | OnChangeValueDerivationLogicConfig\n | DebouncedValueDerivationLogicConfig\n | OnChangeFunctionDerivationLogicConfig\n | DebouncedFunctionDerivationLogicConfig;\n\n/** Union type for all logic configurations. */\nexport type LogicConfig = StateLogicConfig | DerivationLogicConfig;\n\n/** Log level for derivation debug output. */\nexport type DerivationLogLevel = 'none' | 'summary' | 'verbose';\n\n/** Configuration for derivation debug logging. */\nexport interface DerivationLogConfig {\n /** Log level for derivation debugging. Defaults to 'none'. */\n level: DerivationLogLevel;\n}\n\n/**\n * Creates the default derivation log configuration.\n *\n * @returns Default DerivationLogConfig\n */\nexport function createDefaultDerivationLogConfig(): DerivationLogConfig {\n return {\n level: 'none',\n };\n}\n\n/**\n * Checks if logging should occur at the specified level.\n *\n * @param config - Current log configuration\n * @param minLevel - Minimum level required for logging\n * @returns True if logging should occur\n */\nexport function shouldLog(config: DerivationLogConfig, minLevel: 'summary' | 'verbose'): boolean {\n if (config.level === 'none') return false;\n if (minLevel === 'summary') return config.level === 'summary' || config.level === 'verbose';\n return config.level === 'verbose';\n}\n\n/**\n * Type guard to check if a condition is a FormStateCondition.\n *\n * @param condition - The condition to check\n * @returns true if the condition is a FormStateCondition\n */\nexport function isFormStateCondition(\n condition: StateLogicConfig['condition'] | DerivationLogicConfig['condition'],\n): condition is FormStateCondition {\n return typeof condition === 'string' && ['formInvalid', 'formSubmitting', 'pageInvalid'].includes(condition);\n}\n\n/**\n * Type guard to check if a logic config is a StateLogicConfig.\n *\n * @param config - The logic config to check\n * @returns true if the config is for field state logic\n */\nexport function isStateLogicConfig(config: LogicConfig): config is StateLogicConfig {\n return config.type === 'hidden' || config.type === 'readonly' || config.type === 'disabled' || config.type === 'required';\n}\n\n/**\n * Type guard to check if a logic config is a DerivationLogicConfig.\n *\n * @param config - The logic config to check\n * @returns true if the config is for value derivation\n */\nexport function isDerivationLogicConfig(config: LogicConfig): config is DerivationLogicConfig {\n return config.type === 'derivation';\n}\n\n/**\n * Type guard to check if a derivation config targets a property rather than the field value.\n *\n * @param config - The derivation logic config to check\n * @returns true if the config has a targetProperty (property derivation)\n */\nexport function hasTargetProperty(config: DerivationLogicConfig): config is DerivationLogicConfig & { targetProperty: string } {\n return 'targetProperty' in config && typeof config.targetProperty === 'string' && config.targetProperty.length > 0;\n}\n","import { ConditionalExpression } from '../../models/expressions/conditional-expression';\nimport { FormStateCondition, StateLogicConfig } from '../../models/logic/logic-config';\nimport { isFormStateCondition } from '../../models/logic';\nimport { CustomValidatorConfig, ValidatorConfig } from '../../models/validation/validator-config';\nimport { SchemaApplicationConfig } from '../../models/schemas/schema-definition';\nimport { CustomFunctionScope } from '../expressions/custom-function-types';\n\n/**\n * Regular expression to detect formValue property access in expressions.\n * Matches patterns like: formValue.fieldName, formValue['fieldName'], formValue[\"fieldName\"]\n */\nconst FORM_VALUE_ACCESS_PATTERN = /\\bformValue\\s*(?:\\.|\\[)/;\n\n/** Combined regex for extracting field paths from formValue expressions. */\nconst FORM_VALUE_PATTERN = /\\bformValue\\s*(?:\\.([\\w.]+)|\\[\\s*'([\\w.-]+)'\\s*\\]|\\[\\s*\"([\\w.-]+)\"\\s*\\])/g;\n\n/** Context for cross-field detection, providing access to function scope information. */\nexport interface CrossFieldDetectionContext {\n /**\n * Lookup function to get the scope of a registered custom function.\n *\n * @param functionName The name of the custom function\n * @returns The function's scope, or undefined if not registered\n */\n getFunctionScope?: (functionName: string) => CustomFunctionScope | undefined;\n}\n\n/**\n * Detects if a ConditionalExpression references other fields (cross-field).\n *\n * @param expr The conditional expression to analyze\n * @param context Optional context providing function scope lookup\n * @returns true if the expression references other fields\n */\nexport function isCrossFieldExpression(\n expr: ConditionalExpression | boolean | FormStateCondition | undefined,\n context?: CrossFieldDetectionContext,\n): boolean {\n // Boolean, undefined, or FormStateCondition (form-level state checks) are not cross-field\n if (expr === undefined || typeof expr === 'boolean' || isFormStateCondition(expr)) {\n return false;\n }\n\n switch (expr.type) {\n case 'fieldValue':\n // fieldPath means it references another field's value\n return !!expr.fieldPath;\n\n case 'javascript':\n // Check for formValue.* patterns in the expression string\n return FORM_VALUE_ACCESS_PATTERN.test(expr.expression || '');\n\n case 'custom': {\n // For custom functions, determine scope from registry or default to cross-field.\n // Look up function scope from registry if context is provided\n const functionName = expr.functionName;\n if (context?.getFunctionScope && functionName) {\n const scope = context.getFunctionScope(functionName);\n if (scope === 'field') {\n // Function is explicitly marked as field-scoped (no cross-field deps)\n return false;\n }\n // 'form' scope or undefined → treat as cross-field\n }\n\n // Default: conservative approach - assume cross-field\n return true;\n }\n\n case 'async':\n // Async conditions create their own toObservable → toSignal pipeline\n // and manage reactivity independently — not cross-field in the sync graph\n return false;\n\n case 'and':\n case 'or':\n // Recursively check nested conditions, passing context through\n return (expr.conditions || []).some((c) => isCrossFieldExpression(c, context));\n\n default:\n return false;\n }\n}\n\n/**\n * Extracts field dependencies from a ConditionalExpression.\n *\n * @param expr The conditional expression to analyze\n * @returns Array of field keys that this expression depends on\n */\nexport function extractExpressionDependencies(expr: ConditionalExpression | boolean | undefined): string[] {\n if (expr === undefined || typeof expr === 'boolean') {\n return [];\n }\n\n const deps = new Set<string>();\n\n switch (expr.type) {\n case 'fieldValue':\n if (expr.fieldPath) {\n // Extract root field name (before any dots for nested paths)\n deps.add(expr.fieldPath.split('.')[0]);\n }\n break;\n\n case 'javascript':\n // Extract from formValue.* patterns in expression\n extractFromExpressionString(expr.expression || '', deps);\n break;\n\n case 'custom':\n // Custom functions have full form access - conservative approach\n deps.add('*');\n break;\n\n case 'async':\n // Async conditions manage their own reactivity — no auto-extractable dependencies\n break;\n\n case 'and':\n case 'or':\n // Recursively extract from nested conditions\n for (const condition of expr.conditions || []) {\n extractExpressionDependencies(condition).forEach((d) => deps.add(d));\n }\n break;\n }\n\n return Array.from(deps);\n}\n\n/**\n * Extracts field names from a JavaScript expression string.\n * Handles both dot notation (formValue.field) and bracket notation (formValue['field']).\n */\nexport function extractStringDependencies(expression: string): string[] {\n const deps = new Set<string>();\n extractFromExpressionString(expression, deps);\n return Array.from(deps);\n}\n\n/** Internal helper that populates a Set with dependencies from an expression string. */\nfunction extractFromExpressionString(expression: string, deps: Set<string>): void {\n for (const match of expression.matchAll(FORM_VALUE_PATTERN)) {\n const fullPath = match[1] ?? match[2] ?? match[3];\n if (!fullPath) continue;\n // Always add the root field (first segment) as the primary dependency\n const rootField = fullPath.split('.')[0];\n deps.add(rootField);\n // Also add the full path for more precise dependency tracking if nested\n if (fullPath.includes('.')) {\n deps.add(fullPath);\n }\n }\n}\n\n// ============================================================================\n// Validator Detection\n// ============================================================================\n\n/**\n * Detects if a custom validator configuration references formValue (cross-field validation).\n *\n * @param config The custom validator configuration to check\n * @returns true if the validator references formValue\n */\nexport function isCrossFieldValidator(config: CustomValidatorConfig): boolean {\n if (!config.expression) {\n return false;\n }\n return FORM_VALUE_ACCESS_PATTERN.test(config.expression);\n}\n\n/**\n * Detects if a built-in validator has a dynamic expression that references formValue.\n *\n * @param config The validator configuration to check\n * @returns true if the validator has a cross-field dynamic expression\n */\nexport function isCrossFieldBuiltInValidator(config: ValidatorConfig): boolean {\n if (config.type === 'custom') {\n return false; // Custom validators handled separately\n }\n\n // Check if expression property references formValue\n if ('expression' in config && typeof config.expression === 'string') {\n return FORM_VALUE_ACCESS_PATTERN.test(config.expression);\n }\n\n return false;\n}\n\n/** Checks if a validator uses Angular's resource API (validateHttp / validateAsync). */\nexport function isResourceBasedValidator(config: ValidatorConfig): boolean {\n return config.type === 'async' || config.type === 'http';\n}\n\n/**\n * Detects if a validator's `when` condition is cross-field.\n *\n * @param config The validator configuration to check\n * @param context Optional context providing function scope lookup\n * @returns true if the when condition references other fields\n */\nexport function hasCrossFieldWhenCondition(config: ValidatorConfig, context?: CrossFieldDetectionContext): boolean {\n if (!('when' in config) || !config.when) {\n return false;\n }\n return isCrossFieldExpression(config.when as ConditionalExpression, context);\n}\n\n// ============================================================================\n// Logic Detection\n// ============================================================================\n\n/**\n * Detects if a StateLogicConfig has a cross-field condition.\n *\n * @param config The state logic configuration to check\n * @param context Optional context providing function scope lookup\n * @returns true if the logic condition references other fields\n */\nexport function isCrossFieldStateLogic(config: StateLogicConfig, context?: CrossFieldDetectionContext): boolean {\n return isCrossFieldExpression(config.condition, context);\n}\n\n// ============================================================================\n// Schema Detection\n// ============================================================================\n\n/**\n * Detects if a SchemaApplicationConfig has a cross-field condition.\n *\n * @param config The schema application configuration to check\n * @param context Optional context providing function scope lookup\n * @returns true if the schema condition references other fields\n */\nexport function isCrossFieldSchema(config: SchemaApplicationConfig, context?: CrossFieldDetectionContext): boolean {\n if (config.type !== 'applyWhen' || !config.condition) {\n return false;\n }\n return isCrossFieldExpression(config.condition, context);\n}\n","import { type Signal, isSignal, untracked } from '@angular/core';\nimport type { FieldTree, ReadonlyFieldState } from '@angular/forms/signals';\nimport type { FieldStateInfo, FormFieldStateMap } from '../../models/expressions/field-state-context';\nimport type { FieldTreeRecord } from '../field-tree-utils';\n\n/**\n * Known state properties on an Angular Signal Forms FieldState.\n * Each is a Signal<boolean> on the FieldState interface.\n */\nconst STATE_PROPERTIES = ['touched', 'dirty', 'valid', 'invalid', 'pending', 'hidden', 'readonly', 'disabled'] as const;\n\ntype StateProperty = (typeof STATE_PROPERTIES)[number];\n\n/**\n * Reads a boolean signal from a FieldState, respecting the reactive flag.\n *\n * @internal\n */\nfunction readStateSignal(fieldState: ReadonlyFieldState<unknown>, prop: StateProperty, reactive: boolean): boolean {\n const signalFn: Signal<boolean> | undefined = fieldState[prop];\n if (!signalFn || !isSignal(signalFn)) return false;\n return reactive ? !!signalFn() : !!untracked(signalFn);\n}\n\n/**\n * Resolves a field source (FieldTree or FieldState) to its FieldState.\n *\n * @internal\n */\nfunction resolveFieldState(\n fieldSource: FieldTree<unknown> | ReadonlyFieldState<unknown> | undefined,\n reactive: boolean,\n): ReadonlyFieldState<unknown> | undefined {\n if (!fieldSource) return undefined;\n\n // FieldTree: signal or callable Proxy — invoke to get the FieldState\n if (typeof fieldSource === 'function') {\n return reactive ? (fieldSource as FieldTree<unknown>)() : untracked(fieldSource as FieldTree<unknown>);\n }\n\n // Direct FieldState object (from FieldContext)\n if (typeof fieldSource === 'object') {\n return fieldSource as ReadonlyFieldState<unknown>;\n }\n\n return undefined;\n}\n\n/**\n * Creates a lazy FieldStateInfo that only reads state properties when accessed.\n *\n * @param fieldSource - A FieldTree or direct FieldState instance\n * @param reactive - If true, reads signals reactively (creates dependencies).\n * If false, reads signals with `untracked()` (no dependencies).\n * @returns A FieldStateInfo proxy with lazy property access, or undefined if the source is invalid\n * @internal\n */\nexport function readFieldStateInfo(\n fieldSource: FieldTree<unknown> | ReadonlyFieldState<unknown> | undefined,\n reactive: boolean,\n): FieldStateInfo | undefined {\n const state = resolveFieldState(fieldSource, reactive);\n if (!state || typeof state !== 'object') return undefined;\n\n const knownProperties = new Set<string>([...STATE_PROPERTIES, 'pristine']);\n\n return new Proxy({} as FieldStateInfo, {\n get(_target, prop: string): boolean {\n if (prop === 'pristine') {\n return !readStateSignal(state, 'dirty', reactive);\n }\n if (STATE_PROPERTIES.includes(prop as StateProperty)) {\n return readStateSignal(state, prop as StateProperty, reactive);\n }\n return false;\n },\n has(_target, prop: string): boolean {\n return knownProperties.has(prop);\n },\n });\n}\n\n/**\n * Creates a Proxy-based FormFieldStateMap for accessing any field's state by key.\n *\n * @param rootForm - The root FieldTree of the form\n * @param reactive - If true, reads signals reactively (creates dependencies).\n * If false, reads signals with `untracked()` (no dependencies).\n * @returns A FormFieldStateMap proxy\n * @internal\n */\nexport function createFormFieldStateMap(rootForm: FieldTree<unknown>, reactive: boolean): FormFieldStateMap {\n const cache = new Map<string, FieldStateInfo | undefined>();\n\n return new Proxy({} as FormFieldStateMap, {\n get(_target, key: string): FieldStateInfo | undefined {\n if (cache.has(key)) {\n return cache.get(key);\n }\n\n const fieldAccessor = navigateToFieldAccessor(rootForm, key);\n const snapshot = readFieldStateInfo(fieldAccessor, reactive);\n cache.set(key, snapshot);\n return snapshot;\n },\n has(_target, key: string): boolean {\n return navigateToFieldAccessor(rootForm, key) !== undefined;\n },\n });\n}\n\n/**\n * Navigates the form tree to find a field accessor (FieldTree) for the given key path.\n *\n * @internal\n */\nfunction navigateToFieldAccessor(rootForm: FieldTree<unknown>, keyPath: string): FieldTree<unknown> | undefined {\n if (!keyPath) return undefined;\n\n const parts = keyPath.split('.');\n // FieldTree<T> = (() => FieldState<T>) & Subfields<T>, so sub-fields\n // are directly on the FieldTree — no need to call it during navigation\n let current = rootForm as FieldTreeRecord;\n\n for (let i = 0; i < parts.length - 1; i++) {\n const next = current[parts[i]];\n if (!next) return undefined;\n current = next as FieldTreeRecord;\n }\n\n return current[parts[parts.length - 1]];\n}\n","import { LogicFn } from '@angular/forms/signals';\n\n/** DI-scoped cache for async condition logic functions. */\nexport class AsyncConditionFunctionCacheService {\n /** Cache for async condition logic functions, keyed by serialized condition. */\n readonly asyncConditionFunctionCache = new Map<string, LogicFn<unknown, boolean>>();\n}\n","/**\n * Compares two values using the specified comparison operator.\n *\n * @param actual - The value to compare\n * @param expected - The value to compare against\n * @param operator - Comparison operator to use\n * @returns True if the comparison succeeds, false otherwise\n */\nexport function compareValues(actual: unknown, expected: unknown, operator: string): boolean {\n switch (operator) {\n case 'equals':\n return actual === expected;\n case 'notEquals':\n return actual !== expected;\n case 'greater':\n return Number(actual) > Number(expected);\n case 'less':\n return Number(actual) < Number(expected);\n case 'greaterOrEqual':\n return Number(actual) >= Number(expected);\n case 'lessOrEqual':\n return Number(actual) <= Number(expected);\n case 'contains':\n return String(actual).includes(String(expected));\n case 'startsWith':\n return String(actual).startsWith(String(expected));\n case 'endsWith':\n return String(actual).endsWith(String(expected));\n case 'matches':\n try {\n return toRegExp(String(expected)).test(String(actual));\n } catch {\n return false;\n }\n default:\n return false;\n }\n}\n\n/** Builds a RegExp, supporting the `/pattern/flags` literal form (e.g. `/john/i`) for flags like `i`. */\nfunction toRegExp(value: string): RegExp {\n const literal = /^\\/(.+)\\/([dgimsuvy]*)$/.exec(value);\n return literal ? new RegExp(literal[1], literal[2]) : new RegExp(value);\n}\n\n/**\n * Retrieves a nested value from an object using dot notation path.\n *\n * @param obj - The object to traverse\n * @param path - Dot-separated path to the desired property\n * @returns The value at the specified path, or undefined if not found\n */\nexport function getNestedValue(obj: unknown, path: string): unknown {\n return path.split('.').reduce((current: unknown, key: string) => {\n return current && typeof current === 'object' ? (current as Record<string, unknown>)[key] : undefined;\n }, obj);\n}\n\n/**\n * Checks whether a nested property exists in an object using dot notation path.\n *\n * @param obj - The object to check\n * @param path - Dot-separated path to the property\n * @returns True if the property exists (even if its value is undefined)\n */\nexport function hasNestedProperty(obj: unknown, path: string): boolean {\n const keys = path.split('.');\n let current: unknown = obj;\n\n for (let i = 0; i < keys.length - 1; i++) {\n if (!current || typeof current !== 'object') return false;\n current = (current as Record<string, unknown>)[keys[i]];\n }\n\n if (!current || typeof current !== 'object') return false;\n return keys[keys.length - 1] in (current as Record<string, unknown>);\n}\n","import { DynamicFormError } from '../../../errors/dynamic-form-error';\n\n/** Token types for expression parsing */\nexport enum TokenType {\n // Literals\n NUMBER = 'NUMBER',\n STRING = 'STRING',\n TRUE = 'TRUE',\n FALSE = 'FALSE',\n NULL = 'NULL',\n UNDEFINED = 'UNDEFINED',\n\n // Identifiers\n IDENTIFIER = 'IDENTIFIER',\n\n // Operators\n PLUS = 'PLUS',\n MINUS = 'MINUS',\n MULTIPLY = 'MULTIPLY',\n DIVIDE = 'DIVIDE',\n MODULO = 'MODULO',\n\n // Comparison\n EQUAL = 'EQUAL',\n NOT_EQUAL = 'NOT_EQUAL',\n GREATER = 'GREATER',\n LESS = 'LESS',\n GREATER_EQUAL = 'GREATER_EQUAL',\n LESS_EQUAL = 'LESS_EQUAL',\n\n // Logical\n AND = 'AND',\n OR = 'OR',\n NOT = 'NOT',\n\n // Punctuation\n DOT = 'DOT',\n COMMA = 'COMMA',\n COLON = 'COLON',\n LPAREN = 'LPAREN',\n RPAREN = 'RPAREN',\n LBRACKET = 'LBRACKET',\n RBRACKET = 'RBRACKET',\n LBRACE = 'LBRACE',\n RBRACE = 'RBRACE',\n ARROW = 'ARROW',\n QUESTION = 'QUESTION',\n OPTIONAL_DOT = 'OPTIONAL_DOT',\n\n // Special\n EOF = 'EOF',\n}\n\n/** Token with type, value, and position */\nexport interface Token {\n type: TokenType;\n value: string;\n position: number;\n}\n\n/** AST Node types */\nexport type ASTNode =\n | LiteralNode\n | IdentifierNode\n | MemberAccessNode\n | ComputedMemberAccessNode\n | BinaryOpNode\n | UnaryOpNode\n | CallExpressionNode\n | ConditionalNode\n | ArrayLiteralNode\n | ObjectLiteralNode\n | ArrowFunctionNode;\n\nexport interface LiteralNode {\n type: 'Literal';\n value: string | number | boolean | null | undefined;\n}\n\nexport interface IdentifierNode {\n type: 'Identifier';\n name: string;\n}\n\nexport interface MemberAccessNode {\n type: 'MemberAccess';\n object: ASTNode;\n property: string;\n /** True when this access used `?.` (optional chaining). */\n optional?: boolean;\n}\n\nexport interface ComputedMemberAccessNode {\n type: 'ComputedMemberAccess';\n object: ASTNode;\n /** Expression evaluated at runtime to produce the property key. */\n property: ASTNode;\n /** True when this access used `?.[ ... ]` (optional chaining). */\n optional?: boolean;\n}\n\nexport interface ConditionalNode {\n type: 'Conditional';\n test: ASTNode;\n consequent: ASTNode;\n alternate: ASTNode;\n}\n\nexport interface BinaryOpNode {\n type: 'BinaryOp';\n operator: string;\n left: ASTNode;\n right: ASTNode;\n}\n\nexport interface UnaryOpNode {\n type: 'UnaryOp';\n operator: string;\n operand: ASTNode;\n}\n\nexport interface CallExpressionNode {\n type: 'CallExpression';\n callee: ASTNode;\n arguments: ASTNode[];\n /** True when this call used `?.( ... )` (optional chaining). */\n optional?: boolean;\n}\n\nexport interface ArrayLiteralNode {\n type: 'ArrayLiteral';\n elements: ASTNode[];\n}\n\nexport interface ObjectLiteralNode {\n type: 'ObjectLiteral';\n properties: Array<{ key: string; value: ASTNode }>;\n}\n\nexport interface ArrowFunctionNode {\n type: 'ArrowFunction';\n params: string[];\n body: ASTNode;\n}\n\n/** Parser error with position information */\nexport class ExpressionParserError extends DynamicFormError {\n constructor(\n message: string,\n public position: number,\n public expression: string,\n ) {\n super(`${message} at position ${position} in expression: ${expression}`);\n this.name = 'ExpressionParserError';\n }\n}\n","import { Token, TokenType, ExpressionParserError } from './types';\n\n/** Tokenizes an expression string into tokens */\nexport class Tokenizer {\n private position = 0;\n private readonly expression: string;\n\n constructor(expression: string) {\n this.expression = expression;\n }\n\n /** Tokenize the entire expression */\n tokenize(): Token[] {\n const tokens: Token[] = [];\n\n while (this.position < this.expression.length) {\n this.skipWhitespace();\n\n if (this.position >= this.expression.length) {\n break;\n }\n\n const token = this.nextToken();\n tokens.push(token);\n }\n\n tokens.push({ type: TokenType.EOF, value: '', position: this.position });\n return tokens;\n }\n\n private skipWhitespace(): void {\n while (this.position < this.expression.length && /\\s/.test(this.expression[this.position])) {\n this.position++;\n }\n }\n\n private nextToken(): Token {\n const char = this.expression[this.position];\n\n // String literals\n if (char === '\"' || char === \"'\") {\n return this.readString(char);\n }\n\n // Numbers\n if (/[0-9]/.test(char)) {\n return this.readNumber();\n }\n\n // Identifiers and keywords\n if (/[a-zA-Z_$]/.test(char)) {\n return this.readIdentifierOrKeyword();\n }\n\n // Operators and punctuation\n return this.readOperatorOrPunctuation();\n }\n\n private readString(quote: string): Token {\n const start = this.position;\n this.position++; // Skip opening quote\n\n let value = '';\n while (this.position < this.expression.length) {\n const char = this.expression[this.position];\n\n if (char === quote) {\n this.position++; // Skip closing quote\n return { type: TokenType.STRING, value, position: start };\n }\n\n if (char === '\\\\' && this.position + 1 < this.expression.length) {\n // Handle escape sequences\n this.position++;\n const nextChar = this.expression[this.position];\n switch (nextChar) {\n case 'n':\n value += '\\n';\n break;\n case 't':\n value += '\\t';\n break;\n case 'r':\n value += '\\r';\n break;\n case '\\\\':\n value += '\\\\';\n break;\n case quote:\n value += quote;\n break;\n default:\n value += nextChar;\n }\n this.position++;\n } else {\n value += char;\n this.position++;\n }\n }\n\n throw new ExpressionParserError('Unterminated string literal', start, this.expression);\n }\n\n private readNumber(): Token {\n const start = this.position;\n let value = '';\n\n while (this.position < this.expression.length && /[0-9.]/.test(this.expression[this.position])) {\n value += this.expression[this.position];\n this.position++;\n }\n\n if (!/^\\d+(\\.\\d+)?$/.test(value)) {\n throw new ExpressionParserError(`Invalid number: ${value}`, start, this.expression);\n }\n\n return { type: TokenType.NUMBER, value, position: start };\n }\n\n private readIdentifierOrKeyword(): Token {\n const start = this.position;\n let value = '';\n\n while (this.position < this.expression.length && /[a-zA-Z0-9_$]/.test(this.expression[this.position])) {\n value += this.expression[this.position];\n this.position++;\n }\n\n // Check for keywords\n const type = this.getKeywordType(value);\n return { type, value, position: start };\n }\n\n private getKeywordType(value: string): TokenType {\n switch (value) {\n case 'true':\n return TokenType.TRUE;\n case 'false':\n return TokenType.FALSE;\n case 'null':\n return TokenType.NULL;\n case 'undefined':\n return TokenType.UNDEFINED;\n default:\n return TokenType.IDENTIFIER;\n }\n }\n\n private readOperatorOrPunctuation(): Token {\n const start = this.position;\n const char = this.expression[this.position];\n const nextChar = this.position + 1 < this.expression.length ? this.expression[this.position + 1] : '';\n const thirdChar = this.position + 2 < this.expression.length ? this.expression[this.position + 2] : '';\n\n // Three-character operators - check these first!\n const threeChar = char + nextChar + thirdChar;\n if (threeChar === '===') {\n this.position += 3;\n return { type: TokenType.EQUAL, value: '===', position: start };\n }\n if (threeChar === '!==') {\n this.position += 3;\n return { type: TokenType.NOT_EQUAL, value: '!==', position: start };\n }\n\n // Two-character operators\n const twoChar = char + nextChar;\n switch (twoChar) {\n case '==':\n this.position += 2;\n return { type: TokenType.EQUAL, value: '==', position: start };\n case '!=':\n this.position += 2;\n return { type: TokenType.NOT_EQUAL, value: '!=', position: start };\n case '>=':\n this.position += 2;\n return { type: TokenType.GREATER_EQUAL, value: '>=', position: start };\n case '<=':\n this.position += 2;\n return { type: TokenType.LESS_EQUAL, value: '<=', position: start };\n case '&&':\n this.position += 2;\n return { type: TokenType.AND, value: '&&', position: start };\n case '||':\n this.position += 2;\n return { type: TokenType.OR, value: '||', position: start };\n case '=>':\n this.position += 2;\n return { type: TokenType.ARROW, value: '=>', position: start };\n case '?.':\n this.position += 2;\n return { type: TokenType.OPTIONAL_DOT, value: '?.', position: start };\n }\n\n // Single-character operators and punctuation\n this.position++;\n switch (char) {\n case '+':\n return { type: TokenType.PLUS, value: '+', position: start };\n case '-':\n return { type: TokenType.MINUS, value: '-', position: start };\n case '*':\n return { type: TokenType.MULTIPLY, value: '*', position: start };\n case '/':\n return { type: TokenType.DIVIDE, value: '/', position: start };\n case '%':\n return { type: TokenType.MODULO, value: '%', position: start };\n case '>':\n return { type: TokenType.GREATER, value: '>', position: start };\n case '<':\n return { type: TokenType.LESS, value: '<', position: start };\n case '!':\n return { type: TokenType.NOT, value: '!', position: start };\n case '.':\n return { type: TokenType.DOT, value: '.', position: start };\n case ',':\n return { type: TokenType.COMMA, value: ',', position: start };\n case ':':\n return { type: TokenType.COLON, value: ':', position: start };\n case '(':\n return { type: TokenType.LPAREN, value: '(', position: start };\n case ')':\n return { type: TokenType.RPAREN, value: ')', position: start };\n case '[':\n return { type: TokenType.LBRACKET, value: '[', position: start };\n case ']':\n return { type: TokenType.RBRACKET, value: ']', position: start };\n case '{':\n return { type: TokenType.LBRACE, value: '{', position: start };\n case '}':\n return { type: TokenType.RBRACE, value: '}', position: start };\n case '?':\n return { type: TokenType.QUESTION, value: '?', position: start };\n default:\n throw new ExpressionParserError(`Unexpected character: ${char}`, start, this.expression);\n }\n }\n}\n","import { Token, TokenType, ASTNode, ExpressionParserError } from './types';\nimport { Tokenizer } from './tokenizer';\n\n/**\n * Parses tokens into an Abstract Syntax Tree (AST)\n * Uses recursive descent parsing with operator precedence\n */\nexport class Parser {\n private tokens: Token[] = [];\n private current = 0;\n private readonly expression: string;\n\n constructor(expression: string) {\n this.expression = expression;\n }\n\n /** Parse the expression into an AST */\n parse(): ASTNode {\n const tokenizer = new Tokenizer(this.expression);\n this.tokens = tokenizer.tokenize();\n this.current = 0;\n\n if (this.isAtEnd()) {\n throw new ExpressionParserError('Empty expression', 0, this.expression);\n }\n\n const result = this.parseExpression();\n\n if (!this.isAtEnd()) {\n const token = this.peek();\n throw new ExpressionParserError(\n `Unexpected token '${token.value}' at position ${token.position} — expression has trailing content`,\n token.position,\n this.expression,\n );\n }\n\n return result;\n }\n\n private parseExpression(): ASTNode {\n // Try arrow function first — it has the lowest precedence and would otherwise\n // be ambiguous with grouped expressions / bare identifiers.\n const arrow = this.tryParseArrowFunction();\n if (arrow) return arrow;\n return this.parseConditional();\n }\n\n /**\n * Ternary `test ? consequent : alternate` — right-associative, sits between\n * the arrow-function check and logical-or in precedence.\n */\n private parseConditional(): ASTNode {\n const test = this.parseLogicalOr();\n if (!this.match(TokenType.QUESTION)) return test;\n\n const consequent = this.parseExpression();\n this.consume(TokenType.COLON, 'Expected \":\" between ternary branches');\n const alternate = this.parseExpression();\n return { type: 'Conditional', test, consequent, alternate };\n }\n\n /**\n * Attempts to parse an arrow function. Returns null and leaves position\n * unchanged if the next tokens don't form an arrow function head.\n */\n private tryParseArrowFunction(): ASTNode | null {\n const savedPos = this.current;\n\n // Single unparenthesized param: IDENTIFIER =>\n if (this.check(TokenType.IDENTIFIER) && this.checkAt(this.current + 1, TokenType.ARROW)) {\n const param = this.advance().value;\n this.advance(); // consume =>\n const body = this.parseExpression();\n return { type: 'ArrowFunction', params: [param], body };\n }\n\n // Parenthesized params: ( ) => or ( id (, id)* ) =>\n if (this.check(TokenType.LPAREN)) {\n this.advance(); // tentatively consume (\n const params: string[] = [];\n\n if (this.check(TokenType.RPAREN)) {\n this.advance(); // )\n } else if (this.check(TokenType.IDENTIFIER)) {\n params.push(this.advance().value);\n while (this.check(TokenType.COMMA)) {\n this.advance(); // ,\n if (!this.check(TokenType.IDENTIFIER)) {\n this.current = savedPos;\n return null;\n }\n params.push(this.advance().value);\n }\n if (!this.check(TokenType.RPAREN)) {\n this.current = savedPos;\n return null;\n }\n this.advance(); // )\n } else {\n this.current = savedPos;\n return null;\n }\n\n if (!this.check(TokenType.ARROW)) {\n this.current = savedPos;\n return null;\n }\n this.advance(); // =>\n\n const body = this.parseExpression();\n return { type: 'ArrowFunction', params, body };\n }\n\n return null;\n }\n\n private checkAt(index: number, type: TokenType): boolean {\n if (index < 0 || index >= this.tokens.length) return false;\n return this.tokens[index].type === type;\n }\n\n private parseLogicalOr(): ASTNode {\n let node = this.parseLogicalAnd();\n\n while (this.match(TokenType.OR)) {\n const operator = this.previous().value;\n const right = this.parseLogicalAnd();\n node = { type: 'BinaryOp', operator, left: node, right };\n }\n\n return node;\n }\n\n private parseLogicalAnd(): ASTNode {\n let node = this.parseEquality();\n\n while (this.match(TokenType.AND)) {\n const operator = this.previous().value;\n const right = this.parseEquality();\n node = { type: 'BinaryOp', operator, left: node, right };\n }\n\n return node;\n }\n\n private parseEquality(): ASTNode {\n let node = this.parseComparison();\n\n while (this.match(TokenType.EQUAL, TokenType.NOT_EQUAL)) {\n const operator = this.previous().value;\n const right = this.parseComparison();\n node = { type: 'BinaryOp', operator, left: node, right };\n }\n\n return node;\n }\n\n private parseComparison(): ASTNode {\n let node = this.parseAddition();\n\n while (this.match(TokenType.GREATER, TokenType.GREATER_EQUAL, TokenType.LESS, TokenType.LESS_EQUAL)) {\n const operator = this.previous().value;\n const right = this.parseAddition();\n node = { type: 'BinaryOp', operator, left: node, right };\n }\n\n return node;\n }\n\n private parseAddition(): ASTNode {\n let node = this.parseMultiplication();\n\n while (this.match(TokenType.PLUS, TokenType.MINUS)) {\n const operator = this.previous().value;\n const right = this.parseMultiplication();\n node = { type: 'BinaryOp', operator, left: node, right };\n }\n\n return node;\n }\n\n private parseMultiplication(): ASTNode {\n let node = this.parseUnary();\n\n while (this.match(TokenType.MULTIPLY, TokenType.DIVIDE, TokenType.MODULO)) {\n const operator = this.previous().value;\n const right = this.parseUnary();\n node = { type: 'BinaryOp', operator, left: node, right };\n }\n\n return node;\n }\n\n private parseUnary(): ASTNode {\n if (this.match(TokenType.NOT, TokenType.MINUS, TokenType.PLUS)) {\n const operator = this.previous().value;\n const operand = this.parseUnary();\n return { type: 'UnaryOp', operator, operand };\n }\n\n return this.parsePostfix();\n }\n\n private parsePostfix(): ASTNode {\n let node = this.parsePrimary();\n\n while (true) {\n if (this.match(TokenType.DOT)) {\n // Member access: obj.property\n if (!this.check(TokenType.IDENTIFIER)) {\n throw new ExpressionParserError('Expected property name after \".\"', this.peek().position, this.expression);\n }\n const property = this.advance().value;\n node = { type: 'MemberAccess', object: node, property };\n } else if (this.match(TokenType.OPTIONAL_DOT)) {\n // Optional chain — supports `?.prop`, `?.[expr]`, and `?.(args)`.\n if (this.match(TokenType.LBRACKET)) {\n const property = this.parseExpression();\n this.consume(TokenType.RBRACKET, 'Expected \"]\" after computed property');\n node = { type: 'ComputedMemberAccess', object: node, property, optional: true };\n } else if (this.match(TokenType.LPAREN)) {\n const args = this.parseArgumentList();\n this.consume(TokenType.RPAREN, 'Expected \")\" after arguments');\n node = { type: 'CallExpression', callee: node, arguments: args, optional: true };\n } else if (this.check(TokenType.IDENTIFIER)) {\n const property = this.advance().value;\n node = { type: 'MemberAccess', object: node, property, optional: true };\n } else {\n throw new ExpressionParserError('Expected property name, \"[\", or \"(\" after \"?.\"', this.peek().position, this.expression);\n }\n } else if (this.match(TokenType.LBRACKET)) {\n // Computed member access: obj[expr]\n const property = this.parseExpression();\n this.consume(TokenType.RBRACKET, 'Expected \"]\" after computed property');\n node = { type: 'ComputedMemberAccess', object: node, property };\n } else if (this.match(TokenType.LPAREN)) {\n // Function call: func()\n const args = this.parseArgumentList();\n this.consume(TokenType.RPAREN, 'Expected \")\" after arguments');\n node = { type: 'CallExpression', callee: node, arguments: args };\n } else {\n break;\n }\n }\n\n return node;\n }\n\n private parsePrimary(): ASTNode {\n // Literals\n if (this.match(TokenType.TRUE)) {\n return { type: 'Literal', value: true };\n }\n\n if (this.match(TokenType.FALSE)) {\n return { type: 'Literal', value: false };\n }\n\n if (this.match(TokenType.NULL)) {\n return { type: 'Literal', value: null };\n }\n\n if (this.match(TokenType.UNDEFINED)) {\n return { type: 'Literal', value: undefined };\n }\n\n if (this.match(TokenType.NUMBER)) {\n return { type: 'Literal', value: parseFloat(this.previous().value) };\n }\n\n if (this.match(TokenType.STRING)) {\n return { type: 'Literal', value: this.previous().value };\n }\n\n // Array literal\n if (this.match(TokenType.LBRACKET)) {\n const elements = this.parseArrayElements();\n this.consume(TokenType.RBRACKET, 'Expected \"]\" after array elements');\n return { type: 'ArrayLiteral', elements };\n }\n\n // Object literal\n if (this.match(TokenType.LBRACE)) {\n const properties = this.parseObjectProperties();\n this.consume(TokenType.RBRACE, 'Expected \"}\" after object properties');\n return { type: 'ObjectLiteral', properties };\n }\n\n // Identifier\n if (this.match(TokenType.IDENTIFIER)) {\n return { type: 'Identifier', name: this.previous().value };\n }\n\n // Grouped expression\n if (this.match(TokenType.LPAREN)) {\n const expr = this.parseExpression();\n this.consume(TokenType.RPAREN, 'Expected \")\" after expression');\n return expr;\n }\n\n throw new ExpressionParserError(`Unexpected token: ${this.peek().value}`, this.peek().position, this.expression);\n }\n\n private parseArgumentList(): ASTNode[] {\n const args: ASTNode[] = [];\n\n if (!this.check(TokenType.RPAREN)) {\n do {\n args.push(this.parseExpression());\n } while (this.match(TokenType.COMMA));\n }\n\n return args;\n }\n\n private parseArrayElements(): ASTNode[] {\n const elements: ASTNode[] = [];\n\n if (!this.check(TokenType.RBRACKET)) {\n do {\n elements.push(this.parseExpression());\n } while (this.match(TokenType.COMMA));\n }\n\n return elements;\n }\n\n private parseObjectProperties(): Array<{ key: string; value: ASTNode }> {\n const properties: Array<{ key: string; value: ASTNode }> = [];\n\n if (this.check(TokenType.RBRACE)) {\n return properties;\n }\n\n do {\n // Trailing comma support: `{ a: 1, }`\n if (this.check(TokenType.RBRACE)) break;\n\n let key: string;\n if (this.check(TokenType.IDENTIFIER) || this.check(TokenType.STRING)) {\n key = this.advance().value;\n } else {\n throw new ExpressionParserError(\n `Expected property name (identifier or string) in object literal, got '${this.peek().value}'`,\n this.peek().position,\n this.expression,\n );\n }\n\n this.consume(TokenType.COLON, 'Expected \":\" after property name in object literal');\n const value = this.parseExpression();\n properties.push({ key, value });\n } while (this.match(TokenType.COMMA));\n\n return properties;\n }\n\n private match(...types: TokenType[]): boolean {\n for (const type of types) {\n if (this.check(type)) {\n this.advance();\n return true;\n }\n }\n return false;\n }\n\n private check(type: TokenType): boolean {\n if (this.isAtEnd()) return false;\n return this.peek().type === type;\n }\n\n private advance(): Token {\n if (!this.isAtEnd()) this.current++;\n return this.previous();\n }\n\n private isAtEnd(): boolean {\n return this.peek().type === TokenType.EOF;\n }\n\n private peek(): Token {\n return this.tokens[this.current];\n }\n\n private previous(): Token {\n return this.tokens[this.current - 1];\n }\n\n private consume(type: TokenType, message: string): Token {\n if (this.check(type)) return this.advance();\n\n throw new ExpressionParserError(message, this.peek().position, this.expression);\n }\n}\n","import { ASTNode, ExpressionParserError } from './types';\n\n/** Evaluation context for safe expression evaluation */\nexport interface EvaluationScope {\n [key: string]: unknown;\n}\n\n/** Security Model: Whitelist-only approach for method calls */\n\n/**\n * Type-safe method whitelists derived from TypeScript primitive types\n * Only methods explicitly listed here are allowed - this is a whitelist-only approach\n */\nconst STRING_SAFE_METHODS: ReadonlyArray<keyof string> = [\n 'charAt',\n 'charCodeAt',\n 'concat',\n 'endsWith',\n 'includes',\n 'indexOf',\n 'lastIndexOf',\n 'match',\n 'padEnd',\n 'padStart',\n 'repeat',\n 'replace',\n 'search',\n 'slice',\n 'split',\n 'startsWith',\n 'substring',\n 'toLowerCase',\n 'toUpperCase',\n 'trim',\n 'trimEnd',\n 'trimStart',\n 'toString',\n] as const;\n\nconst NUMBER_SAFE_METHODS: ReadonlyArray<keyof number> = ['toExponential', 'toFixed', 'toPrecision', 'toString'] as const;\n\nconst ARRAY_SAFE_METHODS: ReadonlyArray<keyof Array<unknown>> = [\n 'concat',\n 'every',\n 'filter',\n 'find',\n 'findIndex',\n 'flat',\n 'flatMap',\n 'includes',\n 'indexOf',\n 'join',\n 'lastIndexOf',\n 'map',\n 'reduce',\n 'reduceRight',\n 'slice',\n 'some',\n 'toString',\n 'entries',\n 'keys',\n 'values',\n] as const;\n\nconst DATE_SAFE_METHODS: ReadonlyArray<keyof Date> = [\n 'getDate',\n 'getDay',\n 'getFullYear',\n 'getHours',\n 'getMilliseconds',\n 'getMinutes',\n 'getMonth',\n 'getSeconds',\n 'getTime',\n 'getTimezoneOffset',\n 'getUTCDate',\n 'getUTCDay',\n 'getUTCFullYear',\n 'getUTCHours',\n 'getUTCMilliseconds',\n 'getUTCMinutes',\n 'getUTCMonth',\n 'getUTCSeconds',\n 'toDateString',\n 'toISOString',\n 'toJSON',\n 'toString',\n 'toTimeString',\n 'toUTCString',\n] as const;\n\n/**\n * Whitelist of safe methods that can be called on values\n * Using Set for O(1) lookup performance\n */\nconst SAFE_METHODS: Record<string, ReadonlySet<string>> = {\n string: new Set(STRING_SAFE_METHODS as readonly string[]),\n number: new Set(NUMBER_SAFE_METHODS as readonly string[]),\n array: new Set(ARRAY_SAFE_METHODS as readonly string[]),\n date: new Set(DATE_SAFE_METHODS as readonly string[]),\n};\n\n/**\n * Blacklist of property names that should not be accessible\n * These properties can leak information about object structure or enable attacks\n */\nconst BLOCKED_PROPERTIES = new Set<string>([\n 'constructor',\n '__proto__',\n 'prototype',\n '__defineGetter__',\n '__defineSetter__',\n '__lookupGetter__',\n '__lookupSetter__',\n]);\n\n/** Safely evaluates an AST node with a given context */\nexport class Evaluator {\n private readonly scope: EvaluationScope;\n private readonly expression: string;\n\n constructor(scope: EvaluationScope, expression: string) {\n this.scope = scope;\n this.expression = expression;\n }\n\n /** Evaluate an AST node */\n evaluate(node: ASTNode): unknown {\n switch (node.type) {\n case 'Literal':\n return node.value;\n\n case 'Identifier':\n return this.evaluateIdentifier(node.name);\n\n case 'MemberAccess':\n return this.evaluateMemberAccess(node);\n\n case 'ComputedMemberAccess':\n return this.evaluateComputedMemberAccess(node);\n\n case 'BinaryOp':\n return this.evaluateBinaryOp(node);\n\n case 'UnaryOp':\n return this.evaluateUnaryOp(node);\n\n case 'CallExpression':\n return this.evaluateCallExpression(node);\n\n case 'Conditional':\n return this.evaluate(node.test) ? this.evaluate(node.consequent) : this.evaluate(node.alternate);\n\n case 'ArrayLiteral':\n return this.evaluateArrayLiteral(node);\n\n case 'ObjectLiteral':\n return this.evaluateObjectLiteral(node);\n\n case 'ArrowFunction':\n return this.evaluateArrowFunction(node);\n\n default:\n throw new ExpressionParserError(`Unknown node type: ${(node as { type: string }).type}`, 0, this.expression);\n }\n }\n\n private evaluateIdentifier(name: string): unknown {\n if (name in this.scope) {\n return this.scope[name];\n }\n return undefined;\n }\n\n private evaluateMemberAccess(node: { object: ASTNode; property: string }): unknown {\n const obj = this.evaluate(node.object);\n\n if (obj === null || obj === undefined) {\n return undefined;\n }\n\n // Block access to dangerous properties that could leak information or enable attacks\n if (BLOCKED_PROPERTIES.has(node.property)) {\n throw new ExpressionParserError(`Property \"${node.property}\" is not accessible for security reasons`, 0, this.expression);\n }\n\n // Allow property access on plain objects and primitives\n if (typeof obj === 'object' || typeof obj === 'string' || typeof obj === 'number') {\n return (obj as Record<string, unknown>)[node.property];\n }\n\n return undefined;\n }\n\n private evaluateComputedMemberAccess(node: { object: ASTNode; property: ASTNode; optional?: boolean }): unknown {\n const obj = this.evaluate(node.object);\n\n if (obj === null || obj === undefined) {\n // Both `obj?.[expr]` and `obj[expr]` short-circuit on null/undefined —\n // matches the safe-access behavior of dotted member access above.\n return undefined;\n }\n\n const rawKey = this.evaluate(node.property);\n if (rawKey === null || rawKey === undefined) return undefined;\n const key = String(rawKey);\n\n // Same blocklist as dotted access — `obj['__proto__']` must be rejected\n // even though the property name was computed.\n if (BLOCKED_PROPERTIES.has(key)) {\n throw new ExpressionParserError(`Property \"${key}\" is not accessible for security reasons`, 0, this.expression);\n }\n\n if (typeof obj === 'object' || typeof obj === 'string' || typeof obj === 'number') {\n return (obj as Record<string, unknown>)[key];\n }\n\n return undefined;\n }\n\n private evaluateBinaryOp(node: { operator: string; left: ASTNode; right: ASTNode }): unknown {\n // Handle short-circuit evaluation for logical operators\n if (node.operator === '&&') {\n const left = this.evaluate(node.left);\n return left ? this.evaluate(node.right) : left;\n }\n if (node.operator === '||') {\n const left = this.evaluate(node.left);\n return left ? left : this.evaluate(node.right);\n }\n\n // For all other operators, evaluate both sides\n const left = this.evaluate(node.left);\n const right = this.evaluate(node.right);\n\n switch (node.operator) {\n // Arithmetic\n case '+':\n return (left as number) + (right as number);\n case '-':\n return (left as number) - (right as number);\n case '*':\n return (left as number) * (right as number);\n case '/':\n if (right === 0) return null;\n return (left as number) / (right as number);\n case '%':\n if (right === 0) return null;\n return (left as number) % (right as number);\n\n // Comparison - using == and != intentionally for loose equality\n case '==':\n return left == right;\n case '===':\n return left === right;\n case '!=':\n return left != right;\n case '!==':\n return left !== right;\n case '>':\n return (left as number) > (right as number);\n case '<':\n return (left as number) < (right as number);\n case '>=':\n return (left as number) >= (right as number);\n case '<=':\n return (left as number) <= (right as number);\n\n default:\n throw new ExpressionParserError(`Unknown binary operator: ${node.operator}`, 0, this.expression);\n }\n }\n\n private evaluateUnaryOp(node: { operator: string; operand: ASTNode }): unknown {\n const operand = this.evaluate(node.operand);\n\n switch (node.operator) {\n case '!':\n return !operand;\n case '-':\n return -(operand as number);\n case '+':\n return +(operand as number);\n default:\n throw new ExpressionParserError(`Unknown unary operator: ${node.operator}`, 0, this.expression);\n }\n }\n\n private evaluateCallExpression(node: { callee: ASTNode; arguments: ASTNode[]; optional?: boolean }): unknown {\n // Only allow method calls (member access). Computed access (`obj['method']()`)\n // is also rejected — the security model whitelists method names statically.\n if (node.callee.type !== 'MemberAccess') {\n throw new ExpressionParserError('Only method calls are allowed, not arbitrary function calls', 0, this.expression);\n }\n\n const obj = this.evaluate(node.callee.object);\n\n // Calling a method on a nullish receiver silently returns undefined —\n // consistent with how dotted member access (`obj.prop`) treats nullish\n // receivers above. This means `a?.b()`, `a?.b.c()`, and `nullable.method()`\n // all short-circuit to undefined rather than tripping the whitelist check\n // with a misleading \"method not allowed\" error. The whitelist still\n // enforces strictly on non-nullish receivers.\n if (obj === null || obj === undefined) {\n return undefined;\n }\n\n const methodName = node.callee.property;\n\n // Block access to dangerous properties (must check before method call)\n if (BLOCKED_PROPERTIES.has(methodName)) {\n throw new ExpressionParserError(`Property \"${methodName}\" is not accessible for security reasons`, 0, this.expression);\n }\n\n // Check if the method is in the instance method whitelist\n if (!this.isMethodSafe(obj, methodName)) {\n throw new ExpressionParserError(`Method \"${methodName}\" is not allowed for security reasons`, 0, this.expression);\n }\n\n // Evaluate arguments\n const args = node.arguments.map((arg) => this.evaluate(arg));\n\n // Call the method with proper `this` binding\n const method = (obj as Record<string, unknown>)[methodName];\n if (typeof method === 'function') {\n // Use .call() to properly bind `this` context\n return (method as (...args: unknown[]) => unknown).call(obj, ...args);\n }\n\n return undefined;\n }\n\n private evaluateArrayLiteral(node: { elements: ASTNode[] }): unknown {\n return node.elements.map((element) => this.evaluate(element));\n }\n\n private evaluateObjectLiteral(node: { properties: Array<{ key: string; value: ASTNode }> }): unknown {\n const result: Record<string, unknown> = {};\n for (const { key, value } of node.properties) {\n if (BLOCKED_PROPERTIES.has(key)) {\n throw new ExpressionParserError(`Property \"${key}\" is not allowed in object literals for security reasons`, 0, this.expression);\n }\n result[key] = this.evaluate(value);\n }\n return result;\n }\n\n private evaluateArrowFunction(node: { params: string[]; body: ASTNode }): unknown {\n const parentScope = this.scope;\n const expression = this.expression;\n return (...args: unknown[]) => {\n const childScope: EvaluationScope = { ...parentScope };\n for (let i = 0; i < node.params.length; i++) {\n childScope[node.params[i]] = args[i];\n }\n const childEvaluator = new Evaluator(childScope, expression);\n return childEvaluator.evaluate(node.body);\n };\n }\n\n private isMethodSafe(obj: unknown, methodName: string): boolean {\n if (obj === null || obj === undefined) {\n return false;\n }\n\n const type = Array.isArray(obj) ? 'array' : obj instanceof Date ? 'date' : typeof obj;\n\n const safeMethods = SAFE_METHODS[type];\n if (!safeMethods) {\n return false;\n }\n\n return safeMethods.has(methodName);\n }\n}\n","import { Parser } from './parser';\nimport { Evaluator, EvaluationScope } from './evaluator';\nimport { ASTNode, ExpressionParserError } from './types';\n\n/** Maximum cache size to prevent memory issues */\nconst MAX_AST_CACHE_SIZE = 1000;\n\n/**\n * LRU Cache implementation for AST nodes\n * Tracks access order to evict least-recently-used entries\n */\nclass LRUCache<K, V> {\n private cache = new Map<K, V>();\n private maxSize: number;\n\n constructor(maxSize: number) {\n this.maxSize = maxSize;\n }\n\n get(key: K): V | undefined {\n const value = this.cache.get(key);\n if (value !== undefined) {\n // Move to end (most recently used)\n this.cache.delete(key);\n this.cache.set(key, value);\n }\n return value;\n }\n\n set(key: K, value: V): void {\n // Delete if exists to update position\n if (this.cache.has(key)) {\n this.cache.delete(key);\n }\n\n // Add to end (most recently used)\n this.cache.set(key, value);\n\n // Evict least recently used if over limit\n if (this.cache.size > this.maxSize) {\n const firstKey = this.cache.keys().next().value;\n if (firstKey !== undefined) {\n this.cache.delete(firstKey);\n }\n }\n }\n\n clear(): void {\n this.cache.clear();\n }\n\n get size(): number {\n return this.cache.size;\n }\n}\n\n/** Cache for parsed AST nodes to improve performance */\nconst astCache = new LRUCache<string, ASTNode>(MAX_AST_CACHE_SIZE);\n\n/** Secure expression parser that uses AST-based evaluation */\nexport class ExpressionParser {\n /**\n * Parse an expression string into an AST\n * Results are cached for performance using LRU eviction\n */\n static parse(expression: string): ASTNode {\n // Check cache first\n const cached = astCache.get(expression);\n if (cached) {\n return cached;\n }\n\n // Parse the expression\n const parser = new Parser(expression);\n const ast = parser.parse();\n\n // Cache the result (LRU cache handles eviction automatically)\n astCache.set(expression, ast);\n\n return ast;\n }\n\n /** Evaluate an expression with a given scope */\n static evaluate(expression: string, scope: EvaluationScope): unknown {\n try {\n const ast = this.parse(expression);\n const evaluator = new Evaluator(scope, expression);\n return evaluator.evaluate(ast);\n } catch (error) {\n if (error instanceof ExpressionParserError) {\n throw error;\n }\n // Wrap other errors for consistency\n throw new ExpressionParserError(\n `Error evaluating expression: ${error instanceof Error ? error.message : String(error)}`,\n 0,\n expression,\n );\n }\n }\n\n /** Clear the AST cache */\n static clearCache(): void {\n astCache.clear();\n }\n\n /** Get cache statistics */\n static getCacheStats(): { size: number; maxSize: number } {\n return {\n size: astCache.size,\n maxSize: MAX_AST_CACHE_SIZE,\n };\n }\n}\n","import {\n AndCondition,\n ConditionalExpression,\n FieldValueCondition,\n JavascriptCondition,\n OrCondition,\n} from '../../models/expressions/conditional-expression';\nimport { EvaluationContext } from '../../models/expressions/evaluation-context';\nimport { compareValues, getNestedValue, hasNestedProperty } from './value-utils';\nimport { ExpressionParser } from './parser/expression-parser';\n\n/** True for a Promise/Observable wrongly returned into a sync condition slot (truthy under `!!`). */\nfunction isThenableOrObservable(value: unknown): boolean {\n if (value === null || typeof value !== 'object') return false;\n const candidate = value as { then?: unknown; subscribe?: unknown };\n return typeof candidate.then === 'function' || typeof candidate.subscribe === 'function';\n}\n\n/**\n * Evaluate conditional expression\n * Uses secure AST-based parsing for JavaScript expressions\n */\nexport function evaluateCondition(expression: ConditionalExpression, context: EvaluationContext): boolean {\n switch (expression.type) {\n case 'fieldValue':\n return evaluateFieldValueCondition(expression, context);\n\n case 'javascript':\n return evaluateJavaScriptExpression(expression, context);\n\n case 'custom': {\n // Read both keys via cast — the XOR type narrows to `never` when both\n // would be truthy, but JSON-loaded configs can produce that state at\n // runtime. We accept it here, warn, and let inline win.\n const bothSet = expression as { fn?: unknown; functionName?: string };\n const inlineFn = bothSet.fn as ((ctx: EvaluationContext) => unknown) | undefined;\n const registeredName = bothSet.functionName;\n\n if (inlineFn && registeredName) {\n context.logger.warn(\n 'Both \"fn\" and \"functionName\" are set on custom condition. Inline \"fn\" takes precedence; \"functionName\" is ignored.',\n );\n }\n\n const customFn = inlineFn ?? (registeredName ? context.customFunctions?.[registeredName] : undefined);\n if (!customFn) {\n context.logger.error('Custom function not found:', registeredName);\n return false;\n }\n\n try {\n const result = customFn(context);\n if (isThenableOrObservable(result)) {\n context.logger.warn(\n `Custom condition '${registeredName ?? '<inline>'}' returned a Promise or Observable. ` +\n `Synchronous conditions must return a value; use an 'async' or 'http' condition type for asynchronous logic. Treating as false.`,\n );\n return false;\n }\n return !!result;\n } catch (error) {\n context.logger.error('Error executing custom function:', registeredName ?? '<inline>', error);\n return false;\n }\n }\n\n case 'and':\n return evaluateAndCondition(expression, context);\n\n case 'or':\n return evaluateOrCondition(expression, context);\n\n case 'http':\n context.logger.warn(\n 'HTTP conditions are resolved asynchronously via createHttpConditionLogicFunction(). ' +\n 'When used inside and/or composites, the HTTP result is not available synchronously. Returning false.',\n );\n return false;\n\n case 'async':\n context.logger.warn(\n 'Async Condition - resolved asynchronously via createAsyncConditionLogicFunction(). ' +\n 'When used inside and/or composites, the async result is not available synchronously. Returning false.',\n );\n return false;\n\n default:\n return false;\n }\n}\n\nfunction evaluateFieldValueCondition(expression: FieldValueCondition, context: EvaluationContext): boolean {\n // Guard against missing fieldPath — invalid config, return false\n if (!expression.fieldPath) return false;\n\n // Try scoped formValue first (handles sibling field lookups within array items).\n // Fall back to rootFormValue for fields outside the current array scope.\n // Use hasNestedProperty to distinguish \"field exists with value undefined\"\n // from \"field path doesn't exist\" — prevents incorrect fallback when an\n // optional array item field is intentionally undefined.\n const fieldValue = hasNestedProperty(context.formValue, expression.fieldPath)\n ? getNestedValue(context.formValue, expression.fieldPath)\n : context.rootFormValue\n ? getNestedValue(context.rootFormValue, expression.fieldPath)\n : undefined;\n return compareValues(fieldValue, expression.value, expression.operator);\n}\n\nfunction evaluateJavaScriptExpression(expression: JavascriptCondition, context: EvaluationContext): boolean {\n try {\n // Use secure AST-based expression parser instead of dynamic code execution\n const result = ExpressionParser.evaluate(expression.expression, context);\n return !!result;\n } catch (error) {\n context.logger.error('Error evaluating JavaScript expression:', expression.expression, error);\n return false;\n }\n}\n\nfunction evaluateAndCondition(expression: AndCondition, context: EvaluationContext): boolean {\n if (expression.conditions.length === 0) return false;\n // All conditions must be true for AND logic\n return expression.conditions.every((condition) => evaluateCondition(condition, context));\n}\n\nfunction evaluateOrCondition(expression: OrCondition, context: EvaluationContext): boolean {\n if (expression.conditions.length === 0) return false;\n // At least one condition must be true for OR logic\n return expression.conditions.some((condition) => evaluateCondition(condition, context));\n}\n","import { LogicFn } from '@angular/forms/signals';\n\n/** DI-scoped cache for HTTP condition logic functions. */\nexport class HttpConditionFunctionCacheService {\n /** Cache for HTTP condition logic functions, keyed by serialized condition. */\n readonly httpConditionFunctionCache = new Map<string, LogicFn<unknown, boolean>>();\n}\n","/**\n * Serializes a value to a deterministic string for cache key generation.\n * Unlike JSON.stringify, this sorts object keys to ensure consistent output\n * regardless of property insertion order.\n */\nexport function stableStringify(value: unknown): string {\n if (value === null || value === undefined) {\n return String(value);\n }\n\n if (typeof value !== 'object') {\n return JSON.stringify(value);\n }\n\n if (Array.isArray(value)) {\n return '[' + value.map(stableStringify).join(',') + ']';\n }\n\n const obj = value as Record<string, unknown>;\n const sortedKeys = Object.keys(obj).sort();\n const pairs = sortedKeys.map((key) => JSON.stringify(key) + ':' + stableStringify(obj[key]));\n return '{' + pairs.join(',') + '}';\n}\n","import { inject, InjectionToken, Signal } from '@angular/core';\nimport { DynamicFormError } from '../errors/dynamic-form-error';\nimport type { FieldSignalContext, ArrayContext, GroupContext } from '../mappers/types';\nimport type { WrapperConfig } from './wrapper-type';\nimport type { ValidationMessages } from './validation-types';\nimport type { FormOptions } from './form-config';\n\n/** Injection token for form-level default wrappers. */\nexport const DEFAULT_WRAPPERS = new InjectionToken<Signal<readonly WrapperConfig[] | undefined>>('DEFAULT_WRAPPERS');\n\n/** Injection token for providing field signal context to mappers and components. */\nexport const FIELD_SIGNAL_CONTEXT = new InjectionToken<FieldSignalContext>('FIELD_SIGNAL_CONTEXT');\n\n/** Throwing accessor for {@link FIELD_SIGNAL_CONTEXT}. */\nexport function injectFieldSignalContext<TModel extends Record<string, unknown> = Record<string, unknown>>(): FieldSignalContext<TModel> {\n const ctx = inject(FIELD_SIGNAL_CONTEXT, { optional: true });\n if (!ctx) {\n throw new DynamicFormError('FIELD_SIGNAL_CONTEXT is not provided. This consumer must run inside a <df-dynamic-form> component tree.');\n }\n return ctx as FieldSignalContext<TModel>;\n}\n\n/** Injection token for providing array context metadata to mappers and components. */\nexport const ARRAY_CONTEXT = new InjectionToken<ArrayContext>('ARRAY_CONTEXT');\n\n/** Injection token for providing group ancestry to mappers and components. */\n// No factory: GROUP_CONTEXT is optional-by-design — `GroupFieldComponent` and\n// `createArrayItemInjector` re-provide it where it makes sense, and consumers\n// inject with `{ optional: true }` to handle the top-level case. Mirrors the\n// ARRAY_CONTEXT pattern. Adding a throwing factory here fires even when\n// injected optionally (Angular still evaluates the tree-shakable factory\n// before honoring `optional`), so non-group fields would crash on every render.\nexport const GROUP_CONTEXT = new InjectionToken<GroupContext>('GROUP_CONTEXT');\n\n/** Injection token for form-level default props. */\nexport const DEFAULT_PROPS = new InjectionToken<Signal<Record<string, unknown> | undefined>>('DEFAULT_PROPS');\n\n/** Injection token for form-level default validation messages. */\nexport const DEFAULT_VALIDATION_MESSAGES = new InjectionToken<Signal<ValidationMessages | undefined>>('DEFAULT_VALIDATION_MESSAGES');\n\n/** Injection token for form-level options. */\nexport const FORM_OPTIONS = new InjectionToken<Signal<FormOptions | undefined>>('FORM_OPTIONS');\n\n/** Injection token for form-level external data. */\nexport const EXTERNAL_DATA = new InjectionToken<Signal<Record<string, Signal<unknown>> | undefined>>('EXTERNAL_DATA');\n\n/**\n * Form-level DOM-id prefix, provided once at the DynamicForm level and read by\n * `mapFieldToInputs` as the outermost id segment: `{idPrefix}_{group}_{key}_{index}`.\n * Scopes ids to one form instance so duplicate forms on a page don't collide.\n * Inject `{ optional: true }` — absent under mock injectors / standalone usage.\n */\nexport const FORM_ID_PREFIX = new InjectionToken<Signal<string>>('FORM_ID_PREFIX');\n","import { Signal } from '@angular/core';\nimport { FieldTree } from '@angular/forms/signals';\n\n/** Registry service that provides access to the root form and its values. */\nexport class RootFormRegistryService {\n constructor(\n readonly formValue: Signal<Record<string, unknown>>,\n readonly rootForm: Signal<FieldTree<Record<string, unknown>> | undefined>,\n ) {}\n}\n","import { untracked } from '@angular/core';\nimport { FieldContext } from '@angular/forms/signals';\n\n/**\n * Safely reads `pathKeys` from a FieldContext.\n * Returns an empty array if `pathKeys` is not available (e.g., in tests or older Angular versions).\n */\nexport function safeReadPathKeys(fieldContext: FieldContext<unknown>): readonly string[] {\n if (!('pathKeys' in fieldContext) || typeof fieldContext.pathKeys !== 'function') {\n return [];\n }\n return untracked(() => fieldContext.pathKeys());\n}\n","import { inject, Injectable, isSignal, Signal, untracked } from '@angular/core';\nimport { ChildFieldContext, FieldContext, FieldTree, ReadonlyFieldState } from '@angular/forms/signals';\nimport { EvaluationContext } from '../../models/expressions/evaluation-context';\nimport { EXTERNAL_DATA } from '../../models/field-signal-context.token';\nimport { DynamicFormError } from '../../errors/dynamic-form-error';\nimport { RootFormRegistryService } from './root-form-registry.service';\nimport { DynamicFormLogger } from '../../providers/features/logger/logger.token';\nimport { DEPRECATION_WARNING_TRACKER } from '../../utils/deprecation-warning-tracker';\nimport { getNestedValue } from '../expressions/value-utils';\nimport { readFieldStateInfo, createFormFieldStateMap } from '../derivation/field-state-extractor';\nimport { safeReadPathKeys } from '../../utils/safe-read-path-keys';\n\nfunction isChildFieldContext<TValue>(context: FieldContext<TValue>): context is ChildFieldContext<TValue> {\n return 'key' in context && isSignal(context.key);\n}\n\n/** Extracts the FieldState from a FieldContext using the public `.state` property. */\nfunction extractFieldState(fieldContext: FieldContext<unknown>): ReadonlyFieldState<unknown> | undefined {\n return untracked(() => {\n if (!fieldContext || !('state' in fieldContext)) return undefined;\n return fieldContext.state;\n });\n}\n\n/** Detects whether a field lives inside an array by examining its `pathKeys`. */\nfunction detectArrayScope(pathKeys: readonly string[]): { arrayKey: string; index: number; localKey: string } | undefined {\n // Need at least 3 segments: arrayKey, index, fieldKey\n if (pathKeys.length < 3) return undefined;\n\n // Walk from the end to find the nearest array context.\n // pathKeys looks like ['addresses', '0', 'street'] or\n // ['nested', 'addresses', '1', 'city']\n for (let i = pathKeys.length - 2; i >= 1; i--) {\n const maybeIndex = Number(pathKeys[i]);\n if (Number.isInteger(maybeIndex) && maybeIndex >= 0) {\n return {\n arrayKey: pathKeys.slice(0, i).join('.'),\n index: maybeIndex,\n localKey: pathKeys[pathKeys.length - 1],\n };\n }\n }\n\n return undefined;\n}\n\n/**\n * Service that provides field evaluation context by combining\n * field context with root form registry information.\n */\n@Injectable()\nexport class FieldContextRegistryService {\n private rootFormRegistry = inject(RootFormRegistryService);\n private logger = inject(DynamicFormLogger);\n private deprecationTracker = inject(DEPRECATION_WARNING_TRACKER, { optional: true });\n private externalDataSignal = inject(EXTERNAL_DATA, { optional: true });\n\n /**\n * Creates an evaluation context for a field by combining:\n * - The field's current value\n * - The root form value from the registry\n * - The field path (if available from form context)\n * - Custom functions (if provided)\n */\n createEvaluationContext<TValue>(\n fieldContext: FieldContext<TValue>,\n customFunctions?: Record<string, (context: EvaluationContext) => unknown>,\n ): EvaluationContext {\n // Use untracked() to read the field value WITHOUT creating a reactive dependency.\n // This prevents infinite loops when logic functions are evaluated inside computed signals.\n const fieldValue = untracked(() => fieldContext.value());\n\n // Get form value wrapped in untracked() to prevent reactive dependencies.\n // This allows validators and dynamic values to access form values without\n // causing infinite loops.\n const rootFormValue = untracked(() => this.rootFormRegistry.formValue());\n const localKey = this.extractFieldPath(fieldContext);\n const pathKeys = safeReadPathKeys(fieldContext);\n const arrayScope = detectArrayScope(pathKeys);\n\n if (arrayScope) {\n return this.buildArrayScopedContext(rootFormValue, arrayScope, fieldValue, customFunctions, false, fieldContext);\n }\n\n // Use getters for fieldState/formFieldState to defer signal reads until\n // the expression actually accesses them. Validators that only use fieldValue\n // will never trigger these getters, avoiding reactive cycles in Angular's\n // internal signal graph (validator → state → valid → validator).\n const rootFormSignal = this.rootFormRegistry.rootForm;\n return {\n fieldValue,\n formValue: rootFormValue,\n fieldPath: localKey,\n customFunctions: customFunctions || {},\n externalData: this.resolveExternalData(false),\n logger: this.logger,\n deprecationTracker: this.deprecationTracker ?? undefined,\n get fieldState() {\n return readFieldStateInfo(extractFieldState(fieldContext), false);\n },\n get formFieldState() {\n return createFormFieldStateMap(untracked(rootFormSignal) as FieldTree<unknown>, false);\n },\n };\n }\n\n /** Extracts the field path (key) for a given field context. */\n private extractFieldPath(fieldContext: FieldContext<unknown>): string {\n if (isChildFieldContext(fieldContext)) {\n try {\n return String(fieldContext.key());\n } catch (error) {\n this.logger.warn('Unable to extract field key:', error);\n }\n }\n\n // For root fields or when key is not available\n return '';\n }\n\n /** Builds an evaluation context scoped to a specific array item. */\n private buildArrayScopedContext<TValue>(\n rootFormValue: Record<string, unknown>,\n arrayScope: { arrayKey: string; index: number; localKey: string },\n fieldValue: TValue,\n customFunctions: Record<string, (context: EvaluationContext) => unknown> | undefined,\n reactive: boolean,\n fieldContext?: FieldContext<unknown>,\n ): EvaluationContext {\n const { arrayKey, index, localKey } = arrayScope;\n\n // Navigate to the array (supports nested paths like 'nested.addresses')\n const arrayData = getNestedValue(rootFormValue, arrayKey);\n let scopedFormValue: Record<string, unknown> | undefined;\n\n if (Array.isArray(arrayData) && index >= 0 && index < arrayData.length) {\n const item = arrayData[index];\n if (item != null && typeof item === 'object') {\n scopedFormValue = item as Record<string, unknown>;\n }\n }\n\n // Use getters to defer fieldState/formFieldState construction.\n // Same rationale as createEvaluationContext — avoids reactive cycles\n // when expressions don't actually access these properties.\n const rootFormSignal = this.rootFormRegistry.rootForm;\n const fieldStateGetter = fieldContext ? () => readFieldStateInfo(extractFieldState(fieldContext), reactive) : () => undefined;\n const formFieldStateGetter = () =>\n createFormFieldStateMap((reactive ? rootFormSignal() : untracked(rootFormSignal)) as FieldTree<unknown>, reactive);\n\n // Fall back to root form value if array item lookup fails\n if (!scopedFormValue) {\n return {\n fieldValue,\n formValue: rootFormValue,\n fieldPath: localKey,\n customFunctions: customFunctions || {},\n externalData: this.resolveExternalData(reactive),\n logger: this.logger,\n deprecationTracker: this.deprecationTracker ?? undefined,\n get fieldState() {\n return fieldStateGetter();\n },\n get formFieldState() {\n return formFieldStateGetter();\n },\n };\n }\n\n return {\n fieldValue,\n formValue: scopedFormValue,\n rootFormValue,\n arrayIndex: index,\n arrayPath: arrayKey,\n fieldPath: `${arrayKey}.${index}.${localKey}`,\n customFunctions: customFunctions || {},\n externalData: this.resolveExternalData(reactive),\n logger: this.logger,\n deprecationTracker: this.deprecationTracker ?? undefined,\n get fieldState() {\n return fieldStateGetter();\n },\n get formFieldState() {\n return formFieldStateGetter();\n },\n };\n }\n\n /**\n * Resolves external data signals to their current values.\n *\n * @param reactive - If true, reads signals reactively (creates dependencies).\n * If false, reads signals with untracked() (no dependencies).\n * @returns Record of resolved external data values, or undefined if no external data.\n */\n private resolveExternalData(reactive: boolean): Record<string, unknown> | undefined {\n const externalDataSignal = this.externalDataSignal;\n if (!externalDataSignal) return undefined;\n\n const externalDataRecord = reactive ? externalDataSignal() : untracked(() => externalDataSignal());\n\n if (!externalDataRecord) {\n return undefined;\n }\n\n const resolved: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(externalDataRecord)) {\n if (!isSignal(value)) {\n throw new DynamicFormError(`externalData[\"${key}\"] must be a Signal. Got: ${typeof value}. Wrap it with signal(yourValue).`);\n }\n resolved[key] = reactive ? (value as Signal<unknown>)() : untracked(() => (value as Signal<unknown>)());\n }\n\n return resolved;\n }\n\n /** Creates a REACTIVE evaluation context for logic functions. */\n createReactiveEvaluationContext<TValue>(\n fieldContext: FieldContext<TValue>,\n customFunctions?: Record<string, (context: EvaluationContext) => unknown>,\n ): EvaluationContext {\n const fieldValue = fieldContext.value();\n const rootFormValue = this.rootFormRegistry.formValue();\n const pathKeys = safeReadPathKeys(fieldContext);\n const arrayScope = detectArrayScope(pathKeys);\n\n if (arrayScope) {\n return this.buildArrayScopedContext(rootFormValue, arrayScope, fieldValue, customFunctions, true, fieldContext);\n }\n\n const localKey = this.extractFieldPath(fieldContext);\n const rootFormSignal = this.rootFormRegistry.rootForm;\n\n return {\n fieldValue,\n formValue: rootFormValue,\n fieldPath: localKey,\n customFunctions: customFunctions || {},\n externalData: this.resolveExternalData(true),\n logger: this.logger,\n deprecationTracker: this.deprecationTracker ?? undefined,\n get fieldState() {\n return readFieldStateInfo(extractFieldState(fieldContext), true);\n },\n get formFieldState() {\n return createFormFieldStateMap(rootFormSignal() as FieldTree<unknown>, true);\n },\n };\n }\n\n /**\n * Creates an evaluation context for display-only components (text fields, pages)\n * that don't have their own FieldContext.\n *\n * @param fieldPath - The key/path of the display-only component\n * @param customFunctions - Optional custom functions for expression evaluation\n * @param arrayScope - When the element lives inside an array item, scopes `formValue`\n * to that item so conditions resolve against sibling fields, matching leaf fields.\n */\n createDisplayOnlyContext(\n fieldPath: string,\n customFunctions?: Record<string, (context: EvaluationContext) => unknown>,\n arrayScope?: { arrayKey: string; index: number; localKey: string },\n ): EvaluationContext {\n const rootFormValue = this.rootFormRegistry.formValue();\n\n if (arrayScope) {\n return this.buildArrayScopedContext(rootFormValue, arrayScope, undefined, customFunctions, true);\n }\n\n const rootFormSignal = this.rootFormRegistry.rootForm;\n\n return {\n fieldValue: undefined,\n formValue: rootFormValue,\n fieldPath,\n customFunctions: customFunctions || {},\n externalData: this.resolveExternalData(true),\n logger: this.logger,\n deprecationTracker: this.deprecationTracker ?? undefined,\n get formFieldState() {\n return createFormFieldStateMap(rootFormSignal() as FieldTree<unknown>, true);\n },\n };\n }\n}\n","import { Injectable } from '@angular/core';\nimport { CustomFunction, CustomFunctionOptions, CustomFunctionScope } from '../expressions/custom-function-types';\nimport type { AsyncConditionFunction, AsyncDerivationFunction } from '../expressions/async-custom-function-types';\nimport { AsyncCustomValidator, CustomValidator, HttpCustomValidator } from '../validation/validator-types';\n\n/** Registry service for custom functions and validators */\n@Injectable()\nexport class FunctionRegistryService {\n private readonly customFunctions = new Map<string, CustomFunction>();\n private readonly customFunctionScopes = new Map<string, CustomFunctionScope>();\n private readonly derivationFunctions = new Map<string, CustomFunction>();\n private readonly asyncDerivationFunctions = new Map<string, AsyncDerivationFunction>();\n private readonly asyncConditionFunctions = new Map<string, AsyncConditionFunction>();\n private readonly validators = new Map<string, CustomValidator>();\n private readonly asyncValidators = new Map<string, AsyncCustomValidator>();\n private readonly httpValidators = new Map<string, HttpCustomValidator>();\n\n /**\n * Register a custom function for conditional expressions\n *\n * @param name - Unique identifier for the function\n * @param fn - Function that receives EvaluationContext and returns any value\n * @param options - Optional configuration for the function\n */\n registerCustomFunction<TFormValue extends Record<string, unknown> = Record<string, unknown>>(\n name: string,\n fn: CustomFunction<TFormValue>,\n options?: CustomFunctionOptions,\n ): void {\n // Registry stores type-erased functions; the caller's strongly-typed fn is safe to widen here.\n this.customFunctions.set(name, fn as CustomFunction);\n // Store scope (default to 'form' for conservative cross-field detection)\n this.customFunctionScopes.set(name, options?.scope ?? 'form');\n }\n\n /**\n * Get the scope of a registered custom function.\n *\n * @param name - The function name\n * @returns The function scope, or undefined if not registered\n */\n getCustomFunctionScope(name: string): CustomFunctionScope | undefined {\n return this.customFunctionScopes.get(name);\n }\n\n /**\n * Check if a custom function is field-scoped (no cross-field dependencies).\n *\n * @param name - The function name\n * @returns true if the function is field-scoped, false if form-scoped or not registered\n */\n isFieldScopedFunction(name: string): boolean {\n return this.customFunctionScopes.get(name) === 'field';\n }\n\n /** Get all custom functions as an object */\n getCustomFunctions(): Record<string, CustomFunction> {\n return Object.fromEntries(this.customFunctions);\n }\n\n /** Clear all custom functions and their scopes */\n clearCustomFunctions(): void {\n this.customFunctions.clear();\n this.customFunctionScopes.clear();\n }\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Derivation Functions\n // ─────────────────────────────────────────────────────────────────────────────\n\n /**\n * Register a derivation function for value derivation logic.\n *\n * @param name - Unique identifier for the function\n * @param fn - Function that receives EvaluationContext and returns the derived value\n */\n registerDerivationFunction(name: string, fn: CustomFunction): void {\n this.derivationFunctions.set(name, fn);\n }\n\n /** Get a derivation function by name */\n getDerivationFunction(name: string): CustomFunction | undefined {\n return this.derivationFunctions.get(name);\n }\n\n /** Get all derivation functions as an object */\n getDerivationFunctions(): Record<string, CustomFunction> {\n return Object.fromEntries(this.derivationFunctions);\n }\n\n /**\n * Set derivation functions from a config object.\n * Only updates functions if their references have changed.\n *\n * @param derivations - Object mapping function names to derivation functions\n */\n setDerivationFunctions<TFormValue extends Record<string, unknown> = Record<string, unknown>>(\n derivations: Record<string, CustomFunction<TFormValue>> | undefined,\n ): void {\n this.setRegistryIfChanged(this.derivationFunctions, derivations as Record<string, CustomFunction> | undefined);\n }\n\n /** Clear all derivation functions */\n clearDerivationFunctions(): void {\n this.derivationFunctions.clear();\n }\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Async Derivation Functions\n // ─────────────────────────────────────────────────────────────────────────────\n\n /**\n * Register an async derivation function.\n *\n * @param name - Unique identifier for the function\n * @param fn - Function that receives EvaluationContext and returns Promise/Observable\n */\n registerAsyncDerivationFunction(name: string, fn: AsyncDerivationFunction): void {\n this.asyncDerivationFunctions.set(name, fn);\n }\n\n /** Get an async derivation function by name */\n getAsyncDerivationFunction(name: string): AsyncDerivationFunction | undefined {\n return this.asyncDerivationFunctions.get(name);\n }\n\n /** Get all async derivation functions as an object */\n getAsyncDerivationFunctions(): Record<string, AsyncDerivationFunction> {\n return Object.fromEntries(this.asyncDerivationFunctions);\n }\n\n /**\n * Set async derivation functions from a config object.\n * Only updates functions if their references have changed.\n *\n * @param fns - Object mapping function names to async derivation functions\n */\n setAsyncDerivationFunctions<TFormValue extends Record<string, unknown> = Record<string, unknown>>(\n fns: Record<string, AsyncDerivationFunction<TFormValue>> | undefined,\n ): void {\n this.setRegistryIfChanged(this.asyncDerivationFunctions, fns as Record<string, AsyncDerivationFunction> | undefined);\n }\n\n /** Clear all async derivation functions */\n clearAsyncDerivationFunctions(): void {\n this.asyncDerivationFunctions.clear();\n }\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Async Condition Functions\n // ─────────────────────────────────────────────────────────────────────────────\n\n /**\n * Register an async condition function.\n *\n * @param name - Unique identifier for the function\n * @param fn - Function that receives EvaluationContext and returns Promise/Observable of boolean\n */\n registerAsyncConditionFunction(name: string, fn: AsyncConditionFunction): void {\n this.asyncConditionFunctions.set(name, fn);\n }\n\n /** Get an async condition function by name */\n getAsyncConditionFunction(name: string): AsyncConditionFunction | undefined {\n return this.asyncConditionFunctions.get(name);\n }\n\n /** Get all async condition functions as an object */\n getAsyncConditionFunctions(): Record<string, AsyncConditionFunction> {\n return Object.fromEntries(this.asyncConditionFunctions);\n }\n\n /**\n * Set async condition functions from a config object.\n * Only updates functions if their references have changed.\n *\n * @param fns - Object mapping function names to async condition functions\n */\n setAsyncConditionFunctions<TFormValue extends Record<string, unknown> = Record<string, unknown>>(\n fns: Record<string, AsyncConditionFunction<TFormValue>> | undefined,\n ): void {\n this.setRegistryIfChanged(this.asyncConditionFunctions, fns as Record<string, AsyncConditionFunction> | undefined);\n }\n\n /** Clear all async condition functions */\n clearAsyncConditionFunctions(): void {\n this.asyncConditionFunctions.clear();\n }\n\n /**\n * Register a custom validator using Angular's public FieldContext API\n *\n * @param name - Unique identifier for the validator\n * @param fn - Validator function (ctx, params?) => ValidationError | ValidationError[] | null\n */\n registerValidator(name: string, fn: CustomValidator): void {\n this.validators.set(name, fn);\n }\n\n /** Get a validator by name */\n getValidator(name: string): CustomValidator | undefined {\n return this.validators.get(name);\n }\n\n /** Clear all validators */\n clearValidators(): void {\n this.validators.clear();\n }\n\n /**\n * Register an async validator using Angular's public validateAsync() API\n *\n * @param name - Unique identifier for the async validator\n * @param fn - Async validator function (ctx, params?) => Observable<ValidationError | ValidationError[] | null>\n */\n registerAsyncValidator(name: string, fn: AsyncCustomValidator): void {\n this.asyncValidators.set(name, fn);\n }\n\n /** Get an async validator by name */\n getAsyncValidator(name: string): AsyncCustomValidator | undefined {\n return this.asyncValidators.get(name);\n }\n\n /** Clear all async validators */\n clearAsyncValidators(): void {\n this.asyncValidators.clear();\n }\n\n /**\n * Register an HTTP validator configuration using Angular's public validateHttp() API\n *\n * @param name - Unique identifier for the HTTP validator\n * @param config - HTTP validator configuration object\n */\n registerHttpValidator(name: string, config: HttpCustomValidator): void {\n this.httpValidators.set(name, config);\n }\n\n /** Get an HTTP validator by name */\n getHttpValidator(name: string): HttpCustomValidator | undefined {\n return this.httpValidators.get(name);\n }\n\n /** Clear all HTTP validators */\n clearHttpValidators(): void {\n this.httpValidators.clear();\n }\n\n /**\n * Generic helper to set registry values only if they have changed\n *\n * @param registry - The Map to update\n * @param values - Object mapping keys to values\n */\n private setRegistryIfChanged<T>(registry: Map<string, T>, values: Record<string, T> | undefined): void {\n if (!values) return;\n\n const entries = Object.entries(values);\n const hasChanges = entries.some(([name, value]) => registry.get(name) !== value);\n\n if (hasChanges) {\n entries.forEach(([name, value]) => registry.set(name, value));\n }\n }\n\n /**\n * Set validators from a config object\n * Only updates validators if their references have changed\n *\n * @param validators - Object mapping validator names to validator functions\n */\n setValidators(validators: Record<string, CustomValidator> | undefined): void {\n this.setRegistryIfChanged(this.validators, validators);\n }\n\n /**\n * Set async validators from a config object\n * Only updates validators if their references have changed\n *\n * @param asyncValidators - Object mapping validator names to async validator configs\n */\n setAsyncValidators(asyncValidators: Record<string, AsyncCustomValidator> | undefined): void {\n this.setRegistryIfChanged(this.asyncValidators, asyncValidators);\n }\n\n /**\n * Set HTTP validators from a config object\n * Only updates validators if their references have changed\n *\n * @param httpValidators - Object mapping validator names to HTTP validator configs\n */\n setHttpValidators(httpValidators: Record<string, HttpCustomValidator> | undefined): void {\n this.setRegistryIfChanged(this.httpValidators, httpValidators);\n }\n\n /** Clear everything (functions and all validators) */\n clearAll(): void {\n this.clearCustomFunctions();\n this.clearDerivationFunctions();\n this.clearAsyncDerivationFunctions();\n this.clearAsyncConditionFunctions();\n this.clearValidators();\n this.clearAsyncValidators();\n this.clearHttpValidators();\n }\n}\n","import { InjectionToken } from '@angular/core';\n\ninterface CacheEntry {\n value: boolean;\n expiresAt: number;\n}\n\n/** TTL cache for HTTP condition responses with max entries eviction. */\nexport class HttpConditionCache {\n private readonly cache = new Map<string, CacheEntry>();\n private readonly maxEntries: number;\n\n constructor(maxEntries = 100) {\n this.maxEntries = Math.max(1, maxEntries);\n }\n\n get(key: string): boolean | undefined {\n const entry = this.cache.get(key);\n if (!entry) return undefined;\n\n if (Date.now() > entry.expiresAt) {\n this.cache.delete(key);\n return undefined;\n }\n\n return entry.value;\n }\n\n set(key: string, value: boolean, ttlMs: number): void {\n // Evict oldest entries if at capacity (Map preserves insertion order)\n while (this.cache.size >= this.maxEntries && !this.cache.has(key)) {\n const oldestKey = this.cache.keys().next().value;\n if (oldestKey !== undefined) {\n this.cache.delete(oldestKey);\n }\n }\n\n this.cache.set(key, { value, expiresAt: Date.now() + ttlMs });\n }\n\n clear(): void {\n this.cache.clear();\n }\n}\n\n/** @internal */\nexport const HTTP_CONDITION_CACHE = new InjectionToken<HttpConditionCache>('HttpConditionCache');\n","import { HttpRequestConfig } from '../../models/http/http-request-config';\nimport { EvaluationContext } from '../../models/expressions/evaluation-context';\nimport { HttpResourceRequest } from '../validation/validator-types';\nimport { ExpressionParser } from '../expressions/parser/expression-parser';\n\n/**\n * Resolves an `HttpRequestConfig` into an `HttpResourceRequest` by evaluating\n * expression-based query params and (optionally) body values.\n */\nexport function resolveHttpRequest(config: HttpRequestConfig, context: EvaluationContext): HttpResourceRequest | null {\n let url = config.url;\n\n if (config.params) {\n for (const [key, expression] of Object.entries(config.params)) {\n const value = ExpressionParser.evaluate(expression, context);\n if (value == null) {\n context.logger.debug(\n `HTTP request suppressed: path param '${key}' resolved to ${String(value)} ` +\n `(expression: '${expression}'). The request will not be sent.`,\n );\n return null;\n }\n url = url.replace(`:${key}`, encodeURIComponent(String(value)));\n }\n }\n\n if (config.queryParams) {\n const params = new URLSearchParams();\n for (const [key, expression] of Object.entries(config.queryParams)) {\n const value = ExpressionParser.evaluate(expression, context);\n params.set(key, value != null ? String(value) : '');\n }\n const queryString = params.toString();\n if (queryString) {\n url += (url.includes('?') ? '&' : '?') + queryString;\n }\n }\n\n const request: HttpResourceRequest = {\n url,\n method: config.method,\n };\n\n if (config.body) {\n if (config.evaluateBodyExpressions) {\n const resolvedBody: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(config.body)) {\n if (typeof value === 'string') {\n resolvedBody[key] = ExpressionParser.evaluate(value, context);\n } else {\n resolvedBody[key] = value;\n }\n }\n request.body = resolvedBody;\n } else {\n request.body = config.body;\n }\n }\n\n if (config.headers) {\n request.headers = config.headers;\n }\n\n return request;\n}\n","import { Injector, isSignal, Signal } from '@angular/core';\nimport { toObservable, toSignal } from '@angular/core/rxjs-interop';\nimport { combineLatest, defer, Observable, OperatorFunction } from 'rxjs';\n\n/**\n * Type guard to check if a value is an array of Signals.\n *\n * @internal\n */\nfunction isSignalArray(value: unknown): value is readonly Signal<unknown>[] {\n return Array.isArray(value) && value.every((item) => isSignal(item));\n}\n\n/**\n * Type guard to check if a value is a record (object) of Signals.\n *\n * @internal\n */\nfunction isSignalRecord(value: unknown): value is Record<string, Signal<unknown>> {\n return typeof value === 'object' && value !== null && !Array.isArray(value) && Object.values(value).every((item) => isSignal(item));\n}\n\n/** Options for derivedFromDeferred. */\nexport interface DerivedFromDeferredOptions<R> {\n initialValue: R;\n injector: Injector;\n}\n\n/** Type helper to extract the value type from a Signal. */\ntype SignalValue<T> = T extends Signal<infer V> ? V : never;\n\n/** Type helper to map an array of Signals to an array of their value types. */\ntype SignalValues<T extends readonly Signal<unknown>[]> = {\n [K in keyof T]: SignalValue<T[K]>;\n};\n\n/** Type helper to map an object of Signals to an object of their value types. */\ntype SignalObjectValues<T extends Record<string, Signal<unknown>>> = {\n [K in keyof T]: SignalValue<T[K]>;\n};\n\n/**\n * Like derivedFrom from ngxtension, but uses defer() to avoid input availability issues.\n * This ensures toObservable is called at subscription time, not at field initialization.\n */\n\n// Overload: Single signal\nexport function derivedFromDeferred<T, R>(\n source: Signal<T>,\n operator: OperatorFunction<T, R>,\n options: DerivedFromDeferredOptions<R>,\n): Signal<R>;\n\n// Overload: Array of signals\nexport function derivedFromDeferred<T extends readonly Signal<unknown>[], R>(\n sources: [...T],\n operator: OperatorFunction<SignalValues<T>, R>,\n options: DerivedFromDeferredOptions<R>,\n): Signal<R>;\n\n// Overload: Object of signals\nexport function derivedFromDeferred<T extends Record<string, Signal<unknown>>, R>(\n sources: T,\n operator: OperatorFunction<SignalObjectValues<T>, R>,\n options: DerivedFromDeferredOptions<R>,\n): Signal<R>;\n\n// Implementation\nexport function derivedFromDeferred<T, R>(\n source: Signal<T> | Signal<unknown>[] | Record<string, Signal<unknown>>,\n operator: OperatorFunction<T | unknown[] | Record<string, unknown>, R>,\n options: DerivedFromDeferredOptions<R>,\n): Signal<R> {\n const deferred$: Observable<R> = defer(() => {\n // Handle single signal\n if (isSignal(source)) {\n return toObservable(source, { injector: options.injector });\n }\n\n // Handle array of signals\n if (isSignalArray(source)) {\n const observables = source.map((s) => toObservable(s, { injector: options.injector }));\n return combineLatest(observables);\n }\n\n // Handle object of signals\n if (isSignalRecord(source)) {\n const keys = Object.keys(source);\n const observables = keys.map((key) => toObservable(source[key], { injector: options.injector }));\n\n return combineLatest(observables, (...values) => {\n const result: Record<string, unknown> = {};\n keys.forEach((key, index) => {\n result[key] = values[index];\n });\n return result;\n });\n }\n\n // Fallback - should not happen with proper typing\n throw new Error('Invalid source type. Expected a Signal, array of Signals, or object of Signals.');\n }).pipe(operator);\n\n return toSignal(deferred$, { initialValue: options.initialValue, injector: options.injector });\n}\n","import { linkedSignal, Resource, ResourceSnapshot, resourceFromSnapshots } from '@angular/core';\n\n/**\n * Wraps a `Resource` so that the previous resolved value is preserved while the resource\n * is loading or reloading. This prevents UI flicker when params change and a new request\n * is in-flight — the consumer sees the stale value with a `loading`/`reloading` status\n * instead of `undefined`.\n */\nexport function withPreviousValue<T>(input: Resource<T>): Resource<T> {\n const derived = linkedSignal<ResourceSnapshot<T>, ResourceSnapshot<T>>({\n source: input.snapshot,\n computation: (snapshot, previous) => {\n // During loading/reloading, keep the previous resolved value if available.\n // Don't preserve values from error states — those aren't useful to show.\n if (\n (snapshot.status === 'loading' || snapshot.status === 'reloading') &&\n previous !== undefined &&\n previous.value.status !== 'error'\n ) {\n return { status: snapshot.status, value: previous.value.value } as ResourceSnapshot<T>;\n }\n return snapshot;\n },\n });\n\n return resourceFromSnapshots(derived);\n}\n","import { Injector, Resource, signal, untracked, WritableSignal } from '@angular/core';\nimport { rxResource } from '@angular/core/rxjs-interop';\nimport { FieldContext, LogicFn } from '@angular/forms/signals';\nimport { debounceTime, distinctUntilChanged, Observable, pipe } from 'rxjs';\nimport { derivedFromDeferred } from '../../utils/derived-from-deferred/derived-from-deferred';\nimport { withPreviousValue } from '../../utils/resource-composition/with-previous-value';\nimport { safeReadPathKeys } from '../../utils/safe-read-path-keys';\n\n/**\n * Trigger value carried through the debounced reactive pipeline.\n *\n * @internal\n */\nexport interface Trigger<TPayload> {\n key: string;\n payload: TPayload;\n}\n\n/**\n * Configuration passed to {@link createDebouncedResourceLogicFn}.\n *\n * @internal\n */\nexport interface DebouncedResourceLogicFnConfig<TValue, TPayload, TResult> {\n /** Default value returned by the resource while loading / pending. */\n pendingValue: TResult;\n /** Debounce window (ms) applied to the trigger signal before the resource sees it. */\n debounceMs: number;\n injector: Injector;\n /**\n * Builds the per-emission inner observable. Called every time the debounced\n * trigger key changes; the resource swaps to the new stream automatically.\n */\n buildStream: (payload: TPayload) => Observable<TResult>;\n /**\n * Called per LogicFn invocation. Returns one of:\n * - `{ kind: 'trigger', key, payload }` — set the trigger to this and\n * return the (potentially still-loading) resource value.\n * - `{ kind: 'returnEarly', value }` — bypass the resource and return\n * this value directly (used by HTTP for response-cache hits — the\n * cached value is authoritative and overrides any sticky resource value).\n * - `{ kind: 'holdPrevious' }` — bypass the resource update but return\n * the resource's current sticky value (its last resolved value via\n * `withPreviousValue`, or `pendingValue` if it never resolved). Used\n * by HTTP when the request template is temporarily unresolvable so\n * the UI doesn't flicker back to `pendingValue`.\n */\n resolve: (\n ctx: FieldContext<TValue>,\n ) => { kind: 'trigger'; key: string; payload: TPayload } | { kind: 'returnEarly'; value: TResult } | { kind: 'holdPrevious' };\n}\n\n/**\n * Per-field resource handle held in the LogicFn's signal store. Each call to\n * the LogicFn with a unique field path reuses the same handle so the\n * underlying `rxResource` lifecycle persists.\n *\n * @internal\n */\ninterface ResourceHandle<TPayload, TResult> {\n trigger: WritableSignal<Trigger<TPayload> | undefined>;\n resultResource: Resource<TResult>;\n /**\n * Bumped on every `returnEarly` so the next `trigger.set` carries a fresh dedup key.\n * Without this, a cache hit → cache expiry → same-key recall would be dropped by\n * both the helper's same-key check and the downstream `distinctUntilChanged`, and\n * the resource would never re-fetch.\n */\n refetchVersion: number;\n}\n\n/**\n * Shared scaffolding for both async-function and HTTP condition LogicFns.\n *\n * @internal\n */\nexport function createDebouncedResourceLogicFn<TValue, TPayload, TResult>(\n config: DebouncedResourceLogicFnConfig<TValue, TPayload, TResult>,\n): LogicFn<TValue, TResult> {\n const perFunctionSignalStore = new Map<string, ResourceHandle<TPayload, TResult>>();\n\n return (ctx: FieldContext<TValue>) => {\n const contextKey = safeReadPathKeys(ctx as FieldContext<unknown>).join('.');\n let handle = perFunctionSignalStore.get(contextKey);\n\n if (!handle) {\n const trigger = signal<Trigger<TPayload> | undefined>(undefined);\n\n // untracked: rxResource creates an internal effect, which Angular forbids\n // inside a reactive consumer (LogicFn runs inside a computed). Clearing\n // the consumer keeps the resource's lifecycle independent of the LogicFn's\n // reactive context.\n const resultResource = untracked(() => {\n const debouncedTrigger = derivedFromDeferred(\n trigger,\n pipe(\n debounceTime(config.debounceMs),\n // distinctUntilChanged on key alone — payload identity doesn't matter\n // (the latest payload for a given key always wins).\n distinctUntilChanged((a, b) => a?.key === b?.key),\n ),\n { initialValue: undefined as Trigger<TPayload> | undefined, injector: config.injector },\n );\n\n const resource = rxResource<TResult, Trigger<TPayload> | undefined>({\n params: () => debouncedTrigger(),\n // When params() returns undefined, rxResource enters idle state.\n stream: ({ params }) => config.buildStream(params!.payload),\n defaultValue: config.pendingValue,\n injector: config.injector,\n });\n\n return withPreviousValue(resource);\n });\n\n handle = { trigger, resultResource, refetchVersion: 0 };\n perFunctionSignalStore.set(contextKey, handle);\n }\n\n const resolved = config.resolve(ctx);\n if (resolved.kind === 'returnEarly') {\n handle.refetchVersion++;\n return resolved.value;\n }\n if (resolved.kind === 'holdPrevious') {\n return handle.resultResource.value();\n }\n\n const dedupKey = `${resolved.key}::${handle.refetchVersion}`;\n const newTrigger: Trigger<TPayload> = { key: dedupKey, payload: resolved.payload };\n untracked(() => {\n const current = handle!.trigger();\n // Key-only dedup assumes same key implies equivalent payload — true for both current callers.\n if (current?.key !== newTrigger.key) {\n handle!.trigger.set(newTrigger);\n }\n });\n\n return handle.resultResource.value();\n };\n}\n","import { HttpClient } from '@angular/common/http';\nimport { inject, Injector } from '@angular/core';\nimport { LogicFn } from '@angular/forms/signals';\nimport { catchError, map, of, tap } from 'rxjs';\nimport { HttpCondition } from '../../models/expressions/conditional-expression';\nimport { stableStringify } from '../../utils/stable-stringify';\nimport { HttpResourceRequest } from '../validation/validator-types';\nimport { FieldContextRegistryService } from '../registry/field-context-registry.service';\nimport { FunctionRegistryService } from '../registry/function-registry.service';\nimport { HTTP_CONDITION_CACHE } from '../http/http-condition-cache';\nimport { DynamicFormError } from '../../errors/dynamic-form-error';\nimport { DynamicFormLogger } from '../../providers/features/logger/logger.token';\nimport { Logger } from '../../providers/features/logger/logger.interface';\nimport { resolveHttpRequest } from '../http/http-request-resolver';\nimport { ExpressionParser } from './parser/expression-parser';\nimport { HttpConditionFunctionCacheService } from './http-condition-function-cache.service';\nimport { createDebouncedResourceLogicFn } from './debounced-resource-logic-fn';\n\n/**\n * Extracts a boolean from an HTTP response using an optional expression.\n * When `responseExpression` is provided, evaluates it with `{ response }` scope.\n * Truthy values are treated as `true` — non-boolean results trigger a warning to encourage explicit boolean expressions.\n * Otherwise, coerces the response to boolean.\n */\nfunction extractBoolean(response: unknown, responseExpression: string | undefined, pendingValue: boolean, logger: Logger): boolean {\n if (responseExpression) {\n try {\n const result = ExpressionParser.evaluate(responseExpression, { response });\n if (typeof result !== 'boolean') {\n logger.warn(\n `responseExpression \"${responseExpression}\" returned non-boolean value:`,\n result,\n 'Consider returning a boolean explicitly.',\n );\n }\n return !!result;\n } catch (error) {\n logger.warn(`Failed to evaluate responseExpression '${responseExpression}':`, error);\n return pendingValue;\n }\n }\n return !!response;\n}\n\n/** Creates a logic function for an HTTP condition. */\nexport function createHttpConditionLogicFunction<TValue>(condition: HttpCondition): LogicFn<TValue, boolean> {\n const httpClient = inject(HttpClient, { optional: true });\n if (!httpClient) {\n throw new DynamicFormError('HttpClient is required for HTTP conditions. Add provideHttpClient() to your providers.');\n }\n const fieldContextRegistry = inject(FieldContextRegistryService);\n const functionRegistry = inject(FunctionRegistryService);\n const injector = inject(Injector);\n const cache = inject(HTTP_CONDITION_CACHE);\n const logger = inject(DynamicFormLogger) as Logger;\n const cacheService = inject(HttpConditionFunctionCacheService);\n\n const pendingValue = condition.pendingValue ?? false;\n const cacheDurationMs = condition.cacheDurationMs ?? 30000;\n const debounceMs = condition.debounceMs ?? 300;\n\n // Function cache: across-condition reuse for identical condition shapes.\n const cacheKey = stableStringify(condition);\n const cached = cacheService.httpConditionFunctionCache.get(cacheKey);\n if (cached) {\n return cached as LogicFn<TValue, boolean>;\n }\n\n const fn = createDebouncedResourceLogicFn<TValue, HttpResourceRequest, boolean>({\n pendingValue,\n debounceMs,\n injector,\n resolve: (ctx) => {\n const evaluationContext = fieldContextRegistry.createReactiveEvaluationContext(ctx, functionRegistry.getCustomFunctions());\n const resolved = resolveHttpRequest(condition.http, evaluationContext);\n // Hold the previous resolved value rather than flicker to pendingValue while a path\n // param is temporarily undefined between two valid states.\n if (!resolved) return { kind: 'holdPrevious' };\n\n const requestKey = stableStringify(resolved);\n const cachedResult = cache.get(requestKey);\n if (cachedResult !== undefined) return { kind: 'returnEarly', value: cachedResult };\n\n return { kind: 'trigger', key: requestKey, payload: resolved };\n },\n buildStream: (request) => {\n const method = request.method ?? 'GET';\n const options: Record<string, unknown> = {};\n if (request.body !== undefined) options['body'] = request.body;\n if (request.headers) options['headers'] = request.headers;\n\n return httpClient.request(method, request.url, options).pipe(\n map((response) => extractBoolean(response, condition.responseExpression, pendingValue, logger)),\n tap((value) => {\n const requestKey = stableStringify(request);\n cache.set(requestKey, value, cacheDurationMs);\n }),\n catchError((error) => {\n logger.warn('HTTP condition request failed:', error);\n return of(pendingValue);\n }),\n );\n },\n });\n\n cacheService.httpConditionFunctionCache.set(cacheKey, fn as LogicFn<unknown, boolean>);\n return fn;\n}\n","import { inject, Injector } from '@angular/core';\nimport { FieldContext, LogicFn } from '@angular/forms/signals';\nimport { catchError, from, map, of } from 'rxjs';\nimport { AsyncCondition } from '../../models/expressions/conditional-expression';\nimport { stableStringify } from '../../utils/stable-stringify';\nimport { FieldContextRegistryService } from '../registry/field-context-registry.service';\nimport { FunctionRegistryService } from '../registry/function-registry.service';\nimport { DynamicFormLogger } from '../../providers/features/logger/logger.token';\nimport { Logger } from '../../providers/features/logger/logger.interface';\nimport { AsyncConditionFunctionCacheService } from './async-condition-function-cache.service';\nimport { createDebouncedResourceLogicFn } from './debounced-resource-logic-fn';\n\n/** Creates a logic function for an async condition. */\nexport function createAsyncConditionLogicFunction<TValue>(condition: AsyncCondition): LogicFn<TValue, boolean> {\n const fieldContextRegistry = inject(FieldContextRegistryService);\n const functionRegistry = inject(FunctionRegistryService);\n const injector = inject(Injector);\n const logger = inject(DynamicFormLogger) as Logger;\n const cacheService = inject(AsyncConditionFunctionCacheService);\n\n const pendingValue = condition.pendingValue ?? false;\n const debounceMs = condition.debounceMs ?? 300;\n\n if (condition.asyncFn && condition.asyncFunctionName) {\n logger.warn(\n 'Both \"asyncFn\" and \"asyncFunctionName\" are set on async condition. Inline \"asyncFn\" takes precedence; \"asyncFunctionName\" is ignored.',\n );\n }\n\n // Skip cross-condition caching when an inline function is used: stableStringify\n // cannot distinguish two different inline functions, which would cause cache hits\n // to wrongly reuse a LogicFn closing over a different function reference.\n const cacheKey = condition.asyncFn ? undefined : stableStringify(condition);\n\n if (cacheKey !== undefined) {\n const cached = cacheService.asyncConditionFunctionCache.get(cacheKey);\n if (cached) {\n return cached as LogicFn<TValue, boolean>;\n }\n }\n\n const fn = createDebouncedResourceLogicFn<TValue, FieldContext<TValue>, boolean>({\n pendingValue,\n debounceMs,\n injector,\n resolve: (ctx) => {\n // Build reactive evaluation context here so the LogicFn establishes\n // signal dependencies on form values — the resource's reactive graph\n // alone wouldn't notice changes that didn't flow through the trigger.\n const evaluationContext = fieldContextRegistry.createReactiveEvaluationContext(ctx, functionRegistry.getCustomFunctions());\n const key = stableStringify({\n formValue: evaluationContext.formValue,\n fieldValue: evaluationContext.fieldValue,\n externalData: evaluationContext.externalData,\n });\n return { kind: 'trigger', key, payload: ctx };\n },\n buildStream: (capturedCtx) => {\n // Inline `asyncFn` wins; otherwise look up the registered function at call time\n // (fresh reference, in case the registry was updated after LogicFn creation).\n const asyncFn =\n condition.asyncFn ??\n (condition.asyncFunctionName ? functionRegistry.getAsyncConditionFunction(condition.asyncFunctionName) : undefined);\n if (!asyncFn) {\n logger.warn(\n `Async Condition - function '${condition.asyncFunctionName}' not found. ` + `Register it in customFnConfig.asyncConditions.`,\n );\n return of(pendingValue);\n }\n\n const evaluationContext = fieldContextRegistry.createReactiveEvaluationContext(capturedCtx, functionRegistry.getCustomFunctions());\n\n // asyncFn may return Promise<boolean> or Observable<boolean>.\n // from() normalizes both to Observable.\n return from(asyncFn(evaluationContext)).pipe(\n map((result) => !!result),\n catchError((error) => {\n logger.warn('Async Condition - function failed:', error);\n return of(pendingValue);\n }),\n );\n },\n });\n\n if (cacheKey !== undefined) {\n cacheService.asyncConditionFunctionCache.set(cacheKey, fn as LogicFn<unknown, boolean>);\n }\n return fn;\n}\n","import { Signal, WritableSignal } from '@angular/core';\nimport { LogicFn } from '@angular/forms/signals';\n\n/** Per-field debounced signal pair. */\ninterface DebouncedSignalPair {\n immediateValue: WritableSignal<boolean>;\n debouncedValue: Signal<boolean | undefined>;\n}\n\n/** DI-scoped cache for logic functions and debounced signals. */\nexport class LogicFunctionCacheService {\n /** Cache for memoized logic functions, keyed by serialized expression. */\n readonly logicFunctionCache = new Map<string, LogicFn<unknown, boolean>>();\n\n /** Cache for debounced logic functions, keyed by serialized expression + debounceMs. */\n readonly debouncedLogicFunctionCache = new Map<string, LogicFn<unknown, boolean>>();\n\n /** Per-field signal store for debounced logic, keyed by field path. */\n readonly debouncedSignalStore = new Map<string, DebouncedSignalPair>();\n}\n","import { inject, Injector, signal, untracked } from '@angular/core';\nimport { toObservable, toSignal } from '@angular/core/rxjs-interop';\nimport { FieldContext, LogicFn } from '@angular/forms/signals';\nimport { debounceTime, distinctUntilChanged, startWith } from 'rxjs';\nimport { ConditionalExpression } from '../../models/expressions/conditional-expression';\nimport { DynamicFormError } from '../../errors/dynamic-form-error';\nimport { stableStringify } from '../../utils/stable-stringify';\nimport { FieldContextRegistryService } from '../registry/field-context-registry.service';\nimport { FunctionRegistryService } from '../registry/function-registry.service';\nimport { evaluateCondition } from './condition-evaluator';\nimport { createHttpConditionLogicFunction } from './http-condition-logic-function';\nimport { createAsyncConditionLogicFunction } from './async-condition-logic-function';\nimport { LogicFunctionCacheService } from './logic-function-cache.service';\nimport { safeReadPathKeys } from '../../utils/safe-read-path-keys';\n\n/**\n * Recursively validates that HTTP/async conditions are not nested inside and/or composites.\n * These conditions require async resolution and cannot be evaluated synchronously\n * inside composite conditions.\n */\nfunction validateNoNestedHttpConditions(expression: ConditionalExpression): void {\n if (expression.type === 'and' || expression.type === 'or') {\n for (const sub of expression.conditions) {\n if (sub.type === 'http' || sub.type === 'async') {\n const label = sub.type === 'http' ? 'HTTP' : 'Async';\n throw new DynamicFormError(\n `${label} conditions cannot be nested inside '${expression.type}' composites. ` +\n `Move the ${label} condition to a separate logic entry on the field.`,\n );\n }\n validateNoNestedHttpConditions(sub);\n }\n }\n}\n\n/**\n * Create a logic function from a conditional expression.\n *\n * @param expression The conditional expression to evaluate\n * @returns A LogicFn that evaluates the condition in the context of a field\n */\nexport function createLogicFunction<TValue>(expression: ConditionalExpression): LogicFn<TValue, boolean> {\n // Async conditions handle their own debouncing and async resolution\n if (expression?.type === 'async') {\n return createAsyncConditionLogicFunction(expression);\n }\n\n // HTTP conditions handle their own debouncing and async resolution\n if (expression?.type === 'http') {\n return createHttpConditionLogicFunction(expression);\n }\n\n // Validate that HTTP conditions aren't nested inside and/or composites\n if (expression) {\n validateNoNestedHttpConditions(expression);\n }\n\n // Inject services during factory creation, not during execution\n const functionRegistry = inject(FunctionRegistryService);\n const fieldContextRegistry = inject(FieldContextRegistryService);\n const cacheService = inject(LogicFunctionCacheService);\n\n // Generate cache key from serialized expression\n const cacheKey = stableStringify(expression);\n\n // Check cache first\n const cached = cacheService.logicFunctionCache.get(cacheKey);\n if (cached) {\n return cached as LogicFn<TValue, boolean>;\n }\n\n const fn: LogicFn<TValue, boolean> = (ctx: FieldContext<TValue>) => {\n // Create REACTIVE evaluation context for logic functions\n // This allows logic to re-evaluate when dependent fields change\n const evaluationContext = fieldContextRegistry.createReactiveEvaluationContext(ctx, functionRegistry.getCustomFunctions());\n\n return evaluateCondition(expression, evaluationContext);\n };\n\n // Cache the function\n cacheService.logicFunctionCache.set(cacheKey, fn as LogicFn<unknown, boolean>);\n return fn;\n}\n\n/**\n * Create a debounced logic function from a conditional expression.\n *\n * @param expression The conditional expression to evaluate\n * @param debounceMs The debounce duration in milliseconds\n * @returns A LogicFn that evaluates the condition with debouncing\n */\nexport function createDebouncedLogicFunction<TValue>(expression: ConditionalExpression, debounceMs: number): LogicFn<TValue, boolean> {\n // Async conditions handle their own debouncing via condition.debounceMs\n if (expression?.type === 'async') {\n return createAsyncConditionLogicFunction(expression);\n }\n\n // HTTP conditions handle their own debouncing via condition.http.debounceMs\n if (expression?.type === 'http') {\n return createHttpConditionLogicFunction(expression);\n }\n\n // Validate that HTTP conditions aren't nested inside and/or composites\n if (expression) {\n validateNoNestedHttpConditions(expression);\n }\n\n // Inject services during factory creation, not during execution\n const functionRegistry = inject(FunctionRegistryService);\n const fieldContextRegistry = inject(FieldContextRegistryService);\n const injector = inject(Injector);\n const cacheService = inject(LogicFunctionCacheService);\n\n // Generate cache key including debounceMs\n const cacheKey = `${stableStringify(expression)}:${debounceMs}`;\n\n // Check cache first\n const cached = cacheService.debouncedLogicFunctionCache.get(cacheKey);\n if (cached) {\n return cached as LogicFn<TValue, boolean>;\n }\n\n const fn: LogicFn<TValue, boolean> = (ctx: FieldContext<TValue>) => {\n const contextKey = safeReadPathKeys(ctx as FieldContext<unknown>).join('.');\n let signalPair = cacheService.debouncedSignalStore.get(contextKey);\n\n if (!signalPair) {\n // Create a signal to hold the immediate evaluation result\n const immediateValue = signal(false);\n\n // Wrap in untracked() to avoid NG0602: toObservable() internally calls effect(),\n // which cannot be created inside a reactive context (computed). The LogicFn runs\n // inside Angular Signal Forms' BooleanOrLogic.compute (a computed).\n const debouncedValue = untracked(() => {\n const immediateValue$ = toObservable(immediateValue, { injector }).pipe(\n debounceTime(debounceMs),\n distinctUntilChanged(),\n startWith(false),\n );\n\n return toSignal(immediateValue$, { injector, initialValue: false });\n });\n\n signalPair = { immediateValue, debouncedValue };\n cacheService.debouncedSignalStore.set(contextKey, signalPair);\n }\n\n // Create REACTIVE evaluation context for logic functions\n const evaluationContext = fieldContextRegistry.createReactiveEvaluationContext(ctx, functionRegistry.getCustomFunctions());\n\n // Evaluate the condition and update the immediate signal\n const result = evaluateCondition(expression, evaluationContext);\n\n // Update immediate value (this triggers the debounce chain)\n // signalPair is guaranteed to exist here due to the initialization above\n const { immediateValue, debouncedValue } = signalPair;\n untracked(() => {\n if (immediateValue() !== result) {\n immediateValue.set(result);\n }\n });\n\n // Return the debounced value\n return debouncedValue() ?? false;\n };\n\n // Cache the function\n cacheService.debouncedLogicFunctionCache.set(cacheKey, fn as LogicFn<unknown, boolean>);\n return fn;\n}\n","import { InjectionToken, Signal } from '@angular/core';\nimport { FieldTree } from '@angular/forms/signals';\n\n/** Represents a FieldTree node for dynamic bracket-access navigation. */\nexport type FieldTreeRecord = Record<string, FieldTree<unknown> | undefined>;\n\n/**\n * Type representing a FieldTree for an array.\n * Extends FieldTree to add numeric indexing for accessing item FieldTrees.\n */\nexport type ArrayFieldTree<T> = FieldTree<T[]> & {\n readonly [index: number]: FieldTree<T> | undefined;\n};\n\n/** Get the length of an array FieldTree. */\nexport function getArrayLength<T>(arrayFieldTree: ArrayFieldTree<T>): number {\n const value = arrayFieldTree().value();\n return Array.isArray(value) ? value.length : 0;\n}\n\n/** Read-only view of a single field's observable state. */\nexport interface ReadonlyFieldTree<TValue = unknown> {\n readonly value: Signal<TValue>;\n readonly valid: Signal<boolean>;\n readonly invalid: Signal<boolean>;\n readonly touched: Signal<boolean>;\n readonly dirty: Signal<boolean>;\n readonly required: Signal<boolean>;\n readonly disabled: Signal<boolean>;\n readonly hidden: Signal<boolean>;\n readonly errors: Signal<readonly unknown[]>;\n}\n\n/**\n * Build a `ReadonlyFieldTree` by extracting the whitelisted read signals from a\n * Signal Forms `FieldTree`. Returns a fresh plain object — no casting, no proxying,\n * so consumers only see the narrow surface and Angular's `WritableSignal` capability\n * on `value` is hidden.\n */\nexport function toReadonlyFieldTree<TValue>(field: FieldTree<TValue>): ReadonlyFieldTree<TValue> {\n const state = field();\n return {\n value: state.value,\n valid: state.valid,\n invalid: state.invalid,\n touched: state.touched,\n dirty: state.dirty,\n required: state.required,\n disabled: state.disabled,\n hidden: state.hidden,\n errors: state.errors,\n };\n}\n\n/**\n * Write a value into a FieldTree's underlying signal. Signal Forms stores\n * `FieldState.value` as a `WritableSignal`; this helper centralises the cast\n * and warns if that contract ever changes (vs a silent no-op).\n */\nexport function writeToFieldValue<T>(value: Signal<T>, next: T, logger?: { warn: (msg: string) => void }): boolean {\n const candidate = value as { set?: (v: T) => void };\n if (typeof candidate.set !== 'function') {\n logger?.warn('writeToFieldValue: FieldState.value is not a WritableSignal — Signal Forms contract changed.');\n return false;\n }\n candidate.set(next);\n return true;\n}\n\n/**\n * DI-scoped cache for `ReadonlyFieldTree` views, keyed on the source FieldTree\n * identity. A fresh WeakMap per root injector keeps renders isolated under SSR —\n * Angular creates a new root injector per request, so there's no shared state\n * between server renders (same pattern as `COMPONENT_CACHE`).\n *\n * @internal\n */\nexport const READONLY_FIELD_TREE_CACHE = new InjectionToken<WeakMap<FieldTree<unknown>, ReadonlyFieldTree<unknown>>>(\n 'READONLY_FIELD_TREE_CACHE',\n {\n providedIn: 'root',\n factory: () => new WeakMap(),\n },\n);\n\n/**\n * Cached variant of {@link toReadonlyFieldTree}. Each FieldTree instance is a\n * stable reference for the lifetime of its form, so one `ReadonlyFieldTree`\n * view per tree is enough. Callers on hot paths (`pushInputs` on every mapper\n * emission) avoid re-allocating the nine-property view object.\n *\n * @internal\n */\nexport function toReadonlyFieldTreeCached<TValue>(\n cache: WeakMap<FieldTree<unknown>, ReadonlyFieldTree<unknown>>,\n field: FieldTree<TValue>,\n): ReadonlyFieldTree<TValue> {\n const key = field as FieldTree<unknown>;\n const cached = cache.get(key);\n if (cached) return cached as ReadonlyFieldTree<TValue>;\n\n const view = toReadonlyFieldTree(field);\n cache.set(key, view as ReadonlyFieldTree<unknown>);\n return view;\n}\n","import { computed, inject, Signal } from '@angular/core';\nimport { FieldTree } from '@angular/forms/signals';\nimport { FormOptions, NextButtonOptions, SubmitButtonOptions } from '../../models/form-config';\nimport { LogicConfig, FormStateCondition, isFormStateCondition } from '../../models/logic';\nimport { ConditionalExpression } from '../../models/expressions';\nimport { evaluateCondition } from '../expressions';\nimport { FieldContextRegistryService } from '../registry/field-context-registry.service';\nimport { FunctionRegistryService } from '../registry/function-registry.service';\nimport { ARRAY_CONTEXT } from '../../models/field-signal-context.token';\nimport type { EvaluationContext } from '../../models/expressions/evaluation-context';\nimport type { Logger } from '../../providers/features/logger/logger.interface';\n\n/**\n * No-op logger for when no logger is provided.\n * Used as fallback in button logic evaluation to avoid breaking downstream packages.\n */\nconst noOpLogger: Logger = {\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n debug: () => {},\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n info: () => {},\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n warn: () => {},\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n error: () => {},\n};\n\n/** Context for resolving button disabled state. */\nexport interface ButtonLogicContext {\n /** The form's FieldTree instance (supports both string and number keys for array indices) */\n form: FieldTree<unknown, string | number>;\n\n /** Form-level options */\n formOptions?: FormOptions;\n\n /** Field-level logic array (if provided) */\n fieldLogic?: LogicConfig[];\n\n /** Explicit disabled state from field definition */\n explicitlyDisabled?: boolean;\n\n /** Current page validity signal (for paged forms) */\n currentPageValid?: Signal<boolean>;\n\n /** Current form value for evaluating conditional expressions */\n formValue?: unknown;\n\n /**\n * Optional factory returning the full evaluation context (including `externalData`\n * and `customFunctions`) used to evaluate `ConditionalExpression` conditions.\n * Invoked lazily during evaluation so external-data signal reads stay reactive.\n * When omitted, a minimal context (form value only) is used.\n */\n evaluationContext?: () => EvaluationContext;\n\n /** Optional logger for diagnostic output. Falls back to no-op logger if not provided. */\n logger?: Logger;\n}\n\n/**\n * Allowed logic types for non-form-bound elements (buttons, text fields, etc.).\n * These elements only support hidden and disabled states - not readonly or required\n * since they don't participate in form validation.\n */\nexport type NonFieldLogicType = 'hidden' | 'disabled';\n\n/**\n * Logic config restricted to types valid for non-form-bound elements.\n * This is a subset of LogicConfig that only includes hidden and disabled types.\n */\nexport type NonFieldLogicConfig = LogicConfig & { type: NonFieldLogicType };\n\n/** Context for resolving state (hidden/disabled) for non-form-bound elements. */\nexport interface NonFieldLogicContext {\n /** The form's FieldTree instance (supports both string and number keys for array indices) */\n form: FieldTree<unknown, string | number>;\n\n /**\n * Field-level logic array containing conditions.\n * Accepts full LogicConfig[] for compatibility, but only hidden and disabled types are processed.\n */\n fieldLogic?: LogicConfig[];\n\n /** Explicit state from field definition (e.g., `hidden: true` or `disabled: true`) */\n explicitValue?: boolean;\n\n /** Current form value for evaluating conditional expressions */\n formValue?: unknown;\n\n /**\n * Optional factory returning the full evaluation context (including `externalData`\n * and `customFunctions`) used to evaluate `ConditionalExpression` conditions.\n * Invoked lazily during evaluation so external-data signal reads stay reactive.\n * When omitted, a minimal context (form value only) is used.\n */\n evaluationContext?: () => EvaluationContext;\n\n /** Optional logger for diagnostic output. Falls back to no-op logger if not provided. */\n logger?: Logger;\n}\n\n/** Default options for submit button disabled behavior. */\nconst DEFAULT_SUBMIT_BUTTON_OPTIONS: Required<SubmitButtonOptions> = {\n disableWhenInvalid: true,\n disableWhileSubmitting: true,\n};\n\n/** Default options for next button disabled behavior. */\nconst DEFAULT_NEXT_BUTTON_OPTIONS: Required<NextButtonOptions> = {\n disableWhenPageInvalid: true,\n disableWhileSubmitting: true,\n};\n\n/**\n * Evaluates a FormStateCondition against the current form/page state.\n *\n * @param condition - The form state condition to evaluate\n * @param ctx - The button logic context\n * @returns true if the condition is met (button should be disabled)\n */\nfunction evaluateFormStateCondition(condition: FormStateCondition, ctx: ButtonLogicContext): boolean {\n const form = ctx.form();\n\n switch (condition) {\n case 'formInvalid':\n return !form.valid();\n\n case 'formSubmitting':\n return form.submitting();\n\n case 'pageInvalid':\n return ctx.currentPageValid ? !ctx.currentPageValid() : false;\n\n default:\n return false;\n }\n}\n\n/**\n * Evaluates a single logic condition (boolean, FormStateCondition, or ConditionalExpression).\n *\n * @param condition - The condition to evaluate\n * @param ctx - The button logic context\n * @returns true if the condition is met\n */\nfunction evaluateLogicCondition(condition: LogicConfig['condition'], ctx: ButtonLogicContext): boolean {\n // Boolean condition\n if (typeof condition === 'boolean') {\n return condition;\n }\n\n // FormStateCondition (string)\n if (isFormStateCondition(condition)) {\n return evaluateFormStateCondition(condition, ctx);\n }\n\n // ConditionalExpression. Prefer the full context (externalData + customFunctions) when the\n // caller supplies a factory, so container/button conditions match leaf-field scope. The\n // factory context carries its own formValue (the root form value), so ctx.formValue is\n // intentionally superseded here; a future scoped caller (array-item or page-scoped value)\n // must build that scope into the factory rather than rely on ctx.formValue.\n if (ctx.evaluationContext) {\n return evaluateCondition(condition as ConditionalExpression, ctx.evaluationContext());\n }\n\n // Legacy fallback: no factory supplied, so this context omits externalData/customFunctions.\n // javascript expressions referencing externalData and custom (functionName) conditions\n // cannot resolve on this path. Every production caller now passes a factory; this remains\n // only for boolean/form-state conditions and direct callers that need no expression scope.\n const formValue = (ctx.formValue ?? ctx.form().value()) as Record<string, unknown>;\n const evaluationContext = {\n fieldValue: undefined,\n formValue,\n fieldPath: '',\n logger: ctx.logger ?? noOpLogger,\n };\n\n return evaluateCondition(condition as ConditionalExpression, evaluationContext);\n}\n\n/**\n * Checks if any disabled logic condition is met in the field's logic array.\n *\n * @param fieldLogic - Array of logic configurations\n * @param ctx - The button logic context\n * @returns true if any disabled condition is met\n */\nfunction hasFieldLevelDisabledCondition(fieldLogic: LogicConfig[] | undefined, ctx: ButtonLogicContext): boolean {\n if (!fieldLogic || fieldLogic.length === 0) {\n return false;\n }\n\n return fieldLogic.filter((logic) => logic.type === 'disabled').some((logic) => evaluateLogicCondition(logic.condition, ctx));\n}\n\n/**\n * Checks if the field has custom logic of a specific type.\n *\n * @param fieldLogic - Array of logic configurations\n * @param logicType - The type of logic to check for ('hidden', 'disabled', 'readonly', 'required')\n * @returns true if field has logic of the specified type\n */\nfunction hasCustomLogicOfType(fieldLogic: LogicConfig[] | undefined, logicType: LogicConfig['type']): boolean {\n if (!fieldLogic || fieldLogic.length === 0) {\n return false;\n }\n\n return fieldLogic.some((logic) => logic.type === logicType);\n}\n\n/**\n * Evaluates all logic conditions of a specific type and returns true if any condition is met.\n *\n * @param fieldLogic - Array of logic configurations\n * @param logicType - The type of logic to evaluate ('hidden', 'disabled', 'readonly', 'required')\n * @param ctx - Context containing form and formValue for evaluation\n * @returns true if any condition of the specified type is met\n */\nfunction evaluateLogicOfType(\n fieldLogic: LogicConfig[] | undefined,\n logicType: LogicConfig['type'],\n ctx: { form: FieldTree<unknown, string | number>; formValue?: unknown; evaluationContext?: () => EvaluationContext; logger?: Logger },\n): boolean {\n if (!fieldLogic || fieldLogic.length === 0) {\n return false;\n }\n\n const buttonCtx: ButtonLogicContext = {\n form: ctx.form,\n formValue: ctx.formValue,\n evaluationContext: ctx.evaluationContext,\n logger: ctx.logger,\n };\n\n return fieldLogic.filter((logic) => logic.type === logicType).some((logic) => evaluateLogicCondition(logic.condition, buttonCtx));\n}\n\n/**\n * Resolves the disabled state for a submit button.\n *\n * @param ctx - The button logic context\n * @returns A computed signal that returns true when the button should be disabled\n */\nexport function resolveSubmitButtonDisabled(ctx: ButtonLogicContext): Signal<boolean> {\n return computed(() => {\n // 1. Explicit disabled always wins\n if (ctx.explicitlyDisabled) {\n return true;\n }\n\n // 2. If field has custom disabled logic, use it exclusively\n if (hasCustomLogicOfType(ctx.fieldLogic, 'disabled')) {\n return hasFieldLevelDisabledCondition(ctx.fieldLogic, ctx);\n }\n\n // 3. Apply form-level defaults\n const options = {\n ...DEFAULT_SUBMIT_BUTTON_OPTIONS,\n ...ctx.formOptions?.submitButton,\n };\n\n const form = ctx.form();\n\n if (options.disableWhenInvalid && !form.valid()) {\n return true;\n }\n\n if (options.disableWhileSubmitting && form.submitting()) {\n return true;\n }\n\n return false;\n });\n}\n\n/**\n * Resolves the disabled state for a next page button.\n *\n * @param ctx - The button logic context\n * @returns A computed signal that returns true when the button should be disabled\n */\nexport function resolveNextButtonDisabled(ctx: ButtonLogicContext): Signal<boolean> {\n return computed(() => {\n // 1. Explicit disabled always wins\n if (ctx.explicitlyDisabled) {\n return true;\n }\n\n // 2. If field has custom disabled logic, use it exclusively\n if (hasCustomLogicOfType(ctx.fieldLogic, 'disabled')) {\n return hasFieldLevelDisabledCondition(ctx.fieldLogic, ctx);\n }\n\n // 3. Apply form-level defaults\n const options = {\n ...DEFAULT_NEXT_BUTTON_OPTIONS,\n ...ctx.formOptions?.nextButton,\n };\n\n const form = ctx.form();\n\n if (options.disableWhenPageInvalid && ctx.currentPageValid && !ctx.currentPageValid()) {\n return true;\n }\n\n if (options.disableWhileSubmitting && form.submitting()) {\n return true;\n }\n\n return false;\n });\n}\n\n/**\n * Evaluates the hidden state for non-form-bound elements synchronously.\n *\n * @param ctx - The context containing form, logic array, and explicit hidden state\n * @returns true when the element should be hidden\n */\nexport function evaluateNonFieldHidden(ctx: NonFieldLogicContext): boolean {\n // 1. Explicit hidden always wins\n if (ctx.explicitValue) {\n return true;\n }\n\n // 2. Evaluate hidden logic conditions\n if (hasCustomLogicOfType(ctx.fieldLogic, 'hidden')) {\n return evaluateLogicOfType(ctx.fieldLogic, 'hidden', {\n form: ctx.form,\n formValue: ctx.formValue,\n evaluationContext: ctx.evaluationContext,\n logger: ctx.logger,\n });\n }\n\n // 3. No hidden conditions - element is visible\n return false;\n}\n\n/**\n * Resolves the hidden state for non-form-bound elements as a reactive signal.\n *\n * @param ctx - The context containing form, logic array, and explicit hidden state\n * @returns A computed signal that returns true when the element should be hidden\n */\nexport function resolveNonFieldHidden(ctx: NonFieldLogicContext): Signal<boolean> {\n return computed(() => evaluateNonFieldHidden(ctx));\n}\n\n/**\n * Evaluates the disabled state for non-form-bound elements synchronously.\n *\n * @param ctx - The context containing form, logic array, and explicit disabled state\n * @returns true when the element should be disabled\n */\nexport function evaluateNonFieldDisabled(ctx: NonFieldLogicContext): boolean {\n // 1. Explicit disabled always wins\n if (ctx.explicitValue) {\n return true;\n }\n\n // 2. Evaluate disabled logic conditions\n if (hasCustomLogicOfType(ctx.fieldLogic, 'disabled')) {\n return evaluateLogicOfType(ctx.fieldLogic, 'disabled', {\n form: ctx.form,\n formValue: ctx.formValue,\n evaluationContext: ctx.evaluationContext,\n logger: ctx.logger,\n });\n }\n\n // 3. No disabled conditions - element is enabled\n return false;\n}\n\n/**\n * Resolves the disabled state for non-form-bound elements as a reactive signal.\n *\n * @param ctx - The context containing form, logic array, and explicit disabled state\n * @returns A computed signal that returns true when the element should be disabled\n */\nexport function resolveNonFieldDisabled(ctx: NonFieldLogicContext): Signal<boolean> {\n return computed(() => evaluateNonFieldDisabled(ctx));\n}\n\n/**\n * Builds a lazy full evaluation-context factory (with `externalData` and `customFunctions`)\n * for a non-form-bound element. Call from a mapper factory body (injection context); the\n * returned thunk is invoked lazily inside the mapper/resolver computed so external-data\n * signal reads stay reactive. Single source for that factory, shared by the container/text\n * mappers (via `applyHiddenLogic`) and the button mappers.\n */\nexport function injectNonFieldEvaluationContext(\n fieldDef: { key?: string },\n options: { scopeToArrayItem?: boolean } = {},\n): () => EvaluationContext {\n const fieldContextRegistry = inject(FieldContextRegistryService);\n const functionRegistry = inject(FunctionRegistryService);\n // Only layout containers (grouping item fields) scope to the enclosing array item, matching\n // leaf fields. Buttons — including array-control buttons whose conditions reference the array\n // itself (e.g. `formValue.items.length`) — must keep root scope, so they opt out.\n const arrayContext = options.scopeToArrayItem ? inject(ARRAY_CONTEXT, { optional: true }) : null;\n return () => {\n const arrayScope = arrayContext\n ? { arrayKey: arrayContext.arrayKey, index: arrayContext.index(), localKey: fieldDef.key ?? '' }\n : undefined;\n return fieldContextRegistry.createDisplayOnlyContext(fieldDef.key ?? '', functionRegistry.getCustomFunctions(), arrayScope);\n };\n}\n","import type { AsyncCustomValidator, CustomValidator, HttpCustomValidator } from '../../core/validation/validator-types';\nimport { ConditionalExpression } from '../expressions/conditional-expression';\nimport { HttpRequestConfig } from '../http/http-request-config';\nimport { HttpValidationResponseMapping } from '../http/http-response-mapping';\n\n/** Base configuration shared by all validators */\nexport interface BaseValidatorConfig {\n /** Conditional logic for when validator applies */\n when?: ConditionalExpression;\n}\n\n/** Built-in validator configuration (required, email, min, max, etc.) */\nexport interface BuiltInValidatorConfig extends BaseValidatorConfig {\n /** Validator type identifier */\n type: 'required' | 'email' | 'min' | 'max' | 'minLength' | 'maxLength' | 'pattern';\n\n /** Static value for the validator (e.g., min value, pattern) */\n value?: number | string | RegExp;\n\n /** Dynamic value expression that evaluates to validator parameter */\n expression?: string;\n}\n\n/**\n * Custom validator configuration using Angular's public FieldContext API.\n * Returns ValidationError | ValidationError[] | null synchronously.\n */\nexport interface CustomValidatorConfig extends BaseValidatorConfig {\n /** Validator type identifier */\n type: 'custom';\n\n /** Name of registered validator function (function-based pattern) */\n functionName?: string;\n\n /**\n * Inline custom validator (code-only authoring).\n * Mutually exclusive with `functionName`. NOT JSON-serializable.\n */\n fn?: CustomValidator;\n\n /** Optional parameters to pass to validator function */\n params?: Record<string, unknown>;\n\n /** JavaScript expression to evaluate (expression-based pattern) */\n expression?: string;\n\n /** Error kind for expression-based validators - links to validationMessages */\n kind?: string;\n\n /**\n * Parameters to include in error object for message interpolation (expression-based pattern)\n * Map of parameter names to expressions that will be evaluated in the same context as the validation expression\n */\n errorParams?: Record<string, string>;\n}\n\n/**\n * Shared fields for both branches of {@link AsyncValidatorConfig}.\n *\n * @internal\n */\ninterface AsyncValidatorConfigShared extends BaseValidatorConfig {\n /** Validator type identifier. */\n type: 'async';\n /** Optional parameters to pass to validator function */\n params?: Record<string, unknown>;\n}\n\n/**\n * Async custom validator configuration using Angular's `validateAsync` API.\n * Returns Observable<ValidationError | ValidationError[] | null>.\n */\nexport type AsyncValidatorConfig =\n | (AsyncValidatorConfigShared & {\n /** Name of registered async validator function */\n functionName: string;\n /** Inline form is forbidden when `functionName` is set */\n fn?: never;\n })\n | (AsyncValidatorConfigShared & {\n /** Inline async validator. Mutually exclusive with `functionName`. */\n fn: AsyncCustomValidator;\n /** Registered form is forbidden when `fn` is set */\n functionName?: never;\n });\n\n/**\n * Shared fields for both branches of {@link FunctionHttpValidatorConfig}.\n *\n * @internal\n */\ninterface FunctionHttpValidatorConfigShared extends BaseValidatorConfig {\n /** Validator type identifier. */\n type: 'http';\n /** Optional parameters to pass to HTTP validator */\n params?: Record<string, unknown>;\n}\n\n/** Function-based HTTP validator configuration. Uses Angular's `validateHttp` API. */\nexport type FunctionHttpValidatorConfig =\n | (FunctionHttpValidatorConfigShared & {\n /** Name of registered HTTP validator configuration */\n functionName: string;\n /** Inline form is forbidden when `functionName` is set */\n fn?: never;\n })\n | (FunctionHttpValidatorConfigShared & {\n /** Inline HTTP validator. Mutually exclusive with `functionName`. */\n fn: HttpCustomValidator;\n /** Registered form is forbidden when `fn` is set */\n functionName?: never;\n });\n\n/** Declarative HTTP validator configuration — fully JSON-serializable, no function registration required. */\nexport interface DeclarativeHttpValidatorConfig extends BaseValidatorConfig {\n /** Validator type identifier */\n type: 'http';\n\n /** HTTP request configuration with expression-based query params and body */\n http: HttpRequestConfig;\n\n /** Mapping that interprets the HTTP response as a validation result */\n responseMapping: HttpValidationResponseMapping;\n}\n\n/**\n * Configuration for signal forms validator functions that can be serialized from API.\n * Discriminated union type for type-safe validator configuration.\n */\nexport type ValidatorConfig =\n | BuiltInValidatorConfig\n | CustomValidatorConfig\n | AsyncValidatorConfig\n | FunctionHttpValidatorConfig\n | DeclarativeHttpValidatorConfig;\n\n/** Type guard to distinguish function-based HTTP validators from declarative ones. */\nexport function isFunctionHttpValidator(\n config: FunctionHttpValidatorConfig | DeclarativeHttpValidatorConfig,\n): config is FunctionHttpValidatorConfig {\n const c = config as { functionName?: unknown; fn?: unknown };\n return typeof c.functionName === 'string' || (c.fn !== null && typeof c.fn === 'object');\n}\n","import { ValidationError } from '@angular/forms/signals';\nimport { HttpValidationResponseMapping } from '../../models/http/http-response-mapping';\nimport { Logger } from '../../providers/features/logger/logger.interface';\nimport { ExpressionParser } from '../expressions/parser/expression-parser';\n\n/** Evaluates an HTTP response against a `HttpValidationResponseMapping` to produce a validation result. */\nexport function evaluateHttpValidationResponse(\n response: unknown,\n mapping: HttpValidationResponseMapping,\n logger: Logger,\n): ValidationError | null {\n const scope = { response };\n\n try {\n const result = ExpressionParser.evaluate(mapping.validWhen, scope);\n\n if (typeof result !== 'boolean') {\n logger.warn(`validWhen expression \"${mapping.validWhen}\" returned non-boolean value:`, result, 'Expected true or false.');\n }\n\n if (result === true) {\n return null;\n }\n\n const error: ValidationError & Record<string, unknown> = { kind: mapping.errorKind };\n\n if (mapping.errorParams) {\n for (const [key, expression] of Object.entries(mapping.errorParams)) {\n try {\n error[key] = ExpressionParser.evaluate(expression, scope);\n } catch (err) {\n logger.warn(`Error evaluating errorParam \"${key}\":`, expression, err);\n }\n }\n }\n\n return error;\n } catch (err) {\n logger.warn('Error evaluating validWhen expression:', mapping.validWhen, err);\n return { kind: mapping.errorKind };\n }\n}\n","import { LogicFn } from '@angular/forms/signals';\n\n/** DI-scoped cache for dynamic value functions. */\nexport class DynamicValueFunctionCacheService {\n /** Cache for memoized dynamic value functions, keyed by expression string. */\n readonly dynamicValueFunctionCache = new Map<string, LogicFn<unknown, unknown>>();\n}\n","import { inject } from '@angular/core';\nimport { FieldContext, LogicFn } from '@angular/forms/signals';\nimport { FieldContextRegistryService } from '../registry/field-context-registry.service';\nimport { ExpressionParser } from '../expressions/parser/expression-parser';\nimport { DynamicFormLogger } from '../../providers/features/logger/logger.token';\nimport { DynamicValueFunctionCacheService } from './dynamic-value-function-cache.service';\n\n/**\n * Create a dynamic value function from an expression string.\n * Uses secure AST-based parsing.\n *\n * @param expression The expression string to evaluate\n * @returns A LogicFn that evaluates the expression in the context of a field\n */\nexport function createDynamicValueFunction<TValue, TReturn>(expression: string): LogicFn<TValue, TReturn> {\n // Inject services during factory creation, not during execution\n // This captures the service instance in the closure\n const fieldContextRegistry = inject(FieldContextRegistryService);\n const logger = inject(DynamicFormLogger);\n const cacheService = inject(DynamicValueFunctionCacheService);\n\n // Check cache first\n const cached = cacheService.dynamicValueFunctionCache.get(expression);\n if (cached) {\n return cached as LogicFn<TValue, TReturn>;\n }\n\n const fn: LogicFn<TValue, TReturn> = (ctx: FieldContext<TValue>) => {\n // Create evaluation context using the registry-based approach\n const evaluationContext = fieldContextRegistry.createEvaluationContext(ctx);\n\n try {\n // Use secure AST-based expression parser (already has LRU cache)\n return ExpressionParser.evaluate(expression, evaluationContext) as TReturn;\n } catch (error) {\n logger.error('Error evaluating dynamic expression:', expression, error);\n return undefined as TReturn;\n }\n };\n\n // Cache the function\n cacheService.dynamicValueFunctionCache.set(expression, fn as LogicFn<unknown, unknown>);\n return fn;\n}\n","import type { SchemaPath, SchemaPathTree } from '@angular/forms/signals';\nimport {\n email,\n FieldContext,\n LogicFn,\n max,\n maxLength,\n min,\n minLength,\n pattern,\n required,\n validate,\n validateAsync,\n validateHttp,\n ValidationError,\n} from '@angular/forms/signals';\nimport { inject } from '@angular/core';\nimport { DynamicFormLogger } from '../../providers/features/logger/logger.token';\nimport { DynamicFormError } from '../../errors/dynamic-form-error';\nimport {\n AsyncValidatorConfig,\n CustomValidatorConfig,\n DeclarativeHttpValidatorConfig,\n FunctionHttpValidatorConfig,\n isFunctionHttpValidator,\n ValidatorConfig,\n} from '../../models/validation/validator-config';\nimport { resolveHttpRequest } from '../http/http-request-resolver';\nimport { evaluateHttpValidationResponse } from '../http/http-response-evaluator';\nimport { createLogicFunction } from '../expressions/logic-function-factory';\nimport { createDynamicValueFunction } from '../values/dynamic-value-factory';\nimport { ConditionalExpression } from '../../models/expressions/conditional-expression';\nimport { FunctionRegistryService } from '../registry/function-registry.service';\nimport { FieldContextRegistryService } from '../registry/field-context-registry.service';\nimport { ExpressionParser } from '../expressions/parser/expression-parser';\nimport {\n isCrossFieldValidator,\n isCrossFieldBuiltInValidator,\n hasCrossFieldWhenCondition,\n isResourceBasedValidator,\n} from '../cross-field/cross-field-detector';\n\nfunction applyEmailValidator(path: SchemaPath<string>): void {\n email(path);\n}\n\nfunction applyMinValidator(path: SchemaPath<number>, value: number, expression?: string): void {\n if (expression) {\n min(path, createDynamicValueFunction<string | number | null, number | undefined>(expression));\n } else {\n min(path, value);\n }\n}\n\nfunction applyMaxValidator(path: SchemaPath<number>, value: number, expression?: string): void {\n if (expression) {\n max(path, createDynamicValueFunction<string | number | null, number | undefined>(expression));\n } else {\n max(path, value);\n }\n}\n\nfunction applyMinLengthValidator(path: SchemaPath<string>, value: number, expression?: string): void {\n if (expression) {\n minLength(path, createDynamicValueFunction<string, number>(expression));\n } else {\n minLength(path, value);\n }\n}\n\nfunction applyMaxLengthValidator(path: SchemaPath<string>, value: number, expression?: string): void {\n if (expression) {\n maxLength(path, createDynamicValueFunction<string, number>(expression));\n } else {\n maxLength(path, value);\n }\n}\n\nfunction applyPatternValidator(path: SchemaPath<string>, value: RegExp, expression?: string): void {\n if (expression) {\n pattern(\n path,\n createDynamicValueFunction<string | undefined, RegExp | undefined>(expression) as LogicFn<string | undefined, RegExp | undefined>,\n );\n } else {\n pattern(path, value);\n }\n}\n\nfunction createConditionalLogic(when: ConditionalExpression | undefined): LogicFn<unknown, boolean> | undefined {\n return when ? (createLogicFunction(when) as LogicFn<unknown, boolean>) : undefined;\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Dynamic forms require any at the Angular API boundary\nexport function applyValidator(config: ValidatorConfig, fieldPath: SchemaPath<any> | SchemaPathTree<any>): void {\n const path = fieldPath as SchemaPath<unknown>;\n\n if (!isResourceBasedValidator(config) && (isCrossFieldBuiltInValidator(config) || hasCrossFieldWhenCondition(config))) {\n return;\n }\n\n switch (config.type) {\n case 'required':\n if (config.when) {\n required(path, { when: createLogicFunction(config.when) });\n } else {\n required(path);\n }\n break;\n case 'email':\n applyEmailValidator(fieldPath as SchemaPath<string>);\n break;\n case 'min':\n if (typeof config.value === 'number') {\n applyMinValidator(fieldPath as SchemaPath<number>, config.value, config.expression);\n }\n break;\n case 'max':\n if (typeof config.value === 'number') {\n applyMaxValidator(fieldPath as SchemaPath<number>, config.value, config.expression);\n }\n break;\n case 'minLength':\n if (typeof config.value === 'number') {\n applyMinLengthValidator(fieldPath as SchemaPath<string>, config.value, config.expression);\n }\n break;\n case 'maxLength':\n if (typeof config.value === 'number') {\n applyMaxLengthValidator(fieldPath as SchemaPath<string>, config.value, config.expression);\n }\n break;\n case 'pattern':\n if (config.value instanceof RegExp || typeof config.value === 'string') {\n let regexPattern: RegExp;\n if (typeof config.value === 'string') {\n try {\n regexPattern = new RegExp(config.value);\n } catch (e) {\n throw new DynamicFormError(\n `Invalid regex pattern in validator: '${config.value}' — ${e instanceof Error ? e.message : String(e)}`,\n );\n }\n } else {\n regexPattern = config.value;\n }\n applyPatternValidator(fieldPath as SchemaPath<string>, regexPattern, config.expression);\n }\n break;\n case 'custom':\n applyCustomValidator(config, path);\n break;\n case 'async':\n applyAsyncValidator(config, path);\n break;\n case 'http':\n applyUnifiedHttpValidator(config, path);\n break;\n }\n}\n\nfunction applyCustomValidator(config: CustomValidatorConfig, fieldPath: SchemaPath<unknown>): void {\n if (isCrossFieldValidator(config)) {\n return;\n }\n\n let validatorFn: (ctx: FieldContext<unknown>) => ValidationError | ValidationError[] | null;\n\n if (config.expression) {\n validatorFn = createExpressionValidator(config);\n } else if (config.fn || config.functionName) {\n validatorFn = createFunctionValidator(config);\n } else {\n throw new DynamicFormError('Custom validator requires one of \"expression\", \"functionName\", or \"fn\".');\n }\n\n const whenLogic = createConditionalLogic(config.when);\n validate(fieldPath, (ctx: FieldContext<unknown>) => {\n if (whenLogic && !whenLogic(ctx)) return null;\n return validatorFn(ctx);\n });\n}\n\nfunction createFunctionValidator(\n config: CustomValidatorConfig,\n): (ctx: FieldContext<unknown>) => ValidationError | ValidationError[] | null {\n const logger = inject(DynamicFormLogger);\n\n if (config.fn && config.functionName) {\n logger.warn('Both \"fn\" and \"functionName\" are set on custom validator. Inline \"fn\" takes precedence; \"functionName\" is ignored.');\n }\n\n if (config.fn) {\n const inlineFn = config.fn;\n return (ctx: FieldContext<unknown>) => inlineFn(ctx, config.params);\n }\n\n const functionName = config.functionName;\n if (!functionName) {\n throw new DynamicFormError('Custom validator requires one of \"functionName\" or \"fn\".');\n }\n\n const registry = inject(FunctionRegistryService);\n const validatorFn = registry.getValidator(functionName);\n\n if (!validatorFn) {\n throw new DynamicFormError(`Custom validator \"${functionName}\" not found. Register it with customFnConfig.validators.`);\n }\n\n return (ctx: FieldContext<unknown>) => validatorFn(ctx, config.params);\n}\n\nfunction createExpressionValidator(\n config: CustomValidatorConfig,\n): (ctx: FieldContext<unknown>) => ValidationError | ValidationError[] | null {\n const logger = inject(DynamicFormLogger);\n const expression = config.expression;\n if (!expression) {\n throw new DynamicFormError('Custom validator requires a non-empty \"expression\".');\n }\n\n const fieldContextRegistry = inject(FieldContextRegistryService);\n const functionRegistry = inject(FunctionRegistryService);\n\n return (ctx: FieldContext<unknown>) => {\n try {\n const evaluationContext = fieldContextRegistry.createEvaluationContext(ctx, functionRegistry.getCustomFunctions());\n const result = ExpressionParser.evaluate(expression, evaluationContext);\n\n if (result) {\n return null;\n }\n\n const kind = config.kind || 'custom';\n const validationError: ValidationError & Record<string, unknown> = { kind };\n\n if (config.errorParams) {\n Object.entries(config.errorParams).forEach(([key, expression]) => {\n try {\n validationError[key] = ExpressionParser.evaluate(expression, evaluationContext);\n } catch (err) {\n logger.warn(`Error evaluating errorParam \"${key}\":`, expression, err);\n }\n });\n }\n\n return validationError;\n } catch (error) {\n // Gracefully degrade on errors (e.g., typos in field names, undefined functions)\n // Log for debugging while keeping form functional\n logger.error('Error evaluating custom validator expression:', expression, error);\n return { kind: config.kind || 'custom' };\n }\n };\n}\n\n/** Apply async validator to field path using Angular's public validateAsync() API */\nfunction applyAsyncValidator(config: AsyncValidatorConfig, fieldPath: SchemaPath<unknown>): void {\n if (config.fn && config.functionName) {\n const logger = inject(DynamicFormLogger);\n logger.warn('Both \"fn\" and \"functionName\" are set on async validator. Inline \"fn\" takes precedence; \"functionName\" is ignored.');\n }\n\n let validatorConfig: ReturnType<FunctionRegistryService['getAsyncValidator']>;\n if (config.fn) {\n validatorConfig = config.fn;\n } else if (config.functionName) {\n const registry = inject(FunctionRegistryService);\n validatorConfig = registry.getAsyncValidator(config.functionName);\n if (!validatorConfig) {\n throw new DynamicFormError(`Async validator \"${config.functionName}\" not found. Register it with customFnConfig.asyncValidators.`);\n }\n } else {\n // XOR type rejects this at compile time; JSON-loaded configs can still reach here at runtime.\n throw new DynamicFormError('Async validator requires one of \"functionName\" or \"fn\".');\n }\n\n const whenLogic = createConditionalLogic(config.when);\n const asyncOptions = {\n params: (ctx: FieldContext<unknown>) => {\n if (whenLogic && !whenLogic(ctx)) return undefined;\n return validatorConfig.params(ctx, config.params);\n },\n factory: validatorConfig.factory,\n onSuccess: validatorConfig.onSuccess,\n onError: validatorConfig.onError,\n };\n\n validateAsync(fieldPath, asyncOptions as Parameters<typeof validateAsync>[1]);\n}\n\n/**\n * Unified handler for `type: 'http'` — discriminates between function-based and declarative\n * based on property presence.\n */\nfunction applyUnifiedHttpValidator(\n config: FunctionHttpValidatorConfig | DeclarativeHttpValidatorConfig,\n fieldPath: SchemaPath<unknown>,\n): void {\n if (isFunctionHttpValidator(config)) {\n applyFunctionHttpValidator(config, fieldPath);\n return;\n }\n // Declarative path requires both `http` and `responseMapping` — reject early\n // for JSON-loaded configs that have neither side fully populated. The\n // discriminated union forbids this at compile time.\n const declarative = config as DeclarativeHttpValidatorConfig;\n if (!declarative.http || !declarative.responseMapping) {\n throw new DynamicFormError(\n 'HTTP validator requires one of \"functionName\" / \"fn\" (function-based) or \"http\" + \"responseMapping\" (declarative).',\n );\n }\n applyDeclarativeHttpValidator(config, fieldPath);\n}\n\n/** Apply function-based HTTP validator to field path using Angular's public validateHttp() API. */\nfunction applyFunctionHttpValidator(config: FunctionHttpValidatorConfig, fieldPath: SchemaPath<unknown>): void {\n if (config.fn && config.functionName) {\n const logger = inject(DynamicFormLogger);\n logger.warn('Both \"fn\" and \"functionName\" are set on HTTP validator. Inline \"fn\" takes precedence; \"functionName\" is ignored.');\n }\n\n let httpValidatorConfig: ReturnType<FunctionRegistryService['getHttpValidator']>;\n if (config.fn) {\n httpValidatorConfig = config.fn;\n } else if (config.functionName) {\n const registry = inject(FunctionRegistryService);\n httpValidatorConfig = registry.getHttpValidator(config.functionName);\n if (!httpValidatorConfig) {\n throw new DynamicFormError(`HTTP validator \"${config.functionName}\" not found. Register it with customFnConfig.httpValidators.`);\n }\n } else {\n // XOR type rejects this at compile time; JSON-loaded configs can still reach here at runtime.\n throw new DynamicFormError('HTTP validator requires one of \"functionName\" or \"fn\".');\n }\n\n const whenLogic = createConditionalLogic(config.when);\n const httpOptions = {\n request: (ctx: FieldContext<unknown>) => {\n if (whenLogic && !whenLogic(ctx)) return undefined;\n return httpValidatorConfig.request(ctx, config.params);\n },\n onSuccess: httpValidatorConfig.onSuccess,\n onError: httpValidatorConfig.onError,\n };\n\n validateHttp(fieldPath, httpOptions as Parameters<typeof validateHttp>[1]);\n}\n\n/** Apply declarative HTTP validator — fully JSON-serializable, no function registration needed. */\nfunction applyDeclarativeHttpValidator(config: DeclarativeHttpValidatorConfig, fieldPath: SchemaPath<unknown>): void {\n const fieldContextRegistry = inject(FieldContextRegistryService);\n const functionRegistry = inject(FunctionRegistryService);\n const logger = inject(DynamicFormLogger);\n const whenLogic = createConditionalLogic(config.when);\n\n validateHttp(fieldPath, {\n request: (ctx: FieldContext<unknown>) => {\n if (whenLogic && !whenLogic(ctx)) return undefined;\n // Use createReactiveEvaluationContext (not createEvaluationContext) because\n // validateHttp's request runs inside Angular's resource API — it NEEDS reactive\n // dependencies to re-trigger when the field value changes.\n const evalCtx = fieldContextRegistry.createReactiveEvaluationContext(ctx, functionRegistry.getCustomFunctions());\n // resolveHttpRequest returns null when a path param is undefined — convert to undefined to skip validation\n return resolveHttpRequest(config.http, evalCtx) ?? undefined;\n },\n onSuccess: (response: unknown) => {\n return evaluateHttpValidationResponse(response, config.responseMapping, logger);\n },\n onError: (error: unknown) => {\n logger.warn('HTTP validator request failed:', error);\n return { kind: config.responseMapping.errorKind };\n },\n } as Parameters<typeof validateHttp>[1]);\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Dynamic forms require any at the Angular API boundary\nexport function applyValidators(configs: ValidatorConfig[], fieldPath: SchemaPath<any> | SchemaPathTree<any>): void {\n configs.forEach((config) => applyValidator(config, fieldPath));\n}\n","import { FieldDef } from './field-def';\nimport { FieldMeta } from './field-meta';\nimport { FieldWithValidation } from './field-with-validation';\nimport { WithInputSignals } from '../../models/component-type';\nimport { Prettify } from '../../models/prettify';\nimport { DynamicText } from '../../models/types/dynamic-text';\n\nexport interface BaseCheckedField<TProps, TMeta extends FieldMeta = FieldMeta, TNullable extends boolean = false>\n extends FieldDef<TProps, TMeta>, FieldWithValidation {\n value?: TNullable extends true ? boolean | null : boolean;\n\n /**\n * Placeholder text displayed when the field is empty.\n * Supports static strings, Observables, and Signals for dynamic content.\n */\n placeholder?: DynamicText;\n\n required?: boolean;\n\n /**\n * Whether the field accepts `null` as a valid value.\n *\n * @default false\n */\n nullable?: TNullable;\n}\n\nexport function isCheckedField<TProps, TMeta extends FieldMeta = FieldMeta>(\n field: FieldDef<TProps, TMeta>,\n): field is BaseCheckedField<TProps, TMeta, boolean> {\n return field.type === 'checkbox' || field.type === 'toggle';\n}\n\n// Note: 'meta' is NOT excluded - components must handle meta attributes\ntype ExcludedKeys =\n | 'type'\n | 'conditionals'\n | 'value'\n | 'nullable'\n | 'disabled'\n | 'readonly'\n | 'hidden'\n | 'col'\n | keyof FieldWithValidation\n // Value exclusion config (submission-only, not component inputs)\n | 'excludeValueIfHidden'\n | 'excludeValueIfDisabled'\n | 'excludeValueIfReadonly'\n // Validation execution config (schema-only, not component inputs)\n | 'validateWhenHidden'\n // Wrappers are consumed by DfFieldOutlet / ContainerFieldComponent, not individual fields\n | 'wrappers'\n | 'skipAutoWrappers'\n | 'skipDefaultWrappers'\n // Addons are an opt-in input — checkbox/toggle-style fields are Tier 3 and don't render them\n | 'addons';\n\nexport type CheckedFieldComponent<T extends BaseCheckedField<Record<string, unknown> | unknown, FieldMeta, boolean>> = Prettify<\n WithInputSignals<Omit<T, ExcludedKeys>>\n>;\n","import { FieldDef } from './field-def';\nimport { FieldMeta } from './field-meta';\nimport { FieldWithValidation } from './field-with-validation';\nimport { WithInputSignals } from '../../models/component-type';\nimport { Prettify } from '../../models/prettify';\nimport { DynamicText } from '../../models/types/dynamic-text';\n\n/**\n * Supported primitive value types for form fields.\n * This type represents all possible value types that can be used in form fields.\n */\nexport type ValueType = string | number | boolean | Date | object | unknown[];\n\nexport interface BaseValueField<TProps, TValue, TMeta extends FieldMeta = FieldMeta, TNullable extends boolean = false>\n extends FieldDef<TProps, TMeta>, FieldWithValidation {\n value?: TNullable extends true ? TValue | null : TValue;\n\n /**\n * Placeholder text displayed when the field is empty.\n * Supports static strings, Observables, and Signals for dynamic content.\n */\n placeholder?: DynamicText;\n\n required?: boolean;\n\n /**\n * Whether the field accepts `null` as a valid value.\n *\n * @default false\n */\n nullable?: TNullable;\n}\n\nexport function isValueField<TProps, TMeta extends FieldMeta = FieldMeta>(\n field: FieldDef<TProps, TMeta>,\n): field is BaseValueField<TProps, ValueType, TMeta, boolean> {\n // `nullable: true` without an explicit `value` is still a value-bearing declaration:\n // the field resolves to `null` at runtime. Treating it as a value field keeps\n // downstream narrowing consistent with getFieldDefaultValue's behavior.\n return 'value' in field || 'nullable' in field;\n}\n\ntype ExcludedKeys =\n | 'type'\n | 'conditionals'\n | 'value'\n | 'valueType'\n | 'nullable'\n | 'disabled'\n | 'readonly'\n | 'hidden'\n | 'col'\n | 'minValue'\n | 'maxValue'\n | 'step'\n // Exclude validation config, but keep validationMessages for component use\n | 'required'\n | 'email'\n | 'min'\n | 'max'\n | 'minLength'\n | 'maxLength'\n | 'pattern'\n | 'validators'\n | 'logic'\n | 'derivation'\n | 'schemas'\n // Value exclusion config (submission-only, not component inputs)\n | 'excludeValueIfHidden'\n | 'excludeValueIfDisabled'\n | 'excludeValueIfReadonly'\n // Validation execution config (schema-only, not component inputs)\n | 'validateWhenHidden'\n // Wrappers are consumed by DfFieldOutlet / ContainerFieldComponent, not individual fields\n | 'wrappers'\n | 'skipAutoWrappers'\n | 'skipDefaultWrappers'\n // Addons are an opt-in input — fields that render them declare `addons` themselves\n // (Tier 1/2). Tier 3 field components (toggle, checkbox, radio, slider) ignore them.\n | 'addons';\n// Note: 'meta' is NOT excluded - components must handle meta attributes\n\nexport type ValueFieldComponent<T extends BaseValueField<Record<string, unknown> | unknown, unknown, FieldMeta, boolean>> = Prettify<\n WithInputSignals<Omit<T, ExcludedKeys>>\n>;\n","import { FieldComponent, FieldDef } from '../base/field-def';\nimport { ArrayAllowedChildren } from '../../models/types/nesting-constraints';\nimport { ContainerLogicConfig } from '../base/container-logic-config';\n\n/**\n * An array item template is an array of allowed children that defines one OBJECT array item's fields.\n * Used for object arrays where each item is `{ key1: value1, key2: value2, ... }`.\n */\nexport type ArrayItemTemplate = readonly ArrayAllowedChildren[];\n\n/**\n * An array item definition can be either:\n * - A single field (ArrayAllowedChildren) for PRIMITIVE array items - extracts field value directly\n * - An array of fields (ArrayItemTemplate) for OBJECT array items - merges fields into object\n */\nexport type ArrayItemDefinition = ArrayAllowedChildren | ArrayItemTemplate;\n\n/** Array field interface for creating dynamic field collections that map to array values. */\nexport interface ArrayField<TFields extends readonly ArrayItemDefinition[] = readonly ArrayItemDefinition[]> extends FieldDef<never> {\n type: 'array';\n\n /**\n * Array of item definitions. Each element defines one array item:\n * - Single FieldDef = primitive item (field's value is extracted directly)\n * - Array of FieldDefs = object item (fields are merged into an object)\n * - Empty array `[]` = no initial items (add via buttons with explicit templates)\n * - Items can have different configurations (heterogeneous arrays supported)\n * - Initial values are set via `value` property on each field definition\n */\n readonly fields: TFields;\n\n /** Array fields do not have a label property * */\n readonly label?: never;\n\n /** Arrays do not support meta - they have no native form element * */\n readonly meta?: never;\n\n /**\n * Logic configurations for conditional array visibility.\n * Only 'hidden' type logic is supported for arrays.\n */\n readonly logic?: ContainerLogicConfig[];\n\n /**\n * Minimum number of items required in the array.\n * Validation fails if the array has fewer items than this value.\n */\n readonly minLength?: number;\n\n /**\n * Maximum number of items allowed in the array.\n * Validation fails if the array has more items than this value.\n */\n readonly maxLength?: number;\n}\n\n/**\n * Type guard for ArrayField with proper type narrowing.\n * Validates that the field is an array type with a fields property that is an array.\n * Fields can contain either single FieldDefs (primitive items) or arrays of FieldDefs (object items).\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Type guard must accept any field type\nexport function isArrayField(field: FieldDef<any>): field is ArrayField {\n return field.type === 'array' && 'fields' in field && Array.isArray(field.fields);\n}\n\nexport type ArrayComponent<T extends ArrayItemTemplate[]> = FieldComponent<ArrayField<T>>;\n\n/** Configuration for auto-generated add/remove buttons in simplified array fields. */\nexport interface ArrayButtonConfig {\n /** Custom label for the button */\n readonly label?: string;\n /** Additional properties passed to the button component */\n readonly props?: Record<string, unknown>;\n}\n\n/** Simplified array field interface for common use cases. */\nexport interface SimplifiedArrayField extends FieldDef<never> {\n type: 'array';\n\n /**\n * Template defining the structure of a single array item.\n * - Single field (ArrayAllowedChildren) → primitive array (each item is a single value)\n * - Array of fields (readonly ArrayAllowedChildren[]) → object array (each item is an object)\n */\n readonly template: ArrayAllowedChildren | readonly ArrayAllowedChildren[];\n\n /** Initial values for the array. Each element creates one array item. */\n readonly value?: readonly unknown[];\n\n /**\n * Configuration for the auto-generated \"Add\" button, or `false` to disable it.\n * Defaults to a button with label \"Add\".\n */\n readonly addButton?: ArrayButtonConfig | false;\n\n /**\n * Configuration for the auto-generated \"Remove\" button on each item, or `false` to disable it.\n * Defaults to a button with label \"Remove\".\n */\n readonly removeButton?: ArrayButtonConfig | false;\n\n /** Simplified arrays do not support the label property */\n readonly label?: never;\n\n /** Simplified arrays do not support meta */\n readonly meta?: never;\n\n /**\n * Logic configurations for conditional array visibility.\n * Only 'hidden' type logic is supported for arrays.\n */\n readonly logic?: ContainerLogicConfig[];\n\n /**\n * Minimum number of items required in the array.\n * Validation fails if the array has fewer items than this value.\n */\n readonly minLength?: number;\n\n /**\n * Maximum number of items allowed in the array.\n * Validation fails if the array has more items than this value.\n */\n readonly maxLength?: number;\n\n /** Mutually exclusive with `template` — use `fields` for the full API instead */\n readonly fields?: never;\n}\n\n/**\n * Type guard for SimplifiedArrayField.\n * Checks for `type: 'array'` with a `template` property (discriminant from full ArrayField).\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Type guard must accept any field type\nexport function isSimplifiedArrayField(field: FieldDef<any>): field is SimplifiedArrayField {\n return field.type === 'array' && 'template' in field;\n}\n","import { FieldComponent, FieldDef } from '../base/field-def';\nimport { ContainerAllowedChildren } from '../../models/types/nesting-constraints';\nimport { ContainerLogicConfig } from '../base/container-logic-config';\nimport { WrapperConfig } from '../../models/wrapper-type';\n\n/** Container field interface for wrapping child fields with UI chrome. */\nexport interface ContainerField<\n TFields extends readonly ContainerAllowedChildren[] = readonly ContainerAllowedChildren[],\n TWrapperConfigs extends readonly WrapperConfig[] = readonly WrapperConfig[],\n> extends FieldDef<never> {\n type: 'container';\n\n /** Child definitions to render within this container */\n readonly fields: TFields;\n\n /**\n * Wrapper components to chain around the children.\n * Applied outermost-first: the first wrapper in the array is the outermost.\n * Each wrapper component receives the subsequent wrapper (or children) inside\n * its `#fieldComponent` ViewContainerRef slot.\n */\n readonly wrappers: TWrapperConfigs;\n\n /** Container fields do not have a label property */\n readonly label?: never;\n\n /** Containers do not support meta — they have no native form element */\n readonly meta?: never;\n\n /**\n * Logic configurations for conditional container visibility.\n * Only 'hidden' type logic is supported for containers.\n */\n readonly logic?: ContainerLogicConfig[];\n}\n\n/** Type guard for ContainerField with proper type narrowing. */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Type guard must accept any field type\nexport function isGenericContainerField(field: FieldDef<any>): field is ContainerField {\n return (\n field.type === 'container' &&\n 'wrappers' in field &&\n 'fields' in field &&\n Array.isArray((field as ContainerField).wrappers) &&\n Array.isArray((field as ContainerField).fields)\n );\n}\n\nexport type ContainerComponent = FieldComponent<ContainerField<ContainerAllowedChildren[]>>;\n","import { FieldComponent, FieldDef } from '../base/field-def';\nimport { GroupAllowedChildren } from '../../models/types/nesting-constraints';\nimport { ContainerLogicConfig } from '../base/container-logic-config';\n\n/**\n * Group field interface for creating logical field groupings that map to object values\n * Groups create nested form structures where child field values are collected into an object\n * This is a programmatic grouping only - users cannot customize this field type\n */\nexport interface GroupField<TFields extends readonly GroupAllowedChildren[] = readonly GroupAllowedChildren[]> extends FieldDef<never> {\n type: 'group';\n\n readonly fields: TFields;\n\n /** Groups do not have a label property - they are logical containers only * */\n readonly label?: never;\n\n /** Groups do not support meta - they have no native form element * */\n readonly meta?: never;\n\n /**\n * Logic configurations for conditional group visibility.\n * Only 'hidden' type logic is supported for groups.\n */\n readonly logic?: ContainerLogicConfig[];\n}\n\n/** Type guard for GroupField with proper type narrowing */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Type guard must accept any field type\nexport function isGroupField(field: FieldDef<any>): field is GroupField {\n return field.type === 'group' && 'fields' in field;\n}\n\nexport type GroupComponent<T extends GroupAllowedChildren[]> = FieldComponent<GroupField<T>>;\n","import { FieldDef } from '../base/field-def';\n\n/**\n * Scalar types supported by hidden fields.\n * These are the primitive value types that can be stored.\n */\nexport type HiddenScalar = string | number | boolean;\n\n/**\n * All value types supported by hidden fields.\n * Supports both scalar values and arrays of scalars for storing\n * multiple IDs or similar data.\n */\nexport type HiddenValue = HiddenScalar | HiddenScalar[];\n\n/**\n * Hidden field definition for storing values without rendering UI.\n *\n * @typeParam TValue - The type of value stored (must be HiddenValue compatible)\n */\nexport interface HiddenField<TValue extends HiddenValue = HiddenValue> extends FieldDef<never> {\n /** Discriminant for hidden field type */\n type: 'hidden';\n\n /**\n * The value to store in the form.\n * This is required - a hidden field without a value serves no purpose.\n */\n value: TValue;\n}\n\n/**\n * Type guard to check if a field is a hidden field.\n *\n * @param field - The field to check\n * @returns True if the field is a HiddenField\n */\nexport function isHiddenField(field: FieldDef<unknown>): field is HiddenField {\n return field.type === 'hidden';\n}\n","import { FieldComponent, FieldDef } from '../base/field-def';\nimport { RowAllowedChildren } from '../../models/types/nesting-constraints';\nimport { ContainerLogicConfig } from '../base/container-logic-config';\n\n/**\n * Row field interface for creating horizontal layouts.\n * A row is a synthetic field type that resolves to a Container at runtime,\n * with a synthesized `{ type: 'row' }` wrapper applied for layout. The row\n * itself does not hold a value — its children flatten into the parent form.\n */\nexport interface RowField<TFields extends readonly RowAllowedChildren[] = readonly RowAllowedChildren[]> extends FieldDef<never> {\n type: 'row';\n\n /** Child definitions to render within this row */\n readonly fields: TFields;\n\n /** Row fields do not have a label property * */\n readonly label?: never;\n\n /** Rows do not support meta - they have no native form element * */\n readonly meta?: never;\n\n /**\n * Logic configurations for conditional row visibility.\n * Only 'hidden' type logic is supported for rows.\n */\n readonly logic?: ContainerLogicConfig[];\n}\n\n/** Type guard for RowField with proper type narrowing */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Type guard must accept any field type\nexport function isRowField(field: FieldDef<any>): field is RowField {\n return field.type === 'row' && 'fields' in field && Array.isArray((field as RowField).fields);\n}\n\nexport type RowComponent = FieldComponent<RowField<RowAllowedChildren[]>>;\n","import { FieldDef } from '../base/field-def';\nimport { PageAllowedChildren } from '../../models/types/nesting-constraints';\nimport { isRowField } from './row-field';\nimport { isGroupField } from './group-field';\nimport { isGenericContainerField } from './container-field';\nimport { ContainerLogicConfig } from '../base/container-logic-config';\n\n/**\n * Page field interface for creating top-level page layouts\n * This is a special field type that contains other field definitions\n * The page itself doesn't have a value - it's a layout container like row\n * Pages can only be used at the top level and cannot be nested\n * This is a programmatic field type only - users cannot customize this field type\n */\nexport interface PageField<TFields extends readonly PageAllowedChildren[] = PageAllowedChildren[]> extends FieldDef<never> {\n type: 'page';\n\n /** Child field definitions to render within this page */\n readonly fields: TFields;\n\n /** Page fields do not have a label property * */\n readonly label?: never;\n\n /** Pages do not support meta - they have no native form element * */\n readonly meta?: never;\n\n /**\n * Logic configurations for conditional page visibility.\n * Only 'hidden' type logic is supported for pages.\n */\n readonly logic?: ContainerLogicConfig[];\n}\n\n/** Type guard for PageField with proper type narrowing */\nexport function isPageField(field: FieldDef<unknown>): field is PageField {\n return field.type === 'page' && 'fields' in field && Array.isArray((field as PageField).fields);\n}\n\n/**\n * Validates that a page field doesn't contain nested page fields\n *\n * @param pageField The page field to validate\n * @returns true if valid (no nested pages), false otherwise\n */\nexport function validatePageNesting(pageField: PageField): boolean {\n return !hasNestedPages(pageField.fields);\n}\n\n/** Type guard to check if a field is a container with fields property */\nfunction isContainerWithFields(field: FieldDef<unknown>): field is FieldDef<unknown> & { readonly fields: readonly FieldDef<unknown>[] } {\n return (\n (isRowField(field) || isGroupField(field) || isGenericContainerField(field)) &&\n 'fields' in field &&\n Array.isArray((field as { fields: unknown }).fields)\n );\n}\n\n/**\n * Recursively checks if fields contain any nested page fields\n *\n * @param fields Array of field definitions to check\n * @returns true if nested pages found, false otherwise\n */\nfunction hasNestedPages(fields: readonly FieldDef<unknown>[]): boolean {\n for (const field of fields) {\n if (isPageField(field)) {\n return true;\n }\n\n // Check row and group fields for nested pages\n if (isContainerWithFields(field)) {\n if (hasNestedPages(field.fields)) {\n return true;\n }\n }\n }\n return false;\n}\n","import { FormEvent } from '../interfaces/form-event';\n\nexport class ComponentInitializedEvent implements FormEvent {\n readonly type = 'component-initialized' as const;\n\n constructor(\n public componentType: 'dynamic-form' | 'page' | 'group' | 'array' | 'container',\n public componentId: string,\n ) {}\n}\n","import { InjectionToken } from '@angular/core';\n\n/**\n * Injection token for globally enabling form value emission on events.\n *\n * @internal\n */\nexport const EMIT_FORM_VALUE_ON_EVENTS = new InjectionToken<boolean>('EMIT_FORM_VALUE_ON_EVENTS', {\n providedIn: 'root',\n factory: () => false,\n});\n","import type { Logger } from './logger.interface';\n\n/**\n * No-operation logger implementation.\n * All methods are no-ops - used in production by default.\n */\nexport class NoopLogger implements Logger {\n debug(): void {\n // Intentionally empty\n }\n\n info(): void {\n // Intentionally empty\n }\n\n warn(): void {\n // Intentionally empty\n }\n\n error(): void {\n // Intentionally empty\n }\n}\n","import { inject, Injectable, InjectionToken, Signal, Type } from '@angular/core';\nimport { filter, Observable, Subject } from 'rxjs';\nimport { FormEvent, FormEventConstructor } from './interfaces/form-event';\nimport { EMIT_FORM_VALUE_ON_EVENTS } from '../providers/features/event-form-value/emit-form-value.token';\nimport { RootFormRegistryService } from '../core/registry/root-form-registry.service';\nimport { FORM_OPTIONS } from '../models/field-signal-context.token';\nimport { FormOptions } from '../models/form-config';\nimport { DynamicFormLogger } from '../providers/features/logger/logger.token';\nimport type { Logger } from '../providers/features/logger/logger.interface';\nimport { NoopLogger } from '../providers/features/logger/noop-logger';\n\n/**\n * Safely attempts to inject a dependency using an InjectionToken.\n * Returns the default value if called outside an injection context.\n */\nfunction safeInjectToken<T>(token: InjectionToken<T>, defaultValue: T): T {\n try {\n return inject(token, { optional: true }) ?? defaultValue;\n } catch {\n return defaultValue;\n }\n}\n\n/**\n * Safely attempts to inject a service class.\n * Returns the default value if called outside an injection context.\n */\nfunction safeInjectClass<T>(token: Type<T>, defaultValue: T | null): T | null {\n try {\n return inject(token, { optional: true }) ?? defaultValue;\n } catch {\n return defaultValue;\n }\n}\n\n/**\n * Creates a copy of an event with an additional formValue property.\n * Preserves the event's prototype chain so instanceof checks still work.\n */\nfunction attachFormValue<T extends FormEvent>(event: T, formValue: Record<string, unknown>): T & { formValue: unknown } {\n const prototype = Object.getPrototypeOf(event);\n return Object.assign(Object.create(prototype), event, { formValue });\n}\n\n/**\n * Event bus for form-wide event communication between field components.\n *\n * @see EventDispatcher — for dispatching events from outside the form\n */\n\nconst FALLBACK_LOGGER = new NoopLogger();\n\n@Injectable()\nexport class EventBus {\n private readonly pipeline$ = new Subject<FormEvent>();\n private readonly globalEmitFormValue = safeInjectToken(EMIT_FORM_VALUE_ON_EVENTS, false);\n private readonly rootFormRegistry = safeInjectClass(RootFormRegistryService, null);\n private readonly formOptions = safeInjectToken<Signal<FormOptions | undefined> | null>(FORM_OPTIONS, null);\n private readonly logger = safeInjectToken<Logger>(DynamicFormLogger, FALLBACK_LOGGER);\n\n events$ = this.pipeline$.asObservable();\n\n /**\n * Dispatches a pre-created event instance directly.\n *\n * @param event - A FormEvent instance to dispatch\n */\n dispatch(event: FormEvent): void;\n /**\n * Dispatches an event to all subscribers by instantiating the provided constructor.\n *\n * @param eventConstructor - Constructor function for the event to dispatch\n * @param args - Arguments to pass to the event constructor\n */\n // TypeScript limitation: Must use ConstructorParameters which relies on `any` in FormEventConstructor\n dispatch<T extends FormEventConstructor>(eventConstructor: T, ...args: ConstructorParameters<T>): void;\n dispatch<T extends FormEventConstructor>(eventOrConstructor: FormEvent | T, ...args: ConstructorParameters<T>): void {\n if (typeof eventOrConstructor !== 'function' && 'type' in eventOrConstructor) {\n this.emit(eventOrConstructor);\n } else {\n this.emit(new (eventOrConstructor as T)(...args));\n }\n }\n\n /**\n * Dispatches a pre-created event instance directly.\n * Used internally by EventDispatcher to forward events into the bus.\n *\n * @internal\n */\n emitInstance(event: FormEvent): void {\n this.emit(event);\n }\n\n /**\n * Shared emit path for both dispatch() and emitInstance().\n * Attaches form value if configured, then pushes to the pipeline.\n */\n private emit(event: FormEvent): void {\n if (this.shouldEmitFormValue()) {\n const formValue = this.rootFormRegistry?.formValue();\n // Only attach if form value exists and has at least one field.\n // Empty objects {} are not attached - use hasFormValue() to check.\n if (formValue && Object.keys(formValue).length > 0) {\n this.safeEmit(attachFormValue(event, formValue));\n return;\n }\n }\n\n this.safeEmit(event);\n }\n\n /**\n * Emits an event through the pipeline. Catches any synchronous exception that escapes\n * RxJS's own error handling so dispatch() callers are never disrupted by a failing subscriber.\n */\n private safeEmit(event: FormEvent): void {\n try {\n this.pipeline$.next(event);\n } catch (err: unknown) {\n this.logger.error('Exception in EventBus subscriber during dispatch', err);\n }\n }\n\n /** Determines whether form value should be attached to events. */\n private shouldEmitFormValue(): boolean {\n const formLevelSetting = this.formOptions?.()?.emitFormValueOnEvents;\n return formLevelSetting ?? this.globalEmitFormValue;\n }\n\n /**\n * Subscribes to events of a specific type.\n *\n * @param eventType - The type of event to subscribe to\n * @returns Observable that emits events of the specified type\n */\n on<T extends FormEvent>(eventType: T['type']): Observable<T>;\n /**\n * Subscribes to events of multiple types.\n *\n * @param eventType - Array of event types to subscribe to\n * @returns Observable that emits events matching any of the specified types\n */\n on<T extends FormEvent>(eventType: Array<T['type']>): Observable<T>;\n /**\n * Subscribes to form events with type-safe filtering.\n *\n * @param eventType - Event type string or array of event type strings to filter by\n * @returns Observable stream of filtered events\n */\n on<T extends FormEvent>(eventType: T['type'] | Array<T['type']>): Observable<T> {\n if (Array.isArray(eventType)) {\n return this.pipeline$.pipe(filter((event): event is T => eventType.includes(event.type)));\n }\n\n return this.pipeline$.pipe(filter((event): event is T => event.type === eventType));\n }\n}\n","/** Base interface for all form events in the dynamic form system. */\nexport interface FormEvent {\n /** Unique identifier for the event type used for filtering and subscriptions */\n readonly type: string;\n /**\n * Optional form value attached to the event when `withEventFormValue()` is enabled\n * or `options.emitFormValueOnEvents` is set to `true` in the form config.\n */\n readonly formValue?: unknown;\n}\n\n/**\n * Type guard to check if a form event has a form value attached.\n *\n * @param event - The form event to check\n * @returns `true` if the event has a form value attached, narrowing the type\n */\nexport function hasFormValue<T extends FormEvent>(event: T): event is T & { formValue: unknown } {\n return 'formValue' in event && event.formValue !== undefined;\n}\n\n/**\n * Constructor type for form event classes.\n *\n * @typeParam T - The form event type being constructed\n */\n// TypeScript limitation: Constructor types with varying parameter signatures require `any[]`\n// because `unknown[]` cannot be assigned from more specific parameter types (contravariance).\n// Using a generic TArgs parameter here would require changes throughout the event system.\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type FormEventConstructor<T extends FormEvent = FormEvent> = new (...args: any[]) => T;\n","/** Context for token resolution in event args */\nexport interface TokenContext {\n /** Current field key */\n key?: string;\n /** Array index if field is inside an array */\n index?: number;\n /** Parent array field key if field is inside an array */\n arrayKey?: string;\n /** Current form value for complex indexing */\n formValue?: unknown;\n /** Template for array item creation */\n template?: unknown;\n}\n\n/**\n * Resolves special tokens in event arguments to their actual values\n *\n * @param args - Array of arguments that may contain tokens\n * @param context - Context object containing token values\n * @returns Array with resolved values\n */\nexport function resolveTokens(\n args: readonly (string | number | boolean | null | undefined)[],\n context: TokenContext,\n): (string | number | boolean | null | undefined | unknown)[] {\n return args.map((arg) => {\n // Only process string tokens\n if (typeof arg !== 'string') {\n return arg;\n }\n\n // Check for token patterns\n switch (arg) {\n case '$key':\n return context.key;\n case '$index':\n return context.index;\n case '$arrayKey':\n return context.arrayKey;\n case '$template':\n return context.template;\n case 'formValue':\n return context.formValue;\n default:\n // Return as-is if not a recognized token\n return arg;\n }\n });\n}\n","import { FieldDef } from '../../definitions/base/field-def';\n\n/**\n * Generates CSS class string for responsive grid layout based on field column configuration.\n *\n * @param fieldDef - Field definition containing column configuration\n * @returns CSS class string for grid layout, empty if no valid column configuration\n */\nexport function getGridClassString(fieldDef: FieldDef<unknown>): string {\n const col = fieldDef.col;\n if (typeof col === 'number' && Number.isInteger(col) && col > 0 && col <= 12) {\n return `df-col-${col}`;\n }\n return '';\n}\n\n/**\n * Builds a combined className string from a field definition.\n *\n * @param fieldDef - Field definition containing className and col properties\n * @returns Combined class string, or undefined if no classes\n */\nexport function buildClassName(fieldDef: FieldDef<unknown>): string | undefined {\n const gridClass = getGridClassString(fieldDef);\n const userClass = fieldDef.className;\n\n const classes: string[] = [];\n\n if (gridClass) {\n classes.push(gridClass);\n }\n\n if (userClass) {\n classes.push(userClass);\n }\n\n return classes.length > 0 ? classes.join(' ') : undefined;\n}\n","import { RootFormRegistryService } from '../core/registry/root-form-registry.service';\nimport { evaluateNonFieldHidden } from '../core/logic/non-field-logic-resolver';\nimport { ContainerLogicConfig } from '../definitions/base/container-logic-config';\nimport { LogicConfig } from '../models/logic';\nimport type { EvaluationContext } from '../models/expressions/evaluation-context';\n\ninterface FieldWithHiddenLogic {\n hidden?: boolean;\n logic?: readonly (ContainerLogicConfig | LogicConfig)[];\n}\n\n/**\n * Applies hidden logic to a mapper's input record.\n *\n * @param inputs The mutable input record to potentially add `hidden` to\n * @param fieldDef The field definition containing `hidden` and `logic`\n * @param rootFormRegistry The root form registry for accessing form state\n * @param evaluationContext Full evaluation-context factory (externalData + customFunctions),\n * built by the mapper via `injectNonFieldEvaluationContext` and invoked lazily here so\n * external-data signal reads stay reactive. Mirrors the page path.\n */\nexport function applyHiddenLogic(\n inputs: Record<string, unknown>,\n fieldDef: FieldWithHiddenLogic,\n rootFormRegistry: RootFormRegistryService,\n evaluationContext?: () => EvaluationContext,\n): void {\n const rootForm = rootFormRegistry.rootForm();\n if (rootForm && (fieldDef.hidden === true || fieldDef.logic?.some((l) => l.type === 'hidden'))) {\n // Cast is safe: evaluateNonFieldHidden only reads the array, never mutates it.\n // The readonly + union type from container/leaf field definitions is compatible at runtime.\n inputs['hidden'] = evaluateNonFieldHidden({\n form: rootForm,\n fieldLogic: fieldDef.logic as LogicConfig[] | undefined,\n explicitValue: fieldDef.hidden,\n formValue: rootFormRegistry.formValue(),\n evaluationContext,\n });\n }\n}\n","import { computed, inject, Signal } from '@angular/core';\nimport { ArrayField } from '../../definitions/default/array-field';\nimport { buildClassName } from '../../utils/grid-classes/grid-classes';\nimport { RootFormRegistryService } from '../../core/registry/root-form-registry.service';\nimport { injectNonFieldEvaluationContext } from '../../core/logic/non-field-logic-resolver';\nimport { applyHiddenLogic } from '../apply-hidden-logic';\n\n/**\n * Maps an array field definition to component inputs.\n *\n * @param fieldDef The array field definition\n * @returns Signal containing Record of input names to values for ngComponentOutlet\n */\nexport function arrayFieldMapper(fieldDef: ArrayField): Signal<Record<string, unknown>> {\n const rootFormRegistry = inject(RootFormRegistryService);\n const evaluationContext = injectNonFieldEvaluationContext(fieldDef, { scopeToArrayItem: true });\n const className = buildClassName(fieldDef);\n\n return computed(() => {\n const inputs: Record<string, unknown> = {\n key: fieldDef.key,\n field: fieldDef,\n ...(className !== undefined && { className }),\n };\n\n applyHiddenLogic(inputs, fieldDef, rootFormRegistry, evaluationContext);\n\n return inputs;\n });\n}\n","import { computed, Signal } from '@angular/core';\nimport { FieldDef } from '../../definitions/base/field-def';\nimport { getGridClassString } from '../../utils/grid-classes/grid-classes';\n\n/**\n * Builds base input properties from a field definition.\n *\n * @param fieldDef The field definition to extract properties from\n * @param defaultProps Optional form-level default props to merge with field props\n * @returns Record of input names to values\n */\nexport function buildBaseInputs(fieldDef: FieldDef<unknown>, defaultProps?: Record<string, unknown>): Record<string, unknown> {\n const { key, label, className, tabIndex, props, meta, addons } = fieldDef;\n const inputs: Record<string, unknown> = {};\n\n // Always include key — required by components for accessibility and identification.\n // `type` is intentionally NOT propagated to the field component (it's the\n // registry discriminant, not a component input); wrappers and addons read it\n // from `WrapperFieldInputs.type` which is injected at the buildFieldInputs()\n // boundary by `DfFieldOutlet`.\n inputs['key'] = key;\n\n if (label !== undefined) {\n inputs['label'] = label;\n }\n\n // Combine user className with generated grid classes\n const gridClassString = getGridClassString(fieldDef);\n const allClasses: string[] = [];\n\n if (gridClassString) {\n allClasses.push(gridClassString);\n }\n\n if (className) {\n allClasses.push(className);\n }\n\n if (allClasses.length > 0) {\n inputs['className'] = allClasses.join(' ');\n }\n\n if (tabIndex !== undefined) {\n inputs['tabIndex'] = tabIndex;\n }\n\n // Merge props: form-level defaultProps as base, field-level props override\n // Cast props to Record since it's typed as unknown but always represents an object at runtime\n const fieldProps = props as Record<string, unknown> | undefined;\n const mergedProps =\n fieldProps !== undefined || defaultProps !== undefined ? { ...(defaultProps ?? {}), ...(fieldProps ?? {}) } : undefined;\n\n if (mergedProps !== undefined) {\n inputs['props'] = mergedProps;\n }\n\n if (meta !== undefined) {\n inputs['meta'] = meta;\n }\n\n // Forward addons. Field components that don't declare an `addons` input\n // signal (Tier 3 — toggle, checkbox, radio, slider) ignore this key via\n // Angular's `setInput` no-op-on-unknown behaviour, so it's safe to forward\n // unconditionally.\n if (addons !== undefined) {\n inputs['addons'] = addons;\n }\n\n return inputs;\n}\n\n/**\n * Base field mapper that extracts common field properties into component inputs.\n *\n * @param fieldDef The field definition to map\n * @returns Signal containing Record of input names to values\n */\nexport function baseFieldMapper(fieldDef: FieldDef<unknown>): Signal<Record<string, unknown>> {\n // For base mapper, inputs are static (no reactive dependencies)\n // Wrap in computed for consistency with the MapperFn type\n return computed(() => buildBaseInputs(fieldDef));\n}\n","import { computed, inject, Signal } from '@angular/core';\nimport { ContainerField } from '../../definitions/default/container-field';\nimport { buildClassName } from '../../utils/grid-classes/grid-classes';\nimport { RootFormRegistryService } from '../../core/registry/root-form-registry.service';\nimport { injectNonFieldEvaluationContext } from '../../core/logic/non-field-logic-resolver';\nimport { applyHiddenLogic } from '../apply-hidden-logic';\n\n/**\n * Maps a container field definition to component inputs.\n *\n * @param fieldDef The container field definition\n * @returns Signal containing Record of input names to values for ngComponentOutlet\n */\nexport function containerFieldMapper(fieldDef: ContainerField): Signal<Record<string, unknown>> {\n const rootFormRegistry = inject(RootFormRegistryService);\n const evaluationContext = injectNonFieldEvaluationContext(fieldDef, { scopeToArrayItem: true });\n const className = buildClassName(fieldDef);\n\n return computed(() => {\n // Nullify wrapper-related props on the field passed to the container\n // component — DfFieldOutlet handles field-level wrappers at the outlet\n // level. Passing them here would cause double-wrapping since the\n // container's internal wrapper chain also resolves from `this.field()`.\n // We set wrappers to undefined (not stripped) so the object identity\n // stays close to the original fieldDef shape.\n const containerField = { ...fieldDef, wrappers: undefined, skipAutoWrappers: undefined, skipDefaultWrappers: undefined };\n\n const inputs: Record<string, unknown> = {\n key: fieldDef.key,\n field: containerField,\n ...(className !== undefined && { className }),\n };\n\n applyHiddenLogic(inputs, fieldDef, rootFormRegistry, evaluationContext);\n\n return inputs;\n });\n}\n","import { computed, inject, Signal } from '@angular/core';\nimport { GroupField } from '../../definitions/default/group-field';\nimport { buildClassName } from '../../utils/grid-classes/grid-classes';\nimport { RootFormRegistryService } from '../../core/registry/root-form-registry.service';\nimport { injectNonFieldEvaluationContext } from '../../core/logic/non-field-logic-resolver';\nimport { applyHiddenLogic } from '../apply-hidden-logic';\n\n/**\n * Maps a group field definition to component inputs.\n *\n * @param fieldDef The group field definition\n * @returns Signal containing Record of input names to values for ngComponentOutlet\n */\nexport function groupFieldMapper(fieldDef: GroupField): Signal<Record<string, unknown>> {\n const rootFormRegistry = inject(RootFormRegistryService);\n const evaluationContext = injectNonFieldEvaluationContext(fieldDef, { scopeToArrayItem: true });\n const className = buildClassName(fieldDef);\n\n return computed(() => {\n const inputs: Record<string, unknown> = {\n key: fieldDef.key,\n field: fieldDef,\n ...(className !== undefined && { className }),\n };\n\n applyHiddenLogic(inputs, fieldDef, rootFormRegistry, evaluationContext);\n\n return inputs;\n });\n}\n","import { computed, inject, Signal } from '@angular/core';\nimport { RowField } from '../../definitions/default/row-field';\nimport { buildClassName } from '../../utils/grid-classes/grid-classes';\nimport { RootFormRegistryService } from '../../core/registry/root-form-registry.service';\nimport { injectNonFieldEvaluationContext } from '../../core/logic/non-field-logic-resolver';\nimport { applyHiddenLogic } from '../apply-hidden-logic';\n\nconst ROW_WRAPPERS = [{ type: 'row' }] as const;\n\n/** Maps a row field definition to container component inputs. */\nexport function rowFieldMapper(fieldDef: RowField): Signal<Record<string, unknown>> {\n const rootFormRegistry = inject(RootFormRegistryService);\n const evaluationContext = injectNonFieldEvaluationContext(fieldDef, { scopeToArrayItem: true });\n const className = buildClassName(fieldDef);\n\n return computed(() => {\n // Rebuilt each emission so logic-driven updates to fieldDef (e.g. `disabled`)\n // flow through. Shallow spread is safe while RowField has no nested\n // mutable props; add a deep-clone if that stops being true.\n // `type` is rewritten to 'container' because `row` is a synthetic field\n // type — the runtime renders it as a ContainerField with a synthesized\n // `{ type: 'row' }` wrapper.\n const containerField = { ...fieldDef, type: 'container', wrappers: ROW_WRAPPERS };\n\n const inputs: Record<string, unknown> = {\n key: fieldDef.key,\n field: containerField,\n ...(className !== undefined && { className }),\n };\n\n applyHiddenLogic(inputs, fieldDef, rootFormRegistry, evaluationContext);\n\n return inputs;\n });\n}\n","import { computed, Signal } from '@angular/core';\nimport { PageField } from '../../definitions/default/page-field';\nimport { buildClassName } from '../../utils/grid-classes/grid-classes';\n\n/**\n * Maps a page field definition to component inputs.\n *\n * @param fieldDef The page field definition\n * @returns Signal containing Record of input names to values for ngComponentOutlet\n */\nexport function pageFieldMapper(fieldDef: PageField): Signal<Record<string, unknown>> {\n const className = buildClassName(fieldDef);\n\n // Page inputs are static (no reactive dependencies)\n return computed(() => ({\n key: fieldDef.key,\n field: fieldDef,\n ...(className !== undefined && { className }),\n }));\n}\n","import { computed, inject, Signal } from '@angular/core';\nimport { TextField } from '../../definitions/default/text-field';\nimport { buildBaseInputs } from '../base/base-field-mapper';\nimport { DEFAULT_PROPS } from '../../models/field-signal-context.token';\nimport { RootFormRegistryService } from '../../core/registry/root-form-registry.service';\nimport { injectNonFieldEvaluationContext } from '../../core/logic/non-field-logic-resolver';\nimport { applyHiddenLogic } from '../apply-hidden-logic';\n\n/**\n * Maps a text field definition to component inputs.\n *\n * @param fieldDef The text field definition\n * @returns Signal containing Record of input names to values for ngComponentOutlet\n */\nexport function textFieldMapper(fieldDef: TextField): Signal<Record<string, unknown>> {\n const rootFormRegistry = inject(RootFormRegistryService);\n const evaluationContext = injectNonFieldEvaluationContext(fieldDef, { scopeToArrayItem: true });\n const defaultProps = inject(DEFAULT_PROPS);\n\n // Return computed signal for reactive updates\n return computed(() => {\n const baseInputs = buildBaseInputs(fieldDef, defaultProps());\n const inputs: Record<string, unknown> = { ...baseInputs };\n\n applyHiddenLogic(inputs, fieldDef, rootFormRegistry, evaluationContext);\n\n return inputs;\n });\n}\n","import { ReadonlyFieldTree } from '../../core/field-tree-utils';\n\n/**\n * Fields every addon-action context carries, regardless of whether the\n * addon is bound to a host field or rendered orphaned.\n */\ninterface AddonActionContextBase<TValue> {\n /**\n * Identity of the host field this addon is attached to. Empty strings\n * indicate the addon is rendered outside an active field context (rare).\n * Use {@link isFieldBoundContext} or the `form !== null` check to\n * narrow before relying on field identity.\n */\n readonly field: { readonly key: string; readonly type: string };\n /**\n * Current value of the host field at the time the action fires.\n * `undefined` when the host field has no value yet (initial render before\n * defaultValue resolution) or when the addon is rendered outside a field.\n */\n readonly value: TValue | undefined;\n}\n\n/**\n * Field-bound variant — the addon is attached to a real field and has a\n * working value-writer. The common case for `pi-button` / `mat-button` /\n * `bs-button` / `ion-button` inside an adapter input field.\n */\nexport interface FieldBoundAddonActionContext<TValue = unknown> extends AddonActionContextBase<TValue> {\n /** Read-only view of the host field's tree — same view wrappers receive. */\n readonly form: ReadonlyFieldTree<TValue>;\n /** Push a new value into the host field. */\n readonly setValue: (next: TValue) => void;\n}\n\n/**\n * Orphan variant — the addon is rendered outside any field component (rare;\n * happens when consumers compose `<df-addon-slot>` directly without a host\n * field). No writer is reachable; reading `value` may also yield `undefined`.\n */\nexport interface OrphanAddonActionContext<TValue = unknown> extends AddonActionContextBase<TValue> {\n readonly form: null;\n /** Always absent in the orphan variant — distinguishes the union members. */\n readonly setValue?: undefined;\n}\n\n/** Context handed to inline addon actions and registered handler functions. */\nexport type AddonActionContext<TValue = unknown> = FieldBoundAddonActionContext<TValue> | OrphanAddonActionContext<TValue>;\n\n/** Type guard narrowing {@link AddonActionContext} to its field-bound variant. */\nexport function isFieldBoundContext<TValue>(ctx: AddonActionContext<TValue>): ctx is FieldBoundAddonActionContext<TValue> {\n return ctx.form !== null;\n}\n\n/** Built-in preset actions shared across adapters. */\nexport type CommonAddonActionPreset = 'clear' | 'reset' | 'paste' | 'copy' | 'toggle-password-visibility';\n\n/** Module-augmentable registry of adapter-specific preset names. */\n// eslint-disable-next-line @typescript-eslint/no-empty-interface, @typescript-eslint/no-empty-object-type -- Intentionally empty: module-augmentation seam\nexport interface DynamicFormAddonActionPresetRegistry {}\n\n/**\n * Union of every preset name an addon may reference — universal presets\n * plus any contributed via {@link DynamicFormAddonActionPresetRegistry}.\n */\nexport type AddonActionPreset = CommonAddonActionPreset | keyof DynamicFormAddonActionPresetRegistry | (string & {});\n\n/** Module-augmentable registry of user-defined named action handlers. */\n// eslint-disable-next-line @typescript-eslint/no-empty-interface, @typescript-eslint/no-empty-object-type -- Intentionally empty: module-augmentation seam\nexport interface DynamicFormActionRegistry {}\n\n/**\n * Typed string handle to a registered action handler. The empty registry\n * resolves to `never`; augmenting `DynamicFormActionRegistry` populates the\n * union and unlocks autocomplete.\n */\nexport type RegisteredActionRef = keyof DynamicFormActionRegistry;\n","import { inject, InjectionToken } from '@angular/core';\nimport { AddonActionContext } from './addon-action';\n\n/** Handler signature for user-registered addon actions. */\nexport type AddonActionHandler<TValue = unknown> = (ctx: AddonActionContext<TValue>) => void;\n\n/**\n * Multi-provider token populated by `withAddonActions({...})` calls.\n * `ADDON_ACTION_REGISTRY` aggregates these into a single name → handler map.\n *\n * @internal\n */\nexport const ADDON_ACTION_HANDLERS = new InjectionToken<readonly Record<string, AddonActionHandler>[]>('ADDON_ACTION_HANDLERS');\n\n/**\n * Form-scoped name → handler map. Read by adapter button types at click\n * time for `actionRef: 'name'` lookups. Provided at form scope (not root)\n * because `ADDON_ACTION_HANDLERS` is form-scoped via `withAddonActions`.\n */\nexport const ADDON_ACTION_REGISTRY = new InjectionToken<ReadonlyMap<string, AddonActionHandler>>('ADDON_ACTION_REGISTRY');\n\nexport function createAddonActionRegistry(): ReadonlyMap<string, AddonActionHandler> {\n const sources = inject(ADDON_ACTION_HANDLERS, { optional: true }) ?? [];\n const map = new Map<string, AddonActionHandler>();\n for (const handlers of sources) {\n for (const [name, fn] of Object.entries(handlers)) {\n map.set(name, fn);\n }\n }\n return map;\n}\n","import { InjectionToken, Signal, Type, ViewContainerRef } from '@angular/core';\nimport { FieldRegistryWrappers, RegisteredWrapperTypes } from './registry/field-registry';\n\n/** Resolves a wrapper type name to its registered config interface. */\n\nexport type WrapperConfig<TWrappers extends RegisteredWrapperTypes = RegisteredWrapperTypes> = TWrappers extends keyof FieldRegistryWrappers\n ? FieldRegistryWrappers[TWrappers]\n : { readonly type: TWrappers };\n\n/**\n * Signature of a lazy component loader — either a direct component class or\n * an ES module whose `default` export is the component. Shared by field and\n * wrapper registrations so the `loadComponent: () => import('./x.component')`\n * idiom types cleanly in both places.\n */\nexport type LazyComponentLoader<T = unknown> = () => Promise<Type<T> | { default: Type<T> }>;\n\n/**\n * Pre-computed reverse index used by `resolveWrappers`: field type → the\n * wrappers that auto-apply to it. Built once in the `provideDynamicForm(...)`\n * factory from every registered `WrapperTypeDefinition.types` entry.\n */\nexport type WrapperAutoAssociations = ReadonlyMap<string, readonly WrapperConfig[]>;\n\n/** Configuration interface for registering wrapper types. */\nexport interface WrapperTypeDefinition<T extends WrapperConfig = WrapperConfig> {\n /** Unique identifier for the wrapper type (also serves as discriminant from FieldTypeDefinition) */\n wrapperName: string;\n /** Wrapper definition type marker (internal use) */\n _wrapper?: T;\n /**\n * Function to load the wrapper component (supports lazy loading).\n * Returns a Promise that resolves to the component class or module with default export.\n */\n loadComponent: LazyComponentLoader;\n /** Field types this wrapper should auto-apply to. */\n types?: readonly string[];\n}\n\n/** Type guard for WrapperTypeDefinition. */\nexport function isWrapperTypeDefinition(value: unknown): value is WrapperTypeDefinition {\n return typeof value === 'object' && value !== null && 'wrapperName' in value;\n}\n\n/**\n * Contract that wrapper components must satisfy.\n *\n * @Component({\n * template: `\n * <dbx-section [header]=\"header() ?? ''\">\n * <ng-container #fieldComponent></ng-container>\n * </dbx-section>\n * `,\n * })\n * export class SectionWrapperComponent implements FieldWrapper {\n * readonly fieldComponent = viewChild.required('fieldComponent', { read: ViewContainerRef });\n * readonly header = input<string>();\n * readonly fieldInputs = input<WrapperFieldInputs>();\n * }\n * ```\n */\nexport interface FieldWrapper {\n /** ViewContainerRef slot where inner content is rendered */\n readonly fieldComponent: Signal<ViewContainerRef>;\n}\n\n/** Injection token for the wrapper type registry. */\nexport const WRAPPER_REGISTRY = new InjectionToken<Map<string, WrapperTypeDefinition>>('WRAPPER_REGISTRY', {\n providedIn: 'root',\n factory: () => new Map(),\n});\n\n/** Component cache for loaded wrapper components. */\nexport const WRAPPER_COMPONENT_CACHE = new InjectionToken<Map<string, Type<unknown>>>('WRAPPER_COMPONENT_CACHE', {\n providedIn: 'root',\n factory: () => new Map(),\n});\n\n/**\n * Pre-computed reverse index: `fieldType → WrapperConfig[]` for every\n * registered `WrapperTypeDefinition.types` entry. Built once in the\n * `provideDynamicForm(...)` factory so `resolveWrappers` can look up\n * auto-associations in O(1) per field instead of scanning the full\n * wrapper registry on every render.\n */\nexport const WRAPPER_AUTO_ASSOCIATIONS = new InjectionToken<WrapperAutoAssociations>('WRAPPER_AUTO_ASSOCIATIONS', {\n providedIn: 'root',\n factory: () => new Map(),\n});\n","import { FieldDef } from '../../definitions/base/field-def';\nimport { isPageField } from '../../definitions/default/page-field';\nimport { RegisteredFieldTypes } from '../registry/field-registry';\n\n// Re-export isPageField for external usage\nexport { isPageField };\n\n/** Interface for fields that contain child fields */\ninterface ContainerField<T> extends FieldDef<T> {\n fields: FieldDef<unknown>[];\n}\n\n/** Type guard to check if a field is a container with fields */\nfunction isContainerWithFields<T>(field: FieldDef<T>): field is FieldDef<T> & ContainerField<T> {\n return (\n (field.type === 'row' || field.type === 'group' || field.type === 'page' || field.type === 'container') &&\n 'fields' in field &&\n Array.isArray((field as ContainerField<T>).fields)\n );\n}\n\n/** Form mode enumeration distinguishing between paged and non-paged forms */\nexport type FormMode = 'paged' | 'non-paged';\n\n/** Result of form mode detection with validation details */\nexport interface FormModeDetectionResult {\n /** The detected form mode */\n mode: FormMode;\n /** Whether the form configuration is valid for the detected mode */\n isValid: boolean;\n /** Array of validation errors if form is invalid */\n errors: string[];\n}\n\n/**\n * Type guard for paged form configurations\n * A paged form has ALL root-level fields of type 'page'\n */\nexport function isPagedForm<TFields extends RegisteredFieldTypes[]>(fields: TFields): boolean {\n if (!fields || fields.length === 0) {\n return false;\n }\n\n return fields.every((field) => isPageField(field));\n}\n\n/**\n * Type guard for non-paged form configurations\n * A non-paged form has NO page fields at any level\n */\nexport function isNonPagedForm<TFields extends RegisteredFieldTypes[]>(fields: TFields): boolean {\n if (!fields || fields.length === 0) {\n return true; // Empty form is considered non-paged\n }\n\n return !hasAnyPageFields(fields);\n}\n\n/**\n * Detects the form mode and validates the configuration\n *\n * @param fields The form field definitions\n * @returns Detection result with mode and validation status\n */\nexport function detectFormMode<TFields extends RegisteredFieldTypes[]>(fields: TFields): FormModeDetectionResult {\n if (!fields || fields.length === 0) {\n return {\n mode: 'non-paged',\n isValid: true,\n errors: [],\n };\n }\n\n const hasPageFields = fields.some((field) => isPageField(field));\n const allPagesAtRoot = fields.every((field) => isPageField(field));\n const hasNestedPages = hasNestedPageFields(fields);\n\n // Determine mode and validate\n if (hasPageFields) {\n const errors: string[] = [];\n\n // For paged forms, ALL root fields must be pages\n if (!allPagesAtRoot) {\n errors.push('Mixed page and non-page fields at root level. In paged forms, ALL root-level fields must be of type \"page\".');\n }\n\n // Check for nested pages in any page field\n if (hasNestedPages) {\n errors.push('Page fields cannot contain nested page fields at any level.');\n }\n\n return {\n mode: 'paged',\n isValid: errors.length === 0,\n errors,\n };\n } else {\n // Non-paged form\n const errors: string[] = [];\n\n // Non-paged forms cannot have page fields anywhere\n if (hasAnyPageFields(fields)) {\n errors.push('Page fields are not allowed in non-paged forms.');\n }\n\n return {\n mode: 'non-paged',\n isValid: errors.length === 0,\n errors,\n };\n }\n}\n\n/**\n * Recursively checks if any field definition is a page field\n *\n * @param fields Array of field definitions to check\n * @returns true if any page field is found at any level\n */\nfunction hasAnyPageFields(fields: FieldDef<unknown>[]): boolean {\n for (const field of fields) {\n if (isPageField(field)) {\n return true;\n }\n\n // Check nested fields in container types\n if (!isPageField(field) && isContainerWithFields(field)) {\n if (hasAnyPageFields(field.fields)) {\n return true;\n }\n }\n }\n return false;\n}\n\n/**\n * Checks if fields contain nested page fields within their children\n * (This is different from hasAnyPageFields as it checks NESTED pages, not root pages)\n *\n * @param fields Array of field definitions to check\n * @returns true if nested page fields found\n */\nfunction hasNestedPageFields(fields: FieldDef<unknown>[]): boolean {\n for (const field of fields) {\n // If this is a page field, check if its children contain pages\n if (isPageField(field) && isContainerWithFields(field)) {\n if (hasAnyPageFields(field.fields)) {\n return true;\n }\n }\n\n // Check other container types for nested pages\n if (!isPageField(field) && isContainerWithFields(field)) {\n if (hasNestedPageFields(field.fields)) {\n return true;\n }\n }\n }\n return false;\n}\n\n/** Type predicate for valid paged form configurations */\nexport function isValidPagedForm<TFields extends RegisteredFieldTypes[]>(fields: TFields): boolean {\n const result = detectFormMode(fields);\n return result.mode === 'paged' && result.isValid;\n}\n\n/** Type predicate for valid non-paged form configurations */\nexport function isValidNonPagedForm<TFields extends RegisteredFieldTypes[]>(fields: TFields): boolean {\n const result = detectFormMode(fields);\n return result.mode === 'non-paged' && result.isValid;\n}\n","import { FieldDef } from '../../definitions/base/field-def';\nimport { isPageField } from '../../definitions/default/page-field';\nimport { isRowField } from '../../definitions/default/row-field';\nimport { isGroupField } from '../../definitions/default/group-field';\nimport { isArrayField } from '../../definitions/default/array-field';\nimport { isGenericContainerField } from '../../definitions/default/container-field';\nimport { ContainerFieldTypes, LeafFieldTypes, RegisteredFieldTypes } from '../registry/field-registry';\n\n/**\n * Type guard to check if a field definition has child fields.\n * This is a looser check that works with FieldDef<unknown> without requiring RegisteredFieldTypes.\n */\nexport function hasChildFields<T>(\n field: FieldDef<T>,\n): field is FieldDef<T> & { fields: FieldDef<unknown>[] | Record<string, FieldDef<unknown>> } {\n return 'fields' in field && field.fields != null;\n}\n\n/** Container field type names */\nconst CONTAINER_TYPES = new Set(['page', 'row', 'group', 'array', 'container']);\n\n/**\n * Type guard to check if a field is a container field (page, row, group, or array)\n * Container fields have a 'fields' property and don't contribute values directly.\n * This overload works with RegisteredFieldTypes for full type narrowing.\n */\nexport function isContainerField(field: RegisteredFieldTypes): field is ContainerFieldTypes;\n/**\n * Type guard to check if a field is a container field (page, row, group, or array).\n * This overload works with any FieldDef for looser type checking.\n */\nexport function isContainerField(field: FieldDef<unknown>): boolean;\nexport function isContainerField(field: FieldDef<unknown> | RegisteredFieldTypes): boolean {\n return CONTAINER_TYPES.has(field.type);\n}\n\n/**\n * Type guard to check if a field is a leaf field (value or display field)\n * Leaf fields don't have children and either contribute values or display content\n */\nexport function isLeafField(field: RegisteredFieldTypes): field is LeafFieldTypes {\n return !isContainerField(field);\n}\n\n/**\n * Type guard to check if a field has a value property (value-bearing field)\n * These fields contribute to the form value output\n * Note: Using `unknown` in the Extract condition to match any value type\n */\nexport function isValueBearingField(field: RegisteredFieldTypes): field is Extract<RegisteredFieldTypes, { value: unknown }> {\n return 'value' in field;\n}\n\n/**\n * Type guard to check if a field is excluded from form values (display-only field)\n * Currently this includes text fields and any other fields without a value property\n */\nexport function isDisplayOnlyField(field: RegisteredFieldTypes): boolean {\n return field.type === 'text' || isContainerField(field);\n}\n\n// Re-export the specific type guards for convenience\nexport { isPageField, isRowField, isGroupField, isArrayField, isGenericContainerField };\n","import { inject, Injector, isSignal, Pipe, PipeTransform } from '@angular/core';\nimport { toObservable } from '@angular/core/rxjs-interop';\nimport { isObservable, Observable, of } from 'rxjs';\nimport { DynamicText } from '../../models/types/dynamic-text';\n\n/**\n * Pipe that handles dynamic text resolution with support for static strings,\n * Observables, and Signals.\n */\n@Pipe({\n name: 'dynamicText',\n})\nexport class DynamicTextPipe implements PipeTransform {\n private readonly injector = inject(Injector);\n\n /**\n * Transforms dynamic text input into a resolved string value\n *\n * @param value - The dynamic text value to resolve\n * @returns The resolved string value as an Observable\n */\n transform(value: DynamicText | undefined): Observable<string> {\n if (isObservable(value)) {\n return value;\n }\n\n if (isSignal(value)) {\n return toObservable(value, { injector: this.injector });\n }\n\n return of(value || '');\n }\n}\n","import { FieldMeta } from '../definitions/base/field-meta';\n\n/**\n * Applies meta attributes to a DOM element and tracks which attributes were applied.\n *\n * @param element - The DOM element to apply attributes to\n * @param meta - The meta object containing attributes to apply\n * @param previouslyApplied - Set of attribute names that were previously applied\n * @returns A new Set containing the attribute names that were applied\n */\nexport function applyMetaToElement(element: Element, meta: FieldMeta | undefined, previouslyApplied: Set<string>): Set<string> {\n const newApplied = new Set<string>();\n\n // Remove old attributes no longer in meta\n for (const attr of previouslyApplied) {\n if (!meta || !(attr in meta) || meta[attr] === undefined) {\n element.removeAttribute(attr);\n }\n }\n\n if (!meta) return newApplied;\n\n // Apply new attributes\n for (const [key, value] of Object.entries(meta)) {\n if (value === undefined || value === null) continue;\n\n element.setAttribute(key, String(value));\n newApplied.add(key);\n }\n\n return newApplied;\n}\n","import { isSignal, Injector } from '@angular/core';\nimport { toObservable } from '@angular/core/rxjs-interop';\nimport { isObservable, Observable, of } from 'rxjs';\nimport { DynamicText } from '../models/types/dynamic-text';\n\n/**\n * Converts DynamicText (string | Observable | Signal) to Observable<string>\n * Unifies all three types into a consistent Observable stream\n *\n * @param value - The dynamic text value to convert\n * @param injector - Optional injector for signal conversion\n * @returns Observable<string> - The value as an observable stream\n */\nexport function dynamicTextToObservable(value: DynamicText | undefined, injector?: Injector): Observable<string> {\n if (value === undefined) {\n return of('');\n }\n\n if (isObservable(value)) {\n return value;\n }\n\n if (isSignal(value)) {\n return toObservable(value, { injector });\n }\n\n return of(String(value));\n}\n","import { InjectionToken, Injector, Signal } from '@angular/core';\nimport { toObservable } from '@angular/core/rxjs-interop';\nimport { Observable, of, throwError, TimeoutError } from 'rxjs';\nimport { catchError, filter, map, scan, shareReplay, switchMap, take, timeout } from 'rxjs/operators';\nimport { EventBus } from '../../events/event.bus';\nimport { ComponentInitializedEvent } from '../../events/constants/component-initialized.event';\nimport { DynamicFormLogger } from '../../providers/features/logger/logger.token';\n\n/**\n * Injection token for configuring the initialization timeout in milliseconds.\n * Defaults to 10 seconds. When the timeout is reached, a warning is logged\n * and (initialized) emits true as a best-effort fallback.\n */\nexport const INITIALIZATION_TIMEOUT_MS = new InjectionToken<number>('INITIALIZATION_TIMEOUT_MS', {\n providedIn: 'root',\n factory: () => 10_000,\n});\n\n/** Creates an observable that tracks component initialization progress. */\nexport function createInitializationTracker(eventBus: EventBus, expectedCount: number): Observable<boolean> {\n return eventBus.on<ComponentInitializedEvent>('component-initialized').pipe(\n scan((count) => count + 1, 0),\n map((currentCount) => currentCount >= expectedCount),\n filter((isComplete) => isComplete),\n );\n}\n\n/** Options for setting up initialization tracking. */\nexport interface InitializationTrackingOptions {\n eventBus: EventBus;\n totalComponentsCount: Signal<number>;\n injector: Injector;\n componentId: string;\n}\n\n/**\n * Creates an observable that tracks when all form components are initialized.\n * Uses shareReplay({ bufferSize: 1, refCount: false }) to ensure exactly one emission\n * that can be received by late subscribers and keeps the subscription alive.\n */\nexport function setupInitializationTracking(options: InitializationTrackingOptions): Observable<boolean> {\n const { eventBus, totalComponentsCount, injector, componentId } = options;\n\n const timeoutMs = injector.get(INITIALIZATION_TIMEOUT_MS);\n const logger = injector.get(DynamicFormLogger);\n\n return toObservable(totalComponentsCount, { injector }).pipe(\n take(1),\n switchMap((count) => {\n const tracking$: Observable<boolean> =\n count === 1\n ? // Only dynamic-form component, emit immediately when it initializes\n eventBus.on<ComponentInitializedEvent>('component-initialized').pipe(\n filter((event) => event.componentType === 'dynamic-form' && event.componentId === componentId),\n map(() => true),\n take(1),\n )\n : createInitializationTracker(eventBus, count);\n\n // Timeout guard: emit best-effort true if a container throws before initializing\n return tracking$.pipe(\n timeout(timeoutMs),\n catchError((error: unknown) => {\n if (error instanceof TimeoutError) {\n logger.warn(\n `Initialization timed out after ${timeoutMs}ms. ` +\n `Expected ${count} component(s) to initialize but not all reported in time. ` +\n 'This may indicate a container component threw during initialization. ' +\n 'Emitting (initialized) as best-effort.',\n );\n // Emit true as best-effort so consumers are not stuck waiting forever\n return of(true);\n }\n return throwError(() => error);\n }),\n );\n }),\n shareReplay({ bufferSize: 1, refCount: false }),\n );\n}\n","import { ValidationError } from '../models/validation-types';\n\n/**\n * Interpolates {{param}} placeholders in message with error values\n *\n * @param message - Message template with {{param}} placeholders\n * @param error - Validation error containing parameter values\n * @returns Message with interpolated parameters\n */\nexport function interpolateParams(message: string, error: ValidationError): string {\n let result = message;\n const params = extractErrorParams(error);\n\n Object.entries(params).forEach(([key, value]) => {\n const placeholder = new RegExp(`{{\\\\s*${key}\\\\s*}}`, 'g');\n result = result.replace(placeholder, safeToString(value));\n });\n\n return result;\n}\n\n/**\n * Safely converts a value to a string, handling complex objects\n *\n * @internal\n */\nfunction safeToString(value: unknown): string {\n if (value === null || value === undefined) {\n return '';\n }\n\n // Handle primitives directly\n if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {\n return String(value);\n }\n\n // Handle RegExp (has its own toString)\n if (value instanceof RegExp) {\n return value.toString();\n }\n\n // Handle Date\n if (value instanceof Date) {\n return value.toISOString();\n }\n\n // Handle arrays\n if (Array.isArray(value)) {\n return value.join(', ');\n }\n\n // For objects, try JSON.stringify first, fall back to empty string\n try {\n // Check if object has a custom toString that's not Object.prototype.toString\n if (typeof value === 'object' && value.toString !== Object.prototype.toString) {\n return value.toString();\n }\n // Otherwise use JSON representation\n return JSON.stringify(value);\n } catch {\n // If JSON.stringify fails (circular reference, etc.), return empty string\n return '[object]';\n }\n}\n\n/**\n * Extracts parameter values from validation error\n *\n * @internal\n */\nfunction extractErrorParams(error: ValidationError): Record<string, unknown> {\n const params: Record<string, unknown> = {};\n\n // Common validation error parameters\n if ('min' in error) params.min = error.min;\n if ('max' in error) params.max = error.max;\n if ('pattern' in error) params.pattern = error.pattern;\n if ('actual' in error) params.actual = error.actual;\n if ('expected' in error) params.expected = error.expected;\n\n // Handle length validation parameters\n // Angular Signal Forms uses 'minLength'/'maxLength' properties\n // Angular Classic Forms uses 'requiredLength'/'actualLength' properties\n // We support both formats and normalize to 'requiredLength' for message templates\n if ('requiredLength' in error) {\n params.requiredLength = error.requiredLength;\n } else if ('minLength' in error) {\n // Angular Signal Forms: minLength property maps to requiredLength for templates\n params.requiredLength = error.minLength;\n params.minLength = error.minLength;\n } else if ('maxLength' in error) {\n // Angular Signal Forms: maxLength property maps to requiredLength for templates\n params.requiredLength = error.maxLength;\n params.maxLength = error.maxLength;\n }\n if ('actualLength' in error) params.actualLength = error.actualLength;\n\n // Include all custom properties from error object (for custom validators)\n // Skip 'kind' as it's the error type identifier, not a parameter\n Object.keys(error).forEach((key) => {\n if (key !== 'kind' && !(key in params)) {\n params[key] = error[key as keyof ValidationError];\n }\n });\n\n return params;\n}\n","/**\n * Native JavaScript utility functions to replace lodash-es\n * These are simple implementations for common object manipulation patterns\n */\n\nimport { DynamicFormError } from '../errors/dynamic-form-error';\n\n/**\n * Performs a deep equality comparison between two values.\n *\n * @param a - First value\n * @param b - Second value\n * @returns true if values are deeply equal\n */\nexport function isEqual(a: unknown, b: unknown): boolean {\n return isEqualInternal(a, b, new WeakMap(), new WeakMap());\n}\n\n/**\n * Internal implementation with circular reference tracking.\n *\n * @internal\n */\nfunction isEqualInternal(a: unknown, b: unknown, seenA: WeakMap<object, object>, seenB: WeakMap<object, object>): boolean {\n // Same reference or both null/undefined\n if (a === b) return true;\n\n // Handle NaN (NaN === NaN is false, but we want it to be equal)\n if (typeof a === 'number' && typeof b === 'number') {\n return Object.is(a, b);\n }\n\n // One is null/undefined but not both\n if (a == null || b == null) return false;\n\n // Different types\n if (typeof a !== typeof b) return false;\n\n // Non-object primitives (strings, booleans, symbols, bigints)\n if (typeof a !== 'object') {\n return Object.is(a, b);\n }\n\n // At this point, both a and b are objects (typeof a === 'object')\n const objA = a as object;\n const objB = b as object;\n\n // Check for circular references\n if (seenA.has(objA) || seenB.has(objB)) {\n // If we've seen these objects before, check if they were paired together\n return seenA.get(objA) === objB && seenB.get(objB) === objA;\n }\n\n // Mark these objects as seen (paired together)\n seenA.set(objA, objB);\n seenB.set(objB, objA);\n\n // Different constructors means different types (e.g., Date vs Object)\n if (objA.constructor !== objB.constructor) return false;\n\n // Handle dates\n if (a instanceof Date && b instanceof Date) {\n return a.getTime() === b.getTime();\n }\n\n // Handle RegExp\n if (a instanceof RegExp && b instanceof RegExp) {\n return a.source === b.source && a.flags === b.flags;\n }\n\n // Handle Map\n if (a instanceof Map && b instanceof Map) {\n if (a.size !== b.size) return false;\n for (const [key, value] of a) {\n if (!b.has(key) || !isEqualInternal(value, b.get(key), seenA, seenB)) {\n return false;\n }\n }\n return true;\n }\n\n // Handle Set\n if (a instanceof Set && b instanceof Set) {\n if (a.size !== b.size) return false;\n for (const value of a) {\n if (!b.has(value)) {\n // For non-primitive values in Sets, we need deep comparison\n // This is O(n^2) but Sets rarely contain objects\n if (typeof value === 'object' && value !== null) {\n let found = false;\n for (const bValue of b) {\n if (typeof bValue === 'object' && bValue !== null && isEqualInternal(value, bValue, seenA, seenB)) {\n found = true;\n break;\n }\n }\n if (!found) return false;\n } else {\n return false;\n }\n }\n }\n return true;\n }\n\n // Handle arrays\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false;\n return a.every((item, index) => isEqualInternal(item, b[index], seenA, seenB));\n }\n\n // Handle plain objects\n const recordA = a as Record<string | symbol, unknown>;\n const recordB = b as Record<string | symbol, unknown>;\n\n // Get both string and symbol keys\n const keysA = [...Object.keys(recordA), ...Object.getOwnPropertySymbols(recordA)];\n const keysB = [...Object.keys(recordB), ...Object.getOwnPropertySymbols(recordB)];\n\n if (keysA.length !== keysB.length) return false;\n\n // Create a Set for O(1) lookup\n const keySetB = new Set(keysB);\n\n return keysA.every((key) => keySetB.has(key) && isEqualInternal(recordA[key], recordB[key], seenA, seenB));\n}\n\n/**\n * Creates a new object without the specified keys\n * Native replacement for lodash omit()\n *\n * @param obj - Source object\n * @param keys - Keys to omit\n * @returns New object without specified keys\n */\nexport function omit<T extends object, K extends keyof T>(obj: T, keys: K[]): Omit<T, K> {\n const result = { ...obj };\n keys.forEach((key) => delete result[key]);\n return result as Omit<T, K>;\n}\n\n/**\n * Creates an object from an array, keyed by a specified property\n * Native replacement for lodash keyBy()\n *\n * @param array - Source array\n * @param key - Property to use as key\n * @returns Object keyed by the specified property\n */\nexport function keyBy<T extends object>(array: T[], key: keyof T): Record<string, T> {\n return array.reduce(\n (acc, item) => {\n acc[String(item[key])] = item;\n return acc;\n },\n {} as Record<string, T>,\n );\n}\n\n/**\n * Maps object values through a transformation function\n * Native replacement for lodash mapValues()\n *\n * @param obj - Source object\n * @param fn - Transformation function\n * @returns New object with transformed values\n */\nexport function mapValues<T, U>(obj: Record<string, T>, fn: (value: T, key: string) => U): Record<string, U> {\n return Object.entries(obj).reduce(\n (acc, [key, value]) => {\n acc[key] = fn(value, key);\n return acc;\n },\n {} as Record<string, U>,\n );\n}\n\n/**\n * Deep-merges `value` into `defaults`, producing a new object that contains every\n * key from `defaults` plus any overrides from `value`. Plain-object values at\n * matching keys are merged recursively; primitives, dates, and class instances\n * in `value` replace the corresponding default wholesale.\n */\nexport function deepMergeDefaults<T extends Record<string, unknown>>(defaults: T, value: Record<string, unknown> | null | undefined): T {\n if (value == null) return { ...defaults };\n\n const result: Record<string, unknown> = { ...defaults };\n\n for (const key of Object.keys(value)) {\n if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue;\n\n const incoming = value[key];\n const existing = result[key];\n\n if (isPlainObject(existing) && isPlainObject(incoming)) {\n result[key] = deepMergeDefaults(existing, incoming);\n } else if (Array.isArray(existing) && Array.isArray(incoming)) {\n result[key] = mergeArrayDefaults(existing, incoming);\n } else {\n result[key] = incoming;\n }\n }\n\n return result as T;\n}\n\n/**\n * @internal\n * Merges a value array into a defaults array element-wise when their lengths\n * match. When lengths differ — the runtime case: prepend, insert, append,\n * remove — we return the value array reference as-is so we don't produce\n * fresh inner object references for items that already have every declared\n * sub-field key. Replacing references during the linkedSignal source\n * recompute that runs after Signal Forms wrote the array would otherwise\n * desync item FieldTrees from the rendered DOM rows.\n *\n * When lengths match (the declared/initial-value path the array fix targets)\n * each `defaults[i]`/`value[i]` pair is deep-merged when both are plain\n * objects so partial array-item values don't drop sibling sub-field keys.\n */\nfunction mergeArrayDefaults(defaults: readonly unknown[], value: readonly unknown[]): readonly unknown[] {\n if (defaults.length !== value.length) return value;\n\n // Short-circuit when no element pair would be deep-merged. Returning a fresh\n // .map() result for primitive arrays (e.g. tags: ['a', 'b']) hands Signal\n // Forms a new reference on every call and rebuilds item FieldTrees needlessly.\n if (!value.some((item, i) => isPlainObject(defaults[i]) && isPlainObject(item))) return value;\n\n return value.map((item, i) => {\n const defaultItem = defaults[i];\n if (isPlainObject(defaultItem) && isPlainObject(item)) {\n return deepMergeDefaults(defaultItem, item);\n }\n return item;\n });\n}\n\n/**\n * @internal\n * Returns true for objects that should be deep-merged (plain objects produced\n * by literals or `Object.create(null)`); false for arrays, Dates, RegExps,\n * Maps, Sets, class instances, and primitives.\n */\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n if (typeof value !== 'object' || value === null) return false;\n const proto = Object.getPrototypeOf(value);\n return proto === null || proto === Object.prototype;\n}\n\n/** Options for memoize function */\n/** Default maximum cache size for memoized functions */\nconst DEFAULT_MEMOIZE_MAX_SIZE = 100;\n\nexport interface MemoizeOptions<TFunc extends (...args: never[]) => unknown> {\n /** Optional function to generate cache key from arguments */\n resolver?: (...args: Parameters<TFunc>) => string | number;\n /** Maximum number of entries to keep in cache. Uses LRU eviction when exceeded. Defaults to 100. */\n maxSize?: number;\n}\n\n/**\n * Memoizes a function with LRU cache eviction and optional custom cache key resolver.\n *\n * @param fn - Function to memoize\n * @param resolverOrOptions - Optional key resolver function or options object\n * @returns Memoized function\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function memoize<TFunc extends (...args: any[]) => any>(\n fn: TFunc,\n resolverOrOptions?: ((...args: Parameters<TFunc>) => string | number) | MemoizeOptions<TFunc>,\n): TFunc {\n const options: MemoizeOptions<TFunc> =\n typeof resolverOrOptions === 'function' ? { resolver: resolverOrOptions } : (resolverOrOptions ?? {});\n\n const { resolver, maxSize = DEFAULT_MEMOIZE_MAX_SIZE } = options;\n const cache = new Map<string | number, ReturnType<TFunc>>();\n\n return ((...args: Parameters<TFunc>): ReturnType<TFunc> => {\n const key = resolver ? resolver(...args) : JSON.stringify(args);\n\n const cachedValue = cache.get(key);\n if (cachedValue !== undefined) {\n cache.delete(key);\n cache.set(key, cachedValue);\n return cachedValue;\n }\n\n const result = fn(...args);\n\n if (cache.size >= maxSize) {\n const oldestKey = cache.keys().next().value;\n if (oldestKey !== undefined) {\n cache.delete(oldestKey);\n }\n }\n\n cache.set(key, result);\n return result;\n }) as TFunc;\n}\n\n/**\n * Normalizes a fields collection to an array.\n * Handles both array format and object format (keyed by field key).\n *\n * @param fields - Fields as array or object\n * @returns Fields as array\n */\nexport function normalizeFieldsArray<T>(fields: readonly T[] | Record<string, T>): T[] {\n return Array.isArray(fields) ? [...fields] : Object.values(fields);\n}\n\n/**\n * Gets the keys that differ between two objects.\n *\n * @param previous - Previous object state\n * @param current - Current object state\n * @returns Set of keys that have different values\n */\nexport function getChangedKeys(\n previous: Record<string, unknown> | null | undefined,\n current: Record<string, unknown> | null | undefined,\n): Set<string> {\n const changedKeys = new Set<string>();\n\n // Handle null/undefined cases\n if (!previous && !current) return changedKeys;\n if (!previous) {\n return new Set(Object.keys(current ?? {}));\n }\n if (!current) {\n return new Set(Object.keys(previous));\n }\n\n // Collect all keys from both objects\n const allKeys = new Set([...Object.keys(previous), ...Object.keys(current)]);\n\n // Check each key for differences\n for (const key of allKeys) {\n const prevValue = previous[key];\n const currValue = current[key];\n\n // Key added or removed\n if (!(key in previous) || !(key in current)) {\n changedKeys.add(key);\n continue;\n }\n\n // Value changed\n if (!isEqual(prevValue, currValue)) {\n changedKeys.add(key);\n }\n }\n\n return changedKeys;\n}\n\n/** Compile-time exhaustiveness check for discriminated unions. */\nexport function assertNever(value: never): never {\n throw new DynamicFormError(`Unexpected value: ${JSON.stringify(value)}`);\n}\n","/**\n * Shared shape for DI-scoped warning trackers that dedupe log output.\n *\n * @internal\n */\nexport interface WarningTracker {\n warnedKeys: Set<string>;\n}\n\n/**\n * Creates a fresh warning tracker instance with an empty key set.\n * DI-scoped per form instance — never module-scoped (SSR-unsafe).\n *\n * @internal\n */\nexport function createWarningTracker(): WarningTracker {\n return { warnedKeys: new Set<string>() };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"ng-forge-dynamic-forms-internal.mjs","mappings":";;;;;;;;;;;;;;;AAOE;AACE;;AAEF;AACE;;AAEF;AACE;;IAGF;AACF;;ACjBM;AACI;AAER;;AAEE;;;;;ACgFF;AACA;AACD;;;ACnFD;;AAEA;;AAkBE;AACA;;QAIE;AACE;;QAIF;AACE;;;;;AAQF;;;AAQI;;;;;AAMA;;AAGF;;;AAGI;AACA;;AAEF;;;;AAEuC;;;;AAO3C,QAMA;AACE;AAAyB;AACzB;;QAIF;AACE;;;AAGN;;;;AC9DmB;AACA;;;AAGjB,IAMS;;AACT;;;;AAWmB;;AAEnB,IASiB;;;AAGjB,IAMmB;AACjB;AACA;;AACA;;;;AAIF;;AASA,IAOmB;;;AAGjB;AAAY;AACZ;AACA;;;IAIiB;AACjB;AACA;;;AAGF;gCAKE;AACE;AACA;AACA;;AAEE;AACA;;;AAEF;AACF;gBAIA;AACE;AACA;AAAgD,wBAChD;;AAEG;AACA;;AAGK;AAEN;AACJ;;;AA/GS;;;;AARV;;;;AAPQ;AACT;;AAEA;;;;AAIC;AACD;AACE;AACA;AACA;AACD;;AAEF;;;;ACwBC;AACA;AACD;AASK;;AAEJ;AACF;;;ACvCE;;AAEA;AACE;;;;AAGF;AACF;;ACMO;4BAcL;AACE;;;;AAQA;;;;;oCASE;AACA;;;;AAKA;AACA;;;;;AAME;;;AAGF;;AAEE;;;;;;;;;AASA;;;AAGF;gCAGE;;;;;;;;;AASF;;;;;wEAWA;;;;;AAOA;AACA;;;AAIF;AACE;;AAEN;;;;ACoPI;;AAEJ;AASM;AACJ;AAA6B;;;AAE7B;AACF;AAQM;AAGJ;AACF;AAQM;;AAEN;AAQM;AACJ;AACF;AAQM;AACJ;AACF;;AC7aA;AAGA;AAoBM;IAKJ;AACE;;AAGF;AACE;YAEE;AAEF;;;wBAOE;AACA;;AAEE;oBAEE;;;YAMJ;;AAGF;wBAGE;AAEF;AACA;;AAIA;AACE;;AAEN;AAQM;;AAEF;;AAGF;AAEA;AACE;AACE;gBAEE;;;AAIJ;;;AAKA;YAEE;;AAGF;;AAIA;AACA;;AAGI;;;;AAKN;AACF;AAMM;AACJ;AACA;AACA;AACF;AAGA;;AAEI;AACA;;;AAGA;QAEA;AACE;;;AAGN;AAYM;AACJ;AACE;;;AAGJ;AAQM;AACJ;;;;;AASA;AACF;AAGM;;AAEN;AASM;AACJ;AACE;;;AAGJ;AAaM;;AAEN;AAaM;;AAEF;;;AAGJ;;ACzOA;AASA;AACE;AACA;AAAsC;AACtC;AACF;AAOA;AAIE;AAAkB;IAGlB;AACE;;IAIF;AACE;;AAGF;AACF;AAWM;;AAKJ;AAAyC;AAEzC;AAEA;;AAEI;;;AAGA;;;AAGA;;;AAGA;;AAEH;AACH;AAWM;AACJ;AAEA;;AAEI;AACE;;;;AAKF;AACA;;;;;AAKH;AACH;AAOA;AACE;AAAc;;;AAOd;;AAEE;AAAW;;;;AAKf;;;IC9HW;AACV;;;;ACIG;;AAEA;;AAEA;;AAEA;;AAEA;;AAEA;;AAEA;AACE;AACF;AACE;AACF;AACE;AACF;AACE;AACE;;AACA;AACA;;AAEJ;AACE;;AAEN;AAGA;;;AAGA;AASM;AACJ;AACE;;AAEJ;AASM;;;AAIJ;AACE;AAA6C;;;AAI/C;AAA6C;;AAE/C;;;ACzEA;IAEE;AACA;AACA;AACA;AACA;AACA;IAGA;IAGA;AACA;AACA;AACA;AACA;IAGA;AACA;AACA;AACA;AACA;AACA;IAGA;AACA;AACA;IAGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;IAGA;AACF;AA+FM;AAGK;AACA;AAHT;;;;AAME;;AAEH;;;;ACtJkB;AAEjB;AACE;;;;;;;;;AAcE;AACA;;;AAIF;;;;;;;;;;AAcE;;QAIF;AACE;;QAIF;AACE;;QAIF;;AAGM;AACN;AACA;;;AAME;AACE,iCACA;;AAGF;;;;AAKI;;;AAGA;;;AAGA;;;AAGA;;;AAGA;;;AAGA;;;;;;;;;;;;;AAcN;;;;;;;AASE;;AAGF;;;AAIA;;;;;;;;;AAaM;;AAEJ;;AAEA;;AAEA;;AAEA;;AAEA;;;;;AAMF;;AAEA;AACA;QAGA;AACA;AACE;AACA;;AAEF;AACE;AACA;;QAIF;;AAEE;AACE;AACA;AACF;AACE;AACA;AACF;AACE;AACA;AACF;AACE;AACA;AACF;AACE;AACA;AACF;AACE;AACA;AACF;AACE;AACA;AACF;AACE;AACA;;;;AAMF;AACE;AACF;AACE;AACF;AACE;AACF;AACE;AACF;AACE;AACF;AACE;AACF;AACE;AACF;AACE;AACF;AACE;AACF;AACE;AACF;AACE;AACF;AACE;AACF;AACE;AACF;AACE;AACF;AACE;AACF;AACE;AACF;AACE;AACF;AACE;AACF;AACE;;;AAGP;;;;;ACpOkB;AAEjB;AACE;;;;AAMA;AACA;AAEA;;;AAIA;AAEA;AACE;;;AAQF;;;gBAMA;AACA;AAAW;AACX;;AAGF;AAKE;;AACqC;AAErC;;AAEA;;;AAIF;AAKE;;;AAKE,4BACA;AACA;;;AAKA;;AAIE;;;;AAIE;AAEE;AACA;;;;;AAKF;AACA;;AAEF;;AAEA;AACA;;;AAIA;AACA;;AAEF,4BAEA;;;AAIF;;;;AAI8C;;;;AAK9C;;;AAIE;AACA;;AAGF;;;AAIA;;;AAIE;AACA;;AAGF;;;AAIA;AAEA;;AAEE;AACA;;AAGF;;;AAIA;;;AAIE;AACA;;AAGF;;;AAIA;AAEA;;AAEE;AACA;;AAGF;;;AAIA;AAEA;;AAEE;AACA;;AAGF;;;AAIA;;AAEE;;;AAIF;;;AAIA;;;;AAMM;;;AAGF;;;;AAIE;;AAEA;;;AAEA;;AAEA;;;;AAGA;;;AAEA;;;;gBAIF;;AAEA;;;gBAGA;;AAEA;;;;;;AAMJ;;;;;;;;;;;;;;;;AAsBE;;;AAIA;;;AAKA;;AAEA;;;AAKA;;AAEA;;;AAKA;;;AAKA;;AAEA;;;;;;;AAUA;;;;AAKF;;;;;AAOE;;;;AAKF;;;;;AAOE;;AAGF;YAEE;;AAEA;AACA;AACE;;;;;;AAUF;;;AAIF;;;AAIA;AACE;;AAEE;;;AAGJ;;AAGM;;AACc;;;;AAKpB;;AACA;;;;;;;;;;;;AAgBA;AAAsB;AAEtB;;AAEH;;AC9XD;;;;;;;;;;;;;;;;;;;;;;;;;AA0BA;AAEA;;;;;;;;;;;;;;;;;;;;;;AAuBA;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BA;AACE;AACA;AACA;AACA;;AAOF;;;;;;;;AAQC;;AAIkB;AACA;;AAGf;AACA;;IAIF;AACE;AACE;;AAGA;;AAGA;AACE;AAEF;AACE;AAEF;AACE;AAEF;AACE;AAEF;AACE;AAEF;AACE;AAEF;AACE;AAEF;AACE;AAEF;AACE;AAEF;AACE;;;AAIE;AACN;AACE;;AAEF;;AAGM;;;AAIJ;;;AAKA;;QAIF;AACE;;AAGF;;AAGM;;;wBAMJ;;;AAIF;AAA6C;AAC7C;gBAIA;AACE;;AAGF;AACE;;AAGF;;AAGM;QAEN;;AAEE;;AAEF;;AAEE;;;;AAOF;YAEE;;AAEA;;AAEA;;AAEA;;AACmB;;AAEnB;;AACmB;;YAInB;;AAEA;;AAEA;;AAEA;;AAEA;;AAEA;;AAEA;;AAEA;;AAGA;AACE;;;AAIE;;AAGN;AACE;;AAEA;;AAEA;;AAEA;AACE;;;AAIE;;;;AAON;;AASE;;AAGF;QAGA;AACE;;;AAKA;;;QAOF;AACA;;;AAKA;;AAGM;AACN;;AAGM;;;AAGJ;AACE;;;;AAIJ;;AAGM;AACN;AACA;AACA;AACE;AACA;AACE;;;;AAIJ;;;;AAKE;;;AAKF;;AAEE;;AAGF;;AAEH;;ACjXD;AAMA;AACU;AACA;AAER;AACE;;AAGF;;AAEE;YAEE;;;AAGF;;;;AAME;;;;AAQA;AACA;AACE;;;;;AAMJ;;AAGF;AACE;;AAEH;AAGD;;AAIE;;;AAQI;;QAIF;AACA;QAGA;AAEA;;IAIF;AACE;;;AAGE;;;AAEA;AACE;;;;;IAYN;;;IAKA;;;AAGI;;;AAGL;;ACrGD;AACE;AAAiD;;AAEjD;AACF;AAMM;AACJ;AACE;AACE;AAEF;AACE;;;AAOA;AACA;AAEA;AACE;;;;;AAQA;;AAGF;AACE;AACA;;AAGM;AAEJ;;;;;AAIF;AACA;;;AAIJ;AACE;AAEF;AACE;AAEF;AACE;AAEI;AAEJ;AAEF;AACE;AAEI;AAEJ;AAEF;AACE;;AAEN;AAEA;;AAE6B;;;;;;AAY3B;AACF;AAEA;AACE;QAEE;;;;AAGA;AACA;;AAEJ;AAEA;AACE;AAAwC;IAExC;AACF;AAEA;AACE;AAAwC;IAExC;AACF;;;IC5HW;AACV;;ACDK;;AAEF;;AAGF;AACE;;AAGF;AACE;;;;AAKF;;AAEF;;;;;ACPE;;AAEE;;AAEF;AACF;;;;;;;;;;ACda;AACA;;;;;AAEZ;;;;ACFK;AACJ;AACE;;;AAGJ;;ACAA;;AAEA;AAGA;;;AAEqD;;AAEnD;AACF;AAGA;IAEE;AAAyB;YAKzB;;;;AAIM;AACA;;;;;AAMN;AACF;;AAQU;AACA;;;AAIR;gBAaE;wBAKA;;AAEA;AACA;;AAGE;;gCAOF;;;AAGE;AACA;;AAEA;;AAEA;AACA;;;AAGA;;;;;IAOI;AACN;AACE;AACE;;;;;;QAOJ;;;;;AAgBA;AAEA;AACE;;;;;wBASF;;;;;;AASI;AACA;;AAEA;;AAEA;AACA;;;AAGA;;;;;;;AAQF;;AAEA;AACA;AACA;;AAEA;;AAEA;AACA;;;AAGA;;;;;AAMJ,IAOQ;AACN;AACA;AAAyB;AAEzB;;AAGE;;;AAIF;AACE;;;;;AAMF;;;AAQA;;AAEA;AACA;;AAGE;;;AAIF;;;AAIE;AACA;;AAEA;;AAEA;AACA;;;AAGA;AACE;;;;AAKN,IASA;;;AAQI;;AAGF;;AAGE;AACA;;;AAGA;;AAEA;AACA;AACE;;;;;;;;;;;;AClRW;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEjB,IAOA;;QAQE;;AAGF,IAMA;;;AAIA,IAMA;;;;;;;AAWE;AACA;;YAOF;;;IAWA;;;;;;AASA,IAMA;;;;AAQE;;YAOF;;;IAWA;;;;;;AASA,IAMA;;;;AAQE;;YAOF;;;IAWA;;;;;;AASA,IAMA;;;;AAQE;;AAGF;;;IAWA;;;;AAME;;AAGF;;;IAWA;;;;AAME;;AAGF;;;IAWA;;;;AAME;;AAGF;AAOE;;;;;;;;AAUF,IAMA;;;AAIA,IAMA;;;AAIA,IAMA;;;;;;;;;;;;;;;;;;;;AC3RiB;AACA;;;;AAMjB;;AAEE;AAAY;;AAGV;AACA;;;;AAMJ;;AAGI;AACA;AACE;;;;;;AAQJ;;AAEH;;;AClCK;AACJ;AAEA;AACE;;AAEE;AACE;;AAIA;;AAEF;;;AAIJ;AACE;AACA;;;;AAIA;;AAEE;;;AAIJ;;;;AAKA;AACE;;AAEE;AACE;AACE;;;AAEA;;;AAGJ;;;AAEA;;;AAIJ;AACE;;AAGF;AACF;;ACvDA;;AAEA;AAOA;AACE;AACF;;AAqDE;QAEE;AACE;;QAIF;;AAEE;;QAIF;;;;;;;AAQI;AACA;AACF;;QAIF;AACF;AAEA;AACF;;AChGM;;AAGF;wBAGE;AAEE;AACA;AAEA;;AAEF;AACF;AAGF;AACF;;ACkDM;AAGJ;;;;;AAOI;;gDAMA;AACE;gCAMI;;AAMF;oBAEA;;;AAGD;AAED;AACF;;AAGA;;;AAIF;;;;AAIA;AACE;;;AAIF;;AAEE;;AAGE;;AAEJ;AAEA;AACF;AACF;;ACpHA;;AAEI;AACE;AACA;;;;;;;AAUA;;;;AAIN;AAGM;AACJ;;AAEE;;AAEF;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;IAGA;;;AAGE;;;;;;AAOA;AACE;;wBAIA;AAAe;AAEf;;;;AAIA;;AAEF;AACE;;AAEA;AAAgC;;AACX;AAErB;AAGI;;AAEF;AAEE;AACA;;;AAIP;;AAGD;AACF;;AC9FM;AACJ;AACA;AACA;AACA;AACA;AAEA;AACA;;AAGE;;YAQF;AAEA;;;AAGI;;;;;;;AAQF;oCAIE;;;;;AAKC;;;AAGH;wBAGE;AAEE;;;AAKA;;AAGF;;AAOI;AACA;;;AAIP;AAED;;;AAGA;AACF;;;IC5EW;IAGA;IAGA;AACV;;ACCD;AACE;AACE;AACE;AACE;;;;;;;AASR;AAOA;AACE;;;AAGA;AACE;;AAEF;AACE;;AAEF;AACF;AAQM;IAEJ;AACE;;IAIF;AACE;;;;;IASF;AACA;AACA;;IAMA;;;;AAMI;;;AAIJ;gBAGE;AAEA;AACF;;;;AAMA;AACF;AASM;IAEJ;AACE;;IAIF;AACE;;;;;IASF;AACA;AACA;AACA;;;;;;AAYI;;;AAIJ;;;;YAMI;;oCAKA;;AAOE;AACF;AAEA;;;QAKF;;gBAOA;;AAEE;AACE;;AAEJ;QAGA;AACF;;;;AAMA;AACF;;AC1LM;AACJ;AACA;AACF;AAqBM;AACJ;;;;;;;;;;;;AAYF;;;AASE;AACE;AACA;;AAEF;AACA;AACF;;AAaI;AACA;AACD;AAWG;;;AAMJ;AAAY;AAEZ;AACA;AACA;AACF;;ACxFA;IAEE;IAEA;IAEA;IAEA;;AA8EF;AACE;AACA;;AAIF;AACE;AACA;;AAUF;AACE;;AAGE;AACE;AAEF;AACE;AAEF;AACE;AAEF;AACE;;AAEN;AASA;IAEE;AACE;;IAIF;AACE;;oBAQF;;;gBAQA;AACA;AACE;;AAEA;AACA;;AAGF;AACF;AASA;;AAEI;;AAGF;AACF;AASA;;AAEI;;AAGF;AACF;AAUA;;AAMI;;AAGF;;;;;;AAOA;AACF;AAQM;;QAGF;AACE;;;;;QASF;AACE;AACA;;AAGF;;AAGE;;;AAIA;;AAGF;AACF;AACF;AAQM;;QAGF;AACE;;;;;QASF;AACE;AACA;;AAGF;AAEA;AACE;;;AAIA;;AAGF;AACF;AACF;AAQM;IAEJ;AACE;;;AAKA;;;;;AAKC;;IAIH;AACF;AAQM;;AAEN;AAQM;IAEJ;AACE;;;AAKA;;;;;AAKC;;IAIH;AACF;AAQM;;AAEN;;AAaE;AACA;;AAKA;;;;AAIE;AACF;AACF;;AC/QM;;;AAKN;;;ACnIE;AAEA;AACE;AAEA;AACE;;AAGF;AACE;;;AAKF;AACE;AACE;AACE;;;;;;;AAON;;;;AAGA;;AAEJ;;;ICpCW;AACV;;ACQK;QAGJ;AACA;AACA;;;AAKE;;AAGF;;AAIE;;;;;AAKE;;AAEJ;;AAIA;AACF;;ACDA;;AAEA;AAEA;;;;;AAII;;AAEJ;AAEA;;;;;AAII;;AAEJ;AAEA;;;;;AAII;;AAEJ;AAEA;;;;;AAII;;AAEJ;AAEA;;;;;AAOI;;AAEJ;AAEA;AACE;AACF;AAGM;;AAGJ;;;AAIA;AACE;AACE;AACE;;;;;;AAKJ;;;AAGA;AACE;;;;AAIF;AACE;;;;AAIF;AACE;;;;AAIF;AACE;;;;AAIF;AACE;AACE;AACA;AACE;;;;;;;;AAQA;;;;;AAKN;AACE;;AAEF;AACE;;AAEF;AACE;;;AAGN;AAEA;AACE;;;AAIA;AAEA;AACE;;;AAEA;;;AAEA;;;AAIF;AACE;AAAkC;AAClC;AACF;AACF;AAEA;AAGE;;AAGE;;AAGF;AACE;AACA;;AAGF;;AAEE;;AAGF;;;AAIE;;AAGF;AACF;AAEA;AAGE;AACA;;AAEE;;AAGF;AACA;;AAGE;AACE;;;AAIE;;AAGF;AACA;AAEA;AACE;AACE;AACE;;;;;AAIJ;;AAGF;;;;;;AAOJ;AACF;AAGA;;AAEI;AACA;;AAGF;AACA;AACE;;AACK;AACL;;;;;;;QAOA;;;AAIF;AACE;AACE;AAAkC;;;;;;;AAQtC;AACF;AAMA;AAIE;AACE;;;;;AAQA;;AAIF;AACF;AAGA;;AAEI;AACA;;AAGF;AACA;AACE;;AACK;AACL;;;;;;;QAOA;;;AAIF;AACE;AACE;AAAkC;;;;;;AAOtC;AACF;AAGA;AACE;AACA;AACA;;;AAIE;AACE;AAAkC;oCAIlC;;;AAIF;;;AAGA;AACE;;;AAGmC;AACzC;AAGM;AACJ;AACF;;AChWM;;AAIN;;ACEM;YAMJ;AACF;;ACsBM;AACJ;AACF;AAuEM;;AAEN;;ACnGM;AACJ;AAEE;AACA;AACA;;AAGJ;;ACjBM;;AAEN;;ACMM;AACJ;AACF;;ACRM;AACJ;AACF;;ACCM;AACJ;AACF;AAQM;AACJ;AACF;AAGA;AACE;AAEE;;AAGJ;AAQA;AACE;AACE;AACE;;QAIF;AACE;AACE;;;;AAIN;AACF;;;ACvEW;AACA;;;;;;AAEV;;;ACDC;AACA;AACD;;;;;;;;;;;ACYA;;ACPD;AACE;AACE;;AACA;AACA;;AAEJ;AAMA;AACE;AACE;;AACA;AACA;;AAEJ;AAMA;;AAEE;AACF;AAQA;;AAImB;AACA;AACA;AACA;AACA;AAEjB;AAgBA;;AAEI;;;;;;AAMJ,IAMA;AACE;;AAGF,IAIQ;AACN;;wBAIE;;;;;AAMF;;AAGF,IAIQ;AACN;AACE;;;;;;;;AASF;;AAiBF,IAMA;AACE;;;;;;;;;;;;ACtIE;;AAEN;;ACEM;AAIJ;QAEE;AACE;;;AAKA;;AAEA;;AAEA;;AAEA;;AAEA;;AAEA;gBAEE;;AAEN;AACF;;ACxCM;AACJ;;;;AAIA;AACF;AAQM;AACJ;AACA;;;AAKE;;;AAIA;;AAGF;AACF;;AChBM;AAMJ;AACA;gBAGE;AACE;;;AAGA;;AAED;;AAEL;;AC1BM;AACJ;AACA;AACA;;AAGE;;AAEE;;;;AAMF;AACF;AACF;;AClBM;AACJ;;oBAQA;AAEA;AACE;;IAIF;;;AAIE;;;AAIA;;AAGF;;;AAIA;AACE;;;AAMF;AAGA;AACE;;AAGF;AACE;;gBAOF;AACE;;AAGF;AACF;AAQM;;AAIN;;ACpEM;AACJ;AACA;AACA;;gDASE;AAEA;;AAEE;;;;AAMF;AACF;AACF;;ACxBM;AACJ;AACA;AACA;;AAGE;;AAEE;;;;AAMF;AACF;AACF;;ACtBA;AAGM;AACJ;AACA;AACA;;gDASE;AAEA;;AAEE;;;;AAMF;AACF;AACF;;ACxBM;AACJ;IAGA;;AAEE;;AAED;AACH;;ACLM;AACJ;AACA;AACA;;;AAKE;;AAIA;AACF;AACF;;ACqBM;AACJ;AACF;;;;;AC7BE;AACA;AACA;AACE;AACE;;;AAGJ;AACF;;;;ACUM;AACJ;AACF;;AA0BE;AACA;AACD;;AAIC;AACA;AACD;;AAUC;AACA;AACD;;AC3ED;;AAGI;;AAGJ;AAmBM;;AAEF;;AAGF;AACF;AAMM;;;AAKJ;AACF;AAQM;;;AAGA;AACA;AACA;;;AAIJ;AACA;AACA;;;;AAQI;;;AAKA;;;AAIA;AACA;;;;;;QAQF;AACE;;;AAIA;AACA;;;;AAIN;AAQA;AACE;AACE;AACE;;;AAKA;AACE;;;;AAIN;AACF;AASA;AACE;;AAGI;AACE;;;;AAMF;AACE;;;;AAIN;AACF;AAGM;AACJ;;AAEF;AAGM;AACJ;;AAEF;;AC/JM;;AAIN;AAGA;AAaM;;AAEN;AAMM;AACJ;AACF;AAOM;;AAEN;AAMM;;AAEN;;;AC9CM;AACJ;AACE;;AAGF;AACE;;AAGF;QAEE;;AAGF;AACF;;;ACfmB;AAEjB,IAMA;;;;;;;;AAZI;AACJ;AACD;;;;ACAC;IAGA;AACE;AACE;;;AAIJ;AAAW;IAGX;AACE;;;AAGA;;AAGF;AACF;;;ACjBE;AACA;AACD;AAGK;;AAMN;AAeM;;;;;AASA;AAEI,cACE;AAKF;QAGJ;AAGI;AACE;AAEI;;AAEA;gBAGJ;;AAEF;;AAGN;AAGJ;;ACtEM;;AAEJ;AAEA;;AAEE;AACF;AAEA;AACF;AAOA;;AAEI;;IAIF;AACE;;IAIF;AACE;;IAIF;AACE;;IAIF;AACE;;IAIF;QAEE;AACE;;QAGF;;AACA;QAEA;;AAEJ;AAOA;;;AAIsB;;AACA;;AACI;;AACD;;AACE;gBAMzB;AACE;;AACK;QAEL;AACA;;AACK;QAEL;AACA;;;AAE2B;;;;;AAQ7B;AAEA;AACF;;AC5FM;AACJ;AACF;AAOA;;AAEe;;;;IAQb;AAA4B;IAG5B;AAA2B;IAG3B;;;;;IASA;QAEE;;IAIF;AACA;IAGA;AAA2C;;;;;AASzC;;;AAKA;AAAuB;;;AAGnB;;;AAGJ;;;AAKA;AAAuB;AACvB;;;;AAMM;;;;;;AAMA;AAAY;;;AAEZ;;;;AAIN;;IAIF;AACE;AAA2B;;;;;IAS7B;AACA;AAEA;AAAmC;IAGnC;AAEA;AACF;AAUM;AACJ;AACA;AACA;AACF;AAUM;;;AAIA;;AAIN;AAUM;AACJ;;AAGI;;AAIN;AAQM;;AACe;AAEnB;;;;AAKE;AACA;;;;AAIO;;;;AAGL;;;AAIJ;AACF;AAgBA;AACE;AAAsC;;AAK2C;;AAG/E;;AAEE;;AAEF;AACF;AACF;AAQA;AACE;AAAiD;;;AAGnD;AAIA;AAiBM;;;AAQJ;AAEA;;;AAIE;AACE;AACA;AACA;;AAGF;AAEA;;AAEE;AACE;;;AAIJ;AACA;AACF;AACF;AASM;;AAEN;AASM;AAIJ;IAGA;AAA2B;;AAEzB;;;;;;IAUF;AACE;AACA;QAGA;AACE;;;;AAMA;;;AAIJ;AACF;AAGM;AACJ;AACF;;;ACzVE;AACF","names":[],"ignoreList":[],"sources":["../../../../packages/dynamic-forms/internal/src/lib/utils/dynamic-value/resolve-dynamic-value.ts","../../../../packages/dynamic-forms/internal/src/lib/errors/dynamic-form-error.ts","../../../../packages/dynamic-forms/internal/src/lib/models/addon/addon-type.ts","../../../../packages/dynamic-forms/internal/src/lib/addons/inject-addon-type-registry.ts","../../../../packages/dynamic-forms/internal/src/lib/addons/df-addon-slot.component.ts","../../../../packages/dynamic-forms/internal/src/lib/models/field-type.ts","../../../../packages/dynamic-forms/internal/src/lib/addons/inject-fields-supporting-addons.ts","../../../../packages/dynamic-forms/internal/src/lib/addons/run-preset-action.ts","../../../../packages/dynamic-forms/internal/src/lib/models/logic/logic-config.ts","../../../../packages/dynamic-forms/internal/src/lib/core/cross-field/cross-field-detector.ts","../../../../packages/dynamic-forms/internal/src/lib/core/derivation/field-state-extractor.ts","../../../../packages/dynamic-forms/internal/src/lib/core/expressions/async-condition-function-cache.service.ts","../../../../packages/dynamic-forms/internal/src/lib/core/expressions/value-utils.ts","../../../../packages/dynamic-forms/internal/src/lib/core/expressions/parser/types.ts","../../../../packages/dynamic-forms/internal/src/lib/core/expressions/parser/tokenizer.ts","../../../../packages/dynamic-forms/internal/src/lib/core/expressions/parser/parser.ts","../../../../packages/dynamic-forms/internal/src/lib/core/expressions/parser/evaluator.ts","../../../../packages/dynamic-forms/internal/src/lib/core/expressions/parser/expression-parser.ts","../../../../packages/dynamic-forms/internal/src/lib/core/expressions/condition-evaluator.ts","../../../../packages/dynamic-forms/internal/src/lib/core/expressions/http-condition-function-cache.service.ts","../../../../packages/dynamic-forms/internal/src/lib/utils/stable-stringify.ts","../../../../packages/dynamic-forms/internal/src/lib/models/field-signal-context.token.ts","../../../../packages/dynamic-forms/internal/src/lib/core/registry/root-form-registry.service.ts","../../../../packages/dynamic-forms/internal/src/lib/utils/safe-read-path-keys.ts","../../../../packages/dynamic-forms/internal/src/lib/core/registry/field-context-registry.service.ts","../../../../packages/dynamic-forms/internal/src/lib/core/registry/function-registry.service.ts","../../../../packages/dynamic-forms/internal/src/lib/core/http/http-condition-cache.ts","../../../../packages/dynamic-forms/internal/src/lib/core/http/http-request-resolver.ts","../../../../packages/dynamic-forms/internal/src/lib/utils/derived-from-deferred/derived-from-deferred.ts","../../../../packages/dynamic-forms/internal/src/lib/utils/resource-composition/with-previous-value.ts","../../../../packages/dynamic-forms/internal/src/lib/core/expressions/debounced-resource-logic-fn.ts","../../../../packages/dynamic-forms/internal/src/lib/core/expressions/http-condition-logic-function.ts","../../../../packages/dynamic-forms/internal/src/lib/core/expressions/async-condition-logic-function.ts","../../../../packages/dynamic-forms/internal/src/lib/core/expressions/logic-function-cache.service.ts","../../../../packages/dynamic-forms/internal/src/lib/core/expressions/logic-function-factory.ts","../../../../packages/dynamic-forms/internal/src/lib/core/field-tree-utils.ts","../../../../packages/dynamic-forms/internal/src/lib/core/logic/non-field-logic-resolver.ts","../../../../packages/dynamic-forms/internal/src/lib/models/validation/validator-config.ts","../../../../packages/dynamic-forms/internal/src/lib/core/http/http-response-evaluator.ts","../../../../packages/dynamic-forms/internal/src/lib/core/values/dynamic-value-function-cache.service.ts","../../../../packages/dynamic-forms/internal/src/lib/core/values/dynamic-value-factory.ts","../../../../packages/dynamic-forms/internal/src/lib/core/validation/validator-factory.ts","../../../../packages/dynamic-forms/internal/src/lib/definitions/base/base-checked-field.ts","../../../../packages/dynamic-forms/internal/src/lib/definitions/base/base-value-field.ts","../../../../packages/dynamic-forms/internal/src/lib/definitions/default/array-field.ts","../../../../packages/dynamic-forms/internal/src/lib/definitions/default/container-field.ts","../../../../packages/dynamic-forms/internal/src/lib/definitions/default/group-field.ts","../../../../packages/dynamic-forms/internal/src/lib/definitions/default/hidden-field.ts","../../../../packages/dynamic-forms/internal/src/lib/definitions/default/row-field.ts","../../../../packages/dynamic-forms/internal/src/lib/definitions/default/page-field.ts","../../../../packages/dynamic-forms/internal/src/lib/events/constants/component-initialized.event.ts","../../../../packages/dynamic-forms/internal/src/lib/providers/features/event-form-value/emit-form-value.token.ts","../../../../packages/dynamic-forms/internal/src/lib/providers/features/logger/noop-logger.ts","../../../../packages/dynamic-forms/internal/src/lib/events/event.bus.ts","../../../../packages/dynamic-forms/internal/src/lib/events/interfaces/form-event.ts","../../../../packages/dynamic-forms/internal/src/lib/events/utils/token-resolver.ts","../../../../packages/dynamic-forms/internal/src/lib/utils/grid-classes/grid-classes.ts","../../../../packages/dynamic-forms/internal/src/lib/mappers/apply-hidden-logic.ts","../../../../packages/dynamic-forms/internal/src/lib/mappers/array/array-field-mapper.ts","../../../../packages/dynamic-forms/internal/src/lib/mappers/base/base-field-mapper.ts","../../../../packages/dynamic-forms/internal/src/lib/mappers/container/container-field-mapper.ts","../../../../packages/dynamic-forms/internal/src/lib/mappers/group/group-field-mapper.ts","../../../../packages/dynamic-forms/internal/src/lib/mappers/row/row-field-mapper.ts","../../../../packages/dynamic-forms/internal/src/lib/mappers/page/page-field-mapper.ts","../../../../packages/dynamic-forms/internal/src/lib/mappers/text/text-field-mapper.ts","../../../../packages/dynamic-forms/internal/src/lib/models/addon/addon-action.ts","../../../../packages/dynamic-forms/internal/src/lib/models/addon/addon-action-registry.ts","../../../../packages/dynamic-forms/internal/src/lib/models/wrapper-type.ts","../../../../packages/dynamic-forms/internal/src/lib/models/types/form-mode.ts","../../../../packages/dynamic-forms/internal/src/lib/models/types/type-guards.ts","../../../../packages/dynamic-forms/internal/src/lib/utils/dynamic-text-to-observable.ts","../../../../packages/dynamic-forms/internal/src/lib/pipes/dynamic-text/dynamic-text.pipe.ts","../../../../packages/dynamic-forms/internal/src/lib/utils/apply-meta.ts","../../../../packages/dynamic-forms/internal/src/lib/utils/initialization-tracker/initialization-tracker.ts","../../../../packages/dynamic-forms/internal/src/lib/utils/interpolate-params.ts","../../../../packages/dynamic-forms/internal/src/lib/utils/object-utils.ts","../../../../packages/dynamic-forms/internal/src/lib/utils/warning-tracker.ts"],"sourcesContent":["import { computed, Injector, isSignal, Signal } from '@angular/core';\nimport { toSignal } from '@angular/core/rxjs-interop';\nimport { isObservable } from 'rxjs';\nimport { DynamicValue } from '../../models/types/dynamic-value';\n\n/** Normalises a {@link DynamicValue} into a `Signal<T>`. */\nexport function resolveDynamicValue<T>(value: DynamicValue<T> | undefined, fallback: T, injector?: Injector): Signal<T> {\n if (value === undefined) {\n return computed(() => fallback);\n }\n if (isSignal(value)) {\n return value;\n }\n if (isObservable(value)) {\n return toSignal(value, { initialValue: fallback, injector });\n }\n // Static value\n return computed(() => value);\n}\n","/** Base error class for all Dynamic Forms errors. */\nexport class DynamicFormError extends Error {\n private static readonly PREFIX = '[Dynamic Forms]';\n\n constructor(message: string) {\n super(`${DynamicFormError.PREFIX} ${message}`);\n this.name = 'DynamicFormError';\n }\n}\n","import { InjectionToken, Type } from '@angular/core';\nimport { LazyComponentLoader } from '../wrapper-type';\nimport { BaseAddon } from './addon-def';\nimport { AddonSlot } from './addon-slot';\n\n/**\n * Minimal JSON Schema fragment used by the MCP server to surface an addon\n * type's shape to AI form generators. Covers the common subset\n * (`type`/`properties`/`required`/`enum`/etc.) without pulling in a full JSON\n * Schema dependency. Extra keys are allowed via the `[key: string]` index so\n * vendor-specific annotations (`x-foo`, `markdownDescription`) still typecheck.\n */\nexport interface AddonTypeSchema {\n readonly $ref?: string;\n readonly $schema?: string;\n readonly title?: string;\n readonly description?: string;\n readonly type?:\n | 'object'\n | 'array'\n | 'string'\n | 'number'\n | 'integer'\n | 'boolean'\n | 'null'\n | ReadonlyArray<'object' | 'array' | 'string' | 'number' | 'integer' | 'boolean' | 'null'>;\n readonly enum?: ReadonlyArray<unknown>;\n readonly const?: unknown;\n readonly properties?: Readonly<Record<string, AddonTypeSchema>>;\n readonly required?: ReadonlyArray<string>;\n readonly additionalProperties?: boolean | AddonTypeSchema;\n readonly items?: AddonTypeSchema | ReadonlyArray<AddonTypeSchema>;\n readonly oneOf?: ReadonlyArray<AddonTypeSchema>;\n readonly anyOf?: ReadonlyArray<AddonTypeSchema>;\n readonly allOf?: ReadonlyArray<AddonTypeSchema>;\n readonly not?: AddonTypeSchema;\n readonly default?: unknown;\n readonly examples?: ReadonlyArray<unknown>;\n readonly minimum?: number;\n readonly maximum?: number;\n readonly minLength?: number;\n readonly maxLength?: number;\n readonly pattern?: string;\n readonly format?: string;\n /** Vendor extensions and unrecognised keywords pass through untyped. */\n readonly [key: string]: unknown;\n}\n\n/**\n * Optional shape validator a type ships to enforce its required fields at\n * runtime. Throws are caught by the validator and converted into lenient\n * warnings; the offending addon is dropped and the form keeps rendering.\n */\nexport type AddonShapeValidator<T extends BaseAddon = BaseAddon> = (addon: T, fieldKey: string) => void;\n\n/** Per-type registration metadata. */\nexport interface AddonTypeDefinition<T extends BaseAddon = BaseAddon> {\n /** Discriminant matching `addon.type`. */\n readonly type: string;\n /** Lazy loader for the type's renderer component. */\n readonly loadComponent: LazyComponentLoader;\n /** Optional shape validator — throw `DynamicFormError` to drop the addon with a warning. */\n readonly validate?: AddonShapeValidator<T>;\n /** Optional JSON Schema fragment for MCP / pre-flight tooling. */\n readonly schema?: AddonTypeSchema;\n /**\n * Whether the type survives `JSON.stringify` / `JSON.parse`. Types whose\n * shape includes a function payload (e.g., the built-in `'component'`\n * loader, inline `prime-button` `action` handlers) declare `jsonSafe: false`\n * so the validator drops them when the config was loaded from a JSON\n * source (`sanitizeFormConfig(config, { source: 'json' })`). Defaults to\n * `true` — types are JSON-safe unless explicitly opted out.\n */\n readonly jsonSafe?: boolean;\n}\n\n/** Per-field-type addon-support metadata declared at field registration. */\nexport interface FieldAddonSupport {\n /** Slots a field of this type accepts. */\n readonly slots: readonly AddonSlot[];\n /** Optional whitelist of allowed addon types. Omitted = any registered type. */\n readonly allowedTypes?: readonly string[];\n}\n\n/** Global registry of addon types. */\nexport const ADDON_TYPE_REGISTRY = new InjectionToken<Map<string, AddonTypeDefinition>>('ADDON_TYPE_REGISTRY', {\n providedIn: 'root',\n factory: () => new Map<string, AddonTypeDefinition>(),\n});\n\n/**\n * Cache for resolved addon-type components. Provided at form scope by\n * `provideDynamicForm`.\n *\n * @internal\n */\nexport const ADDON_TYPE_COMPONENT_CACHE = new InjectionToken<Map<string, Type<unknown>>>('ADDON_TYPE_COMPONENT_CACHE');\n","import { inject, Type } from '@angular/core';\nimport { DynamicFormError } from '../errors/dynamic-form-error';\nimport { ADDON_TYPE_COMPONENT_CACHE, ADDON_TYPE_REGISTRY, AddonTypeDefinition } from '../models/addon/addon-type';\n\n/** Pick the component class out of whatever a lazy loader returned. */\nfunction resolveDefaultExport<T>(result: Type<T> | { default: Type<T> }): Type<T> {\n return typeof result === 'object' && result !== null && 'default' in result ? result.default : result;\n}\n\n/**\n * Public shape returned by {@link injectAddonTypeRegistry}. Pinned explicitly\n * so the API extractor doesn't churn between versions when the inner factory\n * gets refactored.\n */\nexport interface AddonTypeRegistryRef {\n getType(type: string): AddonTypeDefinition | undefined;\n hasType(type: string): boolean;\n getTypeNames(): string[];\n loadTypeComponent(type: string): Promise<Type<unknown>>;\n getLoadedTypeComponent(type: string): Type<unknown> | undefined;\n readonly raw: ReadonlyMap<string, AddonTypeDefinition>;\n}\n\n/** Injection function for accessing the addon type registry. */\nexport function injectAddonTypeRegistry(): AddonTypeRegistryRef {\n const registry = inject(ADDON_TYPE_REGISTRY);\n const componentCache = inject(ADDON_TYPE_COMPONENT_CACHE);\n\n return {\n /** Look up a type definition by its discriminant. */\n getType(type: string): AddonTypeDefinition | undefined {\n return registry.get(type);\n },\n\n /** Whether a type is currently registered. */\n hasType(type: string): boolean {\n return registry.has(type);\n },\n\n /** All registered type names — useful for actionable validator warnings. */\n getTypeNames(): string[] {\n return Array.from(registry.keys());\n },\n\n /**\n * Resolve a type to its renderer component. Caches on first resolution.\n *\n * @throws DynamicFormError when the type is not registered.\n */\n async loadTypeComponent(type: string): Promise<Type<unknown>> {\n const definition = registry.get(type);\n if (!definition) {\n const known = Array.from(registry.keys()).join(', ') || '(none)';\n throw new DynamicFormError(`Addon type '${type}' is not registered. Currently registered types: ${known}.`);\n }\n\n const cached = componentCache.get(type);\n if (cached) {\n return cached;\n }\n\n try {\n const component = resolveDefaultExport(await definition.loadComponent());\n if (component) {\n componentCache.set(type, component);\n return component;\n }\n throw new DynamicFormError(`Addon type '${type}' loadComponent resolved to null.`);\n } catch (error) {\n if (error instanceof DynamicFormError) throw error;\n throw new DynamicFormError(\n `Failed to load component for addon type '${type}': ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n },\n\n /**\n * Synchronous lookup of an already-loaded type component, or undefined.\n * Returns `undefined` when the type isn't registered in this scope even\n * if a prior scope cached a component under the same name — prevents\n * cross-form cache hits when registrations diverge.\n */\n getLoadedTypeComponent(type: string): Type<unknown> | undefined {\n if (!registry.has(type)) return undefined;\n return componentCache.get(type);\n },\n\n /** Direct read-only access to the registry map. */\n get raw(): ReadonlyMap<string, AddonTypeDefinition> {\n return registry;\n },\n };\n}\n","import { NgComponentOutlet } from '@angular/common';\nimport { ChangeDetectionStrategy, Component, computed, inject, Injector, input, Signal, signal, Type } from '@angular/core';\nimport { explicitEffect } from 'ngxtension/explicit-effect';\nimport { AnyAddon } from '../models/addon/addon-def';\nimport { DynamicFormLogger } from '../providers/features/logger/logger.token';\nimport { resolveDynamicValue } from '../utils/dynamic-value/resolve-dynamic-value';\nimport { injectAddonTypeRegistry } from './inject-addon-type-registry';\nimport { WrapperFieldInputs } from '../wrappers/wrapper-field-inputs';\n\n/**\n * Dispatcher component that renders an addon by looking up its `type` in\n * `ADDON_TYPE_REGISTRY` and instantiating the registered component.\n */\n@Component({\n selector: 'df-addon-slot',\n imports: [NgComponentOutlet],\n template: `\n @if (resolvedComponent(); as cmp) {\n <ng-container *ngComponentOutlet=\"cmp; inputs: typeInputs()\" />\n }\n `,\n host: {\n '[attr.slot]': 'slotAttr()',\n '[class]': 'className()',\n '[style.display]': 'isHidden() ? \"none\" : null',\n },\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class DfAddonSlot {\n private readonly registry = injectAddonTypeRegistry();\n private readonly logger = inject(DynamicFormLogger);\n private readonly hostInjector = inject(Injector);\n\n readonly addon = input.required<AnyAddon>();\n /**\n * Optional read-only view of the host field's mapped inputs — same shape\n * wrappers receive (`field?: ReadonlyFieldTree`, `key`, `props`, etc.).\n * Forwarded to type components so they can read field state without\n * re-injecting `FIELD_SIGNAL_CONTEXT` and rebuilding context themselves.\n */\n readonly fieldInputs = input<WrapperFieldInputs | undefined>();\n /**\n * Pre-resolved `hidden` signal supplied by the host field. When provided,\n * the dispatcher reads this instead of re-resolving `addon.hidden` —\n * lets the host (e.g., `prime-input`) share a single subscription per\n * Observable-typed `hidden` between its own visibility filter and the\n * slot's `[style.display]` binding.\n */\n readonly hidden = input<Signal<boolean> | undefined>(undefined);\n\n /** `slot` HTML attribute forwarded to host — needed for Ionic shadow projection. */\n protected readonly slotAttr = computed(() => this.addon().slot);\n protected readonly className = computed(() => this.addon().className ?? null);\n\n /**\n * Locally-resolved `hidden` signal — always reflects `addon.hidden`\n * regardless of whether the host supplied a pre-resolved signal.\n * Populated via `explicitEffect` so `resolveDynamicValue` (which may call\n * `toSignal` for Observable-typed values) does NOT run inside a reactive\n * context (NG0602). The WeakMap caches per `hidden`-value identity, so\n * swapping `addon` inputs that share the same Observable reuses the\n * subscription instead of leaking a fresh one per swap.\n */\n private readonly _hiddenByValue = new WeakMap<object, Signal<boolean>>();\n private readonly _resolvedHidden = signal<Signal<boolean> | undefined>(undefined);\n\n /**\n * `hidden` resolved from `DynamicValue<boolean>` to a flat `Signal<boolean>`.\n * Prefers the host-supplied signal when present (avoids duplicate `toSignal`\n * subscriptions for Observable-typed `hidden`); falls back to the locally-\n * resolved signal, which is always populated by the addon-tracking effect.\n */\n protected readonly isHidden = computed(() => {\n const pre = this.hidden();\n if (pre) return pre();\n const local = this._resolvedHidden();\n return local ? local() : false;\n });\n\n /**\n * Latest async-loaded `{ type, component }` pair. Tagging the result with\n * its type lets `resolvedComponent` filter stale loads structurally — no\n * monotonic token needed. When `addon.type` changes mid-load, the prior\n * load still completes and writes here, but the computed ignores it\n * because the recorded type no longer matches the current one.\n */\n private readonly asyncLoaded = signal<{ type: string; component: Type<unknown> } | undefined>(undefined);\n\n /**\n * Resolved type component — composed declaratively from two sources of\n * truth: the registry's synchronous cache (warm hits, e.g., a second\n * `<df-addon-slot>` for an already-loaded type) and the type-tagged async\n * loader result. Reading `addon().type` first makes the computed react to\n * addon swaps and naturally invalidate stale async-load results.\n */\n protected readonly resolvedComponent = computed<Type<unknown> | undefined>(() => {\n const type = this.addon().type;\n const cached = this.registry.getLoadedTypeComponent(type);\n if (cached) return cached;\n const loaded = this.asyncLoaded();\n return loaded?.type === type ? loaded.component : undefined;\n });\n\n /** Inputs passed to the type component — addon plus the wrapper-style host bag. */\n protected readonly typeInputs = computed(() => ({\n addon: this.addon(),\n fieldInputs: this.fieldInputs(),\n }));\n\n constructor() {\n // Resolve `addon.hidden` outside any reactive context AND unconditionally —\n // the host-supplied signal is a perf hint, not a precondition. Keeping\n // `_resolvedHidden` always-fresh closes a race where `isHidden` could\n // return `false` between a host-`hidden` clear and the effect re-running.\n explicitEffect([this.addon], ([addon]) => {\n const h = addon.hidden;\n const valueKey = typeof h === 'object' && h !== null ? h : null;\n let resolved = valueKey ? this._hiddenByValue.get(valueKey) : undefined;\n if (!resolved) {\n resolved = resolveDynamicValue(addon.hidden, false, this.hostInjector);\n if (valueKey) this._hiddenByValue.set(valueKey, resolved);\n }\n this._resolvedHidden.set(resolved);\n });\n\n // Kick off async loads on cache miss. `resolvedComponent` filters stale\n // results by type identity, so this effect is a pure side-effect dispatcher.\n explicitEffect([this.addon], ([addon]) => {\n const type = addon.type;\n if (this.registry.getLoadedTypeComponent(type)) return; // sync path; computed picks it up\n this.registry\n .loadTypeComponent(type)\n .then((component) => this.asyncLoaded.set({ type, component }))\n .catch((error: unknown) => {\n this.logger.warn(\n `Failed to load addon type '${type}': ${error instanceof Error ? error.message : String(error)}. ` +\n `Registered types: ${this.registry.getTypeNames().join(', ') || '(none)'}.`,\n );\n });\n });\n }\n}\n","import { InjectionToken } from '@angular/core';\nimport { FieldDef } from '../definitions/base/field-def';\nimport { MapperFn } from '../mappers/types';\nimport { FieldAddonSupport } from './addon/addon-type';\nimport { LazyComponentLoader } from './wrapper-type';\n\n/** Defines how a field type handles form values and data collection. */\nexport type ValueHandlingMode = 'include' | 'exclude' | 'flatten';\n\n/** Semantic grouping of interchangeable field UI alternatives. */\nexport type FieldScope = 'boolean' | 'single-select' | 'multi-select' | 'text-input' | 'numeric' | 'date';\n\n/** Mapped component inputs that must be available before a field component is instantiated. */\nexport type RenderReadyInput = 'field' | (string & {});\n\n/**\n * Configuration interface for registering custom field types.\n *\n * @typeParam T - The field definition type this field type handles\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Intentionally using `any` for maximum flexibility in field type registration\nexport interface FieldTypeDefinition<T extends FieldDef<any> = any> {\n /** Unique identifier for the field type */\n name: string;\n /** Field definition type marker (internal use) */\n _fieldDef?: T;\n /**\n * Function to load the component (supports lazy loading).\n * Returns a Promise that resolves to the component class or module with default export.\n */\n loadComponent?: LazyComponentLoader;\n /** Mapper function that converts field definition to component bindings. */\n mapper?: MapperFn<T>;\n /** How this field type handles form values and data collection (defaults to 'include') */\n valueHandling?: ValueHandlingMode;\n /** List of prop keys to forward to the meta object. */\n propsToMeta?: string[];\n /** Semantic scope for tooling to discover interchangeable field alternatives */\n scope?: FieldScope | FieldScope[];\n /** Mapped component inputs that must exist before the renderer instantiates the component */\n renderReadyWhen?: readonly RenderReadyInput[];\n\n /**\n * Declares which addon slots this field accepts and (optionally) the\n * subset of kinds allowed.\n */\n addons?: FieldAddonSupport;\n}\n\n/** Injection token for the global field type registry. */\nexport const FIELD_REGISTRY = new InjectionToken<Map<string, FieldTypeDefinition>>('FIELD_REGISTRY', {\n providedIn: 'root',\n factory: () => new Map(),\n});\n\n/**\n * Gets the value handling mode for a specific field type from the registry.\n *\n * @param fieldType - The field type identifier\n * @param registry - The field type registry\n * @returns The value handling mode ('include' is the default if not specified)\n */\nexport function getFieldValueHandling(fieldType: string, registry: Map<string, FieldTypeDefinition>): ValueHandlingMode {\n const definition = registry.get(fieldType);\n return definition?.valueHandling ?? 'include';\n}\n","import { inject } from '@angular/core';\nimport { FieldAddonSupport } from '../models/addon/addon-type';\nimport { FIELD_REGISTRY } from '../models/field-type';\n\n/** One entry per field type that declares addon support. */\nexport interface FieldAddonSupportEntry {\n readonly name: string;\n readonly slots: FieldAddonSupport['slots'];\n readonly allowedTypes: FieldAddonSupport['allowedTypes'];\n}\n\n/**\n * Walk `FIELD_REGISTRY` and return every field type that opted into addons\n * (i.e., declared `addons` on its `FieldTypeDefinition`).\n *\n * @Component({ ... })\n * class FormBuilder {\n * protected readonly addonable = injectFieldsSupportingAddons();\n *\n * constructor() {\n * // -> [{ name: 'prime-input', slots: ['prefix', 'suffix'], allowedTypes: [...] }, ...]\n * }\n * }\n * ```\n */\nexport function injectFieldsSupportingAddons(): FieldAddonSupportEntry[] {\n const registry = inject(FIELD_REGISTRY);\n const entries: FieldAddonSupportEntry[] = [];\n for (const [name, def] of registry.entries()) {\n if (def.addons === undefined) continue;\n entries.push({ name, slots: def.addons.slots, allowedTypes: def.addons.allowedTypes });\n }\n return entries;\n}\n","import { WritableSignal } from '@angular/core';\nimport { AddonActionContext, AddonActionPreset } from '../models/addon/addon-action';\nimport type { Logger } from '../providers/features/logger/logger.interface';\n\n/**\n * Optional collaborators a preset may need beyond the base context. Passed\n * by the button kind component which has access to its own DI scope.\n */\nexport interface PresetCollaborators {\n /** Per-field input-type override signal — populated only inside an input field. */\n readonly typeOverride?: WritableSignal<string | undefined>;\n /** Writer that pushes a value into the host field's tree. */\n readonly fieldValueSetter?: (next: unknown) => void;\n /**\n * Read the host field's configured default value (from the form's\n * `defaultValues` map keyed by `field.key`). Used by the `'reset'`\n * preset; absent when the addon is rendered outside an active field\n * context, in which case `'reset'` falls back to clearing.\n */\n readonly fieldDefaultValueGetter?: () => unknown;\n /**\n * Baseline configured input type (from `props().type`), read at preset-run\n * time. Used by `toggle-password-visibility` to refuse to corrupt\n * `'email'`/`'number'`/etc. inputs into `'password'` when no override is\n * yet set.\n */\n readonly baselineType?: () => string | undefined;\n /** Logger for warnings about presets that can't be fulfilled in this context. */\n readonly logger: Logger;\n}\n\n/**\n * Apply a built-in preset action against the addon's host field. Shared\n * across all adapters — each adapter's `ADDON_PRESET_HANDLER` provider\n * delegates here.\n *\n * @param adapterLabel Used in warnings — `'Material'` / `'Bootstrap'` / etc.\n * @param fieldLabel Used in warnings — `'mat-input'` / `'bs-input'` / etc.\n */\nexport async function runPresetAction(\n preset: AddonActionPreset,\n ctx: AddonActionContext,\n collaborators: PresetCollaborators,\n adapterLabel: string,\n fieldLabel: string,\n): Promise<void> {\n // Guard against orphan dispatches — none of the built-in presets do\n // anything useful without a host field, except `copy` and `paste` which\n // only need the clipboard API. The canonical discriminant per the\n // `AddonActionContext` discriminated union is `ctx.form` — non-null on\n // the field-bound variant, null on the orphan variant. `field.key` was\n // unreliable for nested-array scenarios where keys can legitimately be\n // empty strings.\n if (ctx.form === null && preset !== 'copy' && preset !== 'paste') {\n collaborators.logger.warn(`preset '${String(preset)}' fired without a host field context — ignoring.`);\n return;\n }\n\n // A disabled field is non-editable; value-mutating presets must not change it, consistent with\n // the rest of the library treating disabled fields as non-interactive. `readonly` is not exposed\n // on the field-bound `ReadonlyFieldTree`, so the guard is scoped to `disabled`.\n if (ctx.form !== null && (preset === 'clear' || preset === 'reset' || preset === 'paste') && ctx.form.disabled()) {\n collaborators.logger.warn(`preset '${String(preset)}' skipped — host '${fieldLabel}' is disabled.`);\n return;\n }\n\n switch (preset) {\n case 'clear': {\n // Preserve type semantics: strings clear to `''` (browser-friendly for\n // input.value), everything else clears to `undefined` (numeric / object\n // / date fields would otherwise become a string and break their type).\n const next = typeof ctx.value === 'string' ? '' : undefined;\n collaborators.fieldValueSetter?.(next);\n return;\n }\n\n case 'reset': {\n const defaultValue = collaborators.fieldDefaultValueGetter?.();\n collaborators.fieldValueSetter?.(defaultValue !== undefined ? defaultValue : typeof ctx.value === 'string' ? '' : undefined);\n return;\n }\n\n case 'paste': {\n if (typeof navigator === 'undefined' || !navigator.clipboard) {\n collaborators.logger.warn(`preset 'paste' requires a browser clipboard API (not available in this environment).`);\n return;\n }\n try {\n const text = await navigator.clipboard.readText();\n collaborators.fieldValueSetter?.(text);\n } catch (error) {\n collaborators.logger.warn(`preset 'paste' clipboard read failed: ${error instanceof Error ? error.message : String(error)}`);\n }\n return;\n }\n\n case 'copy': {\n if (typeof navigator === 'undefined' || !navigator.clipboard) {\n collaborators.logger.warn(`preset 'copy' requires a browser clipboard API (not available in this environment).`);\n return;\n }\n try {\n // Explicit null check — `String(null) === 'null'` would write the\n // literal string \"null\" to the clipboard.\n const v = ctx.value;\n await navigator.clipboard.writeText(v == null ? '' : String(v));\n } catch (error) {\n collaborators.logger.warn(`preset 'copy' clipboard write failed: ${error instanceof Error ? error.message : String(error)}`);\n }\n return;\n }\n\n case 'toggle-password-visibility': {\n const override = collaborators.typeOverride;\n if (!override) {\n collaborators.logger.warn(`preset 'toggle-password-visibility' has no effect outside a '${fieldLabel}' field.`);\n return;\n }\n // Refuse to flip non-password inputs. Re-check on EVERY dispatch (not\n // just when override is undefined) because `props().type` can change\n // reactively after the first toggle — e.g., 'password' → 'email'. Once\n // the override is touched, only the override is canonical for the\n // current visible/hidden state; the baseline still gates whether\n // toggling makes semantic sense for this field.\n const baseline = collaborators.baselineType?.();\n if (baseline !== undefined && baseline !== 'password') {\n collaborators.logger.warn(\n `preset 'toggle-password-visibility' refused: host '${fieldLabel}' has type '${baseline}', not 'password'.`,\n );\n return;\n }\n const current = override();\n override.set(current === 'text' ? 'password' : 'text');\n return;\n }\n\n default:\n collaborators.logger.warn(`preset '${String(preset)}' is not implemented in the ${adapterLabel} adapter.`);\n }\n}\n","import type { AsyncDerivationFunction } from '../../core/expressions/async-custom-function-types';\nimport type { CustomFunction } from '../../core/expressions/custom-function-types';\nimport { ConditionalExpression } from '../expressions/conditional-expression';\nimport { HttpRequestConfig } from '../http/http-request-config';\n\n/** Special form-state conditions for button disabled logic. */\nexport type FormStateCondition =\n /** True when form.valid() === false */\n | 'formInvalid'\n /** True when form.submitting() === true */\n | 'formSubmitting'\n /** True when fields on the current page are invalid (for paged forms) */\n | 'pageInvalid';\n\n/** Logic type for controlling field state (hidden, readonly, disabled, required). */\nexport type StateLogicType = 'hidden' | 'readonly' | 'disabled' | 'required';\n\n/**\n * Base configuration for conditional field state logic.\n *\n * @internal\n */\ninterface BaseStateLogicConfig {\n /** Logic type identifier for field state. */\n type: StateLogicType;\n\n /** Condition that determines when this logic applies. */\n condition: ConditionalExpression | boolean | FormStateCondition;\n}\n\n/**\n * State logic that evaluates immediately on change (default).\n *\n * @internal\n */\ninterface ImmediateStateLogicConfig extends BaseStateLogicConfig {\n /**\n * Trigger for immediate evaluation.\n *\n * @default 'onChange'\n */\n trigger?: 'onChange';\n /** Not allowed for onChange trigger */\n debounceMs?: never;\n}\n\n/**\n * State logic that evaluates after a debounce period.\n *\n * @internal\n */\ninterface DebouncedStateLogicConfig extends BaseStateLogicConfig {\n /**\n * Trigger for debounced evaluation.\n * Evaluates after the value has stabilized for the debounce duration.\n */\n trigger: 'debounced';\n /**\n * Debounce duration in milliseconds.\n *\n * @default 500\n */\n debounceMs?: number;\n}\n\n/** Configuration for conditional field state logic. */\nexport type StateLogicConfig = ImmediateStateLogicConfig | DebouncedStateLogicConfig;\n\n/** Trigger timing for when logic is evaluated. */\nexport type LogicTrigger = 'onChange' | 'debounced';\n\n/**\n * Shared fields that appear on all derivation logic config variants.\n *\n * @internal\n */\ninterface SharedDerivationFields {\n /** Logic type identifier for value derivation. */\n type: 'derivation';\n\n /** Target property name for property derivation. */\n targetProperty?: string;\n\n /** Optional name for this derivation for debugging purposes. */\n debugName?: string;\n\n /** Condition that determines when this derivation applies. */\n condition?: ConditionalExpression | boolean;\n\n /**\n * When true, the derivation stops running after the user manually\n * edits the target field.\n */\n stopOnUserOverride?: boolean;\n\n /**\n * When true (and `stopOnUserOverride` is also true), clears the\n * user-override flag when any dependency of this derivation changes,\n * allowing the derivation to run again.\n */\n reEngageOnDependencyChange?: boolean;\n}\n\n/**\n * Trigger variants for derivation timing.\n *\n * @internal\n */\ntype ImmediateDerivationTrigger = { trigger?: 'onChange'; debounceMs?: never };\ntype DebouncedDerivationTrigger = { trigger: 'debounced'; debounceMs?: number };\n\n// ============================================================================\n// HTTP derivation mode (source: 'http' — required; dependsOn/responseExpression — required)\n// ============================================================================\n\n/**\n * Base for HTTP derivations. `source: 'http'` is required.\n * TypeScript enforces `dependsOn` and `responseExpression` at the type level.\n *\n * @internal\n */\ninterface HttpDerivationBase extends SharedDerivationFields {\n /** Identifies this derivation as HTTP-driven. */\n source: 'http';\n /** Forbidden on HTTP source */\n fn?: never;\n /** Forbidden on HTTP source */\n asyncFn?: never;\n /** HTTP request configuration for server-driven derivations. */\n http: HttpRequestConfig;\n /**\n * Explicit field dependencies. Required for HTTP derivations to prevent\n * wildcard triggering on every keystroke.\n */\n dependsOn: string[];\n /** Expression to extract the derived value from the HTTP response. */\n responseExpression: string;\n // Mutual exclusivity: other sources are not allowed\n value?: never;\n expression?: never;\n functionName?: never;\n asyncFunctionName?: never;\n}\n\n// ============================================================================\n// Async function derivation mode (source: 'asyncFunction' — required; dependsOn — required)\n// ============================================================================\n\n/**\n * Shared fields for both branches of {@link AsyncFunctionDerivationBase}.\n *\n * @internal\n */\ninterface AsyncFunctionDerivationShared extends SharedDerivationFields {\n /** Identifies this derivation as async-function-driven. */\n source: 'asyncFunction';\n /** Forbidden on asyncFunction source */\n fn?: never;\n /**\n * Explicit field dependencies. Required for async derivations to prevent\n * wildcard triggering on every form change.\n */\n dependsOn: string[];\n // Mutual exclusivity: other sources are not allowed\n value?: never;\n expression?: never;\n functionName?: never;\n http?: never;\n responseExpression?: never;\n}\n\n/**\n * Base for async function derivations. `source: 'asyncFunction'` is required.\n * TypeScript enforces `dependsOn` at the type level, and `asyncFunctionName` /\n * `asyncFn` are mutually exclusive (XOR).\n *\n * @internal\n */\ntype AsyncFunctionDerivationBase =\n | (AsyncFunctionDerivationShared & {\n /** Name of a registered async derivation function. */\n asyncFunctionName: string;\n /** Inline form is forbidden when `asyncFunctionName` is set */\n asyncFn?: never;\n })\n | (AsyncFunctionDerivationShared & {\n /** Inline async derivation function. Mutually exclusive with `asyncFunctionName`. */\n asyncFn: AsyncDerivationFunction;\n /** Registered form is forbidden when `asyncFn` is set */\n asyncFunctionName?: never;\n });\n\n// ============================================================================\n// Sync derivation modes\n// ============================================================================\n\n/**\n * Base for expression derivations.\n *\n * @internal\n */\ninterface ExpressionDerivationBase extends SharedDerivationFields {\n source?: never;\n /** Forbidden on expression mode */\n fn?: never;\n /** Forbidden on expression mode */\n asyncFn?: never;\n /** JavaScript expression to evaluate for the derived value. */\n expression: string;\n /** Explicit field dependencies for expressions. */\n dependsOn?: string[];\n // Mutual exclusivity: other mode payload fields are not allowed\n value?: never;\n functionName?: never;\n http?: never;\n asyncFunctionName?: never;\n responseExpression?: never;\n}\n\n/**\n * Base for value derivations.\n *\n * @internal\n */\ninterface ValueDerivationBase extends SharedDerivationFields {\n source?: never;\n /** Forbidden on value mode */\n fn?: never;\n /** Forbidden on value mode */\n asyncFn?: never;\n /** Static value to set on this field. */\n value: unknown;\n /** Explicit field dependencies for value derivations. */\n dependsOn?: string[];\n // Mutual exclusivity: other mode payload fields are not allowed\n expression?: never;\n functionName?: never;\n http?: never;\n asyncFunctionName?: never;\n responseExpression?: never;\n}\n\n/**\n * Shared fields for both branches of {@link FunctionDerivationBase}.\n *\n * @internal\n */\ninterface FunctionDerivationShared extends SharedDerivationFields {\n source?: never;\n /** Forbidden on function-derivation mode */\n asyncFn?: never;\n /** Explicit field dependencies for function derivations. */\n dependsOn?: string[];\n // Mutual exclusivity: other mode payload fields are not allowed\n value?: never;\n expression?: never;\n http?: never;\n asyncFunctionName?: never;\n responseExpression?: never;\n}\n\n/**\n * Base for custom sync function derivations. `functionName` and `fn` are\n * mutually exclusive (XOR).\n *\n * @internal\n */\ntype FunctionDerivationBase =\n | (FunctionDerivationShared & {\n /** Name of a registered custom derivation function. */\n functionName: string;\n /** Inline form is forbidden when `functionName` is set */\n fn?: never;\n })\n | (FunctionDerivationShared & {\n /** Inline custom derivation function. Mutually exclusive with `functionName`. */\n fn: CustomFunction;\n /** Registered form is forbidden when `fn` is set */\n functionName?: never;\n });\n\n/**\n * HTTP derivation that evaluates immediately on change (default).\n *\n * @internal\n */\ntype OnChangeHttpDerivationLogicConfig = HttpDerivationBase & ImmediateDerivationTrigger;\n\n/**\n * HTTP derivation that evaluates after a debounce period.\n *\n * @internal\n */\ntype DebouncedHttpDerivationLogicConfig = HttpDerivationBase & DebouncedDerivationTrigger;\n\n/**\n * Async function derivation that evaluates immediately on change (default).\n *\n * @internal\n */\ntype OnChangeAsyncFunctionDerivationLogicConfig = AsyncFunctionDerivationBase & ImmediateDerivationTrigger;\n\n/**\n * Async function derivation that evaluates after a debounce period.\n *\n * @internal\n */\ntype DebouncedAsyncFunctionDerivationLogicConfig = AsyncFunctionDerivationBase & DebouncedDerivationTrigger;\n\n/**\n * Expression derivation that evaluates immediately on change (default).\n *\n * @internal\n */\ntype OnChangeExpressionDerivationLogicConfig = ExpressionDerivationBase & ImmediateDerivationTrigger;\n\n/**\n * Expression derivation that evaluates after a debounce period.\n *\n * @internal\n */\ntype DebouncedExpressionDerivationLogicConfig = ExpressionDerivationBase & DebouncedDerivationTrigger;\n\n/**\n * Value derivation that evaluates immediately on change (default).\n *\n * @internal\n */\ntype OnChangeValueDerivationLogicConfig = ValueDerivationBase & ImmediateDerivationTrigger;\n\n/**\n * Value derivation that evaluates after a debounce period.\n *\n * @internal\n */\ntype DebouncedValueDerivationLogicConfig = ValueDerivationBase & DebouncedDerivationTrigger;\n\n/**\n * Function derivation that evaluates immediately on change (default).\n *\n * @internal\n */\ntype OnChangeFunctionDerivationLogicConfig = FunctionDerivationBase & ImmediateDerivationTrigger;\n\n/**\n * Function derivation that evaluates after a debounce period.\n *\n * @internal\n */\ntype DebouncedFunctionDerivationLogicConfig = FunctionDerivationBase & DebouncedDerivationTrigger;\n\n/** Configuration for value derivation logic. */\nexport type DerivationLogicConfig =\n | OnChangeHttpDerivationLogicConfig\n | DebouncedHttpDerivationLogicConfig\n | OnChangeAsyncFunctionDerivationLogicConfig\n | DebouncedAsyncFunctionDerivationLogicConfig\n | OnChangeExpressionDerivationLogicConfig\n | DebouncedExpressionDerivationLogicConfig\n | OnChangeValueDerivationLogicConfig\n | DebouncedValueDerivationLogicConfig\n | OnChangeFunctionDerivationLogicConfig\n | DebouncedFunctionDerivationLogicConfig;\n\n/** Union type for all logic configurations. */\nexport type LogicConfig = StateLogicConfig | DerivationLogicConfig;\n\n/** Log level for derivation debug output. */\nexport type DerivationLogLevel = 'none' | 'summary' | 'verbose';\n\n/** Configuration for derivation debug logging. */\nexport interface DerivationLogConfig {\n /** Log level for derivation debugging. Defaults to 'none'. */\n level: DerivationLogLevel;\n}\n\n/**\n * Creates the default derivation log configuration.\n *\n * @returns Default DerivationLogConfig\n */\nexport function createDefaultDerivationLogConfig(): DerivationLogConfig {\n return {\n level: 'none',\n };\n}\n\n/**\n * Checks if logging should occur at the specified level.\n *\n * @param config - Current log configuration\n * @param minLevel - Minimum level required for logging\n * @returns True if logging should occur\n */\nexport function shouldLog(config: DerivationLogConfig, minLevel: 'summary' | 'verbose'): boolean {\n if (config.level === 'none') return false;\n if (minLevel === 'summary') return config.level === 'summary' || config.level === 'verbose';\n return config.level === 'verbose';\n}\n\n/**\n * Type guard to check if a condition is a FormStateCondition.\n *\n * @param condition - The condition to check\n * @returns true if the condition is a FormStateCondition\n */\nexport function isFormStateCondition(\n condition: StateLogicConfig['condition'] | DerivationLogicConfig['condition'],\n): condition is FormStateCondition {\n return typeof condition === 'string' && ['formInvalid', 'formSubmitting', 'pageInvalid'].includes(condition);\n}\n\n/**\n * Type guard to check if a logic config is a StateLogicConfig.\n *\n * @param config - The logic config to check\n * @returns true if the config is for field state logic\n */\nexport function isStateLogicConfig(config: LogicConfig): config is StateLogicConfig {\n return config.type === 'hidden' || config.type === 'readonly' || config.type === 'disabled' || config.type === 'required';\n}\n\n/**\n * Type guard to check if a logic config is a DerivationLogicConfig.\n *\n * @param config - The logic config to check\n * @returns true if the config is for value derivation\n */\nexport function isDerivationLogicConfig(config: LogicConfig): config is DerivationLogicConfig {\n return config.type === 'derivation';\n}\n\n/**\n * Type guard to check if a derivation config targets a property rather than the field value.\n *\n * @param config - The derivation logic config to check\n * @returns true if the config has a targetProperty (property derivation)\n */\nexport function hasTargetProperty(config: DerivationLogicConfig): config is DerivationLogicConfig & { targetProperty: string } {\n return 'targetProperty' in config && typeof config.targetProperty === 'string' && config.targetProperty.length > 0;\n}\n","import { ConditionalExpression } from '../../models/expressions/conditional-expression';\nimport { FormStateCondition, StateLogicConfig } from '../../models/logic/logic-config';\nimport { isFormStateCondition } from '../../models/logic';\nimport { CustomValidatorConfig, ValidatorConfig } from '../../models/validation/validator-config';\nimport { SchemaApplicationConfig } from '../../models/schemas/schema-definition';\nimport { CustomFunctionScope } from '../expressions/custom-function-types';\n\n/**\n * Regular expression to detect formValue property access in expressions.\n * Matches patterns like: formValue.fieldName, formValue['fieldName'], formValue[\"fieldName\"]\n */\nconst FORM_VALUE_ACCESS_PATTERN = /\\bformValue\\s*(?:\\.|\\[)/;\n\n/** Combined regex for extracting field paths from formValue expressions. */\nconst FORM_VALUE_PATTERN = /\\bformValue\\s*(?:\\.([\\w.]+)|\\[\\s*'([\\w.-]+)'\\s*\\]|\\[\\s*\"([\\w.-]+)\"\\s*\\])/g;\n\n/** Context for cross-field detection, providing access to function scope information. */\nexport interface CrossFieldDetectionContext {\n /**\n * Lookup function to get the scope of a registered custom function.\n *\n * @param functionName The name of the custom function\n * @returns The function's scope, or undefined if not registered\n */\n getFunctionScope?: (functionName: string) => CustomFunctionScope | undefined;\n}\n\n/**\n * Detects if a ConditionalExpression references other fields (cross-field).\n *\n * @param expr The conditional expression to analyze\n * @param context Optional context providing function scope lookup\n * @returns true if the expression references other fields\n */\nexport function isCrossFieldExpression(\n expr: ConditionalExpression | boolean | FormStateCondition | undefined,\n context?: CrossFieldDetectionContext,\n): boolean {\n // Boolean, undefined, or FormStateCondition (form-level state checks) are not cross-field\n if (expr === undefined || typeof expr === 'boolean' || isFormStateCondition(expr)) {\n return false;\n }\n\n switch (expr.type) {\n case 'fieldValue':\n // fieldPath means it references another field's value\n return !!expr.fieldPath;\n\n case 'javascript':\n // Check for formValue.* patterns in the expression string\n return FORM_VALUE_ACCESS_PATTERN.test(expr.expression || '');\n\n case 'custom': {\n // For custom functions, determine scope from registry or default to cross-field.\n // Look up function scope from registry if context is provided\n const functionName = expr.functionName;\n if (context?.getFunctionScope && functionName) {\n const scope = context.getFunctionScope(functionName);\n if (scope === 'field') {\n // Function is explicitly marked as field-scoped (no cross-field deps)\n return false;\n }\n // 'form' scope or undefined → treat as cross-field\n }\n\n // Default: conservative approach - assume cross-field\n return true;\n }\n\n case 'async':\n // Async conditions create their own toObservable → toSignal pipeline\n // and manage reactivity independently — not cross-field in the sync graph\n return false;\n\n case 'and':\n case 'or':\n // Recursively check nested conditions, passing context through\n return (expr.conditions || []).some((c) => isCrossFieldExpression(c, context));\n\n default:\n return false;\n }\n}\n\n/**\n * Extracts field dependencies from a ConditionalExpression.\n *\n * @param expr The conditional expression to analyze\n * @returns Array of field keys that this expression depends on\n */\nexport function extractExpressionDependencies(expr: ConditionalExpression | boolean | undefined): string[] {\n if (expr === undefined || typeof expr === 'boolean') {\n return [];\n }\n\n const deps = new Set<string>();\n\n switch (expr.type) {\n case 'fieldValue':\n if (expr.fieldPath) {\n // Extract root field name (before any dots for nested paths)\n deps.add(expr.fieldPath.split('.')[0]);\n }\n break;\n\n case 'javascript':\n // Extract from formValue.* patterns in expression\n extractFromExpressionString(expr.expression || '', deps);\n break;\n\n case 'custom':\n // Custom functions have full form access - conservative approach\n deps.add('*');\n break;\n\n case 'async':\n // Async conditions manage their own reactivity — no auto-extractable dependencies\n break;\n\n case 'and':\n case 'or':\n // Recursively extract from nested conditions\n for (const condition of expr.conditions || []) {\n extractExpressionDependencies(condition).forEach((d) => deps.add(d));\n }\n break;\n }\n\n return Array.from(deps);\n}\n\n/**\n * Extracts field names from a JavaScript expression string.\n * Handles both dot notation (formValue.field) and bracket notation (formValue['field']).\n */\nexport function extractStringDependencies(expression: string): string[] {\n const deps = new Set<string>();\n extractFromExpressionString(expression, deps);\n return Array.from(deps);\n}\n\n/** Internal helper that populates a Set with dependencies from an expression string. */\nfunction extractFromExpressionString(expression: string, deps: Set<string>): void {\n for (const match of expression.matchAll(FORM_VALUE_PATTERN)) {\n const fullPath = match[1] ?? match[2] ?? match[3];\n if (!fullPath) continue;\n // Always add the root field (first segment) as the primary dependency\n const rootField = fullPath.split('.')[0];\n deps.add(rootField);\n // Also add the full path for more precise dependency tracking if nested\n if (fullPath.includes('.')) {\n deps.add(fullPath);\n }\n }\n}\n\n// ============================================================================\n// Validator Detection\n// ============================================================================\n\n/**\n * Detects if a custom validator configuration references formValue (cross-field validation).\n *\n * @param config The custom validator configuration to check\n * @returns true if the validator references formValue\n */\nexport function isCrossFieldValidator(config: CustomValidatorConfig): boolean {\n if (!config.expression) {\n return false;\n }\n return FORM_VALUE_ACCESS_PATTERN.test(config.expression);\n}\n\n/**\n * Detects if a built-in validator has a dynamic expression that references formValue.\n *\n * @param config The validator configuration to check\n * @returns true if the validator has a cross-field dynamic expression\n */\nexport function isCrossFieldBuiltInValidator(config: ValidatorConfig): boolean {\n if (config.type === 'custom') {\n return false; // Custom validators handled separately\n }\n\n // Check if expression property references formValue\n if ('expression' in config && typeof config.expression === 'string') {\n return FORM_VALUE_ACCESS_PATTERN.test(config.expression);\n }\n\n return false;\n}\n\n/** Checks if a validator uses Angular's resource API (validateHttp / validateAsync). */\nexport function isResourceBasedValidator(config: ValidatorConfig): boolean {\n return config.type === 'async' || config.type === 'http';\n}\n\n/**\n * Detects if a validator's `when` condition is cross-field.\n *\n * @param config The validator configuration to check\n * @param context Optional context providing function scope lookup\n * @returns true if the when condition references other fields\n */\nexport function hasCrossFieldWhenCondition(config: ValidatorConfig, context?: CrossFieldDetectionContext): boolean {\n if (!('when' in config) || !config.when) {\n return false;\n }\n return isCrossFieldExpression(config.when as ConditionalExpression, context);\n}\n\n// ============================================================================\n// Logic Detection\n// ============================================================================\n\n/**\n * Detects if a StateLogicConfig has a cross-field condition.\n *\n * @param config The state logic configuration to check\n * @param context Optional context providing function scope lookup\n * @returns true if the logic condition references other fields\n */\nexport function isCrossFieldStateLogic(config: StateLogicConfig, context?: CrossFieldDetectionContext): boolean {\n return isCrossFieldExpression(config.condition, context);\n}\n\n// ============================================================================\n// Schema Detection\n// ============================================================================\n\n/**\n * Detects if a SchemaApplicationConfig has a cross-field condition.\n *\n * @param config The schema application configuration to check\n * @param context Optional context providing function scope lookup\n * @returns true if the schema condition references other fields\n */\nexport function isCrossFieldSchema(config: SchemaApplicationConfig, context?: CrossFieldDetectionContext): boolean {\n if (config.type !== 'applyWhen' || !config.condition) {\n return false;\n }\n return isCrossFieldExpression(config.condition, context);\n}\n","import { type Signal, isSignal, untracked } from '@angular/core';\nimport type { FieldTree, ReadonlyFieldState } from '@angular/forms/signals';\nimport type { FieldStateInfo, FormFieldStateMap } from '../../models/expressions/field-state-context';\nimport type { FieldTreeRecord } from '../field-tree-utils';\n\n/**\n * Known state properties on an Angular Signal Forms FieldState.\n * Each is a Signal<boolean> on the FieldState interface.\n */\nconst STATE_PROPERTIES = ['touched', 'dirty', 'valid', 'invalid', 'pending', 'hidden', 'readonly', 'disabled'] as const;\n\ntype StateProperty = (typeof STATE_PROPERTIES)[number];\n\n/**\n * Reads a boolean signal from a FieldState, respecting the reactive flag.\n *\n * @internal\n */\nfunction readStateSignal(fieldState: ReadonlyFieldState<unknown>, prop: StateProperty, reactive: boolean): boolean {\n const signalFn: Signal<boolean> | undefined = fieldState[prop];\n if (!signalFn || !isSignal(signalFn)) return false;\n return reactive ? !!signalFn() : !!untracked(signalFn);\n}\n\n/**\n * Resolves a field source (FieldTree or FieldState) to its FieldState.\n *\n * @internal\n */\nfunction resolveFieldState(\n fieldSource: FieldTree<unknown> | ReadonlyFieldState<unknown> | undefined,\n reactive: boolean,\n): ReadonlyFieldState<unknown> | undefined {\n if (!fieldSource) return undefined;\n\n // FieldTree: signal or callable Proxy — invoke to get the FieldState\n if (typeof fieldSource === 'function') {\n return reactive ? (fieldSource as FieldTree<unknown>)() : untracked(fieldSource as FieldTree<unknown>);\n }\n\n // Direct FieldState object (from FieldContext)\n if (typeof fieldSource === 'object') {\n return fieldSource as ReadonlyFieldState<unknown>;\n }\n\n return undefined;\n}\n\n/**\n * Creates a lazy FieldStateInfo that only reads state properties when accessed.\n *\n * @param fieldSource - A FieldTree or direct FieldState instance\n * @param reactive - If true, reads signals reactively (creates dependencies).\n * If false, reads signals with `untracked()` (no dependencies).\n * @returns A FieldStateInfo proxy with lazy property access, or undefined if the source is invalid\n * @internal\n */\nexport function readFieldStateInfo(\n fieldSource: FieldTree<unknown> | ReadonlyFieldState<unknown> | undefined,\n reactive: boolean,\n): FieldStateInfo | undefined {\n const state = resolveFieldState(fieldSource, reactive);\n if (!state || typeof state !== 'object') return undefined;\n\n const knownProperties = new Set<string>([...STATE_PROPERTIES, 'pristine']);\n\n return new Proxy({} as FieldStateInfo, {\n get(_target, prop: string): boolean {\n if (prop === 'pristine') {\n return !readStateSignal(state, 'dirty', reactive);\n }\n if (STATE_PROPERTIES.includes(prop as StateProperty)) {\n return readStateSignal(state, prop as StateProperty, reactive);\n }\n return false;\n },\n has(_target, prop: string): boolean {\n return knownProperties.has(prop);\n },\n });\n}\n\n/**\n * Creates a Proxy-based FormFieldStateMap for accessing any field's state by key.\n *\n * @param rootForm - The root FieldTree of the form\n * @param reactive - If true, reads signals reactively (creates dependencies).\n * If false, reads signals with `untracked()` (no dependencies).\n * @returns A FormFieldStateMap proxy\n * @internal\n */\nexport function createFormFieldStateMap(rootForm: FieldTree<unknown>, reactive: boolean): FormFieldStateMap {\n const cache = new Map<string, FieldStateInfo | undefined>();\n\n return new Proxy({} as FormFieldStateMap, {\n get(_target, key: string): FieldStateInfo | undefined {\n if (cache.has(key)) {\n return cache.get(key);\n }\n\n const fieldAccessor = navigateToFieldAccessor(rootForm, key);\n const snapshot = readFieldStateInfo(fieldAccessor, reactive);\n cache.set(key, snapshot);\n return snapshot;\n },\n has(_target, key: string): boolean {\n return navigateToFieldAccessor(rootForm, key) !== undefined;\n },\n });\n}\n\n/**\n * Navigates the form tree to find a field accessor (FieldTree) for the given key path.\n *\n * @internal\n */\nfunction navigateToFieldAccessor(rootForm: FieldTree<unknown>, keyPath: string): FieldTree<unknown> | undefined {\n if (!keyPath) return undefined;\n\n const parts = keyPath.split('.');\n // FieldTree<T> = (() => FieldState<T>) & Subfields<T>, so sub-fields\n // are directly on the FieldTree — no need to call it during navigation\n let current = rootForm as FieldTreeRecord;\n\n for (let i = 0; i < parts.length - 1; i++) {\n const next = current[parts[i]];\n if (!next) return undefined;\n current = next as FieldTreeRecord;\n }\n\n return current[parts[parts.length - 1]];\n}\n","import { LogicFn } from '@angular/forms/signals';\n\n/** DI-scoped cache for async condition logic functions. */\nexport class AsyncConditionFunctionCacheService {\n /** Cache for async condition logic functions, keyed by serialized condition. */\n readonly asyncConditionFunctionCache = new Map<string, LogicFn<unknown, boolean>>();\n}\n","/**\n * Compares two values using the specified comparison operator.\n *\n * @param actual - The value to compare\n * @param expected - The value to compare against\n * @param operator - Comparison operator to use\n * @returns True if the comparison succeeds, false otherwise\n */\nexport function compareValues(actual: unknown, expected: unknown, operator: string): boolean {\n switch (operator) {\n case 'equals':\n return actual === expected;\n case 'notEquals':\n return actual !== expected;\n case 'greater':\n return Number(actual) > Number(expected);\n case 'less':\n return Number(actual) < Number(expected);\n case 'greaterOrEqual':\n return Number(actual) >= Number(expected);\n case 'lessOrEqual':\n return Number(actual) <= Number(expected);\n case 'contains':\n return String(actual).includes(String(expected));\n case 'startsWith':\n return String(actual).startsWith(String(expected));\n case 'endsWith':\n return String(actual).endsWith(String(expected));\n case 'matches':\n try {\n return toRegExp(String(expected)).test(String(actual));\n } catch {\n return false;\n }\n default:\n return false;\n }\n}\n\n/** Builds a RegExp, supporting the `/pattern/flags` literal form (e.g. `/john/i`) for flags like `i`. */\nfunction toRegExp(value: string): RegExp {\n const literal = /^\\/(.+)\\/([dgimsuvy]*)$/.exec(value);\n return literal ? new RegExp(literal[1], literal[2]) : new RegExp(value);\n}\n\n/**\n * Retrieves a nested value from an object using dot notation path.\n *\n * @param obj - The object to traverse\n * @param path - Dot-separated path to the desired property\n * @returns The value at the specified path, or undefined if not found\n */\nexport function getNestedValue(obj: unknown, path: string): unknown {\n return path.split('.').reduce((current: unknown, key: string) => {\n return current && typeof current === 'object' ? (current as Record<string, unknown>)[key] : undefined;\n }, obj);\n}\n\n/**\n * Checks whether a nested property exists in an object using dot notation path.\n *\n * @param obj - The object to check\n * @param path - Dot-separated path to the property\n * @returns True if the property exists (even if its value is undefined)\n */\nexport function hasNestedProperty(obj: unknown, path: string): boolean {\n const keys = path.split('.');\n let current: unknown = obj;\n\n for (let i = 0; i < keys.length - 1; i++) {\n if (!current || typeof current !== 'object') return false;\n current = (current as Record<string, unknown>)[keys[i]];\n }\n\n if (!current || typeof current !== 'object') return false;\n return keys[keys.length - 1] in (current as Record<string, unknown>);\n}\n","import { DynamicFormError } from '../../../errors/dynamic-form-error';\n\n/** Token types for expression parsing */\nexport enum TokenType {\n // Literals\n NUMBER = 'NUMBER',\n STRING = 'STRING',\n TRUE = 'TRUE',\n FALSE = 'FALSE',\n NULL = 'NULL',\n UNDEFINED = 'UNDEFINED',\n\n // Identifiers\n IDENTIFIER = 'IDENTIFIER',\n\n // Operators\n PLUS = 'PLUS',\n MINUS = 'MINUS',\n MULTIPLY = 'MULTIPLY',\n DIVIDE = 'DIVIDE',\n MODULO = 'MODULO',\n\n // Comparison\n EQUAL = 'EQUAL',\n NOT_EQUAL = 'NOT_EQUAL',\n GREATER = 'GREATER',\n LESS = 'LESS',\n GREATER_EQUAL = 'GREATER_EQUAL',\n LESS_EQUAL = 'LESS_EQUAL',\n\n // Logical\n AND = 'AND',\n OR = 'OR',\n NOT = 'NOT',\n\n // Punctuation\n DOT = 'DOT',\n COMMA = 'COMMA',\n COLON = 'COLON',\n LPAREN = 'LPAREN',\n RPAREN = 'RPAREN',\n LBRACKET = 'LBRACKET',\n RBRACKET = 'RBRACKET',\n LBRACE = 'LBRACE',\n RBRACE = 'RBRACE',\n ARROW = 'ARROW',\n QUESTION = 'QUESTION',\n OPTIONAL_DOT = 'OPTIONAL_DOT',\n\n // Special\n EOF = 'EOF',\n}\n\n/** Token with type, value, and position */\nexport interface Token {\n type: TokenType;\n value: string;\n position: number;\n}\n\n/** AST Node types */\nexport type ASTNode =\n | LiteralNode\n | IdentifierNode\n | MemberAccessNode\n | ComputedMemberAccessNode\n | BinaryOpNode\n | UnaryOpNode\n | CallExpressionNode\n | ConditionalNode\n | ArrayLiteralNode\n | ObjectLiteralNode\n | ArrowFunctionNode;\n\nexport interface LiteralNode {\n type: 'Literal';\n value: string | number | boolean | null | undefined;\n}\n\nexport interface IdentifierNode {\n type: 'Identifier';\n name: string;\n}\n\nexport interface MemberAccessNode {\n type: 'MemberAccess';\n object: ASTNode;\n property: string;\n /** True when this access used `?.` (optional chaining). */\n optional?: boolean;\n}\n\nexport interface ComputedMemberAccessNode {\n type: 'ComputedMemberAccess';\n object: ASTNode;\n /** Expression evaluated at runtime to produce the property key. */\n property: ASTNode;\n /** True when this access used `?.[ ... ]` (optional chaining). */\n optional?: boolean;\n}\n\nexport interface ConditionalNode {\n type: 'Conditional';\n test: ASTNode;\n consequent: ASTNode;\n alternate: ASTNode;\n}\n\nexport interface BinaryOpNode {\n type: 'BinaryOp';\n operator: string;\n left: ASTNode;\n right: ASTNode;\n}\n\nexport interface UnaryOpNode {\n type: 'UnaryOp';\n operator: string;\n operand: ASTNode;\n}\n\nexport interface CallExpressionNode {\n type: 'CallExpression';\n callee: ASTNode;\n arguments: ASTNode[];\n /** True when this call used `?.( ... )` (optional chaining). */\n optional?: boolean;\n}\n\nexport interface ArrayLiteralNode {\n type: 'ArrayLiteral';\n elements: ASTNode[];\n}\n\nexport interface ObjectLiteralNode {\n type: 'ObjectLiteral';\n properties: Array<{ key: string; value: ASTNode }>;\n}\n\nexport interface ArrowFunctionNode {\n type: 'ArrowFunction';\n params: string[];\n body: ASTNode;\n}\n\n/** Parser error with position information */\nexport class ExpressionParserError extends DynamicFormError {\n constructor(\n message: string,\n public position: number,\n public expression: string,\n ) {\n super(`${message} at position ${position} in expression: ${expression}`);\n this.name = 'ExpressionParserError';\n }\n}\n","import { Token, TokenType, ExpressionParserError } from './types';\n\n/** Tokenizes an expression string into tokens */\nexport class Tokenizer {\n private position = 0;\n private readonly expression: string;\n\n constructor(expression: string) {\n this.expression = expression;\n }\n\n /** Tokenize the entire expression */\n tokenize(): Token[] {\n const tokens: Token[] = [];\n\n while (this.position < this.expression.length) {\n this.skipWhitespace();\n\n if (this.position >= this.expression.length) {\n break;\n }\n\n const token = this.nextToken();\n tokens.push(token);\n }\n\n tokens.push({ type: TokenType.EOF, value: '', position: this.position });\n return tokens;\n }\n\n private skipWhitespace(): void {\n while (this.position < this.expression.length && /\\s/.test(this.expression[this.position])) {\n this.position++;\n }\n }\n\n private nextToken(): Token {\n const char = this.expression[this.position];\n\n // String literals\n if (char === '\"' || char === \"'\") {\n return this.readString(char);\n }\n\n // Numbers\n if (/[0-9]/.test(char)) {\n return this.readNumber();\n }\n\n // Identifiers and keywords\n if (/[a-zA-Z_$]/.test(char)) {\n return this.readIdentifierOrKeyword();\n }\n\n // Operators and punctuation\n return this.readOperatorOrPunctuation();\n }\n\n private readString(quote: string): Token {\n const start = this.position;\n this.position++; // Skip opening quote\n\n let value = '';\n while (this.position < this.expression.length) {\n const char = this.expression[this.position];\n\n if (char === quote) {\n this.position++; // Skip closing quote\n return { type: TokenType.STRING, value, position: start };\n }\n\n if (char === '\\\\' && this.position + 1 < this.expression.length) {\n // Handle escape sequences\n this.position++;\n const nextChar = this.expression[this.position];\n switch (nextChar) {\n case 'n':\n value += '\\n';\n break;\n case 't':\n value += '\\t';\n break;\n case 'r':\n value += '\\r';\n break;\n case '\\\\':\n value += '\\\\';\n break;\n case quote:\n value += quote;\n break;\n default:\n value += nextChar;\n }\n this.position++;\n } else {\n value += char;\n this.position++;\n }\n }\n\n throw new ExpressionParserError('Unterminated string literal', start, this.expression);\n }\n\n private readNumber(): Token {\n const start = this.position;\n let value = '';\n\n while (this.position < this.expression.length && /[0-9.]/.test(this.expression[this.position])) {\n value += this.expression[this.position];\n this.position++;\n }\n\n if (!/^\\d+(\\.\\d+)?$/.test(value)) {\n throw new ExpressionParserError(`Invalid number: ${value}`, start, this.expression);\n }\n\n return { type: TokenType.NUMBER, value, position: start };\n }\n\n private readIdentifierOrKeyword(): Token {\n const start = this.position;\n let value = '';\n\n while (this.position < this.expression.length && /[a-zA-Z0-9_$]/.test(this.expression[this.position])) {\n value += this.expression[this.position];\n this.position++;\n }\n\n // Check for keywords\n const type = this.getKeywordType(value);\n return { type, value, position: start };\n }\n\n private getKeywordType(value: string): TokenType {\n switch (value) {\n case 'true':\n return TokenType.TRUE;\n case 'false':\n return TokenType.FALSE;\n case 'null':\n return TokenType.NULL;\n case 'undefined':\n return TokenType.UNDEFINED;\n default:\n return TokenType.IDENTIFIER;\n }\n }\n\n private readOperatorOrPunctuation(): Token {\n const start = this.position;\n const char = this.expression[this.position];\n const nextChar = this.position + 1 < this.expression.length ? this.expression[this.position + 1] : '';\n const thirdChar = this.position + 2 < this.expression.length ? this.expression[this.position + 2] : '';\n\n // Three-character operators - check these first!\n const threeChar = char + nextChar + thirdChar;\n if (threeChar === '===') {\n this.position += 3;\n return { type: TokenType.EQUAL, value: '===', position: start };\n }\n if (threeChar === '!==') {\n this.position += 3;\n return { type: TokenType.NOT_EQUAL, value: '!==', position: start };\n }\n\n // Two-character operators\n const twoChar = char + nextChar;\n switch (twoChar) {\n case '==':\n this.position += 2;\n return { type: TokenType.EQUAL, value: '==', position: start };\n case '!=':\n this.position += 2;\n return { type: TokenType.NOT_EQUAL, value: '!=', position: start };\n case '>=':\n this.position += 2;\n return { type: TokenType.GREATER_EQUAL, value: '>=', position: start };\n case '<=':\n this.position += 2;\n return { type: TokenType.LESS_EQUAL, value: '<=', position: start };\n case '&&':\n this.position += 2;\n return { type: TokenType.AND, value: '&&', position: start };\n case '||':\n this.position += 2;\n return { type: TokenType.OR, value: '||', position: start };\n case '=>':\n this.position += 2;\n return { type: TokenType.ARROW, value: '=>', position: start };\n case '?.':\n this.position += 2;\n return { type: TokenType.OPTIONAL_DOT, value: '?.', position: start };\n }\n\n // Single-character operators and punctuation\n this.position++;\n switch (char) {\n case '+':\n return { type: TokenType.PLUS, value: '+', position: start };\n case '-':\n return { type: TokenType.MINUS, value: '-', position: start };\n case '*':\n return { type: TokenType.MULTIPLY, value: '*', position: start };\n case '/':\n return { type: TokenType.DIVIDE, value: '/', position: start };\n case '%':\n return { type: TokenType.MODULO, value: '%', position: start };\n case '>':\n return { type: TokenType.GREATER, value: '>', position: start };\n case '<':\n return { type: TokenType.LESS, value: '<', position: start };\n case '!':\n return { type: TokenType.NOT, value: '!', position: start };\n case '.':\n return { type: TokenType.DOT, value: '.', position: start };\n case ',':\n return { type: TokenType.COMMA, value: ',', position: start };\n case ':':\n return { type: TokenType.COLON, value: ':', position: start };\n case '(':\n return { type: TokenType.LPAREN, value: '(', position: start };\n case ')':\n return { type: TokenType.RPAREN, value: ')', position: start };\n case '[':\n return { type: TokenType.LBRACKET, value: '[', position: start };\n case ']':\n return { type: TokenType.RBRACKET, value: ']', position: start };\n case '{':\n return { type: TokenType.LBRACE, value: '{', position: start };\n case '}':\n return { type: TokenType.RBRACE, value: '}', position: start };\n case '?':\n return { type: TokenType.QUESTION, value: '?', position: start };\n default:\n throw new ExpressionParserError(`Unexpected character: ${char}`, start, this.expression);\n }\n }\n}\n","import { Token, TokenType, ASTNode, ExpressionParserError } from './types';\nimport { Tokenizer } from './tokenizer';\n\n/**\n * Parses tokens into an Abstract Syntax Tree (AST)\n * Uses recursive descent parsing with operator precedence\n */\nexport class Parser {\n private tokens: Token[] = [];\n private current = 0;\n private readonly expression: string;\n\n constructor(expression: string) {\n this.expression = expression;\n }\n\n /** Parse the expression into an AST */\n parse(): ASTNode {\n const tokenizer = new Tokenizer(this.expression);\n this.tokens = tokenizer.tokenize();\n this.current = 0;\n\n if (this.isAtEnd()) {\n throw new ExpressionParserError('Empty expression', 0, this.expression);\n }\n\n const result = this.parseExpression();\n\n if (!this.isAtEnd()) {\n const token = this.peek();\n throw new ExpressionParserError(\n `Unexpected token '${token.value}' at position ${token.position} — expression has trailing content`,\n token.position,\n this.expression,\n );\n }\n\n return result;\n }\n\n private parseExpression(): ASTNode {\n // Try arrow function first — it has the lowest precedence and would otherwise\n // be ambiguous with grouped expressions / bare identifiers.\n const arrow = this.tryParseArrowFunction();\n if (arrow) return arrow;\n return this.parseConditional();\n }\n\n /**\n * Ternary `test ? consequent : alternate` — right-associative, sits between\n * the arrow-function check and logical-or in precedence.\n */\n private parseConditional(): ASTNode {\n const test = this.parseLogicalOr();\n if (!this.match(TokenType.QUESTION)) return test;\n\n const consequent = this.parseExpression();\n this.consume(TokenType.COLON, 'Expected \":\" between ternary branches');\n const alternate = this.parseExpression();\n return { type: 'Conditional', test, consequent, alternate };\n }\n\n /**\n * Attempts to parse an arrow function. Returns null and leaves position\n * unchanged if the next tokens don't form an arrow function head.\n */\n private tryParseArrowFunction(): ASTNode | null {\n const savedPos = this.current;\n\n // Single unparenthesized param: IDENTIFIER =>\n if (this.check(TokenType.IDENTIFIER) && this.checkAt(this.current + 1, TokenType.ARROW)) {\n const param = this.advance().value;\n this.advance(); // consume =>\n const body = this.parseExpression();\n return { type: 'ArrowFunction', params: [param], body };\n }\n\n // Parenthesized params: ( ) => or ( id (, id)* ) =>\n if (this.check(TokenType.LPAREN)) {\n this.advance(); // tentatively consume (\n const params: string[] = [];\n\n if (this.check(TokenType.RPAREN)) {\n this.advance(); // )\n } else if (this.check(TokenType.IDENTIFIER)) {\n params.push(this.advance().value);\n while (this.check(TokenType.COMMA)) {\n this.advance(); // ,\n if (!this.check(TokenType.IDENTIFIER)) {\n this.current = savedPos;\n return null;\n }\n params.push(this.advance().value);\n }\n if (!this.check(TokenType.RPAREN)) {\n this.current = savedPos;\n return null;\n }\n this.advance(); // )\n } else {\n this.current = savedPos;\n return null;\n }\n\n if (!this.check(TokenType.ARROW)) {\n this.current = savedPos;\n return null;\n }\n this.advance(); // =>\n\n const body = this.parseExpression();\n return { type: 'ArrowFunction', params, body };\n }\n\n return null;\n }\n\n private checkAt(index: number, type: TokenType): boolean {\n if (index < 0 || index >= this.tokens.length) return false;\n return this.tokens[index].type === type;\n }\n\n private parseLogicalOr(): ASTNode {\n let node = this.parseLogicalAnd();\n\n while (this.match(TokenType.OR)) {\n const operator = this.previous().value;\n const right = this.parseLogicalAnd();\n node = { type: 'BinaryOp', operator, left: node, right };\n }\n\n return node;\n }\n\n private parseLogicalAnd(): ASTNode {\n let node = this.parseEquality();\n\n while (this.match(TokenType.AND)) {\n const operator = this.previous().value;\n const right = this.parseEquality();\n node = { type: 'BinaryOp', operator, left: node, right };\n }\n\n return node;\n }\n\n private parseEquality(): ASTNode {\n let node = this.parseComparison();\n\n while (this.match(TokenType.EQUAL, TokenType.NOT_EQUAL)) {\n const operator = this.previous().value;\n const right = this.parseComparison();\n node = { type: 'BinaryOp', operator, left: node, right };\n }\n\n return node;\n }\n\n private parseComparison(): ASTNode {\n let node = this.parseAddition();\n\n while (this.match(TokenType.GREATER, TokenType.GREATER_EQUAL, TokenType.LESS, TokenType.LESS_EQUAL)) {\n const operator = this.previous().value;\n const right = this.parseAddition();\n node = { type: 'BinaryOp', operator, left: node, right };\n }\n\n return node;\n }\n\n private parseAddition(): ASTNode {\n let node = this.parseMultiplication();\n\n while (this.match(TokenType.PLUS, TokenType.MINUS)) {\n const operator = this.previous().value;\n const right = this.parseMultiplication();\n node = { type: 'BinaryOp', operator, left: node, right };\n }\n\n return node;\n }\n\n private parseMultiplication(): ASTNode {\n let node = this.parseUnary();\n\n while (this.match(TokenType.MULTIPLY, TokenType.DIVIDE, TokenType.MODULO)) {\n const operator = this.previous().value;\n const right = this.parseUnary();\n node = { type: 'BinaryOp', operator, left: node, right };\n }\n\n return node;\n }\n\n private parseUnary(): ASTNode {\n if (this.match(TokenType.NOT, TokenType.MINUS, TokenType.PLUS)) {\n const operator = this.previous().value;\n const operand = this.parseUnary();\n return { type: 'UnaryOp', operator, operand };\n }\n\n return this.parsePostfix();\n }\n\n private parsePostfix(): ASTNode {\n let node = this.parsePrimary();\n\n while (true) {\n if (this.match(TokenType.DOT)) {\n // Member access: obj.property\n if (!this.check(TokenType.IDENTIFIER)) {\n throw new ExpressionParserError('Expected property name after \".\"', this.peek().position, this.expression);\n }\n const property = this.advance().value;\n node = { type: 'MemberAccess', object: node, property };\n } else if (this.match(TokenType.OPTIONAL_DOT)) {\n // Optional chain — supports `?.prop`, `?.[expr]`, and `?.(args)`.\n if (this.match(TokenType.LBRACKET)) {\n const property = this.parseExpression();\n this.consume(TokenType.RBRACKET, 'Expected \"]\" after computed property');\n node = { type: 'ComputedMemberAccess', object: node, property, optional: true };\n } else if (this.match(TokenType.LPAREN)) {\n const args = this.parseArgumentList();\n this.consume(TokenType.RPAREN, 'Expected \")\" after arguments');\n node = { type: 'CallExpression', callee: node, arguments: args, optional: true };\n } else if (this.check(TokenType.IDENTIFIER)) {\n const property = this.advance().value;\n node = { type: 'MemberAccess', object: node, property, optional: true };\n } else {\n throw new ExpressionParserError('Expected property name, \"[\", or \"(\" after \"?.\"', this.peek().position, this.expression);\n }\n } else if (this.match(TokenType.LBRACKET)) {\n // Computed member access: obj[expr]\n const property = this.parseExpression();\n this.consume(TokenType.RBRACKET, 'Expected \"]\" after computed property');\n node = { type: 'ComputedMemberAccess', object: node, property };\n } else if (this.match(TokenType.LPAREN)) {\n // Function call: func()\n const args = this.parseArgumentList();\n this.consume(TokenType.RPAREN, 'Expected \")\" after arguments');\n node = { type: 'CallExpression', callee: node, arguments: args };\n } else {\n break;\n }\n }\n\n return node;\n }\n\n private parsePrimary(): ASTNode {\n // Literals\n if (this.match(TokenType.TRUE)) {\n return { type: 'Literal', value: true };\n }\n\n if (this.match(TokenType.FALSE)) {\n return { type: 'Literal', value: false };\n }\n\n if (this.match(TokenType.NULL)) {\n return { type: 'Literal', value: null };\n }\n\n if (this.match(TokenType.UNDEFINED)) {\n return { type: 'Literal', value: undefined };\n }\n\n if (this.match(TokenType.NUMBER)) {\n return { type: 'Literal', value: parseFloat(this.previous().value) };\n }\n\n if (this.match(TokenType.STRING)) {\n return { type: 'Literal', value: this.previous().value };\n }\n\n // Array literal\n if (this.match(TokenType.LBRACKET)) {\n const elements = this.parseArrayElements();\n this.consume(TokenType.RBRACKET, 'Expected \"]\" after array elements');\n return { type: 'ArrayLiteral', elements };\n }\n\n // Object literal\n if (this.match(TokenType.LBRACE)) {\n const properties = this.parseObjectProperties();\n this.consume(TokenType.RBRACE, 'Expected \"}\" after object properties');\n return { type: 'ObjectLiteral', properties };\n }\n\n // Identifier\n if (this.match(TokenType.IDENTIFIER)) {\n return { type: 'Identifier', name: this.previous().value };\n }\n\n // Grouped expression\n if (this.match(TokenType.LPAREN)) {\n const expr = this.parseExpression();\n this.consume(TokenType.RPAREN, 'Expected \")\" after expression');\n return expr;\n }\n\n throw new ExpressionParserError(`Unexpected token: ${this.peek().value}`, this.peek().position, this.expression);\n }\n\n private parseArgumentList(): ASTNode[] {\n const args: ASTNode[] = [];\n\n if (!this.check(TokenType.RPAREN)) {\n do {\n args.push(this.parseExpression());\n } while (this.match(TokenType.COMMA));\n }\n\n return args;\n }\n\n private parseArrayElements(): ASTNode[] {\n const elements: ASTNode[] = [];\n\n if (!this.check(TokenType.RBRACKET)) {\n do {\n elements.push(this.parseExpression());\n } while (this.match(TokenType.COMMA));\n }\n\n return elements;\n }\n\n private parseObjectProperties(): Array<{ key: string; value: ASTNode }> {\n const properties: Array<{ key: string; value: ASTNode }> = [];\n\n if (this.check(TokenType.RBRACE)) {\n return properties;\n }\n\n do {\n // Trailing comma support: `{ a: 1, }`\n if (this.check(TokenType.RBRACE)) break;\n\n let key: string;\n if (this.check(TokenType.IDENTIFIER) || this.check(TokenType.STRING)) {\n key = this.advance().value;\n } else {\n throw new ExpressionParserError(\n `Expected property name (identifier or string) in object literal, got '${this.peek().value}'`,\n this.peek().position,\n this.expression,\n );\n }\n\n this.consume(TokenType.COLON, 'Expected \":\" after property name in object literal');\n const value = this.parseExpression();\n properties.push({ key, value });\n } while (this.match(TokenType.COMMA));\n\n return properties;\n }\n\n private match(...types: TokenType[]): boolean {\n for (const type of types) {\n if (this.check(type)) {\n this.advance();\n return true;\n }\n }\n return false;\n }\n\n private check(type: TokenType): boolean {\n if (this.isAtEnd()) return false;\n return this.peek().type === type;\n }\n\n private advance(): Token {\n if (!this.isAtEnd()) this.current++;\n return this.previous();\n }\n\n private isAtEnd(): boolean {\n return this.peek().type === TokenType.EOF;\n }\n\n private peek(): Token {\n return this.tokens[this.current];\n }\n\n private previous(): Token {\n return this.tokens[this.current - 1];\n }\n\n private consume(type: TokenType, message: string): Token {\n if (this.check(type)) return this.advance();\n\n throw new ExpressionParserError(message, this.peek().position, this.expression);\n }\n}\n","import { ASTNode, ExpressionParserError } from './types';\n\n/** Evaluation context for safe expression evaluation */\nexport interface EvaluationScope {\n [key: string]: unknown;\n}\n\n/** Security Model: Whitelist-only approach for method calls */\n\n/**\n * Type-safe method whitelists derived from TypeScript primitive types\n * Only methods explicitly listed here are allowed - this is a whitelist-only approach\n */\nconst STRING_SAFE_METHODS: ReadonlyArray<keyof string> = [\n 'charAt',\n 'charCodeAt',\n 'concat',\n 'endsWith',\n 'includes',\n 'indexOf',\n 'lastIndexOf',\n 'match',\n 'padEnd',\n 'padStart',\n 'repeat',\n 'replace',\n 'search',\n 'slice',\n 'split',\n 'startsWith',\n 'substring',\n 'toLowerCase',\n 'toUpperCase',\n 'trim',\n 'trimEnd',\n 'trimStart',\n 'toString',\n] as const;\n\nconst NUMBER_SAFE_METHODS: ReadonlyArray<keyof number> = ['toExponential', 'toFixed', 'toPrecision', 'toString'] as const;\n\nconst ARRAY_SAFE_METHODS: ReadonlyArray<keyof Array<unknown>> = [\n 'concat',\n 'every',\n 'filter',\n 'find',\n 'findIndex',\n 'flat',\n 'flatMap',\n 'includes',\n 'indexOf',\n 'join',\n 'lastIndexOf',\n 'map',\n 'reduce',\n 'reduceRight',\n 'slice',\n 'some',\n 'toString',\n 'entries',\n 'keys',\n 'values',\n] as const;\n\nconst DATE_SAFE_METHODS: ReadonlyArray<keyof Date> = [\n 'getDate',\n 'getDay',\n 'getFullYear',\n 'getHours',\n 'getMilliseconds',\n 'getMinutes',\n 'getMonth',\n 'getSeconds',\n 'getTime',\n 'getTimezoneOffset',\n 'getUTCDate',\n 'getUTCDay',\n 'getUTCFullYear',\n 'getUTCHours',\n 'getUTCMilliseconds',\n 'getUTCMinutes',\n 'getUTCMonth',\n 'getUTCSeconds',\n 'toDateString',\n 'toISOString',\n 'toJSON',\n 'toString',\n 'toTimeString',\n 'toUTCString',\n] as const;\n\n/**\n * Whitelist of safe methods that can be called on values\n * Using Set for O(1) lookup performance\n */\nconst SAFE_METHODS: Record<string, ReadonlySet<string>> = {\n string: new Set(STRING_SAFE_METHODS as readonly string[]),\n number: new Set(NUMBER_SAFE_METHODS as readonly string[]),\n array: new Set(ARRAY_SAFE_METHODS as readonly string[]),\n date: new Set(DATE_SAFE_METHODS as readonly string[]),\n};\n\n/**\n * Blacklist of property names that should not be accessible\n * These properties can leak information about object structure or enable attacks\n */\nconst BLOCKED_PROPERTIES = new Set<string>([\n 'constructor',\n '__proto__',\n 'prototype',\n '__defineGetter__',\n '__defineSetter__',\n '__lookupGetter__',\n '__lookupSetter__',\n]);\n\n/** Safely evaluates an AST node with a given context */\nexport class Evaluator {\n private readonly scope: EvaluationScope;\n private readonly expression: string;\n\n constructor(scope: EvaluationScope, expression: string) {\n this.scope = scope;\n this.expression = expression;\n }\n\n /** Evaluate an AST node */\n evaluate(node: ASTNode): unknown {\n switch (node.type) {\n case 'Literal':\n return node.value;\n\n case 'Identifier':\n return this.evaluateIdentifier(node.name);\n\n case 'MemberAccess':\n return this.evaluateMemberAccess(node);\n\n case 'ComputedMemberAccess':\n return this.evaluateComputedMemberAccess(node);\n\n case 'BinaryOp':\n return this.evaluateBinaryOp(node);\n\n case 'UnaryOp':\n return this.evaluateUnaryOp(node);\n\n case 'CallExpression':\n return this.evaluateCallExpression(node);\n\n case 'Conditional':\n return this.evaluate(node.test) ? this.evaluate(node.consequent) : this.evaluate(node.alternate);\n\n case 'ArrayLiteral':\n return this.evaluateArrayLiteral(node);\n\n case 'ObjectLiteral':\n return this.evaluateObjectLiteral(node);\n\n case 'ArrowFunction':\n return this.evaluateArrowFunction(node);\n\n default:\n throw new ExpressionParserError(`Unknown node type: ${(node as { type: string }).type}`, 0, this.expression);\n }\n }\n\n private evaluateIdentifier(name: string): unknown {\n if (name in this.scope) {\n return this.scope[name];\n }\n return undefined;\n }\n\n private evaluateMemberAccess(node: { object: ASTNode; property: string }): unknown {\n const obj = this.evaluate(node.object);\n\n if (obj === null || obj === undefined) {\n return undefined;\n }\n\n // Block access to dangerous properties that could leak information or enable attacks\n if (BLOCKED_PROPERTIES.has(node.property)) {\n throw new ExpressionParserError(`Property \"${node.property}\" is not accessible for security reasons`, 0, this.expression);\n }\n\n // Allow property access on plain objects and primitives\n if (typeof obj === 'object' || typeof obj === 'string' || typeof obj === 'number') {\n return (obj as Record<string, unknown>)[node.property];\n }\n\n return undefined;\n }\n\n private evaluateComputedMemberAccess(node: { object: ASTNode; property: ASTNode; optional?: boolean }): unknown {\n const obj = this.evaluate(node.object);\n\n if (obj === null || obj === undefined) {\n // Both `obj?.[expr]` and `obj[expr]` short-circuit on null/undefined —\n // matches the safe-access behavior of dotted member access above.\n return undefined;\n }\n\n const rawKey = this.evaluate(node.property);\n if (rawKey === null || rawKey === undefined) return undefined;\n const key = String(rawKey);\n\n // Same blocklist as dotted access — `obj['__proto__']` must be rejected\n // even though the property name was computed.\n if (BLOCKED_PROPERTIES.has(key)) {\n throw new ExpressionParserError(`Property \"${key}\" is not accessible for security reasons`, 0, this.expression);\n }\n\n if (typeof obj === 'object' || typeof obj === 'string' || typeof obj === 'number') {\n return (obj as Record<string, unknown>)[key];\n }\n\n return undefined;\n }\n\n private evaluateBinaryOp(node: { operator: string; left: ASTNode; right: ASTNode }): unknown {\n // Handle short-circuit evaluation for logical operators\n if (node.operator === '&&') {\n const left = this.evaluate(node.left);\n return left ? this.evaluate(node.right) : left;\n }\n if (node.operator === '||') {\n const left = this.evaluate(node.left);\n return left ? left : this.evaluate(node.right);\n }\n\n // For all other operators, evaluate both sides\n const left = this.evaluate(node.left);\n const right = this.evaluate(node.right);\n\n switch (node.operator) {\n // Arithmetic\n case '+':\n return (left as number) + (right as number);\n case '-':\n return (left as number) - (right as number);\n case '*':\n return (left as number) * (right as number);\n case '/':\n if (right === 0) return null;\n return (left as number) / (right as number);\n case '%':\n if (right === 0) return null;\n return (left as number) % (right as number);\n\n // Comparison - using == and != intentionally for loose equality\n case '==':\n return left == right;\n case '===':\n return left === right;\n case '!=':\n return left != right;\n case '!==':\n return left !== right;\n case '>':\n return (left as number) > (right as number);\n case '<':\n return (left as number) < (right as number);\n case '>=':\n return (left as number) >= (right as number);\n case '<=':\n return (left as number) <= (right as number);\n\n default:\n throw new ExpressionParserError(`Unknown binary operator: ${node.operator}`, 0, this.expression);\n }\n }\n\n private evaluateUnaryOp(node: { operator: string; operand: ASTNode }): unknown {\n const operand = this.evaluate(node.operand);\n\n switch (node.operator) {\n case '!':\n return !operand;\n case '-':\n return -(operand as number);\n case '+':\n return +(operand as number);\n default:\n throw new ExpressionParserError(`Unknown unary operator: ${node.operator}`, 0, this.expression);\n }\n }\n\n private evaluateCallExpression(node: { callee: ASTNode; arguments: ASTNode[]; optional?: boolean }): unknown {\n // Only allow method calls (member access). Computed access (`obj['method']()`)\n // is also rejected — the security model whitelists method names statically.\n if (node.callee.type !== 'MemberAccess') {\n throw new ExpressionParserError('Only method calls are allowed, not arbitrary function calls', 0, this.expression);\n }\n\n const obj = this.evaluate(node.callee.object);\n\n // Calling a method on a nullish receiver silently returns undefined —\n // consistent with how dotted member access (`obj.prop`) treats nullish\n // receivers above. This means `a?.b()`, `a?.b.c()`, and `nullable.method()`\n // all short-circuit to undefined rather than tripping the whitelist check\n // with a misleading \"method not allowed\" error. The whitelist still\n // enforces strictly on non-nullish receivers.\n if (obj === null || obj === undefined) {\n return undefined;\n }\n\n const methodName = node.callee.property;\n\n // Block access to dangerous properties (must check before method call)\n if (BLOCKED_PROPERTIES.has(methodName)) {\n throw new ExpressionParserError(`Property \"${methodName}\" is not accessible for security reasons`, 0, this.expression);\n }\n\n // Check if the method is in the instance method whitelist\n if (!this.isMethodSafe(obj, methodName)) {\n throw new ExpressionParserError(`Method \"${methodName}\" is not allowed for security reasons`, 0, this.expression);\n }\n\n // Evaluate arguments\n const args = node.arguments.map((arg) => this.evaluate(arg));\n\n // Call the method with proper `this` binding\n const method = (obj as Record<string, unknown>)[methodName];\n if (typeof method === 'function') {\n // Use .call() to properly bind `this` context\n return (method as (...args: unknown[]) => unknown).call(obj, ...args);\n }\n\n return undefined;\n }\n\n private evaluateArrayLiteral(node: { elements: ASTNode[] }): unknown {\n return node.elements.map((element) => this.evaluate(element));\n }\n\n private evaluateObjectLiteral(node: { properties: Array<{ key: string; value: ASTNode }> }): unknown {\n const result: Record<string, unknown> = {};\n for (const { key, value } of node.properties) {\n if (BLOCKED_PROPERTIES.has(key)) {\n throw new ExpressionParserError(`Property \"${key}\" is not allowed in object literals for security reasons`, 0, this.expression);\n }\n result[key] = this.evaluate(value);\n }\n return result;\n }\n\n private evaluateArrowFunction(node: { params: string[]; body: ASTNode }): unknown {\n const parentScope = this.scope;\n const expression = this.expression;\n return (...args: unknown[]) => {\n const childScope: EvaluationScope = { ...parentScope };\n for (let i = 0; i < node.params.length; i++) {\n childScope[node.params[i]] = args[i];\n }\n const childEvaluator = new Evaluator(childScope, expression);\n return childEvaluator.evaluate(node.body);\n };\n }\n\n private isMethodSafe(obj: unknown, methodName: string): boolean {\n if (obj === null || obj === undefined) {\n return false;\n }\n\n const type = Array.isArray(obj) ? 'array' : obj instanceof Date ? 'date' : typeof obj;\n\n const safeMethods = SAFE_METHODS[type];\n if (!safeMethods) {\n return false;\n }\n\n return safeMethods.has(methodName);\n }\n}\n","import { Parser } from './parser';\nimport { Evaluator, EvaluationScope } from './evaluator';\nimport { ASTNode, ExpressionParserError } from './types';\n\n/** Maximum cache size to prevent memory issues */\nconst MAX_AST_CACHE_SIZE = 1000;\n\n/**\n * LRU Cache implementation for AST nodes\n * Tracks access order to evict least-recently-used entries\n */\nclass LRUCache<K, V> {\n private cache = new Map<K, V>();\n private maxSize: number;\n\n constructor(maxSize: number) {\n this.maxSize = maxSize;\n }\n\n get(key: K): V | undefined {\n const value = this.cache.get(key);\n if (value !== undefined) {\n // Move to end (most recently used)\n this.cache.delete(key);\n this.cache.set(key, value);\n }\n return value;\n }\n\n set(key: K, value: V): void {\n // Delete if exists to update position\n if (this.cache.has(key)) {\n this.cache.delete(key);\n }\n\n // Add to end (most recently used)\n this.cache.set(key, value);\n\n // Evict least recently used if over limit\n if (this.cache.size > this.maxSize) {\n const firstKey = this.cache.keys().next().value;\n if (firstKey !== undefined) {\n this.cache.delete(firstKey);\n }\n }\n }\n\n clear(): void {\n this.cache.clear();\n }\n\n get size(): number {\n return this.cache.size;\n }\n}\n\n/** Cache for parsed AST nodes to improve performance */\nconst astCache = new LRUCache<string, ASTNode>(MAX_AST_CACHE_SIZE);\n\n/** Secure expression parser that uses AST-based evaluation */\nexport class ExpressionParser {\n /**\n * Parse an expression string into an AST\n * Results are cached for performance using LRU eviction\n */\n static parse(expression: string): ASTNode {\n // Check cache first\n const cached = astCache.get(expression);\n if (cached) {\n return cached;\n }\n\n // Parse the expression\n const parser = new Parser(expression);\n const ast = parser.parse();\n\n // Cache the result (LRU cache handles eviction automatically)\n astCache.set(expression, ast);\n\n return ast;\n }\n\n /** Evaluate an expression with a given scope */\n static evaluate(expression: string, scope: EvaluationScope): unknown {\n try {\n const ast = this.parse(expression);\n const evaluator = new Evaluator(scope, expression);\n return evaluator.evaluate(ast);\n } catch (error) {\n if (error instanceof ExpressionParserError) {\n throw error;\n }\n // Wrap other errors for consistency\n throw new ExpressionParserError(\n `Error evaluating expression: ${error instanceof Error ? error.message : String(error)}`,\n 0,\n expression,\n );\n }\n }\n\n /** Clear the AST cache */\n static clearCache(): void {\n astCache.clear();\n }\n\n /** Get cache statistics */\n static getCacheStats(): { size: number; maxSize: number } {\n return {\n size: astCache.size,\n maxSize: MAX_AST_CACHE_SIZE,\n };\n }\n}\n","import {\n AndCondition,\n ConditionalExpression,\n FieldValueCondition,\n JavascriptCondition,\n OrCondition,\n} from '../../models/expressions/conditional-expression';\nimport { EvaluationContext } from '../../models/expressions/evaluation-context';\nimport { compareValues, getNestedValue, hasNestedProperty } from './value-utils';\nimport { ExpressionParser } from './parser/expression-parser';\n\n/** True for a Promise/Observable wrongly returned into a sync condition slot (truthy under `!!`). */\nfunction isThenableOrObservable(value: unknown): boolean {\n if (value === null || typeof value !== 'object') return false;\n const candidate = value as { then?: unknown; subscribe?: unknown };\n return typeof candidate.then === 'function' || typeof candidate.subscribe === 'function';\n}\n\n/**\n * Evaluate conditional expression\n * Uses secure AST-based parsing for JavaScript expressions\n */\nexport function evaluateCondition(expression: ConditionalExpression, context: EvaluationContext): boolean {\n switch (expression.type) {\n case 'fieldValue':\n return evaluateFieldValueCondition(expression, context);\n\n case 'javascript':\n return evaluateJavaScriptExpression(expression, context);\n\n case 'custom': {\n // Read both keys via cast — the XOR type narrows to `never` when both\n // would be truthy, but JSON-loaded configs can produce that state at\n // runtime. We accept it here, warn, and let inline win.\n const bothSet = expression as { fn?: unknown; functionName?: string };\n const inlineFn = bothSet.fn as ((ctx: EvaluationContext) => unknown) | undefined;\n const registeredName = bothSet.functionName;\n\n if (inlineFn && registeredName) {\n context.logger.warn(\n 'Both \"fn\" and \"functionName\" are set on custom condition. Inline \"fn\" takes precedence; \"functionName\" is ignored.',\n );\n }\n\n const customFn = inlineFn ?? (registeredName ? context.customFunctions?.[registeredName] : undefined);\n if (!customFn) {\n context.logger.error('Custom function not found:', registeredName);\n return false;\n }\n\n try {\n const result = customFn(context);\n if (isThenableOrObservable(result)) {\n context.logger.warn(\n `Custom condition '${registeredName ?? '<inline>'}' returned a Promise or Observable. ` +\n `Synchronous conditions must return a value; use an 'async' or 'http' condition type for asynchronous logic. Treating as false.`,\n );\n return false;\n }\n return !!result;\n } catch (error) {\n context.logger.error('Error executing custom function:', registeredName ?? '<inline>', error);\n return false;\n }\n }\n\n case 'and':\n return evaluateAndCondition(expression, context);\n\n case 'or':\n return evaluateOrCondition(expression, context);\n\n case 'http':\n context.logger.warn(\n 'HTTP conditions are resolved asynchronously via createHttpConditionLogicFunction(). ' +\n 'When used inside and/or composites, the HTTP result is not available synchronously. Returning false.',\n );\n return false;\n\n case 'async':\n context.logger.warn(\n 'Async Condition - resolved asynchronously via createAsyncConditionLogicFunction(). ' +\n 'When used inside and/or composites, the async result is not available synchronously. Returning false.',\n );\n return false;\n\n default:\n return false;\n }\n}\n\nfunction evaluateFieldValueCondition(expression: FieldValueCondition, context: EvaluationContext): boolean {\n // Guard against missing fieldPath — invalid config, return false\n if (!expression.fieldPath) return false;\n\n // Try scoped formValue first (handles sibling field lookups within array items).\n // Fall back to rootFormValue for fields outside the current array scope.\n // Use hasNestedProperty to distinguish \"field exists with value undefined\"\n // from \"field path doesn't exist\" — prevents incorrect fallback when an\n // optional array item field is intentionally undefined.\n const fieldValue = hasNestedProperty(context.formValue, expression.fieldPath)\n ? getNestedValue(context.formValue, expression.fieldPath)\n : context.rootFormValue\n ? getNestedValue(context.rootFormValue, expression.fieldPath)\n : undefined;\n return compareValues(fieldValue, expression.value, expression.operator);\n}\n\nfunction evaluateJavaScriptExpression(expression: JavascriptCondition, context: EvaluationContext): boolean {\n try {\n // Use secure AST-based expression parser instead of dynamic code execution\n const result = ExpressionParser.evaluate(expression.expression, context);\n return !!result;\n } catch (error) {\n context.logger.error('Error evaluating JavaScript expression:', expression.expression, error);\n return false;\n }\n}\n\nfunction evaluateAndCondition(expression: AndCondition, context: EvaluationContext): boolean {\n if (expression.conditions.length === 0) return false;\n // All conditions must be true for AND logic\n return expression.conditions.every((condition) => evaluateCondition(condition, context));\n}\n\nfunction evaluateOrCondition(expression: OrCondition, context: EvaluationContext): boolean {\n if (expression.conditions.length === 0) return false;\n // At least one condition must be true for OR logic\n return expression.conditions.some((condition) => evaluateCondition(condition, context));\n}\n","import { LogicFn } from '@angular/forms/signals';\n\n/** DI-scoped cache for HTTP condition logic functions. */\nexport class HttpConditionFunctionCacheService {\n /** Cache for HTTP condition logic functions, keyed by serialized condition. */\n readonly httpConditionFunctionCache = new Map<string, LogicFn<unknown, boolean>>();\n}\n","/**\n * Serializes a value to a deterministic string for cache key generation.\n * Unlike JSON.stringify, this sorts object keys to ensure consistent output\n * regardless of property insertion order.\n */\nexport function stableStringify(value: unknown): string {\n if (value === null || value === undefined) {\n return String(value);\n }\n\n if (typeof value !== 'object') {\n return JSON.stringify(value);\n }\n\n if (Array.isArray(value)) {\n return '[' + value.map(stableStringify).join(',') + ']';\n }\n\n const obj = value as Record<string, unknown>;\n const sortedKeys = Object.keys(obj).sort();\n const pairs = sortedKeys.map((key) => JSON.stringify(key) + ':' + stableStringify(obj[key]));\n return '{' + pairs.join(',') + '}';\n}\n","import { inject, InjectionToken, Signal } from '@angular/core';\nimport { DynamicFormError } from '../errors/dynamic-form-error';\nimport type { FieldSignalContext, ArrayContext, GroupContext } from '../mappers/types';\nimport type { WrapperConfig } from './wrapper-type';\nimport type { ValidationMessages } from './validation-types';\nimport type { FormOptions } from './form-config';\n\n/** Injection token for form-level default wrappers. */\nexport const DEFAULT_WRAPPERS = new InjectionToken<Signal<readonly WrapperConfig[] | undefined>>('DEFAULT_WRAPPERS');\n\n/** Injection token for providing field signal context to mappers and components. */\nexport const FIELD_SIGNAL_CONTEXT = new InjectionToken<FieldSignalContext>('FIELD_SIGNAL_CONTEXT');\n\n/** Throwing accessor for {@link FIELD_SIGNAL_CONTEXT}. */\nexport function injectFieldSignalContext<TModel extends Record<string, unknown> = Record<string, unknown>>(): FieldSignalContext<TModel> {\n const ctx = inject(FIELD_SIGNAL_CONTEXT, { optional: true });\n if (!ctx) {\n throw new DynamicFormError('FIELD_SIGNAL_CONTEXT is not provided. This consumer must run inside a <df-dynamic-form> component tree.');\n }\n return ctx as FieldSignalContext<TModel>;\n}\n\n/** Injection token for providing array context metadata to mappers and components. */\nexport const ARRAY_CONTEXT = new InjectionToken<ArrayContext>('ARRAY_CONTEXT');\n\n/** Injection token for providing group ancestry to mappers and components. */\n// No factory: GROUP_CONTEXT is optional-by-design — `GroupFieldComponent` and\n// `createArrayItemInjector` re-provide it where it makes sense, and consumers\n// inject with `{ optional: true }` to handle the top-level case. Mirrors the\n// ARRAY_CONTEXT pattern. Adding a throwing factory here fires even when\n// injected optionally (Angular still evaluates the tree-shakable factory\n// before honoring `optional`), so non-group fields would crash on every render.\nexport const GROUP_CONTEXT = new InjectionToken<GroupContext>('GROUP_CONTEXT');\n\n/** Injection token for form-level default props. */\nexport const DEFAULT_PROPS = new InjectionToken<Signal<Record<string, unknown> | undefined>>('DEFAULT_PROPS');\n\n/** Injection token for form-level default validation messages. */\nexport const DEFAULT_VALIDATION_MESSAGES = new InjectionToken<Signal<ValidationMessages | undefined>>('DEFAULT_VALIDATION_MESSAGES');\n\n/** Injection token for form-level options. */\nexport const FORM_OPTIONS = new InjectionToken<Signal<FormOptions | undefined>>('FORM_OPTIONS');\n\n/** Injection token for form-level external data. */\nexport const EXTERNAL_DATA = new InjectionToken<Signal<Record<string, Signal<unknown>> | undefined>>('EXTERNAL_DATA');\n\n/**\n * Form-level DOM-id prefix, provided once at the DynamicForm level and read by\n * `mapFieldToInputs` as the outermost id segment: `{idPrefix}_{group}_{key}_{index}`.\n * Scopes ids to one form instance so duplicate forms on a page don't collide.\n * Inject `{ optional: true }` — absent under mock injectors / standalone usage.\n */\nexport const FORM_ID_PREFIX = new InjectionToken<Signal<string>>('FORM_ID_PREFIX');\n","import { Signal } from '@angular/core';\nimport { FieldTree } from '@angular/forms/signals';\n\n/** Registry service that provides access to the root form and its values. */\nexport class RootFormRegistryService {\n constructor(\n readonly formValue: Signal<Record<string, unknown>>,\n readonly rootForm: Signal<FieldTree<Record<string, unknown>> | undefined>,\n ) {}\n}\n","import { untracked } from '@angular/core';\nimport { FieldContext } from '@angular/forms/signals';\n\n/**\n * Safely reads `pathKeys` from a FieldContext.\n * Returns an empty array if `pathKeys` is not available (e.g., in tests or older Angular versions).\n */\nexport function safeReadPathKeys(fieldContext: FieldContext<unknown>): readonly string[] {\n if (!('pathKeys' in fieldContext) || typeof fieldContext.pathKeys !== 'function') {\n return [];\n }\n return untracked(() => fieldContext.pathKeys());\n}\n","import { inject, Injectable, isSignal, Signal, untracked } from '@angular/core';\nimport { ChildFieldContext, FieldContext, FieldTree, ReadonlyFieldState } from '@angular/forms/signals';\nimport { EvaluationContext } from '../../models/expressions/evaluation-context';\nimport { EXTERNAL_DATA } from '../../models/field-signal-context.token';\nimport { DynamicFormError } from '../../errors/dynamic-form-error';\nimport { RootFormRegistryService } from './root-form-registry.service';\nimport { DynamicFormLogger } from '../../providers/features/logger/logger.token';\nimport { DEPRECATION_WARNING_TRACKER } from '../../utils/deprecation-warning-tracker';\nimport { getNestedValue } from '../expressions/value-utils';\nimport { readFieldStateInfo, createFormFieldStateMap } from '../derivation/field-state-extractor';\nimport { safeReadPathKeys } from '../../utils/safe-read-path-keys';\n\nfunction isChildFieldContext<TValue>(context: FieldContext<TValue>): context is ChildFieldContext<TValue> {\n return 'key' in context && isSignal(context.key);\n}\n\n/** Extracts the FieldState from a FieldContext using the public `.state` property. */\nfunction extractFieldState(fieldContext: FieldContext<unknown>): ReadonlyFieldState<unknown> | undefined {\n return untracked(() => {\n if (!fieldContext || !('state' in fieldContext)) return undefined;\n return fieldContext.state;\n });\n}\n\n/** Detects whether a field lives inside an array by examining its `pathKeys`. */\nfunction detectArrayScope(pathKeys: readonly string[]): { arrayKey: string; index: number; localKey: string } | undefined {\n // Need at least 3 segments: arrayKey, index, fieldKey\n if (pathKeys.length < 3) return undefined;\n\n // Walk from the end to find the nearest array context.\n // pathKeys looks like ['addresses', '0', 'street'] or\n // ['nested', 'addresses', '1', 'city']\n for (let i = pathKeys.length - 2; i >= 1; i--) {\n const maybeIndex = Number(pathKeys[i]);\n if (Number.isInteger(maybeIndex) && maybeIndex >= 0) {\n return {\n arrayKey: pathKeys.slice(0, i).join('.'),\n index: maybeIndex,\n localKey: pathKeys[pathKeys.length - 1],\n };\n }\n }\n\n return undefined;\n}\n\n/**\n * Service that provides field evaluation context by combining\n * field context with root form registry information.\n */\n@Injectable()\nexport class FieldContextRegistryService {\n private rootFormRegistry = inject(RootFormRegistryService);\n private logger = inject(DynamicFormLogger);\n private deprecationTracker = inject(DEPRECATION_WARNING_TRACKER, { optional: true });\n private externalDataSignal = inject(EXTERNAL_DATA, { optional: true });\n\n /**\n * Creates an evaluation context for a field by combining:\n * - The field's current value\n * - The root form value from the registry\n * - The field path (if available from form context)\n * - Custom functions (if provided)\n */\n createEvaluationContext<TValue>(\n fieldContext: FieldContext<TValue>,\n customFunctions?: Record<string, (context: EvaluationContext) => unknown>,\n ): EvaluationContext {\n // Use untracked() to read the field value WITHOUT creating a reactive dependency.\n // This prevents infinite loops when logic functions are evaluated inside computed signals.\n const fieldValue = untracked(() => fieldContext.value());\n\n // Get form value wrapped in untracked() to prevent reactive dependencies.\n // This allows validators and dynamic values to access form values without\n // causing infinite loops.\n const rootFormValue = untracked(() => this.rootFormRegistry.formValue());\n const localKey = this.extractFieldPath(fieldContext);\n const pathKeys = safeReadPathKeys(fieldContext);\n const arrayScope = detectArrayScope(pathKeys);\n\n if (arrayScope) {\n return this.buildArrayScopedContext(rootFormValue, arrayScope, fieldValue, customFunctions, false, fieldContext);\n }\n\n // Use getters for fieldState/formFieldState to defer signal reads until\n // the expression actually accesses them. Validators that only use fieldValue\n // will never trigger these getters, avoiding reactive cycles in Angular's\n // internal signal graph (validator → state → valid → validator).\n const rootFormSignal = this.rootFormRegistry.rootForm;\n return {\n fieldValue,\n formValue: rootFormValue,\n fieldPath: localKey,\n customFunctions: customFunctions || {},\n externalData: this.resolveExternalData(false),\n logger: this.logger,\n deprecationTracker: this.deprecationTracker ?? undefined,\n get fieldState() {\n return readFieldStateInfo(extractFieldState(fieldContext), false);\n },\n get formFieldState() {\n return createFormFieldStateMap(untracked(rootFormSignal) as FieldTree<unknown>, false);\n },\n };\n }\n\n /** Extracts the field path (key) for a given field context. */\n private extractFieldPath(fieldContext: FieldContext<unknown>): string {\n if (isChildFieldContext(fieldContext)) {\n try {\n return String(fieldContext.key());\n } catch (error) {\n this.logger.warn('Unable to extract field key:', error);\n }\n }\n\n // For root fields or when key is not available\n return '';\n }\n\n /** Builds an evaluation context scoped to a specific array item. */\n private buildArrayScopedContext<TValue>(\n rootFormValue: Record<string, unknown>,\n arrayScope: { arrayKey: string; index: number; localKey: string },\n fieldValue: TValue,\n customFunctions: Record<string, (context: EvaluationContext) => unknown> | undefined,\n reactive: boolean,\n fieldContext?: FieldContext<unknown>,\n ): EvaluationContext {\n const { arrayKey, index, localKey } = arrayScope;\n\n // Navigate to the array (supports nested paths like 'nested.addresses')\n const arrayData = getNestedValue(rootFormValue, arrayKey);\n let scopedFormValue: Record<string, unknown> | undefined;\n\n if (Array.isArray(arrayData) && index >= 0 && index < arrayData.length) {\n const item = arrayData[index];\n if (item != null && typeof item === 'object') {\n scopedFormValue = item as Record<string, unknown>;\n }\n }\n\n // Use getters to defer fieldState/formFieldState construction.\n // Same rationale as createEvaluationContext — avoids reactive cycles\n // when expressions don't actually access these properties.\n const rootFormSignal = this.rootFormRegistry.rootForm;\n const fieldStateGetter = fieldContext ? () => readFieldStateInfo(extractFieldState(fieldContext), reactive) : () => undefined;\n const formFieldStateGetter = () =>\n createFormFieldStateMap((reactive ? rootFormSignal() : untracked(rootFormSignal)) as FieldTree<unknown>, reactive);\n\n // Fall back to root form value if array item lookup fails\n if (!scopedFormValue) {\n return {\n fieldValue,\n formValue: rootFormValue,\n fieldPath: localKey,\n customFunctions: customFunctions || {},\n externalData: this.resolveExternalData(reactive),\n logger: this.logger,\n deprecationTracker: this.deprecationTracker ?? undefined,\n get fieldState() {\n return fieldStateGetter();\n },\n get formFieldState() {\n return formFieldStateGetter();\n },\n };\n }\n\n return {\n fieldValue,\n formValue: scopedFormValue,\n rootFormValue,\n arrayIndex: index,\n arrayPath: arrayKey,\n fieldPath: `${arrayKey}.${index}.${localKey}`,\n customFunctions: customFunctions || {},\n externalData: this.resolveExternalData(reactive),\n logger: this.logger,\n deprecationTracker: this.deprecationTracker ?? undefined,\n get fieldState() {\n return fieldStateGetter();\n },\n get formFieldState() {\n return formFieldStateGetter();\n },\n };\n }\n\n /**\n * Resolves external data signals to their current values.\n *\n * @param reactive - If true, reads signals reactively (creates dependencies).\n * If false, reads signals with untracked() (no dependencies).\n * @returns Record of resolved external data values, or undefined if no external data.\n */\n private resolveExternalData(reactive: boolean): Record<string, unknown> | undefined {\n const externalDataSignal = this.externalDataSignal;\n if (!externalDataSignal) return undefined;\n\n const externalDataRecord = reactive ? externalDataSignal() : untracked(() => externalDataSignal());\n\n if (!externalDataRecord) {\n return undefined;\n }\n\n const resolved: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(externalDataRecord)) {\n if (!isSignal(value)) {\n throw new DynamicFormError(`externalData[\"${key}\"] must be a Signal. Got: ${typeof value}. Wrap it with signal(yourValue).`);\n }\n resolved[key] = reactive ? (value as Signal<unknown>)() : untracked(() => (value as Signal<unknown>)());\n }\n\n return resolved;\n }\n\n /** Creates a REACTIVE evaluation context for logic functions. */\n createReactiveEvaluationContext<TValue>(\n fieldContext: FieldContext<TValue>,\n customFunctions?: Record<string, (context: EvaluationContext) => unknown>,\n ): EvaluationContext {\n const fieldValue = fieldContext.value();\n const rootFormValue = this.rootFormRegistry.formValue();\n const pathKeys = safeReadPathKeys(fieldContext);\n const arrayScope = detectArrayScope(pathKeys);\n\n if (arrayScope) {\n return this.buildArrayScopedContext(rootFormValue, arrayScope, fieldValue, customFunctions, true, fieldContext);\n }\n\n const localKey = this.extractFieldPath(fieldContext);\n const rootFormSignal = this.rootFormRegistry.rootForm;\n\n return {\n fieldValue,\n formValue: rootFormValue,\n fieldPath: localKey,\n customFunctions: customFunctions || {},\n externalData: this.resolveExternalData(true),\n logger: this.logger,\n deprecationTracker: this.deprecationTracker ?? undefined,\n get fieldState() {\n return readFieldStateInfo(extractFieldState(fieldContext), true);\n },\n get formFieldState() {\n return createFormFieldStateMap(rootFormSignal() as FieldTree<unknown>, true);\n },\n };\n }\n\n /**\n * Creates an evaluation context for display-only components (text fields, pages)\n * that don't have their own FieldContext.\n *\n * @param fieldPath - The key/path of the display-only component\n * @param customFunctions - Optional custom functions for expression evaluation\n * @param arrayScope - When the element lives inside an array item, scopes `formValue`\n * to that item so conditions resolve against sibling fields, matching leaf fields.\n */\n createDisplayOnlyContext(\n fieldPath: string,\n customFunctions?: Record<string, (context: EvaluationContext) => unknown>,\n arrayScope?: { arrayKey: string; index: number; localKey: string },\n ): EvaluationContext {\n const rootFormValue = this.rootFormRegistry.formValue();\n\n if (arrayScope) {\n return this.buildArrayScopedContext(rootFormValue, arrayScope, undefined, customFunctions, true);\n }\n\n const rootFormSignal = this.rootFormRegistry.rootForm;\n\n return {\n fieldValue: undefined,\n formValue: rootFormValue,\n fieldPath,\n customFunctions: customFunctions || {},\n externalData: this.resolveExternalData(true),\n logger: this.logger,\n deprecationTracker: this.deprecationTracker ?? undefined,\n get formFieldState() {\n return createFormFieldStateMap(rootFormSignal() as FieldTree<unknown>, true);\n },\n };\n }\n}\n","import { Injectable } from '@angular/core';\nimport { CustomFunction, CustomFunctionOptions, CustomFunctionScope } from '../expressions/custom-function-types';\nimport type { AsyncConditionFunction, AsyncDerivationFunction } from '../expressions/async-custom-function-types';\nimport { AsyncCustomValidator, CustomValidator, HttpCustomValidator } from '../validation/validator-types';\n\n/** Registry service for custom functions and validators */\n@Injectable()\nexport class FunctionRegistryService {\n private readonly customFunctions = new Map<string, CustomFunction>();\n private readonly customFunctionScopes = new Map<string, CustomFunctionScope>();\n private readonly derivationFunctions = new Map<string, CustomFunction>();\n private readonly asyncDerivationFunctions = new Map<string, AsyncDerivationFunction>();\n private readonly asyncConditionFunctions = new Map<string, AsyncConditionFunction>();\n private readonly validators = new Map<string, CustomValidator>();\n private readonly asyncValidators = new Map<string, AsyncCustomValidator>();\n private readonly httpValidators = new Map<string, HttpCustomValidator>();\n\n /**\n * Register a custom function for conditional expressions\n *\n * @param name - Unique identifier for the function\n * @param fn - Function that receives EvaluationContext and returns any value\n * @param options - Optional configuration for the function\n */\n registerCustomFunction<TFormValue extends Record<string, unknown> = Record<string, unknown>>(\n name: string,\n fn: CustomFunction<TFormValue>,\n options?: CustomFunctionOptions,\n ): void {\n // Registry stores type-erased functions; the caller's strongly-typed fn is safe to widen here.\n this.customFunctions.set(name, fn as CustomFunction);\n // Store scope (default to 'form' for conservative cross-field detection)\n this.customFunctionScopes.set(name, options?.scope ?? 'form');\n }\n\n /**\n * Get the scope of a registered custom function.\n *\n * @param name - The function name\n * @returns The function scope, or undefined if not registered\n */\n getCustomFunctionScope(name: string): CustomFunctionScope | undefined {\n return this.customFunctionScopes.get(name);\n }\n\n /**\n * Check if a custom function is field-scoped (no cross-field dependencies).\n *\n * @param name - The function name\n * @returns true if the function is field-scoped, false if form-scoped or not registered\n */\n isFieldScopedFunction(name: string): boolean {\n return this.customFunctionScopes.get(name) === 'field';\n }\n\n /** Get all custom functions as an object */\n getCustomFunctions(): Record<string, CustomFunction> {\n return Object.fromEntries(this.customFunctions);\n }\n\n /** Clear all custom functions and their scopes */\n clearCustomFunctions(): void {\n this.customFunctions.clear();\n this.customFunctionScopes.clear();\n }\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Derivation Functions\n // ─────────────────────────────────────────────────────────────────────────────\n\n /**\n * Register a derivation function for value derivation logic.\n *\n * @param name - Unique identifier for the function\n * @param fn - Function that receives EvaluationContext and returns the derived value\n */\n registerDerivationFunction(name: string, fn: CustomFunction): void {\n this.derivationFunctions.set(name, fn);\n }\n\n /** Get a derivation function by name */\n getDerivationFunction(name: string): CustomFunction | undefined {\n return this.derivationFunctions.get(name);\n }\n\n /** Get all derivation functions as an object */\n getDerivationFunctions(): Record<string, CustomFunction> {\n return Object.fromEntries(this.derivationFunctions);\n }\n\n /**\n * Set derivation functions from a config object.\n * Only updates functions if their references have changed.\n *\n * @param derivations - Object mapping function names to derivation functions\n */\n setDerivationFunctions<TFormValue extends Record<string, unknown> = Record<string, unknown>>(\n derivations: Record<string, CustomFunction<TFormValue>> | undefined,\n ): void {\n this.setRegistryIfChanged(this.derivationFunctions, derivations as Record<string, CustomFunction> | undefined);\n }\n\n /** Clear all derivation functions */\n clearDerivationFunctions(): void {\n this.derivationFunctions.clear();\n }\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Async Derivation Functions\n // ─────────────────────────────────────────────────────────────────────────────\n\n /**\n * Register an async derivation function.\n *\n * @param name - Unique identifier for the function\n * @param fn - Function that receives EvaluationContext and returns Promise/Observable\n */\n registerAsyncDerivationFunction(name: string, fn: AsyncDerivationFunction): void {\n this.asyncDerivationFunctions.set(name, fn);\n }\n\n /** Get an async derivation function by name */\n getAsyncDerivationFunction(name: string): AsyncDerivationFunction | undefined {\n return this.asyncDerivationFunctions.get(name);\n }\n\n /** Get all async derivation functions as an object */\n getAsyncDerivationFunctions(): Record<string, AsyncDerivationFunction> {\n return Object.fromEntries(this.asyncDerivationFunctions);\n }\n\n /**\n * Set async derivation functions from a config object.\n * Only updates functions if their references have changed.\n *\n * @param fns - Object mapping function names to async derivation functions\n */\n setAsyncDerivationFunctions<TFormValue extends Record<string, unknown> = Record<string, unknown>>(\n fns: Record<string, AsyncDerivationFunction<TFormValue>> | undefined,\n ): void {\n this.setRegistryIfChanged(this.asyncDerivationFunctions, fns as Record<string, AsyncDerivationFunction> | undefined);\n }\n\n /** Clear all async derivation functions */\n clearAsyncDerivationFunctions(): void {\n this.asyncDerivationFunctions.clear();\n }\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Async Condition Functions\n // ─────────────────────────────────────────────────────────────────────────────\n\n /**\n * Register an async condition function.\n *\n * @param name - Unique identifier for the function\n * @param fn - Function that receives EvaluationContext and returns Promise/Observable of boolean\n */\n registerAsyncConditionFunction(name: string, fn: AsyncConditionFunction): void {\n this.asyncConditionFunctions.set(name, fn);\n }\n\n /** Get an async condition function by name */\n getAsyncConditionFunction(name: string): AsyncConditionFunction | undefined {\n return this.asyncConditionFunctions.get(name);\n }\n\n /** Get all async condition functions as an object */\n getAsyncConditionFunctions(): Record<string, AsyncConditionFunction> {\n return Object.fromEntries(this.asyncConditionFunctions);\n }\n\n /**\n * Set async condition functions from a config object.\n * Only updates functions if their references have changed.\n *\n * @param fns - Object mapping function names to async condition functions\n */\n setAsyncConditionFunctions<TFormValue extends Record<string, unknown> = Record<string, unknown>>(\n fns: Record<string, AsyncConditionFunction<TFormValue>> | undefined,\n ): void {\n this.setRegistryIfChanged(this.asyncConditionFunctions, fns as Record<string, AsyncConditionFunction> | undefined);\n }\n\n /** Clear all async condition functions */\n clearAsyncConditionFunctions(): void {\n this.asyncConditionFunctions.clear();\n }\n\n /**\n * Register a custom validator using Angular's public FieldContext API\n *\n * @param name - Unique identifier for the validator\n * @param fn - Validator function (ctx, params?) => ValidationError | ValidationError[] | null\n */\n registerValidator(name: string, fn: CustomValidator): void {\n this.validators.set(name, fn);\n }\n\n /** Get a validator by name */\n getValidator(name: string): CustomValidator | undefined {\n return this.validators.get(name);\n }\n\n /** Clear all validators */\n clearValidators(): void {\n this.validators.clear();\n }\n\n /**\n * Register an async validator using Angular's public validateAsync() API\n *\n * @param name - Unique identifier for the async validator\n * @param fn - Async validator function (ctx, params?) => Observable<ValidationError | ValidationError[] | null>\n */\n registerAsyncValidator(name: string, fn: AsyncCustomValidator): void {\n this.asyncValidators.set(name, fn);\n }\n\n /** Get an async validator by name */\n getAsyncValidator(name: string): AsyncCustomValidator | undefined {\n return this.asyncValidators.get(name);\n }\n\n /** Clear all async validators */\n clearAsyncValidators(): void {\n this.asyncValidators.clear();\n }\n\n /**\n * Register an HTTP validator configuration using Angular's public validateHttp() API\n *\n * @param name - Unique identifier for the HTTP validator\n * @param config - HTTP validator configuration object\n */\n registerHttpValidator(name: string, config: HttpCustomValidator): void {\n this.httpValidators.set(name, config);\n }\n\n /** Get an HTTP validator by name */\n getHttpValidator(name: string): HttpCustomValidator | undefined {\n return this.httpValidators.get(name);\n }\n\n /** Clear all HTTP validators */\n clearHttpValidators(): void {\n this.httpValidators.clear();\n }\n\n /**\n * Generic helper to set registry values only if they have changed\n *\n * @param registry - The Map to update\n * @param values - Object mapping keys to values\n */\n private setRegistryIfChanged<T>(registry: Map<string, T>, values: Record<string, T> | undefined): void {\n if (!values) return;\n\n const entries = Object.entries(values);\n const hasChanges = entries.some(([name, value]) => registry.get(name) !== value);\n\n if (hasChanges) {\n entries.forEach(([name, value]) => registry.set(name, value));\n }\n }\n\n /**\n * Set validators from a config object\n * Only updates validators if their references have changed\n *\n * @param validators - Object mapping validator names to validator functions\n */\n setValidators(validators: Record<string, CustomValidator> | undefined): void {\n this.setRegistryIfChanged(this.validators, validators);\n }\n\n /**\n * Set async validators from a config object\n * Only updates validators if their references have changed\n *\n * @param asyncValidators - Object mapping validator names to async validator configs\n */\n setAsyncValidators(asyncValidators: Record<string, AsyncCustomValidator> | undefined): void {\n this.setRegistryIfChanged(this.asyncValidators, asyncValidators);\n }\n\n /**\n * Set HTTP validators from a config object\n * Only updates validators if their references have changed\n *\n * @param httpValidators - Object mapping validator names to HTTP validator configs\n */\n setHttpValidators(httpValidators: Record<string, HttpCustomValidator> | undefined): void {\n this.setRegistryIfChanged(this.httpValidators, httpValidators);\n }\n\n /** Clear everything (functions and all validators) */\n clearAll(): void {\n this.clearCustomFunctions();\n this.clearDerivationFunctions();\n this.clearAsyncDerivationFunctions();\n this.clearAsyncConditionFunctions();\n this.clearValidators();\n this.clearAsyncValidators();\n this.clearHttpValidators();\n }\n}\n","import { InjectionToken } from '@angular/core';\n\ninterface CacheEntry {\n value: boolean;\n expiresAt: number;\n}\n\n/** TTL cache for HTTP condition responses with max entries eviction. */\nexport class HttpConditionCache {\n private readonly cache = new Map<string, CacheEntry>();\n private readonly maxEntries: number;\n\n constructor(maxEntries = 100) {\n this.maxEntries = Math.max(1, maxEntries);\n }\n\n get(key: string): boolean | undefined {\n const entry = this.cache.get(key);\n if (!entry) return undefined;\n\n if (Date.now() > entry.expiresAt) {\n this.cache.delete(key);\n return undefined;\n }\n\n return entry.value;\n }\n\n set(key: string, value: boolean, ttlMs: number): void {\n // Evict oldest entries if at capacity (Map preserves insertion order)\n while (this.cache.size >= this.maxEntries && !this.cache.has(key)) {\n const oldestKey = this.cache.keys().next().value;\n if (oldestKey !== undefined) {\n this.cache.delete(oldestKey);\n }\n }\n\n this.cache.set(key, { value, expiresAt: Date.now() + ttlMs });\n }\n\n clear(): void {\n this.cache.clear();\n }\n}\n\n/** @internal */\nexport const HTTP_CONDITION_CACHE = new InjectionToken<HttpConditionCache>('HttpConditionCache');\n","import { HttpRequestConfig } from '../../models/http/http-request-config';\nimport { EvaluationContext } from '../../models/expressions/evaluation-context';\nimport { HttpResourceRequest } from '../validation/validator-types';\nimport { ExpressionParser } from '../expressions/parser/expression-parser';\n\n/**\n * Resolves an `HttpRequestConfig` into an `HttpResourceRequest` by evaluating\n * expression-based query params and (optionally) body values.\n */\nexport function resolveHttpRequest(config: HttpRequestConfig, context: EvaluationContext): HttpResourceRequest | null {\n let url = config.url;\n\n if (config.params) {\n for (const [key, expression] of Object.entries(config.params)) {\n const value = ExpressionParser.evaluate(expression, context);\n if (value == null) {\n context.logger.debug(\n `HTTP request suppressed: path param '${key}' resolved to ${String(value)} ` +\n `(expression: '${expression}'). The request will not be sent.`,\n );\n return null;\n }\n url = url.replace(`:${key}`, encodeURIComponent(String(value)));\n }\n }\n\n if (config.queryParams) {\n const params = new URLSearchParams();\n for (const [key, expression] of Object.entries(config.queryParams)) {\n const value = ExpressionParser.evaluate(expression, context);\n params.set(key, value != null ? String(value) : '');\n }\n const queryString = params.toString();\n if (queryString) {\n url += (url.includes('?') ? '&' : '?') + queryString;\n }\n }\n\n const request: HttpResourceRequest = {\n url,\n method: config.method,\n };\n\n if (config.body) {\n if (config.evaluateBodyExpressions) {\n const resolvedBody: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(config.body)) {\n if (typeof value === 'string') {\n resolvedBody[key] = ExpressionParser.evaluate(value, context);\n } else {\n resolvedBody[key] = value;\n }\n }\n request.body = resolvedBody;\n } else {\n request.body = config.body;\n }\n }\n\n if (config.headers) {\n request.headers = config.headers;\n }\n\n return request;\n}\n","import { Injector, isSignal, Signal } from '@angular/core';\nimport { toObservable, toSignal } from '@angular/core/rxjs-interop';\nimport { combineLatest, defer, Observable, OperatorFunction } from 'rxjs';\n\n/**\n * Type guard to check if a value is an array of Signals.\n *\n * @internal\n */\nfunction isSignalArray(value: unknown): value is readonly Signal<unknown>[] {\n return Array.isArray(value) && value.every((item) => isSignal(item));\n}\n\n/**\n * Type guard to check if a value is a record (object) of Signals.\n *\n * @internal\n */\nfunction isSignalRecord(value: unknown): value is Record<string, Signal<unknown>> {\n return typeof value === 'object' && value !== null && !Array.isArray(value) && Object.values(value).every((item) => isSignal(item));\n}\n\n/** Options for derivedFromDeferred. */\nexport interface DerivedFromDeferredOptions<R> {\n initialValue: R;\n injector: Injector;\n}\n\n/** Type helper to extract the value type from a Signal. */\ntype SignalValue<T> = T extends Signal<infer V> ? V : never;\n\n/** Type helper to map an array of Signals to an array of their value types. */\ntype SignalValues<T extends readonly Signal<unknown>[]> = {\n [K in keyof T]: SignalValue<T[K]>;\n};\n\n/** Type helper to map an object of Signals to an object of their value types. */\ntype SignalObjectValues<T extends Record<string, Signal<unknown>>> = {\n [K in keyof T]: SignalValue<T[K]>;\n};\n\n/**\n * Like derivedFrom from ngxtension, but uses defer() to avoid input availability issues.\n * This ensures toObservable is called at subscription time, not at field initialization.\n */\n\n// Overload: Single signal\nexport function derivedFromDeferred<T, R>(\n source: Signal<T>,\n operator: OperatorFunction<T, R>,\n options: DerivedFromDeferredOptions<R>,\n): Signal<R>;\n\n// Overload: Array of signals\nexport function derivedFromDeferred<T extends readonly Signal<unknown>[], R>(\n sources: [...T],\n operator: OperatorFunction<SignalValues<T>, R>,\n options: DerivedFromDeferredOptions<R>,\n): Signal<R>;\n\n// Overload: Object of signals\nexport function derivedFromDeferred<T extends Record<string, Signal<unknown>>, R>(\n sources: T,\n operator: OperatorFunction<SignalObjectValues<T>, R>,\n options: DerivedFromDeferredOptions<R>,\n): Signal<R>;\n\n// Implementation\nexport function derivedFromDeferred<T, R>(\n source: Signal<T> | Signal<unknown>[] | Record<string, Signal<unknown>>,\n operator: OperatorFunction<T | unknown[] | Record<string, unknown>, R>,\n options: DerivedFromDeferredOptions<R>,\n): Signal<R> {\n const deferred$: Observable<R> = defer(() => {\n // Handle single signal\n if (isSignal(source)) {\n return toObservable(source, { injector: options.injector });\n }\n\n // Handle array of signals\n if (isSignalArray(source)) {\n const observables = source.map((s) => toObservable(s, { injector: options.injector }));\n return combineLatest(observables);\n }\n\n // Handle object of signals\n if (isSignalRecord(source)) {\n const keys = Object.keys(source);\n const observables = keys.map((key) => toObservable(source[key], { injector: options.injector }));\n\n return combineLatest(observables, (...values) => {\n const result: Record<string, unknown> = {};\n keys.forEach((key, index) => {\n result[key] = values[index];\n });\n return result;\n });\n }\n\n // Fallback - should not happen with proper typing\n throw new Error('Invalid source type. Expected a Signal, array of Signals, or object of Signals.');\n }).pipe(operator);\n\n return toSignal(deferred$, { initialValue: options.initialValue, injector: options.injector });\n}\n","import { linkedSignal, Resource, ResourceSnapshot, resourceFromSnapshots } from '@angular/core';\n\n/**\n * Wraps a `Resource` so that the previous resolved value is preserved while the resource\n * is loading or reloading. This prevents UI flicker when params change and a new request\n * is in-flight — the consumer sees the stale value with a `loading`/`reloading` status\n * instead of `undefined`.\n */\nexport function withPreviousValue<T>(input: Resource<T>): Resource<T> {\n const derived = linkedSignal<ResourceSnapshot<T>, ResourceSnapshot<T>>({\n source: input.snapshot,\n computation: (snapshot, previous) => {\n // During loading/reloading, keep the previous resolved value if available.\n // Don't preserve values from error states — those aren't useful to show.\n if (\n (snapshot.status === 'loading' || snapshot.status === 'reloading') &&\n previous !== undefined &&\n previous.value.status !== 'error'\n ) {\n return { status: snapshot.status, value: previous.value.value } as ResourceSnapshot<T>;\n }\n return snapshot;\n },\n });\n\n return resourceFromSnapshots(derived);\n}\n","import { Injector, Resource, signal, untracked, WritableSignal } from '@angular/core';\nimport { rxResource } from '@angular/core/rxjs-interop';\nimport { FieldContext, LogicFn } from '@angular/forms/signals';\nimport { debounceTime, distinctUntilChanged, Observable, pipe } from 'rxjs';\nimport { derivedFromDeferred } from '../../utils/derived-from-deferred/derived-from-deferred';\nimport { withPreviousValue } from '../../utils/resource-composition/with-previous-value';\nimport { safeReadPathKeys } from '../../utils/safe-read-path-keys';\n\n/**\n * Trigger value carried through the debounced reactive pipeline.\n *\n * @internal\n */\nexport interface Trigger<TPayload> {\n key: string;\n payload: TPayload;\n}\n\n/**\n * Configuration passed to {@link createDebouncedResourceLogicFn}.\n *\n * @internal\n */\nexport interface DebouncedResourceLogicFnConfig<TValue, TPayload, TResult> {\n /** Default value returned by the resource while loading / pending. */\n pendingValue: TResult;\n /** Debounce window (ms) applied to the trigger signal before the resource sees it. */\n debounceMs: number;\n injector: Injector;\n /**\n * Builds the per-emission inner observable. Called every time the debounced\n * trigger key changes; the resource swaps to the new stream automatically.\n */\n buildStream: (payload: TPayload) => Observable<TResult>;\n /**\n * Called per LogicFn invocation. Returns one of:\n * - `{ kind: 'trigger', key, payload }` — set the trigger to this and\n * return the (potentially still-loading) resource value.\n * - `{ kind: 'returnEarly', value }` — bypass the resource and return\n * this value directly (used by HTTP for response-cache hits — the\n * cached value is authoritative and overrides any sticky resource value).\n * - `{ kind: 'holdPrevious' }` — bypass the resource update but return\n * the resource's current sticky value (its last resolved value via\n * `withPreviousValue`, or `pendingValue` if it never resolved). Used\n * by HTTP when the request template is temporarily unresolvable so\n * the UI doesn't flicker back to `pendingValue`.\n */\n resolve: (\n ctx: FieldContext<TValue>,\n ) => { kind: 'trigger'; key: string; payload: TPayload } | { kind: 'returnEarly'; value: TResult } | { kind: 'holdPrevious' };\n}\n\n/**\n * Per-field resource handle held in the LogicFn's signal store. Each call to\n * the LogicFn with a unique field path reuses the same handle so the\n * underlying `rxResource` lifecycle persists.\n *\n * @internal\n */\ninterface ResourceHandle<TPayload, TResult> {\n trigger: WritableSignal<Trigger<TPayload> | undefined>;\n resultResource: Resource<TResult>;\n /**\n * Bumped on every `returnEarly` so the next `trigger.set` carries a fresh dedup key.\n * Without this, a cache hit → cache expiry → same-key recall would be dropped by\n * both the helper's same-key check and the downstream `distinctUntilChanged`, and\n * the resource would never re-fetch.\n */\n refetchVersion: number;\n}\n\n/**\n * Shared scaffolding for both async-function and HTTP condition LogicFns.\n *\n * @internal\n */\nexport function createDebouncedResourceLogicFn<TValue, TPayload, TResult>(\n config: DebouncedResourceLogicFnConfig<TValue, TPayload, TResult>,\n): LogicFn<TValue, TResult> {\n const perFunctionSignalStore = new Map<string, ResourceHandle<TPayload, TResult>>();\n\n return (ctx: FieldContext<TValue>) => {\n const contextKey = safeReadPathKeys(ctx as FieldContext<unknown>).join('.');\n let handle = perFunctionSignalStore.get(contextKey);\n\n if (!handle) {\n const trigger = signal<Trigger<TPayload> | undefined>(undefined);\n\n // untracked: rxResource creates an internal effect, which Angular forbids\n // inside a reactive consumer (LogicFn runs inside a computed). Clearing\n // the consumer keeps the resource's lifecycle independent of the LogicFn's\n // reactive context.\n const resultResource = untracked(() => {\n const debouncedTrigger = derivedFromDeferred(\n trigger,\n pipe(\n debounceTime(config.debounceMs),\n // distinctUntilChanged on key alone — payload identity doesn't matter\n // (the latest payload for a given key always wins).\n distinctUntilChanged((a, b) => a?.key === b?.key),\n ),\n { initialValue: undefined as Trigger<TPayload> | undefined, injector: config.injector },\n );\n\n const resource = rxResource<TResult, Trigger<TPayload> | undefined>({\n params: () => debouncedTrigger(),\n // When params() returns undefined, rxResource enters idle state.\n stream: ({ params }) => config.buildStream(params!.payload),\n defaultValue: config.pendingValue,\n injector: config.injector,\n });\n\n return withPreviousValue(resource);\n });\n\n handle = { trigger, resultResource, refetchVersion: 0 };\n perFunctionSignalStore.set(contextKey, handle);\n }\n\n const resolved = config.resolve(ctx);\n if (resolved.kind === 'returnEarly') {\n handle.refetchVersion++;\n return resolved.value;\n }\n if (resolved.kind === 'holdPrevious') {\n return handle.resultResource.value();\n }\n\n const dedupKey = `${resolved.key}::${handle.refetchVersion}`;\n const newTrigger: Trigger<TPayload> = { key: dedupKey, payload: resolved.payload };\n untracked(() => {\n const current = handle!.trigger();\n // Key-only dedup assumes same key implies equivalent payload — true for both current callers.\n if (current?.key !== newTrigger.key) {\n handle!.trigger.set(newTrigger);\n }\n });\n\n return handle.resultResource.value();\n };\n}\n","import { HttpClient } from '@angular/common/http';\nimport { inject, Injector } from '@angular/core';\nimport { LogicFn } from '@angular/forms/signals';\nimport { catchError, map, of, tap } from 'rxjs';\nimport { HttpCondition } from '../../models/expressions/conditional-expression';\nimport { stableStringify } from '../../utils/stable-stringify';\nimport { HttpResourceRequest } from '../validation/validator-types';\nimport { FieldContextRegistryService } from '../registry/field-context-registry.service';\nimport { FunctionRegistryService } from '../registry/function-registry.service';\nimport { HTTP_CONDITION_CACHE } from '../http/http-condition-cache';\nimport { DynamicFormError } from '../../errors/dynamic-form-error';\nimport { DynamicFormLogger } from '../../providers/features/logger/logger.token';\nimport { Logger } from '../../providers/features/logger/logger.interface';\nimport { resolveHttpRequest } from '../http/http-request-resolver';\nimport { ExpressionParser } from './parser/expression-parser';\nimport { HttpConditionFunctionCacheService } from './http-condition-function-cache.service';\nimport { createDebouncedResourceLogicFn } from './debounced-resource-logic-fn';\n\n/**\n * Extracts a boolean from an HTTP response using an optional expression.\n * When `responseExpression` is provided, evaluates it with `{ response }` scope.\n * Truthy values are treated as `true` — non-boolean results trigger a warning to encourage explicit boolean expressions.\n * Otherwise, coerces the response to boolean.\n */\nfunction extractBoolean(response: unknown, responseExpression: string | undefined, pendingValue: boolean, logger: Logger): boolean {\n if (responseExpression) {\n try {\n const result = ExpressionParser.evaluate(responseExpression, { response });\n if (typeof result !== 'boolean') {\n logger.warn(\n `responseExpression \"${responseExpression}\" returned non-boolean value:`,\n result,\n 'Consider returning a boolean explicitly.',\n );\n }\n return !!result;\n } catch (error) {\n logger.warn(`Failed to evaluate responseExpression '${responseExpression}':`, error);\n return pendingValue;\n }\n }\n return !!response;\n}\n\n/** Creates a logic function for an HTTP condition. */\nexport function createHttpConditionLogicFunction<TValue>(condition: HttpCondition): LogicFn<TValue, boolean> {\n const httpClient = inject(HttpClient, { optional: true });\n if (!httpClient) {\n throw new DynamicFormError('HttpClient is required for HTTP conditions. Add provideHttpClient() to your providers.');\n }\n const fieldContextRegistry = inject(FieldContextRegistryService);\n const functionRegistry = inject(FunctionRegistryService);\n const injector = inject(Injector);\n const cache = inject(HTTP_CONDITION_CACHE);\n const logger = inject(DynamicFormLogger) as Logger;\n const cacheService = inject(HttpConditionFunctionCacheService);\n\n const pendingValue = condition.pendingValue ?? false;\n const cacheDurationMs = condition.cacheDurationMs ?? 30000;\n const debounceMs = condition.debounceMs ?? 300;\n\n // Function cache: across-condition reuse for identical condition shapes.\n const cacheKey = stableStringify(condition);\n const cached = cacheService.httpConditionFunctionCache.get(cacheKey);\n if (cached) {\n return cached as LogicFn<TValue, boolean>;\n }\n\n const fn = createDebouncedResourceLogicFn<TValue, HttpResourceRequest, boolean>({\n pendingValue,\n debounceMs,\n injector,\n resolve: (ctx) => {\n const evaluationContext = fieldContextRegistry.createReactiveEvaluationContext(ctx, functionRegistry.getCustomFunctions());\n const resolved = resolveHttpRequest(condition.http, evaluationContext);\n // Hold the previous resolved value rather than flicker to pendingValue while a path\n // param is temporarily undefined between two valid states.\n if (!resolved) return { kind: 'holdPrevious' };\n\n const requestKey = stableStringify(resolved);\n const cachedResult = cache.get(requestKey);\n if (cachedResult !== undefined) return { kind: 'returnEarly', value: cachedResult };\n\n return { kind: 'trigger', key: requestKey, payload: resolved };\n },\n buildStream: (request) => {\n const method = request.method ?? 'GET';\n const options: Record<string, unknown> = {};\n if (request.body !== undefined) options['body'] = request.body;\n if (request.headers) options['headers'] = request.headers;\n\n return httpClient.request(method, request.url, options).pipe(\n map((response) => extractBoolean(response, condition.responseExpression, pendingValue, logger)),\n tap((value) => {\n const requestKey = stableStringify(request);\n cache.set(requestKey, value, cacheDurationMs);\n }),\n catchError((error) => {\n logger.warn('HTTP condition request failed:', error);\n return of(pendingValue);\n }),\n );\n },\n });\n\n cacheService.httpConditionFunctionCache.set(cacheKey, fn as LogicFn<unknown, boolean>);\n return fn;\n}\n","import { inject, Injector } from '@angular/core';\nimport { FieldContext, LogicFn } from '@angular/forms/signals';\nimport { catchError, from, map, of } from 'rxjs';\nimport { AsyncCondition } from '../../models/expressions/conditional-expression';\nimport { stableStringify } from '../../utils/stable-stringify';\nimport { FieldContextRegistryService } from '../registry/field-context-registry.service';\nimport { FunctionRegistryService } from '../registry/function-registry.service';\nimport { DynamicFormLogger } from '../../providers/features/logger/logger.token';\nimport { Logger } from '../../providers/features/logger/logger.interface';\nimport { AsyncConditionFunctionCacheService } from './async-condition-function-cache.service';\nimport { createDebouncedResourceLogicFn } from './debounced-resource-logic-fn';\n\n/** Creates a logic function for an async condition. */\nexport function createAsyncConditionLogicFunction<TValue>(condition: AsyncCondition): LogicFn<TValue, boolean> {\n const fieldContextRegistry = inject(FieldContextRegistryService);\n const functionRegistry = inject(FunctionRegistryService);\n const injector = inject(Injector);\n const logger = inject(DynamicFormLogger) as Logger;\n const cacheService = inject(AsyncConditionFunctionCacheService);\n\n const pendingValue = condition.pendingValue ?? false;\n const debounceMs = condition.debounceMs ?? 300;\n\n if (condition.asyncFn && condition.asyncFunctionName) {\n logger.warn(\n 'Both \"asyncFn\" and \"asyncFunctionName\" are set on async condition. Inline \"asyncFn\" takes precedence; \"asyncFunctionName\" is ignored.',\n );\n }\n\n // Skip cross-condition caching when an inline function is used: stableStringify\n // cannot distinguish two different inline functions, which would cause cache hits\n // to wrongly reuse a LogicFn closing over a different function reference.\n const cacheKey = condition.asyncFn ? undefined : stableStringify(condition);\n\n if (cacheKey !== undefined) {\n const cached = cacheService.asyncConditionFunctionCache.get(cacheKey);\n if (cached) {\n return cached as LogicFn<TValue, boolean>;\n }\n }\n\n const fn = createDebouncedResourceLogicFn<TValue, FieldContext<TValue>, boolean>({\n pendingValue,\n debounceMs,\n injector,\n resolve: (ctx) => {\n // Build reactive evaluation context here so the LogicFn establishes\n // signal dependencies on form values — the resource's reactive graph\n // alone wouldn't notice changes that didn't flow through the trigger.\n const evaluationContext = fieldContextRegistry.createReactiveEvaluationContext(ctx, functionRegistry.getCustomFunctions());\n const key = stableStringify({\n formValue: evaluationContext.formValue,\n fieldValue: evaluationContext.fieldValue,\n externalData: evaluationContext.externalData,\n });\n return { kind: 'trigger', key, payload: ctx };\n },\n buildStream: (capturedCtx) => {\n // Inline `asyncFn` wins; otherwise look up the registered function at call time\n // (fresh reference, in case the registry was updated after LogicFn creation).\n const asyncFn =\n condition.asyncFn ??\n (condition.asyncFunctionName ? functionRegistry.getAsyncConditionFunction(condition.asyncFunctionName) : undefined);\n if (!asyncFn) {\n logger.warn(\n `Async Condition - function '${condition.asyncFunctionName}' not found. ` + `Register it in customFnConfig.asyncConditions.`,\n );\n return of(pendingValue);\n }\n\n const evaluationContext = fieldContextRegistry.createReactiveEvaluationContext(capturedCtx, functionRegistry.getCustomFunctions());\n\n // asyncFn may return Promise<boolean> or Observable<boolean>.\n // from() normalizes both to Observable.\n return from(asyncFn(evaluationContext)).pipe(\n map((result) => !!result),\n catchError((error) => {\n logger.warn('Async Condition - function failed:', error);\n return of(pendingValue);\n }),\n );\n },\n });\n\n if (cacheKey !== undefined) {\n cacheService.asyncConditionFunctionCache.set(cacheKey, fn as LogicFn<unknown, boolean>);\n }\n return fn;\n}\n","import { Signal, WritableSignal } from '@angular/core';\nimport { LogicFn } from '@angular/forms/signals';\n\n/** Per-field debounced signal pair. */\ninterface DebouncedSignalPair {\n immediateValue: WritableSignal<boolean>;\n debouncedValue: Signal<boolean | undefined>;\n}\n\n/** DI-scoped cache for logic functions and debounced signals. */\nexport class LogicFunctionCacheService {\n /** Cache for memoized logic functions, keyed by serialized expression. */\n readonly logicFunctionCache = new Map<string, LogicFn<unknown, boolean>>();\n\n /** Cache for debounced logic functions, keyed by serialized expression + debounceMs. */\n readonly debouncedLogicFunctionCache = new Map<string, LogicFn<unknown, boolean>>();\n\n /** Per-field signal store for debounced logic, keyed by field path. */\n readonly debouncedSignalStore = new Map<string, DebouncedSignalPair>();\n}\n","import { inject, Injector, signal, untracked } from '@angular/core';\nimport { toObservable, toSignal } from '@angular/core/rxjs-interop';\nimport { FieldContext, LogicFn } from '@angular/forms/signals';\nimport { debounceTime, distinctUntilChanged, startWith } from 'rxjs';\nimport { ConditionalExpression } from '../../models/expressions/conditional-expression';\nimport { DynamicFormError } from '../../errors/dynamic-form-error';\nimport { stableStringify } from '../../utils/stable-stringify';\nimport { FieldContextRegistryService } from '../registry/field-context-registry.service';\nimport { FunctionRegistryService } from '../registry/function-registry.service';\nimport { evaluateCondition } from './condition-evaluator';\nimport { createHttpConditionLogicFunction } from './http-condition-logic-function';\nimport { createAsyncConditionLogicFunction } from './async-condition-logic-function';\nimport { LogicFunctionCacheService } from './logic-function-cache.service';\nimport { safeReadPathKeys } from '../../utils/safe-read-path-keys';\n\n/**\n * Recursively validates that HTTP/async conditions are not nested inside and/or composites.\n * These conditions require async resolution and cannot be evaluated synchronously\n * inside composite conditions.\n */\nfunction validateNoNestedHttpConditions(expression: ConditionalExpression): void {\n if (expression.type === 'and' || expression.type === 'or') {\n for (const sub of expression.conditions) {\n if (sub.type === 'http' || sub.type === 'async') {\n const label = sub.type === 'http' ? 'HTTP' : 'Async';\n throw new DynamicFormError(\n `${label} conditions cannot be nested inside '${expression.type}' composites. ` +\n `Move the ${label} condition to a separate logic entry on the field.`,\n );\n }\n validateNoNestedHttpConditions(sub);\n }\n }\n}\n\n/**\n * Checks whether the expression tree contains any inline function.\n * Inline functions serialize to nothing in stableStringify, so caching by\n * serialized key would collide across distinct functions.\n */\nfunction containsInlineFunction(expression: ConditionalExpression): boolean {\n if (expression.type === 'and' || expression.type === 'or') {\n return expression.conditions.some(containsInlineFunction);\n }\n if (expression.type === 'custom') {\n return typeof expression.fn === 'function';\n }\n if (expression.type === 'async') {\n return typeof expression.asyncFn === 'function';\n }\n return false;\n}\n\n/**\n * Create a logic function from a conditional expression.\n *\n * @param expression The conditional expression to evaluate\n * @returns A LogicFn that evaluates the condition in the context of a field\n */\nexport function createLogicFunction<TValue>(expression: ConditionalExpression): LogicFn<TValue, boolean> {\n // Async conditions handle their own debouncing and async resolution\n if (expression?.type === 'async') {\n return createAsyncConditionLogicFunction(expression);\n }\n\n // HTTP conditions handle their own debouncing and async resolution\n if (expression?.type === 'http') {\n return createHttpConditionLogicFunction(expression);\n }\n\n // Validate that HTTP conditions aren't nested inside and/or composites\n if (expression) {\n validateNoNestedHttpConditions(expression);\n }\n\n // Inject services during factory creation, not during execution\n const functionRegistry = inject(FunctionRegistryService);\n const fieldContextRegistry = inject(FieldContextRegistryService);\n const cacheService = inject(LogicFunctionCacheService);\n\n // Inline functions serialize to nothing, so their cache keys would collide\n const cacheable = !expression || !containsInlineFunction(expression);\n\n // Generate cache key from serialized expression\n const cacheKey = stableStringify(expression);\n\n // Check cache first\n if (cacheable) {\n const cached = cacheService.logicFunctionCache.get(cacheKey);\n if (cached) {\n return cached as LogicFn<TValue, boolean>;\n }\n }\n\n const fn: LogicFn<TValue, boolean> = (ctx: FieldContext<TValue>) => {\n // Create REACTIVE evaluation context for logic functions\n // This allows logic to re-evaluate when dependent fields change\n const evaluationContext = fieldContextRegistry.createReactiveEvaluationContext(ctx, functionRegistry.getCustomFunctions());\n\n return evaluateCondition(expression, evaluationContext);\n };\n\n // Cache the function\n if (cacheable) {\n cacheService.logicFunctionCache.set(cacheKey, fn as LogicFn<unknown, boolean>);\n }\n return fn;\n}\n\n/**\n * Create a debounced logic function from a conditional expression.\n *\n * @param expression The conditional expression to evaluate\n * @param debounceMs The debounce duration in milliseconds\n * @returns A LogicFn that evaluates the condition with debouncing\n */\nexport function createDebouncedLogicFunction<TValue>(expression: ConditionalExpression, debounceMs: number): LogicFn<TValue, boolean> {\n // Async conditions handle their own debouncing via condition.debounceMs\n if (expression?.type === 'async') {\n return createAsyncConditionLogicFunction(expression);\n }\n\n // HTTP conditions handle their own debouncing via condition.http.debounceMs\n if (expression?.type === 'http') {\n return createHttpConditionLogicFunction(expression);\n }\n\n // Validate that HTTP conditions aren't nested inside and/or composites\n if (expression) {\n validateNoNestedHttpConditions(expression);\n }\n\n // Inject services during factory creation, not during execution\n const functionRegistry = inject(FunctionRegistryService);\n const fieldContextRegistry = inject(FieldContextRegistryService);\n const injector = inject(Injector);\n const cacheService = inject(LogicFunctionCacheService);\n\n // Inline functions serialize to nothing, so their cache keys would collide\n const cacheable = !expression || !containsInlineFunction(expression);\n\n // Generate cache key including debounceMs\n const cacheKey = `${stableStringify(expression)}:${debounceMs}`;\n\n // Check cache first\n if (cacheable) {\n const cached = cacheService.debouncedLogicFunctionCache.get(cacheKey);\n if (cached) {\n return cached as LogicFn<TValue, boolean>;\n }\n }\n\n const fn: LogicFn<TValue, boolean> = (ctx: FieldContext<TValue>) => {\n const contextKey = safeReadPathKeys(ctx as FieldContext<unknown>).join('.');\n let signalPair = cacheService.debouncedSignalStore.get(contextKey);\n\n if (!signalPair) {\n // Create a signal to hold the immediate evaluation result\n const immediateValue = signal(false);\n\n // Wrap in untracked() to avoid NG0602: toObservable() internally calls effect(),\n // which cannot be created inside a reactive context (computed). The LogicFn runs\n // inside Angular Signal Forms' BooleanOrLogic.compute (a computed).\n const debouncedValue = untracked(() => {\n const immediateValue$ = toObservable(immediateValue, { injector }).pipe(\n debounceTime(debounceMs),\n distinctUntilChanged(),\n startWith(false),\n );\n\n return toSignal(immediateValue$, { injector, initialValue: false });\n });\n\n signalPair = { immediateValue, debouncedValue };\n cacheService.debouncedSignalStore.set(contextKey, signalPair);\n }\n\n // Create REACTIVE evaluation context for logic functions\n const evaluationContext = fieldContextRegistry.createReactiveEvaluationContext(ctx, functionRegistry.getCustomFunctions());\n\n // Evaluate the condition and update the immediate signal\n const result = evaluateCondition(expression, evaluationContext);\n\n // Update immediate value (this triggers the debounce chain)\n // signalPair is guaranteed to exist here due to the initialization above\n const { immediateValue, debouncedValue } = signalPair;\n untracked(() => {\n if (immediateValue() !== result) {\n immediateValue.set(result);\n }\n });\n\n // Return the debounced value\n return debouncedValue() ?? false;\n };\n\n // Cache the function\n if (cacheable) {\n cacheService.debouncedLogicFunctionCache.set(cacheKey, fn as LogicFn<unknown, boolean>);\n }\n return fn;\n}\n","import { InjectionToken, Signal } from '@angular/core';\nimport { FieldTree } from '@angular/forms/signals';\n\n/** Represents a FieldTree node for dynamic bracket-access navigation. */\nexport type FieldTreeRecord = Record<string, FieldTree<unknown> | undefined>;\n\n/**\n * Type representing a FieldTree for an array.\n * Extends FieldTree to add numeric indexing for accessing item FieldTrees.\n */\nexport type ArrayFieldTree<T> = FieldTree<T[]> & {\n readonly [index: number]: FieldTree<T> | undefined;\n};\n\n/** Get the length of an array FieldTree. */\nexport function getArrayLength<T>(arrayFieldTree: ArrayFieldTree<T>): number {\n const value = arrayFieldTree().value();\n return Array.isArray(value) ? value.length : 0;\n}\n\n/** Read-only view of a single field's observable state. */\nexport interface ReadonlyFieldTree<TValue = unknown> {\n readonly value: Signal<TValue>;\n readonly valid: Signal<boolean>;\n readonly invalid: Signal<boolean>;\n readonly touched: Signal<boolean>;\n readonly dirty: Signal<boolean>;\n readonly required: Signal<boolean>;\n readonly disabled: Signal<boolean>;\n readonly hidden: Signal<boolean>;\n readonly errors: Signal<readonly unknown[]>;\n}\n\n/**\n * Build a `ReadonlyFieldTree` by extracting the whitelisted read signals from a\n * Signal Forms `FieldTree`. Returns a fresh plain object — no casting, no proxying,\n * so consumers only see the narrow surface and Angular's `WritableSignal` capability\n * on `value` is hidden.\n */\nexport function toReadonlyFieldTree<TValue>(field: FieldTree<TValue>): ReadonlyFieldTree<TValue> {\n const state = field();\n return {\n value: state.value,\n valid: state.valid,\n invalid: state.invalid,\n touched: state.touched,\n dirty: state.dirty,\n required: state.required,\n disabled: state.disabled,\n hidden: state.hidden,\n errors: state.errors,\n };\n}\n\n/**\n * Write a value into a FieldTree's underlying signal. Signal Forms stores\n * `FieldState.value` as a `WritableSignal`; this helper centralises the cast\n * and warns if that contract ever changes (vs a silent no-op).\n */\nexport function writeToFieldValue<T>(value: Signal<T>, next: T, logger?: { warn: (msg: string) => void }): boolean {\n const candidate = value as { set?: (v: T) => void };\n if (typeof candidate.set !== 'function') {\n logger?.warn('writeToFieldValue: FieldState.value is not a WritableSignal — Signal Forms contract changed.');\n return false;\n }\n candidate.set(next);\n return true;\n}\n\n/**\n * DI-scoped cache for `ReadonlyFieldTree` views, keyed on the source FieldTree\n * identity. A fresh WeakMap per root injector keeps renders isolated under SSR —\n * Angular creates a new root injector per request, so there's no shared state\n * between server renders (same pattern as `COMPONENT_CACHE`).\n *\n * @internal\n */\nexport const READONLY_FIELD_TREE_CACHE = new InjectionToken<WeakMap<FieldTree<unknown>, ReadonlyFieldTree<unknown>>>(\n 'READONLY_FIELD_TREE_CACHE',\n {\n providedIn: 'root',\n factory: () => new WeakMap(),\n },\n);\n\n/**\n * Cached variant of {@link toReadonlyFieldTree}. Each FieldTree instance is a\n * stable reference for the lifetime of its form, so one `ReadonlyFieldTree`\n * view per tree is enough. Callers on hot paths (`pushInputs` on every mapper\n * emission) avoid re-allocating the nine-property view object.\n *\n * @internal\n */\nexport function toReadonlyFieldTreeCached<TValue>(\n cache: WeakMap<FieldTree<unknown>, ReadonlyFieldTree<unknown>>,\n field: FieldTree<TValue>,\n): ReadonlyFieldTree<TValue> {\n const key = field as FieldTree<unknown>;\n const cached = cache.get(key);\n if (cached) return cached as ReadonlyFieldTree<TValue>;\n\n const view = toReadonlyFieldTree(field);\n cache.set(key, view as ReadonlyFieldTree<unknown>);\n return view;\n}\n","import { computed, inject, Signal } from '@angular/core';\nimport { FieldTree } from '@angular/forms/signals';\nimport { FormOptions, NextButtonOptions, SubmitButtonOptions } from '../../models/form-config';\nimport { LogicConfig, FormStateCondition, isFormStateCondition } from '../../models/logic';\nimport { ConditionalExpression } from '../../models/expressions';\nimport { evaluateCondition } from '../expressions';\nimport { FieldContextRegistryService } from '../registry/field-context-registry.service';\nimport { FunctionRegistryService } from '../registry/function-registry.service';\nimport { ARRAY_CONTEXT } from '../../models/field-signal-context.token';\nimport type { EvaluationContext } from '../../models/expressions/evaluation-context';\nimport type { Logger } from '../../providers/features/logger/logger.interface';\n\n/**\n * No-op logger for when no logger is provided.\n * Used as fallback in button logic evaluation to avoid breaking downstream packages.\n */\nconst noOpLogger: Logger = {\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n debug: () => {},\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n info: () => {},\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n warn: () => {},\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n error: () => {},\n};\n\n/** Context for resolving button disabled state. */\nexport interface ButtonLogicContext {\n /** The form's FieldTree instance (supports both string and number keys for array indices) */\n form: FieldTree<unknown, string | number>;\n\n /** Form-level options */\n formOptions?: FormOptions;\n\n /** Field-level logic array (if provided) */\n fieldLogic?: LogicConfig[];\n\n /** Explicit disabled state from field definition */\n explicitlyDisabled?: boolean;\n\n /** Current page validity signal (for paged forms) */\n currentPageValid?: Signal<boolean>;\n\n /** Current form value for evaluating conditional expressions */\n formValue?: unknown;\n\n /**\n * Optional factory returning the full evaluation context (including `externalData`\n * and `customFunctions`) used to evaluate `ConditionalExpression` conditions.\n * Invoked lazily during evaluation so external-data signal reads stay reactive.\n * When omitted, a minimal context (form value only) is used.\n */\n evaluationContext?: () => EvaluationContext;\n\n /** Optional logger for diagnostic output. Falls back to no-op logger if not provided. */\n logger?: Logger;\n}\n\n/**\n * Allowed logic types for non-form-bound elements (buttons, text fields, etc.).\n * These elements only support hidden and disabled states - not readonly or required\n * since they don't participate in form validation.\n */\nexport type NonFieldLogicType = 'hidden' | 'disabled';\n\n/**\n * Logic config restricted to types valid for non-form-bound elements.\n * This is a subset of LogicConfig that only includes hidden and disabled types.\n */\nexport type NonFieldLogicConfig = LogicConfig & { type: NonFieldLogicType };\n\n/** Context for resolving state (hidden/disabled) for non-form-bound elements. */\nexport interface NonFieldLogicContext {\n /** The form's FieldTree instance (supports both string and number keys for array indices) */\n form: FieldTree<unknown, string | number>;\n\n /**\n * Field-level logic array containing conditions.\n * Accepts full LogicConfig[] for compatibility, but only hidden and disabled types are processed.\n */\n fieldLogic?: LogicConfig[];\n\n /** Explicit state from field definition (e.g., `hidden: true` or `disabled: true`) */\n explicitValue?: boolean;\n\n /** Current form value for evaluating conditional expressions */\n formValue?: unknown;\n\n /**\n * Optional factory returning the full evaluation context (including `externalData`\n * and `customFunctions`) used to evaluate `ConditionalExpression` conditions.\n * Invoked lazily during evaluation so external-data signal reads stay reactive.\n * When omitted, a minimal context (form value only) is used.\n */\n evaluationContext?: () => EvaluationContext;\n\n /** Optional logger for diagnostic output. Falls back to no-op logger if not provided. */\n logger?: Logger;\n}\n\n/** Default options for submit button disabled behavior. */\nconst DEFAULT_SUBMIT_BUTTON_OPTIONS: Required<SubmitButtonOptions> = {\n disableWhenInvalid: true,\n disableWhileSubmitting: true,\n};\n\n/** Default options for next button disabled behavior. */\nconst DEFAULT_NEXT_BUTTON_OPTIONS: Required<NextButtonOptions> = {\n disableWhenPageInvalid: true,\n disableWhileSubmitting: true,\n};\n\n/**\n * Evaluates a FormStateCondition against the current form/page state.\n *\n * @param condition - The form state condition to evaluate\n * @param ctx - The button logic context\n * @returns true if the condition is met (button should be disabled)\n */\nfunction evaluateFormStateCondition(condition: FormStateCondition, ctx: ButtonLogicContext): boolean {\n const form = ctx.form();\n\n switch (condition) {\n case 'formInvalid':\n return !form.valid();\n\n case 'formSubmitting':\n return form.submitting();\n\n case 'pageInvalid':\n return ctx.currentPageValid ? !ctx.currentPageValid() : false;\n\n default:\n return false;\n }\n}\n\n/**\n * Evaluates a single logic condition (boolean, FormStateCondition, or ConditionalExpression).\n *\n * @param condition - The condition to evaluate\n * @param ctx - The button logic context\n * @returns true if the condition is met\n */\nfunction evaluateLogicCondition(condition: LogicConfig['condition'], ctx: ButtonLogicContext): boolean {\n // Boolean condition\n if (typeof condition === 'boolean') {\n return condition;\n }\n\n // FormStateCondition (string)\n if (isFormStateCondition(condition)) {\n return evaluateFormStateCondition(condition, ctx);\n }\n\n // ConditionalExpression. Prefer the full context (externalData + customFunctions) when the\n // caller supplies a factory, so container/button conditions match leaf-field scope. The\n // factory context carries its own formValue (the root form value), so ctx.formValue is\n // intentionally superseded here; a future scoped caller (array-item or page-scoped value)\n // must build that scope into the factory rather than rely on ctx.formValue.\n if (ctx.evaluationContext) {\n return evaluateCondition(condition as ConditionalExpression, ctx.evaluationContext());\n }\n\n // Legacy fallback: no factory supplied, so this context omits externalData/customFunctions.\n // javascript expressions referencing externalData and custom (functionName) conditions\n // cannot resolve on this path. Every production caller now passes a factory; this remains\n // only for boolean/form-state conditions and direct callers that need no expression scope.\n const formValue = (ctx.formValue ?? ctx.form().value()) as Record<string, unknown>;\n const evaluationContext = {\n fieldValue: undefined,\n formValue,\n fieldPath: '',\n logger: ctx.logger ?? noOpLogger,\n };\n\n return evaluateCondition(condition as ConditionalExpression, evaluationContext);\n}\n\n/**\n * Checks if any disabled logic condition is met in the field's logic array.\n *\n * @param fieldLogic - Array of logic configurations\n * @param ctx - The button logic context\n * @returns true if any disabled condition is met\n */\nfunction hasFieldLevelDisabledCondition(fieldLogic: LogicConfig[] | undefined, ctx: ButtonLogicContext): boolean {\n if (!fieldLogic || fieldLogic.length === 0) {\n return false;\n }\n\n return fieldLogic.filter((logic) => logic.type === 'disabled').some((logic) => evaluateLogicCondition(logic.condition, ctx));\n}\n\n/**\n * Checks if the field has custom logic of a specific type.\n *\n * @param fieldLogic - Array of logic configurations\n * @param logicType - The type of logic to check for ('hidden', 'disabled', 'readonly', 'required')\n * @returns true if field has logic of the specified type\n */\nfunction hasCustomLogicOfType(fieldLogic: LogicConfig[] | undefined, logicType: LogicConfig['type']): boolean {\n if (!fieldLogic || fieldLogic.length === 0) {\n return false;\n }\n\n return fieldLogic.some((logic) => logic.type === logicType);\n}\n\n/**\n * Evaluates all logic conditions of a specific type and returns true if any condition is met.\n *\n * @param fieldLogic - Array of logic configurations\n * @param logicType - The type of logic to evaluate ('hidden', 'disabled', 'readonly', 'required')\n * @param ctx - Context containing form and formValue for evaluation\n * @returns true if any condition of the specified type is met\n */\nfunction evaluateLogicOfType(\n fieldLogic: LogicConfig[] | undefined,\n logicType: LogicConfig['type'],\n ctx: { form: FieldTree<unknown, string | number>; formValue?: unknown; evaluationContext?: () => EvaluationContext; logger?: Logger },\n): boolean {\n if (!fieldLogic || fieldLogic.length === 0) {\n return false;\n }\n\n const buttonCtx: ButtonLogicContext = {\n form: ctx.form,\n formValue: ctx.formValue,\n evaluationContext: ctx.evaluationContext,\n logger: ctx.logger,\n };\n\n return fieldLogic.filter((logic) => logic.type === logicType).some((logic) => evaluateLogicCondition(logic.condition, buttonCtx));\n}\n\n/**\n * Resolves the disabled state for a submit button.\n *\n * @param ctx - The button logic context\n * @returns A computed signal that returns true when the button should be disabled\n */\nexport function resolveSubmitButtonDisabled(ctx: ButtonLogicContext): Signal<boolean> {\n return computed(() => {\n // 1. Explicit disabled always wins\n if (ctx.explicitlyDisabled) {\n return true;\n }\n\n // 2. If field has custom disabled logic, use it exclusively\n if (hasCustomLogicOfType(ctx.fieldLogic, 'disabled')) {\n return hasFieldLevelDisabledCondition(ctx.fieldLogic, ctx);\n }\n\n // 3. Apply form-level defaults\n const options = {\n ...DEFAULT_SUBMIT_BUTTON_OPTIONS,\n ...ctx.formOptions?.submitButton,\n };\n\n const form = ctx.form();\n\n if (options.disableWhenInvalid && !form.valid()) {\n return true;\n }\n\n if (options.disableWhileSubmitting && form.submitting()) {\n return true;\n }\n\n return false;\n });\n}\n\n/**\n * Resolves the disabled state for a next page button.\n *\n * @param ctx - The button logic context\n * @returns A computed signal that returns true when the button should be disabled\n */\nexport function resolveNextButtonDisabled(ctx: ButtonLogicContext): Signal<boolean> {\n return computed(() => {\n // 1. Explicit disabled always wins\n if (ctx.explicitlyDisabled) {\n return true;\n }\n\n // 2. If field has custom disabled logic, use it exclusively\n if (hasCustomLogicOfType(ctx.fieldLogic, 'disabled')) {\n return hasFieldLevelDisabledCondition(ctx.fieldLogic, ctx);\n }\n\n // 3. Apply form-level defaults\n const options = {\n ...DEFAULT_NEXT_BUTTON_OPTIONS,\n ...ctx.formOptions?.nextButton,\n };\n\n const form = ctx.form();\n\n if (options.disableWhenPageInvalid && ctx.currentPageValid && !ctx.currentPageValid()) {\n return true;\n }\n\n if (options.disableWhileSubmitting && form.submitting()) {\n return true;\n }\n\n return false;\n });\n}\n\n/**\n * Evaluates the hidden state for non-form-bound elements synchronously.\n *\n * @param ctx - The context containing form, logic array, and explicit hidden state\n * @returns true when the element should be hidden\n */\nexport function evaluateNonFieldHidden(ctx: NonFieldLogicContext): boolean {\n // 1. Explicit hidden always wins\n if (ctx.explicitValue) {\n return true;\n }\n\n // 2. Evaluate hidden logic conditions\n if (hasCustomLogicOfType(ctx.fieldLogic, 'hidden')) {\n return evaluateLogicOfType(ctx.fieldLogic, 'hidden', {\n form: ctx.form,\n formValue: ctx.formValue,\n evaluationContext: ctx.evaluationContext,\n logger: ctx.logger,\n });\n }\n\n // 3. No hidden conditions - element is visible\n return false;\n}\n\n/**\n * Resolves the hidden state for non-form-bound elements as a reactive signal.\n *\n * @param ctx - The context containing form, logic array, and explicit hidden state\n * @returns A computed signal that returns true when the element should be hidden\n */\nexport function resolveNonFieldHidden(ctx: NonFieldLogicContext): Signal<boolean> {\n return computed(() => evaluateNonFieldHidden(ctx));\n}\n\n/**\n * Evaluates the disabled state for non-form-bound elements synchronously.\n *\n * @param ctx - The context containing form, logic array, and explicit disabled state\n * @returns true when the element should be disabled\n */\nexport function evaluateNonFieldDisabled(ctx: NonFieldLogicContext): boolean {\n // 1. Explicit disabled always wins\n if (ctx.explicitValue) {\n return true;\n }\n\n // 2. Evaluate disabled logic conditions\n if (hasCustomLogicOfType(ctx.fieldLogic, 'disabled')) {\n return evaluateLogicOfType(ctx.fieldLogic, 'disabled', {\n form: ctx.form,\n formValue: ctx.formValue,\n evaluationContext: ctx.evaluationContext,\n logger: ctx.logger,\n });\n }\n\n // 3. No disabled conditions - element is enabled\n return false;\n}\n\n/**\n * Resolves the disabled state for non-form-bound elements as a reactive signal.\n *\n * @param ctx - The context containing form, logic array, and explicit disabled state\n * @returns A computed signal that returns true when the element should be disabled\n */\nexport function resolveNonFieldDisabled(ctx: NonFieldLogicContext): Signal<boolean> {\n return computed(() => evaluateNonFieldDisabled(ctx));\n}\n\n/**\n * Builds a lazy full evaluation-context factory (with `externalData` and `customFunctions`)\n * for a non-form-bound element. Call from a mapper factory body (injection context); the\n * returned thunk is invoked lazily inside the mapper/resolver computed so external-data\n * signal reads stay reactive. Single source for that factory, shared by the container/text\n * mappers (via `applyHiddenLogic`) and the button mappers.\n */\nexport function injectNonFieldEvaluationContext(\n fieldDef: { key?: string },\n options: { scopeToArrayItem?: boolean } = {},\n): () => EvaluationContext {\n const fieldContextRegistry = inject(FieldContextRegistryService);\n const functionRegistry = inject(FunctionRegistryService);\n // Only layout containers (grouping item fields) scope to the enclosing array item, matching\n // leaf fields. Buttons — including array-control buttons whose conditions reference the array\n // itself (e.g. `formValue.items.length`) — must keep root scope, so they opt out.\n const arrayContext = options.scopeToArrayItem ? inject(ARRAY_CONTEXT, { optional: true }) : null;\n return () => {\n const arrayScope = arrayContext\n ? { arrayKey: arrayContext.arrayKey, index: arrayContext.index(), localKey: fieldDef.key ?? '' }\n : undefined;\n return fieldContextRegistry.createDisplayOnlyContext(fieldDef.key ?? '', functionRegistry.getCustomFunctions(), arrayScope);\n };\n}\n","import type { AsyncCustomValidator, CustomValidator, HttpCustomValidator } from '../../core/validation/validator-types';\nimport { ConditionalExpression } from '../expressions/conditional-expression';\nimport { HttpRequestConfig } from '../http/http-request-config';\nimport { HttpValidationResponseMapping } from '../http/http-response-mapping';\n\n/** Base configuration shared by all validators */\nexport interface BaseValidatorConfig {\n /** Conditional logic for when validator applies */\n when?: ConditionalExpression;\n}\n\n/** Built-in validator configuration (required, email, min, max, etc.) */\nexport interface BuiltInValidatorConfig extends BaseValidatorConfig {\n /** Validator type identifier */\n type: 'required' | 'email' | 'min' | 'max' | 'minLength' | 'maxLength' | 'pattern';\n\n /** Static value for the validator (e.g., min value, pattern) */\n value?: number | string | RegExp;\n\n /** Dynamic value expression that evaluates to validator parameter */\n expression?: string;\n}\n\n/**\n * Custom validator configuration using Angular's public FieldContext API.\n * Returns ValidationError | ValidationError[] | null synchronously.\n */\nexport interface CustomValidatorConfig extends BaseValidatorConfig {\n /** Validator type identifier */\n type: 'custom';\n\n /** Name of registered validator function (function-based pattern) */\n functionName?: string;\n\n /**\n * Inline custom validator (code-only authoring).\n * Mutually exclusive with `functionName`. NOT JSON-serializable.\n */\n fn?: CustomValidator;\n\n /** Optional parameters to pass to validator function */\n params?: Record<string, unknown>;\n\n /** JavaScript expression to evaluate (expression-based pattern) */\n expression?: string;\n\n /** Error kind for expression-based validators - links to validationMessages */\n kind?: string;\n\n /**\n * Parameters to include in error object for message interpolation (expression-based pattern)\n * Map of parameter names to expressions that will be evaluated in the same context as the validation expression\n */\n errorParams?: Record<string, string>;\n}\n\n/**\n * Shared fields for both branches of {@link AsyncValidatorConfig}.\n *\n * @internal\n */\ninterface AsyncValidatorConfigShared extends BaseValidatorConfig {\n /** Validator type identifier. */\n type: 'async';\n /** Optional parameters to pass to validator function */\n params?: Record<string, unknown>;\n}\n\n/**\n * Async custom validator configuration using Angular's `validateAsync` API.\n * Returns Observable<ValidationError | ValidationError[] | null>.\n */\nexport type AsyncValidatorConfig =\n | (AsyncValidatorConfigShared & {\n /** Name of registered async validator function */\n functionName: string;\n /** Inline form is forbidden when `functionName` is set */\n fn?: never;\n })\n | (AsyncValidatorConfigShared & {\n /** Inline async validator. Mutually exclusive with `functionName`. */\n fn: AsyncCustomValidator;\n /** Registered form is forbidden when `fn` is set */\n functionName?: never;\n });\n\n/**\n * Shared fields for both branches of {@link FunctionHttpValidatorConfig}.\n *\n * @internal\n */\ninterface FunctionHttpValidatorConfigShared extends BaseValidatorConfig {\n /** Validator type identifier. */\n type: 'http';\n /** Optional parameters to pass to HTTP validator */\n params?: Record<string, unknown>;\n}\n\n/** Function-based HTTP validator configuration. Uses Angular's `validateHttp` API. */\nexport type FunctionHttpValidatorConfig =\n | (FunctionHttpValidatorConfigShared & {\n /** Name of registered HTTP validator configuration */\n functionName: string;\n /** Inline form is forbidden when `functionName` is set */\n fn?: never;\n })\n | (FunctionHttpValidatorConfigShared & {\n /** Inline HTTP validator. Mutually exclusive with `functionName`. */\n fn: HttpCustomValidator;\n /** Registered form is forbidden when `fn` is set */\n functionName?: never;\n });\n\n/** Declarative HTTP validator configuration — fully JSON-serializable, no function registration required. */\nexport interface DeclarativeHttpValidatorConfig extends BaseValidatorConfig {\n /** Validator type identifier */\n type: 'http';\n\n /** HTTP request configuration with expression-based query params and body */\n http: HttpRequestConfig;\n\n /** Mapping that interprets the HTTP response as a validation result */\n responseMapping: HttpValidationResponseMapping;\n}\n\n/**\n * Configuration for signal forms validator functions that can be serialized from API.\n * Discriminated union type for type-safe validator configuration.\n */\nexport type ValidatorConfig =\n | BuiltInValidatorConfig\n | CustomValidatorConfig\n | AsyncValidatorConfig\n | FunctionHttpValidatorConfig\n | DeclarativeHttpValidatorConfig;\n\n/** Type guard to distinguish function-based HTTP validators from declarative ones. */\nexport function isFunctionHttpValidator(\n config: FunctionHttpValidatorConfig | DeclarativeHttpValidatorConfig,\n): config is FunctionHttpValidatorConfig {\n const c = config as { functionName?: unknown; fn?: unknown };\n return typeof c.functionName === 'string' || (c.fn !== null && typeof c.fn === 'object');\n}\n","import { ValidationError } from '@angular/forms/signals';\nimport { HttpValidationResponseMapping } from '../../models/http/http-response-mapping';\nimport { Logger } from '../../providers/features/logger/logger.interface';\nimport { ExpressionParser } from '../expressions/parser/expression-parser';\n\n/** Evaluates an HTTP response against a `HttpValidationResponseMapping` to produce a validation result. */\nexport function evaluateHttpValidationResponse(\n response: unknown,\n mapping: HttpValidationResponseMapping,\n logger: Logger,\n): ValidationError | null {\n const scope = { response };\n\n try {\n const result = ExpressionParser.evaluate(mapping.validWhen, scope);\n\n if (typeof result !== 'boolean') {\n logger.warn(`validWhen expression \"${mapping.validWhen}\" returned non-boolean value:`, result, 'Expected true or false.');\n }\n\n if (result === true) {\n return null;\n }\n\n const error: ValidationError & Record<string, unknown> = { kind: mapping.errorKind };\n\n if (mapping.errorParams) {\n for (const [key, expression] of Object.entries(mapping.errorParams)) {\n try {\n error[key] = ExpressionParser.evaluate(expression, scope);\n } catch (err) {\n logger.warn(`Error evaluating errorParam \"${key}\":`, expression, err);\n }\n }\n }\n\n return error;\n } catch (err) {\n logger.warn('Error evaluating validWhen expression:', mapping.validWhen, err);\n return { kind: mapping.errorKind };\n }\n}\n","import { LogicFn } from '@angular/forms/signals';\n\n/** DI-scoped cache for dynamic value functions. */\nexport class DynamicValueFunctionCacheService {\n /** Cache for memoized dynamic value functions, keyed by expression string. */\n readonly dynamicValueFunctionCache = new Map<string, LogicFn<unknown, unknown>>();\n}\n","import { inject } from '@angular/core';\nimport { FieldContext, LogicFn } from '@angular/forms/signals';\nimport { FieldContextRegistryService } from '../registry/field-context-registry.service';\nimport { ExpressionParser } from '../expressions/parser/expression-parser';\nimport { DynamicFormLogger } from '../../providers/features/logger/logger.token';\nimport { DynamicValueFunctionCacheService } from './dynamic-value-function-cache.service';\n\n/**\n * Create a dynamic value function from an expression string.\n * Uses secure AST-based parsing.\n *\n * @param expression The expression string to evaluate\n * @returns A LogicFn that evaluates the expression in the context of a field\n */\nexport function createDynamicValueFunction<TValue, TReturn>(expression: string): LogicFn<TValue, TReturn> {\n // Inject services during factory creation, not during execution\n // This captures the service instance in the closure\n const fieldContextRegistry = inject(FieldContextRegistryService);\n const logger = inject(DynamicFormLogger);\n const cacheService = inject(DynamicValueFunctionCacheService);\n\n // Check cache first\n const cached = cacheService.dynamicValueFunctionCache.get(expression);\n if (cached) {\n return cached as LogicFn<TValue, TReturn>;\n }\n\n const fn: LogicFn<TValue, TReturn> = (ctx: FieldContext<TValue>) => {\n // Create evaluation context using the registry-based approach\n const evaluationContext = fieldContextRegistry.createEvaluationContext(ctx);\n\n try {\n // Use secure AST-based expression parser (already has LRU cache)\n return ExpressionParser.evaluate(expression, evaluationContext) as TReturn;\n } catch (error) {\n logger.error('Error evaluating dynamic expression:', expression, error);\n return undefined as TReturn;\n }\n };\n\n // Cache the function\n cacheService.dynamicValueFunctionCache.set(expression, fn as LogicFn<unknown, unknown>);\n return fn;\n}\n","import type { SchemaPath, SchemaPathTree } from '@angular/forms/signals';\nimport {\n email,\n FieldContext,\n LogicFn,\n max,\n maxLength,\n min,\n minLength,\n pattern,\n required,\n validate,\n validateAsync,\n validateHttp,\n ValidationError,\n} from '@angular/forms/signals';\nimport { inject } from '@angular/core';\nimport { DynamicFormLogger } from '../../providers/features/logger/logger.token';\nimport { DynamicFormError } from '../../errors/dynamic-form-error';\nimport {\n AsyncValidatorConfig,\n CustomValidatorConfig,\n DeclarativeHttpValidatorConfig,\n FunctionHttpValidatorConfig,\n isFunctionHttpValidator,\n ValidatorConfig,\n} from '../../models/validation/validator-config';\nimport { resolveHttpRequest } from '../http/http-request-resolver';\nimport { evaluateHttpValidationResponse } from '../http/http-response-evaluator';\nimport { createLogicFunction } from '../expressions/logic-function-factory';\nimport { createDynamicValueFunction } from '../values/dynamic-value-factory';\nimport { ConditionalExpression } from '../../models/expressions/conditional-expression';\nimport { FunctionRegistryService } from '../registry/function-registry.service';\nimport { FieldContextRegistryService } from '../registry/field-context-registry.service';\nimport { ExpressionParser } from '../expressions/parser/expression-parser';\nimport {\n isCrossFieldValidator,\n isCrossFieldBuiltInValidator,\n hasCrossFieldWhenCondition,\n isResourceBasedValidator,\n} from '../cross-field/cross-field-detector';\n\nfunction applyEmailValidator(path: SchemaPath<string>): void {\n email(path);\n}\n\nfunction applyMinValidator(path: SchemaPath<number>, value: number, expression?: string): void {\n if (expression) {\n min(path, createDynamicValueFunction<string | number | null, number | undefined>(expression));\n } else {\n min(path, value);\n }\n}\n\nfunction applyMaxValidator(path: SchemaPath<number>, value: number, expression?: string): void {\n if (expression) {\n max(path, createDynamicValueFunction<string | number | null, number | undefined>(expression));\n } else {\n max(path, value);\n }\n}\n\nfunction applyMinLengthValidator(path: SchemaPath<string>, value: number, expression?: string): void {\n if (expression) {\n minLength(path, createDynamicValueFunction<string, number>(expression));\n } else {\n minLength(path, value);\n }\n}\n\nfunction applyMaxLengthValidator(path: SchemaPath<string>, value: number, expression?: string): void {\n if (expression) {\n maxLength(path, createDynamicValueFunction<string, number>(expression));\n } else {\n maxLength(path, value);\n }\n}\n\nfunction applyPatternValidator(path: SchemaPath<string>, value: RegExp, expression?: string): void {\n if (expression) {\n pattern(\n path,\n createDynamicValueFunction<string | undefined, RegExp | undefined>(expression) as LogicFn<string | undefined, RegExp | undefined>,\n );\n } else {\n pattern(path, value);\n }\n}\n\nfunction createConditionalLogic(when: ConditionalExpression | undefined): LogicFn<unknown, boolean> | undefined {\n return when ? (createLogicFunction(when) as LogicFn<unknown, boolean>) : undefined;\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Dynamic forms require any at the Angular API boundary\nexport function applyValidator(config: ValidatorConfig, fieldPath: SchemaPath<any> | SchemaPathTree<any>): void {\n const path = fieldPath as SchemaPath<unknown>;\n\n if (!isResourceBasedValidator(config) && (isCrossFieldBuiltInValidator(config) || hasCrossFieldWhenCondition(config))) {\n return;\n }\n\n switch (config.type) {\n case 'required':\n if (config.when) {\n required(path, { when: createLogicFunction(config.when) });\n } else {\n required(path);\n }\n break;\n case 'email':\n applyEmailValidator(fieldPath as SchemaPath<string>);\n break;\n case 'min':\n if (typeof config.value === 'number') {\n applyMinValidator(fieldPath as SchemaPath<number>, config.value, config.expression);\n }\n break;\n case 'max':\n if (typeof config.value === 'number') {\n applyMaxValidator(fieldPath as SchemaPath<number>, config.value, config.expression);\n }\n break;\n case 'minLength':\n if (typeof config.value === 'number') {\n applyMinLengthValidator(fieldPath as SchemaPath<string>, config.value, config.expression);\n }\n break;\n case 'maxLength':\n if (typeof config.value === 'number') {\n applyMaxLengthValidator(fieldPath as SchemaPath<string>, config.value, config.expression);\n }\n break;\n case 'pattern':\n if (config.value instanceof RegExp || typeof config.value === 'string') {\n let regexPattern: RegExp;\n if (typeof config.value === 'string') {\n try {\n regexPattern = new RegExp(config.value);\n } catch (e) {\n throw new DynamicFormError(\n `Invalid regex pattern in validator: '${config.value}' — ${e instanceof Error ? e.message : String(e)}`,\n );\n }\n } else {\n regexPattern = config.value;\n }\n applyPatternValidator(fieldPath as SchemaPath<string>, regexPattern, config.expression);\n }\n break;\n case 'custom':\n applyCustomValidator(config, path);\n break;\n case 'async':\n applyAsyncValidator(config, path);\n break;\n case 'http':\n applyUnifiedHttpValidator(config, path);\n break;\n }\n}\n\nfunction applyCustomValidator(config: CustomValidatorConfig, fieldPath: SchemaPath<unknown>): void {\n if (isCrossFieldValidator(config)) {\n return;\n }\n\n let validatorFn: (ctx: FieldContext<unknown>) => ValidationError | ValidationError[] | null;\n\n if (config.expression) {\n validatorFn = createExpressionValidator(config);\n } else if (config.fn || config.functionName) {\n validatorFn = createFunctionValidator(config);\n } else {\n throw new DynamicFormError('Custom validator requires one of \"expression\", \"functionName\", or \"fn\".');\n }\n\n const whenLogic = createConditionalLogic(config.when);\n validate(fieldPath, (ctx: FieldContext<unknown>) => {\n if (whenLogic && !whenLogic(ctx)) return null;\n return validatorFn(ctx);\n });\n}\n\nfunction createFunctionValidator(\n config: CustomValidatorConfig,\n): (ctx: FieldContext<unknown>) => ValidationError | ValidationError[] | null {\n const logger = inject(DynamicFormLogger);\n\n if (config.fn && config.functionName) {\n logger.warn('Both \"fn\" and \"functionName\" are set on custom validator. Inline \"fn\" takes precedence; \"functionName\" is ignored.');\n }\n\n if (config.fn) {\n const inlineFn = config.fn;\n return (ctx: FieldContext<unknown>) => inlineFn(ctx, config.params);\n }\n\n const functionName = config.functionName;\n if (!functionName) {\n throw new DynamicFormError('Custom validator requires one of \"functionName\" or \"fn\".');\n }\n\n const registry = inject(FunctionRegistryService);\n const validatorFn = registry.getValidator(functionName);\n\n if (!validatorFn) {\n throw new DynamicFormError(`Custom validator \"${functionName}\" not found. Register it with customFnConfig.validators.`);\n }\n\n return (ctx: FieldContext<unknown>) => validatorFn(ctx, config.params);\n}\n\nfunction createExpressionValidator(\n config: CustomValidatorConfig,\n): (ctx: FieldContext<unknown>) => ValidationError | ValidationError[] | null {\n const logger = inject(DynamicFormLogger);\n const expression = config.expression;\n if (!expression) {\n throw new DynamicFormError('Custom validator requires a non-empty \"expression\".');\n }\n\n const fieldContextRegistry = inject(FieldContextRegistryService);\n const functionRegistry = inject(FunctionRegistryService);\n\n return (ctx: FieldContext<unknown>) => {\n try {\n const evaluationContext = fieldContextRegistry.createEvaluationContext(ctx, functionRegistry.getCustomFunctions());\n const result = ExpressionParser.evaluate(expression, evaluationContext);\n\n if (result) {\n return null;\n }\n\n const kind = config.kind || 'custom';\n const validationError: ValidationError & Record<string, unknown> = { kind };\n\n if (config.errorParams) {\n Object.entries(config.errorParams).forEach(([key, expression]) => {\n try {\n validationError[key] = ExpressionParser.evaluate(expression, evaluationContext);\n } catch (err) {\n logger.warn(`Error evaluating errorParam \"${key}\":`, expression, err);\n }\n });\n }\n\n return validationError;\n } catch (error) {\n // Gracefully degrade on errors (e.g., typos in field names, undefined functions)\n // Log for debugging while keeping form functional\n logger.error('Error evaluating custom validator expression:', expression, error);\n return { kind: config.kind || 'custom' };\n }\n };\n}\n\n/** Apply async validator to field path using Angular's public validateAsync() API */\nfunction applyAsyncValidator(config: AsyncValidatorConfig, fieldPath: SchemaPath<unknown>): void {\n if (config.fn && config.functionName) {\n const logger = inject(DynamicFormLogger);\n logger.warn('Both \"fn\" and \"functionName\" are set on async validator. Inline \"fn\" takes precedence; \"functionName\" is ignored.');\n }\n\n let validatorConfig: ReturnType<FunctionRegistryService['getAsyncValidator']>;\n if (config.fn) {\n validatorConfig = config.fn;\n } else if (config.functionName) {\n const registry = inject(FunctionRegistryService);\n validatorConfig = registry.getAsyncValidator(config.functionName);\n if (!validatorConfig) {\n throw new DynamicFormError(`Async validator \"${config.functionName}\" not found. Register it with customFnConfig.asyncValidators.`);\n }\n } else {\n // XOR type rejects this at compile time; JSON-loaded configs can still reach here at runtime.\n throw new DynamicFormError('Async validator requires one of \"functionName\" or \"fn\".');\n }\n\n const whenLogic = createConditionalLogic(config.when);\n const asyncOptions = {\n params: (ctx: FieldContext<unknown>) => {\n if (whenLogic && !whenLogic(ctx)) return undefined;\n return validatorConfig.params(ctx, config.params);\n },\n factory: validatorConfig.factory,\n onSuccess: validatorConfig.onSuccess,\n onError: validatorConfig.onError,\n };\n\n validateAsync(fieldPath, asyncOptions as Parameters<typeof validateAsync>[1]);\n}\n\n/**\n * Unified handler for `type: 'http'` — discriminates between function-based and declarative\n * based on property presence.\n */\nfunction applyUnifiedHttpValidator(\n config: FunctionHttpValidatorConfig | DeclarativeHttpValidatorConfig,\n fieldPath: SchemaPath<unknown>,\n): void {\n if (isFunctionHttpValidator(config)) {\n applyFunctionHttpValidator(config, fieldPath);\n return;\n }\n // Declarative path requires both `http` and `responseMapping` — reject early\n // for JSON-loaded configs that have neither side fully populated. The\n // discriminated union forbids this at compile time.\n const declarative = config as DeclarativeHttpValidatorConfig;\n if (!declarative.http || !declarative.responseMapping) {\n throw new DynamicFormError(\n 'HTTP validator requires one of \"functionName\" / \"fn\" (function-based) or \"http\" + \"responseMapping\" (declarative).',\n );\n }\n applyDeclarativeHttpValidator(config, fieldPath);\n}\n\n/** Apply function-based HTTP validator to field path using Angular's public validateHttp() API. */\nfunction applyFunctionHttpValidator(config: FunctionHttpValidatorConfig, fieldPath: SchemaPath<unknown>): void {\n if (config.fn && config.functionName) {\n const logger = inject(DynamicFormLogger);\n logger.warn('Both \"fn\" and \"functionName\" are set on HTTP validator. Inline \"fn\" takes precedence; \"functionName\" is ignored.');\n }\n\n let httpValidatorConfig: ReturnType<FunctionRegistryService['getHttpValidator']>;\n if (config.fn) {\n httpValidatorConfig = config.fn;\n } else if (config.functionName) {\n const registry = inject(FunctionRegistryService);\n httpValidatorConfig = registry.getHttpValidator(config.functionName);\n if (!httpValidatorConfig) {\n throw new DynamicFormError(`HTTP validator \"${config.functionName}\" not found. Register it with customFnConfig.httpValidators.`);\n }\n } else {\n // XOR type rejects this at compile time; JSON-loaded configs can still reach here at runtime.\n throw new DynamicFormError('HTTP validator requires one of \"functionName\" or \"fn\".');\n }\n\n const whenLogic = createConditionalLogic(config.when);\n const httpOptions = {\n request: (ctx: FieldContext<unknown>) => {\n if (whenLogic && !whenLogic(ctx)) return undefined;\n return httpValidatorConfig.request(ctx, config.params);\n },\n onSuccess: httpValidatorConfig.onSuccess,\n onError: httpValidatorConfig.onError,\n };\n\n validateHttp(fieldPath, httpOptions as Parameters<typeof validateHttp>[1]);\n}\n\n/** Apply declarative HTTP validator — fully JSON-serializable, no function registration needed. */\nfunction applyDeclarativeHttpValidator(config: DeclarativeHttpValidatorConfig, fieldPath: SchemaPath<unknown>): void {\n const fieldContextRegistry = inject(FieldContextRegistryService);\n const functionRegistry = inject(FunctionRegistryService);\n const logger = inject(DynamicFormLogger);\n const whenLogic = createConditionalLogic(config.when);\n\n validateHttp(fieldPath, {\n request: (ctx: FieldContext<unknown>) => {\n if (whenLogic && !whenLogic(ctx)) return undefined;\n // Use createReactiveEvaluationContext (not createEvaluationContext) because\n // validateHttp's request runs inside Angular's resource API — it NEEDS reactive\n // dependencies to re-trigger when the field value changes.\n const evalCtx = fieldContextRegistry.createReactiveEvaluationContext(ctx, functionRegistry.getCustomFunctions());\n // resolveHttpRequest returns null when a path param is undefined — convert to undefined to skip validation\n return resolveHttpRequest(config.http, evalCtx) ?? undefined;\n },\n onSuccess: (response: unknown) => {\n return evaluateHttpValidationResponse(response, config.responseMapping, logger);\n },\n onError: (error: unknown) => {\n logger.warn('HTTP validator request failed:', error);\n return { kind: config.responseMapping.errorKind };\n },\n } as Parameters<typeof validateHttp>[1]);\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Dynamic forms require any at the Angular API boundary\nexport function applyValidators(configs: ValidatorConfig[], fieldPath: SchemaPath<any> | SchemaPathTree<any>): void {\n configs.forEach((config) => applyValidator(config, fieldPath));\n}\n","import { FieldDef } from './field-def';\nimport { FieldMeta } from './field-meta';\nimport { FieldWithValidation } from './field-with-validation';\nimport { WithInputSignals } from '../../models/component-type';\nimport { Prettify } from '../../models/prettify';\nimport { DynamicText } from '../../models/types/dynamic-text';\n\nexport interface BaseCheckedField<TProps, TMeta extends FieldMeta = FieldMeta, TNullable extends boolean = false>\n extends FieldDef<TProps, TMeta>, FieldWithValidation {\n value?: TNullable extends true ? boolean | null : boolean;\n\n /**\n * Placeholder text displayed when the field is empty.\n * Supports static strings, Observables, and Signals for dynamic content.\n */\n placeholder?: DynamicText;\n\n required?: boolean;\n\n /**\n * Whether the field accepts `null` as a valid value.\n *\n * @default false\n */\n nullable?: TNullable;\n}\n\nexport function isCheckedField<TProps, TMeta extends FieldMeta = FieldMeta>(\n field: FieldDef<TProps, TMeta>,\n): field is BaseCheckedField<TProps, TMeta, boolean> {\n return field.type === 'checkbox' || field.type === 'toggle';\n}\n\n// Note: 'meta' is NOT excluded - components must handle meta attributes\ntype ExcludedKeys =\n | 'type'\n | 'conditionals'\n | 'value'\n | 'nullable'\n | 'disabled'\n | 'readonly'\n | 'hidden'\n | 'col'\n | keyof FieldWithValidation\n // Value exclusion config (submission-only, not component inputs)\n | 'excludeValueIfHidden'\n | 'excludeValueIfDisabled'\n | 'excludeValueIfReadonly'\n // Validation execution config (schema-only, not component inputs)\n | 'validateWhenHidden'\n // Wrappers are consumed by DfFieldOutlet / ContainerFieldComponent, not individual fields\n | 'wrappers'\n | 'skipAutoWrappers'\n | 'skipDefaultWrappers'\n // Addons are an opt-in input — checkbox/toggle-style fields are Tier 3 and don't render them\n | 'addons';\n\nexport type CheckedFieldComponent<T extends BaseCheckedField<Record<string, unknown> | unknown, FieldMeta, boolean>> = Prettify<\n WithInputSignals<Omit<T, ExcludedKeys>>\n>;\n","import { FieldDef } from './field-def';\nimport { FieldMeta } from './field-meta';\nimport { FieldWithValidation } from './field-with-validation';\nimport { WithInputSignals } from '../../models/component-type';\nimport { Prettify } from '../../models/prettify';\nimport { DynamicText } from '../../models/types/dynamic-text';\n\n/**\n * Supported primitive value types for form fields.\n * This type represents all possible value types that can be used in form fields.\n */\nexport type ValueType = string | number | boolean | Date | object | unknown[];\n\nexport interface BaseValueField<TProps, TValue, TMeta extends FieldMeta = FieldMeta, TNullable extends boolean = false>\n extends FieldDef<TProps, TMeta>, FieldWithValidation {\n value?: TNullable extends true ? TValue | null : TValue;\n\n /**\n * Placeholder text displayed when the field is empty.\n * Supports static strings, Observables, and Signals for dynamic content.\n */\n placeholder?: DynamicText;\n\n required?: boolean;\n\n /**\n * Whether the field accepts `null` as a valid value.\n *\n * @default false\n */\n nullable?: TNullable;\n}\n\nexport function isValueField<TProps, TMeta extends FieldMeta = FieldMeta>(\n field: FieldDef<TProps, TMeta>,\n): field is BaseValueField<TProps, ValueType, TMeta, boolean> {\n // `nullable: true` without an explicit `value` is still a value-bearing declaration:\n // the field resolves to `null` at runtime. Treating it as a value field keeps\n // downstream narrowing consistent with getFieldDefaultValue's behavior.\n return 'value' in field || 'nullable' in field;\n}\n\ntype ExcludedKeys =\n | 'type'\n | 'conditionals'\n | 'value'\n | 'valueType'\n | 'nullable'\n | 'disabled'\n | 'readonly'\n | 'hidden'\n | 'col'\n | 'minValue'\n | 'maxValue'\n | 'step'\n // Exclude validation config, but keep validationMessages for component use\n | 'required'\n | 'email'\n | 'min'\n | 'max'\n | 'minLength'\n | 'maxLength'\n | 'pattern'\n | 'validators'\n | 'logic'\n | 'derivation'\n | 'schemas'\n // Value exclusion config (submission-only, not component inputs)\n | 'excludeValueIfHidden'\n | 'excludeValueIfDisabled'\n | 'excludeValueIfReadonly'\n // Validation execution config (schema-only, not component inputs)\n | 'validateWhenHidden'\n // Wrappers are consumed by DfFieldOutlet / ContainerFieldComponent, not individual fields\n | 'wrappers'\n | 'skipAutoWrappers'\n | 'skipDefaultWrappers'\n // Addons are an opt-in input — fields that render them declare `addons` themselves\n // (Tier 1/2). Tier 3 field components (toggle, checkbox, radio, slider) ignore them.\n | 'addons';\n// Note: 'meta' is NOT excluded - components must handle meta attributes\n\nexport type ValueFieldComponent<T extends BaseValueField<Record<string, unknown> | unknown, unknown, FieldMeta, boolean>> = Prettify<\n WithInputSignals<Omit<T, ExcludedKeys>>\n>;\n","import { FieldComponent, FieldDef } from '../base/field-def';\nimport { ArrayAllowedChildren } from '../../models/types/nesting-constraints';\nimport { ContainerLogicConfig } from '../base/container-logic-config';\n\n/**\n * An array item template is an array of allowed children that defines one OBJECT array item's fields.\n * Used for object arrays where each item is `{ key1: value1, key2: value2, ... }`.\n */\nexport type ArrayItemTemplate = readonly ArrayAllowedChildren[];\n\n/**\n * An array item definition can be either:\n * - A single field (ArrayAllowedChildren) for PRIMITIVE array items - extracts field value directly\n * - An array of fields (ArrayItemTemplate) for OBJECT array items - merges fields into object\n */\nexport type ArrayItemDefinition = ArrayAllowedChildren | ArrayItemTemplate;\n\n/** Array field interface for creating dynamic field collections that map to array values. */\nexport interface ArrayField<TFields extends readonly ArrayItemDefinition[] = readonly ArrayItemDefinition[]> extends FieldDef<never> {\n type: 'array';\n\n /**\n * Array of item definitions. Each element defines one array item:\n * - Single FieldDef = primitive item (field's value is extracted directly)\n * - Array of FieldDefs = object item (fields are merged into an object)\n * - Empty array `[]` = no initial items (add via buttons with explicit templates)\n * - Items can have different configurations (heterogeneous arrays supported)\n * - Initial values are set via `value` property on each field definition\n */\n readonly fields: TFields;\n\n /** Array fields do not have a label property * */\n readonly label?: never;\n\n /** Arrays do not support meta - they have no native form element * */\n readonly meta?: never;\n\n /**\n * Logic configurations for conditional array visibility.\n * Only 'hidden' type logic is supported for arrays.\n */\n readonly logic?: ContainerLogicConfig[];\n\n /**\n * Minimum number of items required in the array.\n * Validation fails if the array has fewer items than this value.\n */\n readonly minLength?: number;\n\n /**\n * Maximum number of items allowed in the array.\n * Validation fails if the array has more items than this value.\n */\n readonly maxLength?: number;\n}\n\n/**\n * Type guard for ArrayField with proper type narrowing.\n * Validates that the field is an array type with a fields property that is an array.\n * Fields can contain either single FieldDefs (primitive items) or arrays of FieldDefs (object items).\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Type guard must accept any field type\nexport function isArrayField(field: FieldDef<any>): field is ArrayField {\n return field.type === 'array' && 'fields' in field && Array.isArray(field.fields);\n}\n\nexport type ArrayComponent<T extends ArrayItemTemplate[]> = FieldComponent<ArrayField<T>>;\n\n/** Configuration for auto-generated add/remove buttons in simplified array fields. */\nexport interface ArrayButtonConfig {\n /** Custom label for the button */\n readonly label?: string;\n /** Additional properties passed to the button component */\n readonly props?: Record<string, unknown>;\n}\n\n/** Simplified array field interface for common use cases. */\nexport interface SimplifiedArrayField extends FieldDef<never> {\n type: 'array';\n\n /**\n * Template defining the structure of a single array item.\n * - Single field (ArrayAllowedChildren) → primitive array (each item is a single value)\n * - Array of fields (readonly ArrayAllowedChildren[]) → object array (each item is an object)\n */\n readonly template: ArrayAllowedChildren | readonly ArrayAllowedChildren[];\n\n /** Initial values for the array. Each element creates one array item. */\n readonly value?: readonly unknown[];\n\n /**\n * Configuration for the auto-generated \"Add\" button, or `false` to disable it.\n * Defaults to a button with label \"Add\".\n */\n readonly addButton?: ArrayButtonConfig | false;\n\n /**\n * Configuration for the auto-generated \"Remove\" button on each item, or `false` to disable it.\n * Defaults to a button with label \"Remove\".\n */\n readonly removeButton?: ArrayButtonConfig | false;\n\n /** Simplified arrays do not support the label property */\n readonly label?: never;\n\n /** Simplified arrays do not support meta */\n readonly meta?: never;\n\n /**\n * Logic configurations for conditional array visibility.\n * Only 'hidden' type logic is supported for arrays.\n */\n readonly logic?: ContainerLogicConfig[];\n\n /**\n * Minimum number of items required in the array.\n * Validation fails if the array has fewer items than this value.\n */\n readonly minLength?: number;\n\n /**\n * Maximum number of items allowed in the array.\n * Validation fails if the array has more items than this value.\n */\n readonly maxLength?: number;\n\n /** Mutually exclusive with `template` — use `fields` for the full API instead */\n readonly fields?: never;\n}\n\n/**\n * Type guard for SimplifiedArrayField.\n * Checks for `type: 'array'` with a `template` property (discriminant from full ArrayField).\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Type guard must accept any field type\nexport function isSimplifiedArrayField(field: FieldDef<any>): field is SimplifiedArrayField {\n return field.type === 'array' && 'template' in field;\n}\n","import { FieldComponent, FieldDef } from '../base/field-def';\nimport { ContainerAllowedChildren } from '../../models/types/nesting-constraints';\nimport { ContainerLogicConfig } from '../base/container-logic-config';\nimport { WrapperConfig } from '../../models/wrapper-type';\n\n/** Container field interface for wrapping child fields with UI chrome. */\nexport interface ContainerField<\n TFields extends readonly ContainerAllowedChildren[] = readonly ContainerAllowedChildren[],\n TWrapperConfigs extends readonly WrapperConfig[] = readonly WrapperConfig[],\n> extends FieldDef<never> {\n type: 'container';\n\n /** Child definitions to render within this container */\n readonly fields: TFields;\n\n /**\n * Wrapper components to chain around the children.\n * Applied outermost-first: the first wrapper in the array is the outermost.\n * Each wrapper component receives the subsequent wrapper (or children) inside\n * its `#fieldComponent` ViewContainerRef slot.\n */\n readonly wrappers: TWrapperConfigs;\n\n /** Container fields do not have a label property */\n readonly label?: never;\n\n /** Containers do not support meta — they have no native form element */\n readonly meta?: never;\n\n /**\n * Logic configurations for conditional container visibility.\n * Only 'hidden' type logic is supported for containers.\n */\n readonly logic?: ContainerLogicConfig[];\n}\n\n/** Type guard for ContainerField with proper type narrowing. */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Type guard must accept any field type\nexport function isGenericContainerField(field: FieldDef<any>): field is ContainerField {\n return (\n field.type === 'container' &&\n 'wrappers' in field &&\n 'fields' in field &&\n Array.isArray((field as ContainerField).wrappers) &&\n Array.isArray((field as ContainerField).fields)\n );\n}\n\nexport type ContainerComponent = FieldComponent<ContainerField<ContainerAllowedChildren[]>>;\n","import { FieldComponent, FieldDef } from '../base/field-def';\nimport { GroupAllowedChildren } from '../../models/types/nesting-constraints';\nimport { ContainerLogicConfig } from '../base/container-logic-config';\n\n/**\n * Group field interface for creating logical field groupings that map to object values\n * Groups create nested form structures where child field values are collected into an object\n * This is a programmatic grouping only - users cannot customize this field type\n */\nexport interface GroupField<TFields extends readonly GroupAllowedChildren[] = readonly GroupAllowedChildren[]> extends FieldDef<never> {\n type: 'group';\n\n readonly fields: TFields;\n\n /** Groups do not have a label property - they are logical containers only * */\n readonly label?: never;\n\n /** Groups do not support meta - they have no native form element * */\n readonly meta?: never;\n\n /**\n * Logic configurations for conditional group visibility.\n * Only 'hidden' type logic is supported for groups.\n */\n readonly logic?: ContainerLogicConfig[];\n}\n\n/** Type guard for GroupField with proper type narrowing */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Type guard must accept any field type\nexport function isGroupField(field: FieldDef<any>): field is GroupField {\n return field.type === 'group' && 'fields' in field;\n}\n\nexport type GroupComponent<T extends GroupAllowedChildren[]> = FieldComponent<GroupField<T>>;\n","import { FieldDef } from '../base/field-def';\n\n/**\n * Scalar types supported by hidden fields.\n * These are the primitive value types that can be stored.\n */\nexport type HiddenScalar = string | number | boolean;\n\n/**\n * All value types supported by hidden fields.\n * Supports both scalar values and arrays of scalars for storing\n * multiple IDs or similar data.\n */\nexport type HiddenValue = HiddenScalar | HiddenScalar[];\n\n/**\n * Hidden field definition for storing values without rendering UI.\n *\n * @typeParam TValue - The type of value stored (must be HiddenValue compatible)\n */\nexport interface HiddenField<TValue extends HiddenValue = HiddenValue> extends FieldDef<never> {\n /** Discriminant for hidden field type */\n type: 'hidden';\n\n /**\n * The value to store in the form.\n * This is required - a hidden field without a value serves no purpose.\n */\n value: TValue;\n}\n\n/**\n * Type guard to check if a field is a hidden field.\n *\n * @param field - The field to check\n * @returns True if the field is a HiddenField\n */\nexport function isHiddenField(field: FieldDef<unknown>): field is HiddenField {\n return field.type === 'hidden';\n}\n","import { FieldComponent, FieldDef } from '../base/field-def';\nimport { RowAllowedChildren } from '../../models/types/nesting-constraints';\nimport { ContainerLogicConfig } from '../base/container-logic-config';\n\n/**\n * Row field interface for creating horizontal layouts.\n * A row is a synthetic field type that resolves to a Container at runtime,\n * with a synthesized `{ type: 'row' }` wrapper applied for layout. The row\n * itself does not hold a value — its children flatten into the parent form.\n */\nexport interface RowField<TFields extends readonly RowAllowedChildren[] = readonly RowAllowedChildren[]> extends FieldDef<never> {\n type: 'row';\n\n /** Child definitions to render within this row */\n readonly fields: TFields;\n\n /** Row fields do not have a label property * */\n readonly label?: never;\n\n /** Rows do not support meta - they have no native form element * */\n readonly meta?: never;\n\n /**\n * Logic configurations for conditional row visibility.\n * Only 'hidden' type logic is supported for rows.\n */\n readonly logic?: ContainerLogicConfig[];\n}\n\n/** Type guard for RowField with proper type narrowing */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Type guard must accept any field type\nexport function isRowField(field: FieldDef<any>): field is RowField {\n return field.type === 'row' && 'fields' in field && Array.isArray((field as RowField).fields);\n}\n\nexport type RowComponent = FieldComponent<RowField<RowAllowedChildren[]>>;\n","import { FieldDef } from '../base/field-def';\nimport { PageAllowedChildren } from '../../models/types/nesting-constraints';\nimport { isRowField } from './row-field';\nimport { isGroupField } from './group-field';\nimport { isGenericContainerField } from './container-field';\nimport { ContainerLogicConfig } from '../base/container-logic-config';\n\n/**\n * Page field interface for creating top-level page layouts\n * This is a special field type that contains other field definitions\n * The page itself doesn't have a value - it's a layout container like row\n * Pages can only be used at the top level and cannot be nested\n * This is a programmatic field type only - users cannot customize this field type\n */\nexport interface PageField<TFields extends readonly PageAllowedChildren[] = PageAllowedChildren[]> extends FieldDef<never> {\n type: 'page';\n\n /** Child field definitions to render within this page */\n readonly fields: TFields;\n\n /** Page fields do not have a label property * */\n readonly label?: never;\n\n /** Pages do not support meta - they have no native form element * */\n readonly meta?: never;\n\n /**\n * Logic configurations for conditional page visibility.\n * Only 'hidden' type logic is supported for pages.\n */\n readonly logic?: ContainerLogicConfig[];\n}\n\n/** Type guard for PageField with proper type narrowing */\nexport function isPageField(field: FieldDef<unknown>): field is PageField {\n return field.type === 'page' && 'fields' in field && Array.isArray((field as PageField).fields);\n}\n\n/**\n * Validates that a page field doesn't contain nested page fields\n *\n * @param pageField The page field to validate\n * @returns true if valid (no nested pages), false otherwise\n */\nexport function validatePageNesting(pageField: PageField): boolean {\n return !hasNestedPages(pageField.fields);\n}\n\n/** Type guard to check if a field is a container with fields property */\nfunction isContainerWithFields(field: FieldDef<unknown>): field is FieldDef<unknown> & { readonly fields: readonly FieldDef<unknown>[] } {\n return (\n (isRowField(field) || isGroupField(field) || isGenericContainerField(field)) &&\n 'fields' in field &&\n Array.isArray((field as { fields: unknown }).fields)\n );\n}\n\n/**\n * Recursively checks if fields contain any nested page fields\n *\n * @param fields Array of field definitions to check\n * @returns true if nested pages found, false otherwise\n */\nfunction hasNestedPages(fields: readonly FieldDef<unknown>[]): boolean {\n for (const field of fields) {\n if (isPageField(field)) {\n return true;\n }\n\n // Check row and group fields for nested pages\n if (isContainerWithFields(field)) {\n if (hasNestedPages(field.fields)) {\n return true;\n }\n }\n }\n return false;\n}\n","import { FormEvent } from '../interfaces/form-event';\n\nexport class ComponentInitializedEvent implements FormEvent {\n readonly type = 'component-initialized' as const;\n\n constructor(\n public componentType: 'dynamic-form' | 'page' | 'group' | 'array' | 'container',\n public componentId: string,\n ) {}\n}\n","import { InjectionToken } from '@angular/core';\n\n/**\n * Injection token for globally enabling form value emission on events.\n *\n * @internal\n */\nexport const EMIT_FORM_VALUE_ON_EVENTS = new InjectionToken<boolean>('EMIT_FORM_VALUE_ON_EVENTS', {\n providedIn: 'root',\n factory: () => false,\n});\n","import type { Logger } from './logger.interface';\n\n/**\n * No-operation logger implementation.\n * All methods are no-ops - used in production by default.\n */\nexport class NoopLogger implements Logger {\n debug(): void {\n // Intentionally empty\n }\n\n info(): void {\n // Intentionally empty\n }\n\n warn(): void {\n // Intentionally empty\n }\n\n error(): void {\n // Intentionally empty\n }\n}\n","import { inject, Injectable, InjectionToken, Signal, Type } from '@angular/core';\nimport { filter, Observable, Subject } from 'rxjs';\nimport { FormEvent, FormEventConstructor } from './interfaces/form-event';\nimport { EMIT_FORM_VALUE_ON_EVENTS } from '../providers/features/event-form-value/emit-form-value.token';\nimport { RootFormRegistryService } from '../core/registry/root-form-registry.service';\nimport { FORM_OPTIONS } from '../models/field-signal-context.token';\nimport { FormOptions } from '../models/form-config';\nimport { DynamicFormLogger } from '../providers/features/logger/logger.token';\nimport type { Logger } from '../providers/features/logger/logger.interface';\nimport { NoopLogger } from '../providers/features/logger/noop-logger';\n\n/**\n * Safely attempts to inject a dependency using an InjectionToken.\n * Returns the default value if called outside an injection context.\n */\nfunction safeInjectToken<T>(token: InjectionToken<T>, defaultValue: T): T {\n try {\n return inject(token, { optional: true }) ?? defaultValue;\n } catch {\n return defaultValue;\n }\n}\n\n/**\n * Safely attempts to inject a service class.\n * Returns the default value if called outside an injection context.\n */\nfunction safeInjectClass<T>(token: Type<T>, defaultValue: T | null): T | null {\n try {\n return inject(token, { optional: true }) ?? defaultValue;\n } catch {\n return defaultValue;\n }\n}\n\n/**\n * Creates a copy of an event with an additional formValue property.\n * Preserves the event's prototype chain so instanceof checks still work.\n */\nfunction attachFormValue<T extends FormEvent>(event: T, formValue: Record<string, unknown>): T & { formValue: unknown } {\n const prototype = Object.getPrototypeOf(event);\n return Object.assign(Object.create(prototype), event, { formValue });\n}\n\n/**\n * Event bus for form-wide event communication between field components.\n *\n * @see EventDispatcher — for dispatching events from outside the form\n */\n\nconst FALLBACK_LOGGER = new NoopLogger();\n\n@Injectable()\nexport class EventBus {\n private readonly pipeline$ = new Subject<FormEvent>();\n private readonly globalEmitFormValue = safeInjectToken(EMIT_FORM_VALUE_ON_EVENTS, false);\n private readonly rootFormRegistry = safeInjectClass(RootFormRegistryService, null);\n private readonly formOptions = safeInjectToken<Signal<FormOptions | undefined> | null>(FORM_OPTIONS, null);\n private readonly logger = safeInjectToken<Logger>(DynamicFormLogger, FALLBACK_LOGGER);\n\n events$ = this.pipeline$.asObservable();\n\n /**\n * Dispatches a pre-created event instance directly.\n *\n * @param event - A FormEvent instance to dispatch\n */\n dispatch(event: FormEvent): void;\n /**\n * Dispatches an event to all subscribers by instantiating the provided constructor.\n *\n * @param eventConstructor - Constructor function for the event to dispatch\n * @param args - Arguments to pass to the event constructor\n */\n // TypeScript limitation: Must use ConstructorParameters which relies on `any` in FormEventConstructor\n dispatch<T extends FormEventConstructor>(eventConstructor: T, ...args: ConstructorParameters<T>): void;\n dispatch<T extends FormEventConstructor>(eventOrConstructor: FormEvent | T, ...args: ConstructorParameters<T>): void {\n if (typeof eventOrConstructor !== 'function' && 'type' in eventOrConstructor) {\n this.emit(eventOrConstructor);\n } else {\n this.emit(new (eventOrConstructor as T)(...args));\n }\n }\n\n /**\n * Dispatches a pre-created event instance directly.\n * Used internally by EventDispatcher to forward events into the bus.\n *\n * @internal\n */\n emitInstance(event: FormEvent): void {\n this.emit(event);\n }\n\n /**\n * Shared emit path for both dispatch() and emitInstance().\n * Attaches form value if configured, then pushes to the pipeline.\n */\n private emit(event: FormEvent): void {\n if (this.shouldEmitFormValue()) {\n const formValue = this.rootFormRegistry?.formValue();\n // Only attach if form value exists and has at least one field.\n // Empty objects {} are not attached - use hasFormValue() to check.\n if (formValue && Object.keys(formValue).length > 0) {\n this.safeEmit(attachFormValue(event, formValue));\n return;\n }\n }\n\n this.safeEmit(event);\n }\n\n /**\n * Emits an event through the pipeline. Catches any synchronous exception that escapes\n * RxJS's own error handling so dispatch() callers are never disrupted by a failing subscriber.\n */\n private safeEmit(event: FormEvent): void {\n try {\n this.pipeline$.next(event);\n } catch (err: unknown) {\n this.logger.error('Exception in EventBus subscriber during dispatch', err);\n }\n }\n\n /** Determines whether form value should be attached to events. */\n private shouldEmitFormValue(): boolean {\n const formLevelSetting = this.formOptions?.()?.emitFormValueOnEvents;\n return formLevelSetting ?? this.globalEmitFormValue;\n }\n\n /**\n * Subscribes to events of a specific type.\n *\n * @param eventType - The type of event to subscribe to\n * @returns Observable that emits events of the specified type\n */\n on<T extends FormEvent>(eventType: T['type']): Observable<T>;\n /**\n * Subscribes to events of multiple types.\n *\n * @param eventType - Array of event types to subscribe to\n * @returns Observable that emits events matching any of the specified types\n */\n on<T extends FormEvent>(eventType: Array<T['type']>): Observable<T>;\n /**\n * Subscribes to form events with type-safe filtering.\n *\n * @param eventType - Event type string or array of event type strings to filter by\n * @returns Observable stream of filtered events\n */\n on<T extends FormEvent>(eventType: T['type'] | Array<T['type']>): Observable<T> {\n if (Array.isArray(eventType)) {\n return this.pipeline$.pipe(filter((event): event is T => eventType.includes(event.type)));\n }\n\n return this.pipeline$.pipe(filter((event): event is T => event.type === eventType));\n }\n}\n","/** Base interface for all form events in the dynamic form system. */\nexport interface FormEvent {\n /** Unique identifier for the event type used for filtering and subscriptions */\n readonly type: string;\n /**\n * Optional form value attached to the event when `withEventFormValue()` is enabled\n * or `options.emitFormValueOnEvents` is set to `true` in the form config.\n */\n readonly formValue?: unknown;\n}\n\n/**\n * Type guard to check if a form event has a form value attached.\n *\n * @param event - The form event to check\n * @returns `true` if the event has a form value attached, narrowing the type\n */\nexport function hasFormValue<T extends FormEvent>(event: T): event is T & { formValue: unknown } {\n return 'formValue' in event && event.formValue !== undefined;\n}\n\n/**\n * Constructor type for form event classes.\n *\n * @typeParam T - The form event type being constructed\n */\n// TypeScript limitation: Constructor types with varying parameter signatures require `any[]`\n// because `unknown[]` cannot be assigned from more specific parameter types (contravariance).\n// Using a generic TArgs parameter here would require changes throughout the event system.\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type FormEventConstructor<T extends FormEvent = FormEvent> = new (...args: any[]) => T;\n","/** Context for token resolution in event args */\nexport interface TokenContext {\n /** Current field key */\n key?: string;\n /** Array index if field is inside an array */\n index?: number;\n /** Parent array field key if field is inside an array */\n arrayKey?: string;\n /** Current form value for complex indexing */\n formValue?: unknown;\n /** Template for array item creation */\n template?: unknown;\n}\n\n/**\n * Resolves special tokens in event arguments to their actual values\n *\n * @param args - Array of arguments that may contain tokens\n * @param context - Context object containing token values\n * @returns Array with resolved values\n */\nexport function resolveTokens(\n args: readonly (string | number | boolean | null | undefined)[],\n context: TokenContext,\n): (string | number | boolean | null | undefined | unknown)[] {\n return args.map((arg) => {\n // Only process string tokens\n if (typeof arg !== 'string') {\n return arg;\n }\n\n // Check for token patterns\n switch (arg) {\n case '$key':\n return context.key;\n case '$index':\n return context.index;\n case '$arrayKey':\n return context.arrayKey;\n case '$template':\n return context.template;\n case 'formValue':\n return context.formValue;\n default:\n // Return as-is if not a recognized token\n return arg;\n }\n });\n}\n","import { FieldDef } from '../../definitions/base/field-def';\n\n/**\n * Generates CSS class string for responsive grid layout based on field column configuration.\n *\n * @param fieldDef - Field definition containing column configuration\n * @returns CSS class string for grid layout, empty if no valid column configuration\n */\nexport function getGridClassString(fieldDef: FieldDef<unknown>): string {\n const col = fieldDef.col;\n if (typeof col === 'number' && Number.isInteger(col) && col > 0 && col <= 12) {\n return `df-col-${col}`;\n }\n return '';\n}\n\n/**\n * Builds a combined className string from a field definition.\n *\n * @param fieldDef - Field definition containing className and col properties\n * @returns Combined class string, or undefined if no classes\n */\nexport function buildClassName(fieldDef: FieldDef<unknown>): string | undefined {\n const gridClass = getGridClassString(fieldDef);\n const userClass = fieldDef.className;\n\n const classes: string[] = [];\n\n if (gridClass) {\n classes.push(gridClass);\n }\n\n if (userClass) {\n classes.push(userClass);\n }\n\n return classes.length > 0 ? classes.join(' ') : undefined;\n}\n","import { RootFormRegistryService } from '../core/registry/root-form-registry.service';\nimport { evaluateNonFieldHidden } from '../core/logic/non-field-logic-resolver';\nimport { ContainerLogicConfig } from '../definitions/base/container-logic-config';\nimport { LogicConfig } from '../models/logic';\nimport type { EvaluationContext } from '../models/expressions/evaluation-context';\n\ninterface FieldWithHiddenLogic {\n hidden?: boolean;\n logic?: readonly (ContainerLogicConfig | LogicConfig)[];\n}\n\n/**\n * Applies hidden logic to a mapper's input record.\n *\n * @param inputs The mutable input record to potentially add `hidden` to\n * @param fieldDef The field definition containing `hidden` and `logic`\n * @param rootFormRegistry The root form registry for accessing form state\n * @param evaluationContext Full evaluation-context factory (externalData + customFunctions),\n * built by the mapper via `injectNonFieldEvaluationContext` and invoked lazily here so\n * external-data signal reads stay reactive. Mirrors the page path.\n */\nexport function applyHiddenLogic(\n inputs: Record<string, unknown>,\n fieldDef: FieldWithHiddenLogic,\n rootFormRegistry: RootFormRegistryService,\n evaluationContext?: () => EvaluationContext,\n): void {\n const rootForm = rootFormRegistry.rootForm();\n if (rootForm && (fieldDef.hidden === true || fieldDef.logic?.some((l) => l.type === 'hidden'))) {\n // Cast is safe: evaluateNonFieldHidden only reads the array, never mutates it.\n // The readonly + union type from container/leaf field definitions is compatible at runtime.\n inputs['hidden'] = evaluateNonFieldHidden({\n form: rootForm,\n fieldLogic: fieldDef.logic as LogicConfig[] | undefined,\n explicitValue: fieldDef.hidden,\n formValue: rootFormRegistry.formValue(),\n evaluationContext,\n });\n }\n}\n","import { computed, inject, Signal } from '@angular/core';\nimport { ArrayField } from '../../definitions/default/array-field';\nimport { buildClassName } from '../../utils/grid-classes/grid-classes';\nimport { RootFormRegistryService } from '../../core/registry/root-form-registry.service';\nimport { injectNonFieldEvaluationContext } from '../../core/logic/non-field-logic-resolver';\nimport { applyHiddenLogic } from '../apply-hidden-logic';\n\n/**\n * Maps an array field definition to component inputs.\n *\n * @param fieldDef The array field definition\n * @returns Signal containing Record of input names to values for ngComponentOutlet\n */\nexport function arrayFieldMapper(fieldDef: ArrayField): Signal<Record<string, unknown>> {\n const rootFormRegistry = inject(RootFormRegistryService);\n const evaluationContext = injectNonFieldEvaluationContext(fieldDef, { scopeToArrayItem: true });\n const className = buildClassName(fieldDef);\n\n return computed(() => {\n const inputs: Record<string, unknown> = {\n key: fieldDef.key,\n field: fieldDef,\n ...(className !== undefined && { className }),\n };\n\n applyHiddenLogic(inputs, fieldDef, rootFormRegistry, evaluationContext);\n\n return inputs;\n });\n}\n","import { computed, Signal } from '@angular/core';\nimport { FieldDef } from '../../definitions/base/field-def';\nimport { getGridClassString } from '../../utils/grid-classes/grid-classes';\n\n/**\n * Builds base input properties from a field definition.\n *\n * @param fieldDef The field definition to extract properties from\n * @param defaultProps Optional form-level default props to merge with field props\n * @returns Record of input names to values\n */\nexport function buildBaseInputs(fieldDef: FieldDef<unknown>, defaultProps?: Record<string, unknown>): Record<string, unknown> {\n const { key, label, className, tabIndex, props, meta, addons } = fieldDef;\n const inputs: Record<string, unknown> = {};\n\n // Always include key — required by components for accessibility and identification.\n // `type` is intentionally NOT propagated to the field component (it's the\n // registry discriminant, not a component input); wrappers and addons read it\n // from `WrapperFieldInputs.type` which is injected at the buildFieldInputs()\n // boundary by `DfFieldOutlet`.\n inputs['key'] = key;\n\n if (label !== undefined) {\n inputs['label'] = label;\n }\n\n // Combine user className with generated grid classes\n const gridClassString = getGridClassString(fieldDef);\n const allClasses: string[] = [];\n\n if (gridClassString) {\n allClasses.push(gridClassString);\n }\n\n if (className) {\n allClasses.push(className);\n }\n\n if (allClasses.length > 0) {\n inputs['className'] = allClasses.join(' ');\n }\n\n if (tabIndex !== undefined) {\n inputs['tabIndex'] = tabIndex;\n }\n\n // Merge props: form-level defaultProps as base, field-level props override\n // Cast props to Record since it's typed as unknown but always represents an object at runtime\n const fieldProps = props as Record<string, unknown> | undefined;\n const mergedProps =\n fieldProps !== undefined || defaultProps !== undefined ? { ...(defaultProps ?? {}), ...(fieldProps ?? {}) } : undefined;\n\n if (mergedProps !== undefined) {\n inputs['props'] = mergedProps;\n }\n\n if (meta !== undefined) {\n inputs['meta'] = meta;\n }\n\n // Forward addons. Field components that don't declare an `addons` input\n // signal (Tier 3 — toggle, checkbox, radio, slider) ignore this key via\n // Angular's `setInput` no-op-on-unknown behaviour, so it's safe to forward\n // unconditionally.\n if (addons !== undefined) {\n inputs['addons'] = addons;\n }\n\n return inputs;\n}\n\n/**\n * Base field mapper that extracts common field properties into component inputs.\n *\n * @param fieldDef The field definition to map\n * @returns Signal containing Record of input names to values\n */\nexport function baseFieldMapper(fieldDef: FieldDef<unknown>): Signal<Record<string, unknown>> {\n // For base mapper, inputs are static (no reactive dependencies)\n // Wrap in computed for consistency with the MapperFn type\n return computed(() => buildBaseInputs(fieldDef));\n}\n","import { computed, inject, Signal } from '@angular/core';\nimport { ContainerField } from '../../definitions/default/container-field';\nimport { buildClassName } from '../../utils/grid-classes/grid-classes';\nimport { RootFormRegistryService } from '../../core/registry/root-form-registry.service';\nimport { injectNonFieldEvaluationContext } from '../../core/logic/non-field-logic-resolver';\nimport { applyHiddenLogic } from '../apply-hidden-logic';\n\n/**\n * Maps a container field definition to component inputs.\n *\n * @param fieldDef The container field definition\n * @returns Signal containing Record of input names to values for ngComponentOutlet\n */\nexport function containerFieldMapper(fieldDef: ContainerField): Signal<Record<string, unknown>> {\n const rootFormRegistry = inject(RootFormRegistryService);\n const evaluationContext = injectNonFieldEvaluationContext(fieldDef, { scopeToArrayItem: true });\n const className = buildClassName(fieldDef);\n\n return computed(() => {\n // Nullify wrapper-related props on the field passed to the container\n // component — DfFieldOutlet handles field-level wrappers at the outlet\n // level. Passing them here would cause double-wrapping since the\n // container's internal wrapper chain also resolves from `this.field()`.\n // We set wrappers to undefined (not stripped) so the object identity\n // stays close to the original fieldDef shape.\n const containerField = { ...fieldDef, wrappers: undefined, skipAutoWrappers: undefined, skipDefaultWrappers: undefined };\n\n const inputs: Record<string, unknown> = {\n key: fieldDef.key,\n field: containerField,\n ...(className !== undefined && { className }),\n };\n\n applyHiddenLogic(inputs, fieldDef, rootFormRegistry, evaluationContext);\n\n return inputs;\n });\n}\n","import { computed, inject, Signal } from '@angular/core';\nimport { GroupField } from '../../definitions/default/group-field';\nimport { buildClassName } from '../../utils/grid-classes/grid-classes';\nimport { RootFormRegistryService } from '../../core/registry/root-form-registry.service';\nimport { injectNonFieldEvaluationContext } from '../../core/logic/non-field-logic-resolver';\nimport { applyHiddenLogic } from '../apply-hidden-logic';\n\n/**\n * Maps a group field definition to component inputs.\n *\n * @param fieldDef The group field definition\n * @returns Signal containing Record of input names to values for ngComponentOutlet\n */\nexport function groupFieldMapper(fieldDef: GroupField): Signal<Record<string, unknown>> {\n const rootFormRegistry = inject(RootFormRegistryService);\n const evaluationContext = injectNonFieldEvaluationContext(fieldDef, { scopeToArrayItem: true });\n const className = buildClassName(fieldDef);\n\n return computed(() => {\n const inputs: Record<string, unknown> = {\n key: fieldDef.key,\n field: fieldDef,\n ...(className !== undefined && { className }),\n };\n\n applyHiddenLogic(inputs, fieldDef, rootFormRegistry, evaluationContext);\n\n return inputs;\n });\n}\n","import { computed, inject, Signal } from '@angular/core';\nimport { RowField } from '../../definitions/default/row-field';\nimport { buildClassName } from '../../utils/grid-classes/grid-classes';\nimport { RootFormRegistryService } from '../../core/registry/root-form-registry.service';\nimport { injectNonFieldEvaluationContext } from '../../core/logic/non-field-logic-resolver';\nimport { applyHiddenLogic } from '../apply-hidden-logic';\n\nconst ROW_WRAPPERS = [{ type: 'row' }] as const;\n\n/** Maps a row field definition to container component inputs. */\nexport function rowFieldMapper(fieldDef: RowField): Signal<Record<string, unknown>> {\n const rootFormRegistry = inject(RootFormRegistryService);\n const evaluationContext = injectNonFieldEvaluationContext(fieldDef, { scopeToArrayItem: true });\n const className = buildClassName(fieldDef);\n\n return computed(() => {\n // Rebuilt each emission so logic-driven updates to fieldDef (e.g. `disabled`)\n // flow through. Shallow spread is safe while RowField has no nested\n // mutable props; add a deep-clone if that stops being true.\n // `type` is rewritten to 'container' because `row` is a synthetic field\n // type — the runtime renders it as a ContainerField with a synthesized\n // `{ type: 'row' }` wrapper.\n const containerField = { ...fieldDef, type: 'container', wrappers: ROW_WRAPPERS };\n\n const inputs: Record<string, unknown> = {\n key: fieldDef.key,\n field: containerField,\n ...(className !== undefined && { className }),\n };\n\n applyHiddenLogic(inputs, fieldDef, rootFormRegistry, evaluationContext);\n\n return inputs;\n });\n}\n","import { computed, Signal } from '@angular/core';\nimport { PageField } from '../../definitions/default/page-field';\nimport { buildClassName } from '../../utils/grid-classes/grid-classes';\n\n/**\n * Maps a page field definition to component inputs.\n *\n * @param fieldDef The page field definition\n * @returns Signal containing Record of input names to values for ngComponentOutlet\n */\nexport function pageFieldMapper(fieldDef: PageField): Signal<Record<string, unknown>> {\n const className = buildClassName(fieldDef);\n\n // Page inputs are static (no reactive dependencies)\n return computed(() => ({\n key: fieldDef.key,\n field: fieldDef,\n ...(className !== undefined && { className }),\n }));\n}\n","import { computed, inject, Signal } from '@angular/core';\nimport { TextField } from '../../definitions/default/text-field';\nimport { buildBaseInputs } from '../base/base-field-mapper';\nimport { DEFAULT_PROPS } from '../../models/field-signal-context.token';\nimport { RootFormRegistryService } from '../../core/registry/root-form-registry.service';\nimport { injectNonFieldEvaluationContext } from '../../core/logic/non-field-logic-resolver';\nimport { applyHiddenLogic } from '../apply-hidden-logic';\n\n/**\n * Maps a text field definition to component inputs.\n *\n * @param fieldDef The text field definition\n * @returns Signal containing Record of input names to values for ngComponentOutlet\n */\nexport function textFieldMapper(fieldDef: TextField): Signal<Record<string, unknown>> {\n const rootFormRegistry = inject(RootFormRegistryService);\n const evaluationContext = injectNonFieldEvaluationContext(fieldDef, { scopeToArrayItem: true });\n const defaultProps = inject(DEFAULT_PROPS);\n\n // Return computed signal for reactive updates\n return computed(() => {\n const baseInputs = buildBaseInputs(fieldDef, defaultProps());\n const inputs: Record<string, unknown> = { ...baseInputs };\n\n applyHiddenLogic(inputs, fieldDef, rootFormRegistry, evaluationContext);\n\n return inputs;\n });\n}\n","import { ReadonlyFieldTree } from '../../core/field-tree-utils';\n\n/**\n * Fields every addon-action context carries, regardless of whether the\n * addon is bound to a host field or rendered orphaned.\n */\ninterface AddonActionContextBase<TValue> {\n /**\n * Identity of the host field this addon is attached to. Empty strings\n * indicate the addon is rendered outside an active field context (rare).\n * Use {@link isFieldBoundContext} or the `form !== null` check to\n * narrow before relying on field identity.\n */\n readonly field: { readonly key: string; readonly type: string };\n /**\n * Current value of the host field at the time the action fires.\n * `undefined` when the host field has no value yet (initial render before\n * defaultValue resolution) or when the addon is rendered outside a field.\n */\n readonly value: TValue | undefined;\n}\n\n/**\n * Field-bound variant — the addon is attached to a real field and has a\n * working value-writer. The common case for `pi-button` / `mat-button` /\n * `bs-button` / `ion-button` inside an adapter input field.\n */\nexport interface FieldBoundAddonActionContext<TValue = unknown> extends AddonActionContextBase<TValue> {\n /** Read-only view of the host field's tree — same view wrappers receive. */\n readonly form: ReadonlyFieldTree<TValue>;\n /** Push a new value into the host field. */\n readonly setValue: (next: TValue) => void;\n}\n\n/**\n * Orphan variant — the addon is rendered outside any field component (rare;\n * happens when consumers compose `<df-addon-slot>` directly without a host\n * field). No writer is reachable; reading `value` may also yield `undefined`.\n */\nexport interface OrphanAddonActionContext<TValue = unknown> extends AddonActionContextBase<TValue> {\n readonly form: null;\n /** Always absent in the orphan variant — distinguishes the union members. */\n readonly setValue?: undefined;\n}\n\n/** Context handed to inline addon actions and registered handler functions. */\nexport type AddonActionContext<TValue = unknown> = FieldBoundAddonActionContext<TValue> | OrphanAddonActionContext<TValue>;\n\n/** Type guard narrowing {@link AddonActionContext} to its field-bound variant. */\nexport function isFieldBoundContext<TValue>(ctx: AddonActionContext<TValue>): ctx is FieldBoundAddonActionContext<TValue> {\n return ctx.form !== null;\n}\n\n/** Built-in preset actions shared across adapters. */\nexport type CommonAddonActionPreset = 'clear' | 'reset' | 'paste' | 'copy' | 'toggle-password-visibility';\n\n/** Module-augmentable registry of adapter-specific preset names. */\n// eslint-disable-next-line @typescript-eslint/no-empty-interface, @typescript-eslint/no-empty-object-type -- Intentionally empty: module-augmentation seam\nexport interface DynamicFormAddonActionPresetRegistry {}\n\n/**\n * Union of every preset name an addon may reference — universal presets\n * plus any contributed via {@link DynamicFormAddonActionPresetRegistry}.\n */\nexport type AddonActionPreset = CommonAddonActionPreset | keyof DynamicFormAddonActionPresetRegistry | (string & {});\n\n/** Module-augmentable registry of user-defined named action handlers. */\n// eslint-disable-next-line @typescript-eslint/no-empty-interface, @typescript-eslint/no-empty-object-type -- Intentionally empty: module-augmentation seam\nexport interface DynamicFormActionRegistry {}\n\n/**\n * Typed string handle to a registered action handler. The empty registry\n * resolves to `never`; augmenting `DynamicFormActionRegistry` populates the\n * union and unlocks autocomplete.\n */\nexport type RegisteredActionRef = keyof DynamicFormActionRegistry;\n","import { inject, InjectionToken } from '@angular/core';\nimport { AddonActionContext } from './addon-action';\n\n/** Handler signature for user-registered addon actions. */\nexport type AddonActionHandler<TValue = unknown> = (ctx: AddonActionContext<TValue>) => void;\n\n/**\n * Multi-provider token populated by `withAddonActions({...})` calls.\n * `ADDON_ACTION_REGISTRY` aggregates these into a single name → handler map.\n *\n * @internal\n */\nexport const ADDON_ACTION_HANDLERS = new InjectionToken<readonly Record<string, AddonActionHandler>[]>('ADDON_ACTION_HANDLERS');\n\n/**\n * Form-scoped name → handler map. Read by adapter button types at click\n * time for `actionRef: 'name'` lookups. Provided at form scope (not root)\n * because `ADDON_ACTION_HANDLERS` is form-scoped via `withAddonActions`.\n */\nexport const ADDON_ACTION_REGISTRY = new InjectionToken<ReadonlyMap<string, AddonActionHandler>>('ADDON_ACTION_REGISTRY');\n\nexport function createAddonActionRegistry(): ReadonlyMap<string, AddonActionHandler> {\n const sources = inject(ADDON_ACTION_HANDLERS, { optional: true }) ?? [];\n const map = new Map<string, AddonActionHandler>();\n for (const handlers of sources) {\n for (const [name, fn] of Object.entries(handlers)) {\n map.set(name, fn);\n }\n }\n return map;\n}\n","import { InjectionToken, Signal, Type, ViewContainerRef } from '@angular/core';\nimport { FieldRegistryWrappers, RegisteredWrapperTypes } from './registry/field-registry';\n\n/** Resolves a wrapper type name to its registered config interface. */\n\nexport type WrapperConfig<TWrappers extends RegisteredWrapperTypes = RegisteredWrapperTypes> = TWrappers extends keyof FieldRegistryWrappers\n ? FieldRegistryWrappers[TWrappers]\n : { readonly type: TWrappers };\n\n/**\n * Signature of a lazy component loader — either a direct component class or\n * an ES module whose `default` export is the component. Shared by field and\n * wrapper registrations so the `loadComponent: () => import('./x.component')`\n * idiom types cleanly in both places.\n */\nexport type LazyComponentLoader<T = unknown> = () => Promise<Type<T> | { default: Type<T> }>;\n\n/**\n * Pre-computed reverse index used by `resolveWrappers`: field type → the\n * wrappers that auto-apply to it. Built once in the `provideDynamicForm(...)`\n * factory from every registered `WrapperTypeDefinition.types` entry.\n */\nexport type WrapperAutoAssociations = ReadonlyMap<string, readonly WrapperConfig[]>;\n\n/** Configuration interface for registering wrapper types. */\nexport interface WrapperTypeDefinition<T extends WrapperConfig = WrapperConfig> {\n /** Unique identifier for the wrapper type (also serves as discriminant from FieldTypeDefinition) */\n wrapperName: string;\n /** Wrapper definition type marker (internal use) */\n _wrapper?: T;\n /**\n * Function to load the wrapper component (supports lazy loading).\n * Returns a Promise that resolves to the component class or module with default export.\n */\n loadComponent: LazyComponentLoader;\n /** Field types this wrapper should auto-apply to. */\n types?: readonly string[];\n}\n\n/** Type guard for WrapperTypeDefinition. */\nexport function isWrapperTypeDefinition(value: unknown): value is WrapperTypeDefinition {\n return typeof value === 'object' && value !== null && 'wrapperName' in value;\n}\n\n/**\n * Contract that wrapper components must satisfy.\n *\n * @Component({\n * template: `\n * <dbx-section [header]=\"header() ?? ''\">\n * <ng-container #fieldComponent></ng-container>\n * </dbx-section>\n * `,\n * })\n * export class SectionWrapperComponent implements FieldWrapper {\n * readonly fieldComponent = viewChild.required('fieldComponent', { read: ViewContainerRef });\n * readonly header = input<string>();\n * readonly fieldInputs = input<WrapperFieldInputs>();\n * }\n * ```\n */\nexport interface FieldWrapper {\n /** ViewContainerRef slot where inner content is rendered */\n readonly fieldComponent: Signal<ViewContainerRef>;\n}\n\n/** Injection token for the wrapper type registry. */\nexport const WRAPPER_REGISTRY = new InjectionToken<Map<string, WrapperTypeDefinition>>('WRAPPER_REGISTRY', {\n providedIn: 'root',\n factory: () => new Map(),\n});\n\n/** Component cache for loaded wrapper components. */\nexport const WRAPPER_COMPONENT_CACHE = new InjectionToken<Map<string, Type<unknown>>>('WRAPPER_COMPONENT_CACHE', {\n providedIn: 'root',\n factory: () => new Map(),\n});\n\n/**\n * Pre-computed reverse index: `fieldType → WrapperConfig[]` for every\n * registered `WrapperTypeDefinition.types` entry. Built once in the\n * `provideDynamicForm(...)` factory so `resolveWrappers` can look up\n * auto-associations in O(1) per field instead of scanning the full\n * wrapper registry on every render.\n */\nexport const WRAPPER_AUTO_ASSOCIATIONS = new InjectionToken<WrapperAutoAssociations>('WRAPPER_AUTO_ASSOCIATIONS', {\n providedIn: 'root',\n factory: () => new Map(),\n});\n","import { FieldDef } from '../../definitions/base/field-def';\nimport { isPageField } from '../../definitions/default/page-field';\nimport { RegisteredFieldTypes } from '../registry/field-registry';\n\n// Re-export isPageField for external usage\nexport { isPageField };\n\n/** Interface for fields that contain child fields */\ninterface ContainerField<T> extends FieldDef<T> {\n fields: FieldDef<unknown>[];\n}\n\n/** Type guard to check if a field is a container with fields */\nfunction isContainerWithFields<T>(field: FieldDef<T>): field is FieldDef<T> & ContainerField<T> {\n return (\n (field.type === 'row' || field.type === 'group' || field.type === 'page' || field.type === 'container') &&\n 'fields' in field &&\n Array.isArray((field as ContainerField<T>).fields)\n );\n}\n\n/** Form mode enumeration distinguishing between paged and non-paged forms */\nexport type FormMode = 'paged' | 'non-paged';\n\n/** Result of form mode detection with validation details */\nexport interface FormModeDetectionResult {\n /** The detected form mode */\n mode: FormMode;\n /** Whether the form configuration is valid for the detected mode */\n isValid: boolean;\n /** Array of validation errors if form is invalid */\n errors: string[];\n}\n\n/**\n * Type guard for paged form configurations\n * A paged form has ALL root-level fields of type 'page'\n */\nexport function isPagedForm<TFields extends RegisteredFieldTypes[]>(fields: TFields): boolean {\n if (!fields || fields.length === 0) {\n return false;\n }\n\n return fields.every((field) => isPageField(field));\n}\n\n/**\n * Type guard for non-paged form configurations\n * A non-paged form has NO page fields at any level\n */\nexport function isNonPagedForm<TFields extends RegisteredFieldTypes[]>(fields: TFields): boolean {\n if (!fields || fields.length === 0) {\n return true; // Empty form is considered non-paged\n }\n\n return !hasAnyPageFields(fields);\n}\n\n/**\n * Detects the form mode and validates the configuration\n *\n * @param fields The form field definitions\n * @returns Detection result with mode and validation status\n */\nexport function detectFormMode<TFields extends RegisteredFieldTypes[]>(fields: TFields): FormModeDetectionResult {\n if (!fields || fields.length === 0) {\n return {\n mode: 'non-paged',\n isValid: true,\n errors: [],\n };\n }\n\n const hasPageFields = fields.some((field) => isPageField(field));\n const allPagesAtRoot = fields.every((field) => isPageField(field));\n const hasNestedPages = hasNestedPageFields(fields);\n\n // Determine mode and validate\n if (hasPageFields) {\n const errors: string[] = [];\n\n // For paged forms, ALL root fields must be pages\n if (!allPagesAtRoot) {\n errors.push('Mixed page and non-page fields at root level. In paged forms, ALL root-level fields must be of type \"page\".');\n }\n\n // Check for nested pages in any page field\n if (hasNestedPages) {\n errors.push('Page fields cannot contain nested page fields at any level.');\n }\n\n return {\n mode: 'paged',\n isValid: errors.length === 0,\n errors,\n };\n } else {\n // Non-paged form\n const errors: string[] = [];\n\n // Non-paged forms cannot have page fields anywhere\n if (hasAnyPageFields(fields)) {\n errors.push('Page fields are not allowed in non-paged forms.');\n }\n\n return {\n mode: 'non-paged',\n isValid: errors.length === 0,\n errors,\n };\n }\n}\n\n/**\n * Recursively checks if any field definition is a page field\n *\n * @param fields Array of field definitions to check\n * @returns true if any page field is found at any level\n */\nfunction hasAnyPageFields(fields: FieldDef<unknown>[]): boolean {\n for (const field of fields) {\n if (isPageField(field)) {\n return true;\n }\n\n // Check nested fields in container types\n if (!isPageField(field) && isContainerWithFields(field)) {\n if (hasAnyPageFields(field.fields)) {\n return true;\n }\n }\n }\n return false;\n}\n\n/**\n * Checks if fields contain nested page fields within their children\n * (This is different from hasAnyPageFields as it checks NESTED pages, not root pages)\n *\n * @param fields Array of field definitions to check\n * @returns true if nested page fields found\n */\nfunction hasNestedPageFields(fields: FieldDef<unknown>[]): boolean {\n for (const field of fields) {\n // If this is a page field, check if its children contain pages\n if (isPageField(field) && isContainerWithFields(field)) {\n if (hasAnyPageFields(field.fields)) {\n return true;\n }\n }\n\n // Check other container types for nested pages\n if (!isPageField(field) && isContainerWithFields(field)) {\n if (hasNestedPageFields(field.fields)) {\n return true;\n }\n }\n }\n return false;\n}\n\n/** Type predicate for valid paged form configurations */\nexport function isValidPagedForm<TFields extends RegisteredFieldTypes[]>(fields: TFields): boolean {\n const result = detectFormMode(fields);\n return result.mode === 'paged' && result.isValid;\n}\n\n/** Type predicate for valid non-paged form configurations */\nexport function isValidNonPagedForm<TFields extends RegisteredFieldTypes[]>(fields: TFields): boolean {\n const result = detectFormMode(fields);\n return result.mode === 'non-paged' && result.isValid;\n}\n","import { FieldDef } from '../../definitions/base/field-def';\nimport { isPageField } from '../../definitions/default/page-field';\nimport { isRowField } from '../../definitions/default/row-field';\nimport { isGroupField } from '../../definitions/default/group-field';\nimport { isArrayField } from '../../definitions/default/array-field';\nimport { isGenericContainerField } from '../../definitions/default/container-field';\nimport { ContainerFieldTypes, LeafFieldTypes, RegisteredFieldTypes } from '../registry/field-registry';\n\n/**\n * Type guard to check if a field definition has child fields.\n * This is a looser check that works with FieldDef<unknown> without requiring RegisteredFieldTypes.\n */\nexport function hasChildFields<T>(\n field: FieldDef<T>,\n): field is FieldDef<T> & { fields: FieldDef<unknown>[] | Record<string, FieldDef<unknown>> } {\n return 'fields' in field && field.fields != null;\n}\n\n/** Container field type names */\nconst CONTAINER_TYPES = new Set(['page', 'row', 'group', 'array', 'container']);\n\n/**\n * Type guard to check if a field is a container field (page, row, group, or array)\n * Container fields have a 'fields' property and don't contribute values directly.\n * This overload works with RegisteredFieldTypes for full type narrowing.\n */\nexport function isContainerField(field: RegisteredFieldTypes): field is ContainerFieldTypes;\n/**\n * Type guard to check if a field is a container field (page, row, group, or array).\n * This overload works with any FieldDef for looser type checking.\n */\nexport function isContainerField(field: FieldDef<unknown>): boolean;\nexport function isContainerField(field: FieldDef<unknown> | RegisteredFieldTypes): boolean {\n return CONTAINER_TYPES.has(field.type);\n}\n\n/**\n * Type guard to check if a field is a leaf field (value or display field)\n * Leaf fields don't have children and either contribute values or display content\n */\nexport function isLeafField(field: RegisteredFieldTypes): field is LeafFieldTypes {\n return !isContainerField(field);\n}\n\n/**\n * Type guard to check if a field has a value property (value-bearing field)\n * These fields contribute to the form value output\n * Note: Using `unknown` in the Extract condition to match any value type\n */\nexport function isValueBearingField(field: RegisteredFieldTypes): field is Extract<RegisteredFieldTypes, { value: unknown }> {\n return 'value' in field;\n}\n\n/**\n * Type guard to check if a field is excluded from form values (display-only field)\n * Currently this includes text fields and any other fields without a value property\n */\nexport function isDisplayOnlyField(field: RegisteredFieldTypes): boolean {\n return field.type === 'text' || isContainerField(field);\n}\n\n// Re-export the specific type guards for convenience\nexport { isPageField, isRowField, isGroupField, isArrayField, isGenericContainerField };\n","import { isSignal, Injector, untracked } from '@angular/core';\nimport { toObservable } from '@angular/core/rxjs-interop';\nimport { isObservable, Observable, of } from 'rxjs';\nimport { DynamicText } from '../models/types/dynamic-text';\n\n/**\n * Converts DynamicText (string | Observable | Signal) to Observable<string>\n * Unifies all three types into a consistent Observable stream\n *\n * @param value - The dynamic text value to convert\n * @param injector - Optional injector for signal conversion\n * @returns Observable<string> - The value as an observable stream\n */\nexport function dynamicTextToObservable(value: DynamicText | undefined, injector?: Injector): Observable<string> {\n if (value === undefined) {\n return of('');\n }\n\n if (isObservable(value)) {\n return value;\n }\n\n if (isSignal(value)) {\n // Wrap in untracked() to avoid NG0602: toObservable() internally calls effect(), which cannot be created inside a reactive context (template expression)\n return untracked(() => toObservable(value, { injector }));\n }\n\n return of(String(value));\n}\n","import { inject, Injector, Pipe, PipeTransform } from '@angular/core';\nimport { Observable } from 'rxjs';\nimport { DynamicText } from '../../models/types/dynamic-text';\nimport { dynamicTextToObservable } from '../../utils/dynamic-text-to-observable';\n\n/**\n * Pipe that handles dynamic text resolution with support for static strings,\n * Observables, and Signals.\n */\n@Pipe({\n name: 'dynamicText',\n})\nexport class DynamicTextPipe implements PipeTransform {\n private readonly injector = inject(Injector);\n\n /**\n * Transforms dynamic text input into a resolved string value\n *\n * @param value - The dynamic text value to resolve\n * @returns The resolved string value as an Observable\n */\n transform(value: DynamicText | undefined): Observable<string> {\n // Normalize null to undefined so it resolves to an empty string\n return dynamicTextToObservable(value ?? undefined, this.injector);\n }\n}\n","import { FieldMeta } from '../definitions/base/field-meta';\n\n/**\n * Applies meta attributes to a DOM element and tracks which attributes were applied.\n *\n * @param element - The DOM element to apply attributes to\n * @param meta - The meta object containing attributes to apply\n * @param previouslyApplied - Set of attribute names that were previously applied\n * @returns A new Set containing the attribute names that were applied\n */\nexport function applyMetaToElement(element: Element, meta: FieldMeta | undefined, previouslyApplied: Set<string>): Set<string> {\n const newApplied = new Set<string>();\n\n // Remove old attributes no longer in meta\n for (const attr of previouslyApplied) {\n if (!meta || !(attr in meta) || meta[attr] === undefined) {\n element.removeAttribute(attr);\n }\n }\n\n if (!meta) return newApplied;\n\n // Apply new attributes\n for (const [key, value] of Object.entries(meta)) {\n if (value === undefined || value === null) continue;\n\n element.setAttribute(key, String(value));\n newApplied.add(key);\n }\n\n return newApplied;\n}\n","import { InjectionToken, Injector, Signal } from '@angular/core';\nimport { toObservable } from '@angular/core/rxjs-interop';\nimport { Observable, of, throwError, TimeoutError } from 'rxjs';\nimport { catchError, filter, map, scan, shareReplay, switchMap, take, timeout } from 'rxjs/operators';\nimport { EventBus } from '../../events/event.bus';\nimport { ComponentInitializedEvent } from '../../events/constants/component-initialized.event';\nimport { DynamicFormLogger } from '../../providers/features/logger/logger.token';\n\n/**\n * Injection token for configuring the initialization timeout in milliseconds.\n * Defaults to 10 seconds. When the timeout is reached, a warning is logged\n * and (initialized) emits true as a best-effort fallback.\n */\nexport const INITIALIZATION_TIMEOUT_MS = new InjectionToken<number>('INITIALIZATION_TIMEOUT_MS', {\n providedIn: 'root',\n factory: () => 10_000,\n});\n\n/** Creates an observable that tracks component initialization progress. */\nexport function createInitializationTracker(eventBus: EventBus, expectedCount: number): Observable<boolean> {\n return eventBus.on<ComponentInitializedEvent>('component-initialized').pipe(\n scan((count) => count + 1, 0),\n map((currentCount) => currentCount >= expectedCount),\n filter((isComplete) => isComplete),\n );\n}\n\n/** Options for setting up initialization tracking. */\nexport interface InitializationTrackingOptions {\n eventBus: EventBus;\n totalComponentsCount: Signal<number>;\n injector: Injector;\n componentId: string;\n}\n\n/**\n * Creates an observable that tracks when all form components are initialized.\n * Uses shareReplay({ bufferSize: 1, refCount: false }) to ensure exactly one emission\n * that can be received by late subscribers and keeps the subscription alive.\n */\nexport function setupInitializationTracking(options: InitializationTrackingOptions): Observable<boolean> {\n const { eventBus, totalComponentsCount, injector, componentId } = options;\n\n const timeoutMs = injector.get(INITIALIZATION_TIMEOUT_MS);\n const logger = injector.get(DynamicFormLogger);\n\n return toObservable(totalComponentsCount, { injector }).pipe(\n take(1),\n switchMap((count) => {\n const tracking$: Observable<boolean> =\n count === 1\n ? // Only dynamic-form component, emit immediately when it initializes\n eventBus.on<ComponentInitializedEvent>('component-initialized').pipe(\n filter((event) => event.componentType === 'dynamic-form' && event.componentId === componentId),\n map(() => true),\n take(1),\n )\n : createInitializationTracker(eventBus, count);\n\n // Timeout guard: emit best-effort true if a container throws before initializing\n return tracking$.pipe(\n timeout(timeoutMs),\n catchError((error: unknown) => {\n if (error instanceof TimeoutError) {\n logger.warn(\n `Initialization timed out after ${timeoutMs}ms. ` +\n `Expected ${count} component(s) to initialize but not all reported in time. ` +\n 'This may indicate a container component threw during initialization. ' +\n 'Emitting (initialized) as best-effort.',\n );\n // Emit true as best-effort so consumers are not stuck waiting forever\n return of(true);\n }\n return throwError(() => error);\n }),\n );\n }),\n shareReplay({ bufferSize: 1, refCount: false }),\n );\n}\n","import { ValidationError } from '../models/validation-types';\n\n/**\n * Interpolates {{param}} placeholders in message with error values\n *\n * @param message - Message template with {{param}} placeholders\n * @param error - Validation error containing parameter values\n * @returns Message with interpolated parameters\n */\nexport function interpolateParams(message: string, error: ValidationError): string {\n let result = message;\n const params = extractErrorParams(error);\n\n Object.entries(params).forEach(([key, value]) => {\n const placeholder = new RegExp(`{{\\\\s*${key}\\\\s*}}`, 'g');\n result = result.replace(placeholder, safeToString(value));\n });\n\n return result;\n}\n\n/**\n * Safely converts a value to a string, handling complex objects\n *\n * @internal\n */\nfunction safeToString(value: unknown): string {\n if (value === null || value === undefined) {\n return '';\n }\n\n // Handle primitives directly\n if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {\n return String(value);\n }\n\n // Handle RegExp (has its own toString)\n if (value instanceof RegExp) {\n return value.toString();\n }\n\n // Handle Date\n if (value instanceof Date) {\n return value.toISOString();\n }\n\n // Handle arrays\n if (Array.isArray(value)) {\n return value.join(', ');\n }\n\n // For objects, try JSON.stringify first, fall back to empty string\n try {\n // Check if object has a custom toString that's not Object.prototype.toString\n if (typeof value === 'object' && value.toString !== Object.prototype.toString) {\n return value.toString();\n }\n // Otherwise use JSON representation\n return JSON.stringify(value);\n } catch {\n // If JSON.stringify fails (circular reference, etc.), return empty string\n return '[object]';\n }\n}\n\n/**\n * Extracts parameter values from validation error\n *\n * @internal\n */\nfunction extractErrorParams(error: ValidationError): Record<string, unknown> {\n const params: Record<string, unknown> = {};\n\n // Common validation error parameters\n if ('min' in error) params.min = error.min;\n if ('max' in error) params.max = error.max;\n if ('pattern' in error) params.pattern = error.pattern;\n if ('actual' in error) params.actual = error.actual;\n if ('expected' in error) params.expected = error.expected;\n\n // Handle length validation parameters\n // Angular Signal Forms uses 'minLength'/'maxLength' properties\n // Angular Classic Forms uses 'requiredLength'/'actualLength' properties\n // We support both formats and normalize to 'requiredLength' for message templates\n if ('requiredLength' in error) {\n params.requiredLength = error.requiredLength;\n } else if ('minLength' in error) {\n // Angular Signal Forms: minLength property maps to requiredLength for templates\n params.requiredLength = error.minLength;\n params.minLength = error.minLength;\n } else if ('maxLength' in error) {\n // Angular Signal Forms: maxLength property maps to requiredLength for templates\n params.requiredLength = error.maxLength;\n params.maxLength = error.maxLength;\n }\n if ('actualLength' in error) params.actualLength = error.actualLength;\n\n // Include all custom properties from error object (for custom validators)\n // Skip 'kind' as it's the error type identifier, not a parameter\n Object.keys(error).forEach((key) => {\n if (key !== 'kind' && !(key in params)) {\n params[key] = error[key as keyof ValidationError];\n }\n });\n\n return params;\n}\n","/**\n * Native JavaScript utility functions to replace lodash-es\n * These are simple implementations for common object manipulation patterns\n */\n\nimport { DynamicFormError } from '../errors/dynamic-form-error';\n\n/**\n * Performs a deep equality comparison between two values.\n *\n * @param a - First value\n * @param b - Second value\n * @returns true if values are deeply equal\n */\nexport function isEqual(a: unknown, b: unknown): boolean {\n return isEqualInternal(a, b, new WeakMap(), new WeakMap());\n}\n\n/**\n * Internal implementation with circular reference tracking.\n *\n * @internal\n */\nfunction isEqualInternal(a: unknown, b: unknown, seenA: WeakMap<object, object>, seenB: WeakMap<object, object>): boolean {\n // Same reference or both null/undefined\n if (a === b) return true;\n\n // Handle NaN (NaN === NaN is false, but we want it to be equal)\n if (typeof a === 'number' && typeof b === 'number') {\n return Object.is(a, b);\n }\n\n // One is null/undefined but not both\n if (a == null || b == null) return false;\n\n // Different types\n if (typeof a !== typeof b) return false;\n\n // Non-object primitives (strings, booleans, symbols, bigints)\n if (typeof a !== 'object') {\n return Object.is(a, b);\n }\n\n // At this point, both a and b are objects (typeof a === 'object')\n const objA = a as object;\n const objB = b as object;\n\n // Check for circular references\n if (seenA.has(objA) || seenB.has(objB)) {\n // If we've seen these objects before, check if they were paired together\n return seenA.get(objA) === objB && seenB.get(objB) === objA;\n }\n\n // Mark these objects as seen (paired together)\n seenA.set(objA, objB);\n seenB.set(objB, objA);\n\n // Different constructors means different types (e.g., Date vs Object)\n if (objA.constructor !== objB.constructor) return false;\n\n // Handle dates\n if (a instanceof Date && b instanceof Date) {\n return a.getTime() === b.getTime();\n }\n\n // Handle RegExp\n if (a instanceof RegExp && b instanceof RegExp) {\n return a.source === b.source && a.flags === b.flags;\n }\n\n // Handle Map\n if (a instanceof Map && b instanceof Map) {\n if (a.size !== b.size) return false;\n for (const [key, value] of a) {\n if (!b.has(key) || !isEqualInternal(value, b.get(key), seenA, seenB)) {\n return false;\n }\n }\n return true;\n }\n\n // Handle Set\n if (a instanceof Set && b instanceof Set) {\n if (a.size !== b.size) return false;\n for (const value of a) {\n if (!b.has(value)) {\n // For non-primitive values in Sets, we need deep comparison\n // This is O(n^2) but Sets rarely contain objects\n if (typeof value === 'object' && value !== null) {\n let found = false;\n for (const bValue of b) {\n if (typeof bValue === 'object' && bValue !== null && isEqualInternal(value, bValue, seenA, seenB)) {\n found = true;\n break;\n }\n }\n if (!found) return false;\n } else {\n return false;\n }\n }\n }\n return true;\n }\n\n // Handle arrays\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false;\n return a.every((item, index) => isEqualInternal(item, b[index], seenA, seenB));\n }\n\n // Handle plain objects\n const recordA = a as Record<string | symbol, unknown>;\n const recordB = b as Record<string | symbol, unknown>;\n\n // Get both string and symbol keys\n const keysA = [...Object.keys(recordA), ...Object.getOwnPropertySymbols(recordA)];\n const keysB = [...Object.keys(recordB), ...Object.getOwnPropertySymbols(recordB)];\n\n if (keysA.length !== keysB.length) return false;\n\n // Create a Set for O(1) lookup\n const keySetB = new Set(keysB);\n\n return keysA.every((key) => keySetB.has(key) && isEqualInternal(recordA[key], recordB[key], seenA, seenB));\n}\n\n/**\n * Creates a new object without the specified keys\n * Native replacement for lodash omit()\n *\n * @param obj - Source object\n * @param keys - Keys to omit\n * @returns New object without specified keys\n */\nexport function omit<T extends object, K extends keyof T>(obj: T, keys: K[]): Omit<T, K> {\n const result = { ...obj };\n keys.forEach((key) => delete result[key]);\n return result as Omit<T, K>;\n}\n\n/**\n * Creates an object from an array, keyed by a specified property\n * Native replacement for lodash keyBy()\n *\n * @param array - Source array\n * @param key - Property to use as key\n * @returns Object keyed by the specified property\n */\nexport function keyBy<T extends object>(array: T[], key: keyof T): Record<string, T> {\n return array.reduce(\n (acc, item) => {\n acc[String(item[key])] = item;\n return acc;\n },\n {} as Record<string, T>,\n );\n}\n\n/**\n * Maps object values through a transformation function\n * Native replacement for lodash mapValues()\n *\n * @param obj - Source object\n * @param fn - Transformation function\n * @returns New object with transformed values\n */\nexport function mapValues<T, U>(obj: Record<string, T>, fn: (value: T, key: string) => U): Record<string, U> {\n return Object.entries(obj).reduce(\n (acc, [key, value]) => {\n acc[key] = fn(value, key);\n return acc;\n },\n {} as Record<string, U>,\n );\n}\n\n/**\n * Deep-merges `value` into `defaults`, producing a new object that contains every\n * key from `defaults` plus any overrides from `value`. Plain-object values at\n * matching keys are merged recursively; primitives, dates, and class instances\n * in `value` replace the corresponding default wholesale.\n */\nexport function deepMergeDefaults<T extends Record<string, unknown>>(defaults: T, value: Record<string, unknown> | null | undefined): T {\n if (value == null) return { ...defaults };\n\n const result: Record<string, unknown> = { ...defaults };\n\n for (const key of Object.keys(value)) {\n if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue;\n\n const incoming = value[key];\n const existing = result[key];\n\n if (isPlainObject(existing) && isPlainObject(incoming)) {\n result[key] = deepMergeDefaults(existing, incoming);\n } else if (Array.isArray(existing) && Array.isArray(incoming)) {\n result[key] = mergeArrayDefaults(existing, incoming);\n } else {\n result[key] = incoming;\n }\n }\n\n return result as T;\n}\n\n/**\n * @internal\n * Merges a value array into a defaults array element-wise when their lengths\n * match. When lengths differ — the runtime case: prepend, insert, append,\n * remove — we return the value array reference as-is so we don't produce\n * fresh inner object references for items that already have every declared\n * sub-field key. Replacing references during the linkedSignal source\n * recompute that runs after Signal Forms wrote the array would otherwise\n * desync item FieldTrees from the rendered DOM rows.\n *\n * When lengths match (the declared/initial-value path the array fix targets)\n * each `defaults[i]`/`value[i]` pair is deep-merged when both are plain\n * objects so partial array-item values don't drop sibling sub-field keys.\n */\nfunction mergeArrayDefaults(defaults: readonly unknown[], value: readonly unknown[]): readonly unknown[] {\n if (defaults.length !== value.length) return value;\n\n // Short-circuit when no element pair would be deep-merged. Returning a fresh\n // .map() result for primitive arrays (e.g. tags: ['a', 'b']) hands Signal\n // Forms a new reference on every call and rebuilds item FieldTrees needlessly.\n if (!value.some((item, i) => isPlainObject(defaults[i]) && isPlainObject(item))) return value;\n\n return value.map((item, i) => {\n const defaultItem = defaults[i];\n if (isPlainObject(defaultItem) && isPlainObject(item)) {\n return deepMergeDefaults(defaultItem, item);\n }\n return item;\n });\n}\n\n/**\n * @internal\n * Returns true for objects that should be deep-merged (plain objects produced\n * by literals or `Object.create(null)`); false for arrays, Dates, RegExps,\n * Maps, Sets, class instances, and primitives.\n */\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n if (typeof value !== 'object' || value === null) return false;\n const proto = Object.getPrototypeOf(value);\n return proto === null || proto === Object.prototype;\n}\n\n/** Options for memoize function */\n/** Default maximum cache size for memoized functions */\nconst DEFAULT_MEMOIZE_MAX_SIZE = 100;\n\nexport interface MemoizeOptions<TFunc extends (...args: never[]) => unknown> {\n /** Optional function to generate cache key from arguments */\n resolver?: (...args: Parameters<TFunc>) => string | number;\n /** Maximum number of entries to keep in cache. Uses LRU eviction when exceeded. Defaults to 100. */\n maxSize?: number;\n}\n\n/**\n * Memoizes a function with LRU cache eviction and optional custom cache key resolver.\n *\n * @param fn - Function to memoize\n * @param resolverOrOptions - Optional key resolver function or options object\n * @returns Memoized function\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function memoize<TFunc extends (...args: any[]) => any>(\n fn: TFunc,\n resolverOrOptions?: ((...args: Parameters<TFunc>) => string | number) | MemoizeOptions<TFunc>,\n): TFunc {\n const options: MemoizeOptions<TFunc> =\n typeof resolverOrOptions === 'function' ? { resolver: resolverOrOptions } : (resolverOrOptions ?? {});\n\n const { resolver, maxSize = DEFAULT_MEMOIZE_MAX_SIZE } = options;\n const cache = new Map<string | number, ReturnType<TFunc>>();\n\n return ((...args: Parameters<TFunc>): ReturnType<TFunc> => {\n const key = resolver ? resolver(...args) : JSON.stringify(args);\n\n const cachedValue = cache.get(key);\n if (cachedValue !== undefined) {\n cache.delete(key);\n cache.set(key, cachedValue);\n return cachedValue;\n }\n\n const result = fn(...args);\n\n if (cache.size >= maxSize) {\n const oldestKey = cache.keys().next().value;\n if (oldestKey !== undefined) {\n cache.delete(oldestKey);\n }\n }\n\n cache.set(key, result);\n return result;\n }) as TFunc;\n}\n\n/**\n * Normalizes a fields collection to an array.\n * Handles both array format and object format (keyed by field key).\n *\n * @param fields - Fields as array or object\n * @returns Fields as array\n */\nexport function normalizeFieldsArray<T>(fields: readonly T[] | Record<string, T>): T[] {\n return Array.isArray(fields) ? [...fields] : Object.values(fields);\n}\n\n/**\n * Gets the keys that differ between two objects.\n *\n * @param previous - Previous object state\n * @param current - Current object state\n * @returns Set of keys that have different values\n */\nexport function getChangedKeys(\n previous: Record<string, unknown> | null | undefined,\n current: Record<string, unknown> | null | undefined,\n): Set<string> {\n const changedKeys = new Set<string>();\n\n // Handle null/undefined cases\n if (!previous && !current) return changedKeys;\n if (!previous) {\n return new Set(Object.keys(current ?? {}));\n }\n if (!current) {\n return new Set(Object.keys(previous));\n }\n\n // Collect all keys from both objects\n const allKeys = new Set([...Object.keys(previous), ...Object.keys(current)]);\n\n // Check each key for differences\n for (const key of allKeys) {\n const prevValue = previous[key];\n const currValue = current[key];\n\n // Key added or removed\n if (!(key in previous) || !(key in current)) {\n changedKeys.add(key);\n continue;\n }\n\n // Value changed\n if (!isEqual(prevValue, currValue)) {\n changedKeys.add(key);\n }\n }\n\n return changedKeys;\n}\n\n/** Compile-time exhaustiveness check for discriminated unions. */\nexport function assertNever(value: never): never {\n throw new DynamicFormError(`Unexpected value: ${JSON.stringify(value)}`);\n}\n","/**\n * Shared shape for DI-scoped warning trackers that dedupe log output.\n *\n * @internal\n */\nexport interface WarningTracker {\n warnedKeys: Set<string>;\n}\n\n/**\n * Creates a fresh warning tracker instance with an empty key set.\n * DI-scoped per form instance — never module-scoped (SSR-unsafe).\n *\n * @internal\n */\nexport function createWarningTracker(): WarningTracker {\n return { warnedKeys: new Set<string>() };\n}\n"]}
|