@a.nemreen/dga-dynamic-form 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,93 @@
1
+ # `@a.nemreen/dga-dynamic-form`
2
+
3
+ Schema-driven dynamic forms for Angular, rendered with [`@a.nemreen/dga-ui`](https://www.npmjs.com/package/@a.nemreen/dga-ui) controls.
4
+
5
+ Version **0.1.0** — API may move until freeze.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @a.nemreen/dga-dynamic-form @a.nemreen/dga-ui @a.nemreen/dga-tokens @platformscode/icons
11
+ ```
12
+
13
+ **Peers:** `@angular/core` / `common` / `forms` ^19–22, `@a.nemreen/dga-ui` ^0.2, `@a.nemreen/dga-tokens` ^0.2.
14
+
15
+ ## Setup
16
+
17
+ Provide adapters once in `app.config.ts`:
18
+
19
+ ```ts
20
+ import { provideHttpClient } from '@angular/common/http';
21
+ import { provideDgaDynamicForm } from '@a.nemreen/dga-dynamic-form';
22
+
23
+ export const appConfig = {
24
+ providers: [
25
+ provideHttpClient(),
26
+ provideDgaDynamicForm({ httpSubmit: true, dgaToast: true }),
27
+ ],
28
+ };
29
+ ```
30
+
31
+ Ensure Tailwind scans the package:
32
+
33
+ ```css
34
+ @source "../node_modules/@a.nemreen/dga-dynamic-form/**/*.{mjs,js}";
35
+ @source "../node_modules/@a.nemreen/dga-ui/**/*.{mjs,js}";
36
+ ```
37
+
38
+ ## Usage
39
+
40
+ ```ts
41
+ import { DgaDynamicForm, type DgaDynamicFormConfig } from '@a.nemreen/dga-dynamic-form';
42
+
43
+ readonly config: DgaDynamicFormConfig = {
44
+ emitOnly: true,
45
+ submitButtonLabel: { en: 'Send', ar: 'إرسال' },
46
+ fields: [
47
+ {
48
+ name: 'fullName',
49
+ type: 'text',
50
+ label: { en: 'Full name', ar: 'الاسم الكامل' },
51
+ required: true,
52
+ columns: { md: 6 },
53
+ },
54
+ {
55
+ name: 'email',
56
+ type: 'email',
57
+ label: { en: 'Email', ar: 'البريد' },
58
+ required: true,
59
+ columns: { md: 6 },
60
+ },
61
+ ],
62
+ };
63
+ ```
64
+
65
+ ```html
66
+ <dga-dynamic-form [config]="config" (formSubmitted)="onSubmit($event)" />
67
+ ```
68
+
69
+ ## Adapters
70
+
71
+ Override via InjectionTokens:
72
+
73
+ | Token | Role |
74
+ |-------|------|
75
+ | `DGA_LOOKUP_ADAPTER` | Lookup / search options |
76
+ | `DGA_SUBMIT_ADAPTER` | HTTP submit |
77
+ | `DGA_FORM_TOAST_ADAPTER` | Success / error feedback |
78
+ | `DGA_CAPTCHA_ADAPTER` | Optional captcha token |
79
+ | `DGA_FORM_I18N_ADAPTER` | Label resolution |
80
+ | `DGA_DYNAMIC_FORM_FIELD_REGISTRY` | Custom field type → component map |
81
+
82
+ ## Custom field types
83
+
84
+ ```ts
85
+ {
86
+ provide: DGA_DYNAMIC_FORM_FIELD_REGISTRY,
87
+ useValue: [{ type: 'nationalId', component: NationalIdField }],
88
+ }
89
+ ```
90
+
91
+ ## Built-in field types
92
+
93
+ `text` · `email` · `textarea` · `number` · `otp` · `phone` · `select` · `multiselect` · `checkbox` · `radio` · `date` · `chips` · `toggle` · `file` · `hidden`
@@ -0,0 +1,503 @@
1
+ import*as i from"@angular/core";import{InjectionToken as g,inject as s,Injectable as P,input as u,booleanAttribute as ye,output as S,DestroyRef as E,signal as C,computed as d,ChangeDetectionStrategy as q,Component as z,effect as xe,makeEnvironmentProviders as Se}from"@angular/core";import{NgComponentOutlet as B}from"@angular/common";import{takeUntilDestroyed as _}from"@angular/core/rxjs-interop";import*as p from"@angular/forms";import{Validators as m,ReactiveFormsModule as k,FormsModule as G,FormBuilder as Ce}from"@angular/forms";import{switchMap as $,catchError as Ie,startWith as ke,map as De}from"rxjs/operators";import{DgaField as j,DgaInput as W,DgaSelect as H,DgaCheckbox as K,DgaRadio as U,DgaPhoneInput as Y,DgaDatePicker as J,DgaChipInput as X,DgaSwitch as Z,DgaUpload as Q,DgaNumberInput as ee,DgaOtp as te,DgaButton as oe,DgaStepper as ae,DgaToastService as Oe}from"@a.nemreen/dga-ui";import{of as h}from"rxjs";import{HttpClient as re,HttpHeaders as Le}from"@angular/common/http";const D=new g("DGA_LOOKUP_ADAPTER"),O=new g("DGA_SUBMIT_ADAPTER"),L=new g("DGA_FORM_TOAST_ADAPTER"),T=new g("DGA_CAPTCHA_ADAPTER"),N=new g("DGA_FORM_I18N_ADAPTER"),ne=new g("DGA_DYNAMIC_FORM_FIELD_REGISTRY"),ie=["checkbox","multiselect","chips","file"],Te=["toggle"];function le(a){return a.value!==void 0?a.value:ie.includes(a.type)?[]:Te.includes(a.type)?!1:""}function A(a,e){const t=[],o=a.validation;(e||a.required||o?.required)&&(ie.includes(a.type)?t.push(Ne):a.type==="toggle"?t.push(m.requiredTrue):t.push(m.required)),(a.type==="email"||o?.pattern==="email")&&t.push(m.email);const r=o?.minLength;r!=null&&t.push(m.minLength(r));const n=o?.maxLength??a.maxLength;if(n!=null&&t.push(m.maxLength(n)),o?.pattern&&o.pattern!=="email")try{t.push(m.pattern(o.pattern))}catch{}return o?.min!=null&&t.push(m.min(o.min)),o?.max!=null&&t.push(m.max(o.max)),a.maxChips!=null&&a.type==="chips"&&t.push(Ae(a.maxChips)),t}function Ne(a){const e=a.value;return Array.isArray(e)&&e.length>0?null:{required:!0}}function Ae(a){return e=>{const t=e.value;return Array.isArray(t)&&t.length>a?{maxChips:{max:a,actual:t.length}}:null}}function se(a,e,t=!0){const o=!!e.disabled;return a.control({value:le(e),disabled:o},{validators:A(e,t&&!e.requiredWhen)})}function de(a,e){const t={};for(const o of e){if(o.hidden&&o.type==="hidden"){t[o.name]=a.control(o.value??"");continue}t[o.name]=se(a,o)}return a.group(t)}function ce(a,e){return e?.length?e.flatMap(t=>t.fields):a}function ue(a,e,t){a.setValidators(A(e,t)),a.updateValueAndValidity({emitEvent:!1})}function F(a,e){return Array.isArray(a)?a.some(t=>t==e):a==e}function w(a,e,t){return a?[...t??[],...a.options??[],...a.groupedOptions?.flatMap(r=>r.options)??[]].find(r=>r.value==e):void 0}function M(a,e,t){if(a.hidden&&a.type!=="hidden"||a.type==="hidden")return!1;if(!a.dependsOn)return!0;const o=e[a.dependsOn];if(a.showOnValues?.length&&!a.showOnValues.some(r=>F(o,r)))return!1;if(a.showWhenOptionProperty){const r=t?.(a.dependsOn),n=w({name:a.dependsOn,type:"select",label:{en:"",ar:""}},o,r),l=a.showWhenOptionProperty.property;if(!n||n[l]!==a.showWhenOptionProperty.value)return!1}return!(!a.showOnValues?.length&&!a.showWhenOptionProperty&&(o==null||o===""||Array.isArray(o)&&o.length===0))}function V(a,e,t){const o=a.requiredWhen;if(!o)return!!a.required;const r=e[o.field];if(o.optionProperty!=null){const n=t?.(o.field),l=w({name:o.field,type:"select",label:{en:"",ar:""}},r,n);return!!l&&l[o.optionProperty]===o.optionPropertyValue}return o.values?.length?o.values.some(n=>F(r,n)):o.value!==void 0?F(r,o.value):r!=null&&r!==""}class v{submitAdapter=s(O,{optional:!0});captcha=s(T,{optional:!0});buildPayload(e,t,o){const r=new Set(o.filter(l=>l.excludeFromPayload).map(l=>l.name));let n={};for(const[l,x]of Object.entries(t)){if(r.has(l))continue;const c=e.fieldMapping?.[l]??l;n[c]=x}return e.payloadTransformer?e.payloadTransformer(n):n}hasFileValues(e){return Object.values(e).some(t=>t instanceof File||Array.isArray(t)&&t.some(o=>o instanceof File||o?.file instanceof File))}toFormData(e){const t=new FormData;for(const[o,r]of Object.entries(e))if(r!=null)if(r instanceof File)t.append(o,r);else if(Array.isArray(r))for(const n of r)n instanceof File?t.append(o,n):n?.file instanceof File?t.append(o,n.file,n.name):t.append(o,typeof n=="string"?n:JSON.stringify(n));else typeof r=="object"?t.append(o,JSON.stringify(r)):t.append(o,String(r));return t}submitForm(e,t,o){if(!e.endpoint||!this.submitAdapter)return h(t);const r=this.buildPayload(e,t,o),n=r instanceof FormData?r:this.hasFileValues(r)?this.toFormData(r):r,l={};return e.idempotencyKey&&(l["Idempotency-Key"]=e.idempotencyKey),(this.captcha?.getToken("submit")??h(null)).pipe($(c=>(c&&(l["X-Recaptcha-Token"]=c),h({endpoint:e.endpoint,method:e.method??"POST",body:n,headers:l})))).pipe($(c=>this.submitAdapter.submit(c)),Ie(c=>{throw c}))}saveDraft(e,t){typeof localStorage>"u"||localStorage.setItem(this.storageKey(e),JSON.stringify(t))}loadDraft(e){if(typeof localStorage>"u")return null;const t=localStorage.getItem(this.storageKey(e));if(!t)return null;try{return JSON.parse(t)}catch{return null}}clearDraft(e){typeof localStorage>"u"||localStorage.removeItem(this.storageKey(e))}storageKey(e){return`dga-dynamic-form:${e}`}static \u0275fac=i.\u0275\u0275ngDeclareFactory({minVersion:"12.0.0",version:"22.0.7",ngImport:i,type:v,deps:[],target:i.\u0275\u0275FactoryTarget.Injectable});static \u0275prov=i.\u0275\u0275ngDeclareInjectable({minVersion:"12.0.0",version:"22.0.7",ngImport:i,type:v,providedIn:"root"})}i.\u0275\u0275ngDeclareClassMetadata({minVersion:"12.0.0",version:"22.0.7",ngImport:i,type:v,decorators:[{type:P,args:[{providedIn:"root"}]}]});function b(a,e,t=""){return a?e==="en"?a.en||a.ar:a.ar||a.en:t}function pe(){return typeof document>"u"?"ar":(document.documentElement.lang||"ar").toLowerCase().startsWith("en")?"en":"ar"}class y{extras=s(ne,{optional:!0});map=new Map;constructor(){for(const e of this.extras??[])this.register(e)}register(e){this.map.set(e.type,e.component)}get(e){return this.map.get(e)??null}hasCustom(e){return this.map.has(e)}isBuiltIn(e){return Fe.has(e)}static \u0275fac=i.\u0275\u0275ngDeclareFactory({minVersion:"12.0.0",version:"22.0.7",ngImport:i,type:y,deps:[],target:i.\u0275\u0275FactoryTarget.Injectable});static \u0275prov=i.\u0275\u0275ngDeclareInjectable({minVersion:"12.0.0",version:"22.0.7",ngImport:i,type:y,providedIn:"root"})}i.\u0275\u0275ngDeclareClassMetadata({minVersion:"12.0.0",version:"22.0.7",ngImport:i,type:y,decorators:[{type:P,args:[{providedIn:"root"}]}],ctorParameters:()=>[]});const Fe=new Set(["text","email","textarea","select","multiselect","checkbox","radio","phone","file","date","chips","toggle","hidden","number","otp"]);class f{field=u.required(...ngDevMode?[{debugName:"field"}]:[]);form=u.required(...ngDevMode?[{debugName:"form"}]:[]);locale=u("ar",...ngDevMode?[{debugName:"locale"}]:[]);required=u(!1,{...ngDevMode?{debugName:"required"}:{},transform:ye});error=u("",...ngDevMode?[{debugName:"error"}]:[]);runtimeOptions=u([],...ngDevMode?[{debugName:"runtimeOptions"}]:[]);optionsLoaded=S();lookup=s(D,{optional:!0});destroyRef=s(E);loadedOptions=C([],...ngDevMode?[{debugName:"loadedOptions"}]:[]);controlId=`dga-df-${Math.random().toString(36).slice(2,9)}`;get control(){return this.form().controls[this.field().name]}labelText=d(()=>b(this.field().label,this.locale()),...ngDevMode?[{debugName:"labelText"}]:[]);hintText=d(()=>b(this.field().hint,this.locale()),...ngDevMode?[{debugName:"hintText"}]:[]);placeholderText=d(()=>b(this.field().placeholder,this.locale()),...ngDevMode?[{debugName:"placeholderText"}]:[]);resolvedOptions=d(()=>{const e=this.runtimeOptions();if(e.length)return e;const t=this.loadedOptions();return t.length?t:this.field().options??[]},...ngDevMode?[{debugName:"resolvedOptions"}]:[]);selectOptions=d(()=>this.resolvedOptions().map(e=>({value:String(e.value),label:this.optionLabel(e),disabled:!!e.disabled})),...ngDevMode?[{debugName:"selectOptions"}]:[]);ngOnInit(){const e=this.field();e.lookupDomain&&e.lookupName&&this.lookup&&!e.searchLookup&&this.lookup.lookup({domain:e.lookupDomain,name:e.lookupName}).pipe(_(this.destroyRef)).subscribe(t=>{const o=this.applyLookupFilter(t);this.loadedOptions.set(o),this.optionsLoaded.emit({name:e.name,options:o})})}optionLabel(e){return b(e.label,this.locale())}isChecked(e){const t=this.control?.value;return Array.isArray(t)&&t.some(o=>o==e)}toggleCheckbox(e,t){const o=Array.isArray(this.control.value)?[...this.control.value]:[],r=t?o.some(n=>n==e)?o:[...o,e]:o.filter(n=>n!=e);this.control.setValue(r),this.control.markAsDirty()}onSelectSearch(e){const t=this.field();!t.searchLookup||!this.lookup||!t.lookupDomain||t.lookupName}onRestrict(e){const t=this.field().inputRestriction;if(!t)return;const o=e.target;let r=o.value;t==="numbers"&&(r=r.replace(/\D+/g,"")),t==="english"&&(r=r.replace(/[^a-zA-Z0-9\s.,\-_/]/g,"")),t==="arabic"&&(r=r.replace(/[^\u0600-\u06FF0-9\s.,\-_/]/g,"")),r!==o.value&&(o.value=r,this.control.setValue(r))}applyLookupFilter(e){const t=this.field().lookupFilter;return t?e.filter(o=>o[t.property]===t.value):e}static \u0275fac=i.\u0275\u0275ngDeclareFactory({minVersion:"12.0.0",version:"22.0.7",ngImport:i,type:f,deps:[],target:i.\u0275\u0275FactoryTarget.Component});static \u0275cmp=i.\u0275\u0275ngDeclareComponent({minVersion:"17.0.0",version:"22.0.7",type:f,isStandalone:!0,selector:"dga-dynamic-form-field",inputs:{field:{classPropertyName:"field",publicName:"field",isSignal:!0,isRequired:!0,transformFunction:null},form:{classPropertyName:"form",publicName:"form",isSignal:!0,isRequired:!0,transformFunction:null},locale:{classPropertyName:"locale",publicName:"locale",isSignal:!0,isRequired:!1,transformFunction:null},required:{classPropertyName:"required",publicName:"required",isSignal:!0,isRequired:!1,transformFunction:null},error:{classPropertyName:"error",publicName:"error",isSignal:!0,isRequired:!1,transformFunction:null},runtimeOptions:{classPropertyName:"runtimeOptions",publicName:"runtimeOptions",isSignal:!0,isRequired:!1,transformFunction:null}},outputs:{optionsLoaded:"optionsLoaded"},host:{properties:{"class.hidden":'field().type === "hidden"'},classAttribute:"block w-full"},ngImport:i,template:`
2
+ @if (field().type !== 'hidden') {
3
+ <dga-field
4
+ [label]="labelText()"
5
+ [hint]="hintText()"
6
+ [error]="error()"
7
+ [required]="required()"
8
+ [controlId]="controlId"
9
+ [status]="error() ? 'error' : 'default'"
10
+ >
11
+ @switch (field().type) {
12
+ @case ('textarea') {
13
+ <textarea
14
+ dgaInput
15
+ [id]="controlId"
16
+ [attr.rows]="field().rows || 4"
17
+ [attr.placeholder]="placeholderText()"
18
+ [attr.maxlength]="field().maxLength || null"
19
+ [readonly]="field().readonly || null"
20
+ [formControl]="control"
21
+ [dgaInvalid]="!!error()"
22
+ ></textarea>
23
+ }
24
+ @case ('email') {
25
+ <input
26
+ dgaInput
27
+ type="email"
28
+ [id]="controlId"
29
+ [attr.placeholder]="placeholderText()"
30
+ [attr.maxlength]="field().maxLength || null"
31
+ [readonly]="field().readonly || null"
32
+ [formControl]="control"
33
+ [dgaInvalid]="!!error()"
34
+ (input)="onRestrict($event)"
35
+ />
36
+ }
37
+ @case ('number') {
38
+ <dga-number-input
39
+ [formControl]="control"
40
+ [disabled]="!!field().disabled"
41
+ />
42
+ }
43
+ @case ('otp') {
44
+ <dga-otp [formControl]="control" />
45
+ }
46
+ @case ('phone') {
47
+ <dga-phone-input
48
+ [formControl]="control"
49
+ [placeholder]="placeholderText()"
50
+ [invalid]="!!error()"
51
+ [disabled]="!!field().disabled"
52
+ [required]="required()"
53
+ [controlId]="controlId"
54
+ />
55
+ }
56
+ @case ('date') {
57
+ <dga-date-picker
58
+ [formControl]="control"
59
+ [placeholder]="placeholderText()"
60
+ [dgaInvalid]="!!error()"
61
+ [disabled]="!!field().disabled"
62
+ [locale]="locale()"
63
+ [mode]="field().monthPicker ? 'month' : 'day'"
64
+ />
65
+ }
66
+ @case ('chips') {
67
+ <dga-chip-input
68
+ [formControl]="control"
69
+ [placeholder]="placeholderText()"
70
+ [dgaInvalid]="!!error()"
71
+ [disabled]="!!field().disabled"
72
+ [label]="labelText()"
73
+ />
74
+ }
75
+ @case ('toggle') {
76
+ <dga-switch
77
+ [formControl]="control"
78
+ [label]="labelText()"
79
+ [disabled]="!!field().disabled"
80
+ />
81
+ }
82
+ @case ('file') {
83
+ <dga-upload
84
+ [formControl]="control"
85
+ [accept]="field().acceptedFileTypes || ''"
86
+ [multiple]="(field().maxFiles ?? 1) > 1"
87
+ [maxSize]="field().maxFileSize || 0"
88
+ [disabled]="!!field().disabled"
89
+ />
90
+ }
91
+ @case ('select') {
92
+ <dga-select
93
+ [formControl]="control"
94
+ [options]="selectOptions()"
95
+ [searchable]="!!field().searchLookup || (selectOptions().length > 8)"
96
+ [placeholder]="placeholderText()"
97
+ [label]="labelText()"
98
+ [dgaInvalid]="!!error()"
99
+ [disabled]="!!field().disabled"
100
+ (valueChange)="onSelectSearch($event)"
101
+ />
102
+ }
103
+ @case ('multiselect') {
104
+ <dga-select
105
+ multiple
106
+ [formControl]="control"
107
+ [options]="selectOptions()"
108
+ [searchable]="true"
109
+ [placeholder]="placeholderText()"
110
+ [label]="labelText()"
111
+ [dgaInvalid]="!!error()"
112
+ [disabled]="!!field().disabled"
113
+ />
114
+ }
115
+ @case ('radio') {
116
+ <div
117
+ class="flex flex-wrap gap-lg"
118
+ [class.flex-col]="field().optionsLayout !== 'grid'"
119
+ role="radiogroup"
120
+ [attr.aria-labelledby]="controlId"
121
+ >
122
+ @for (opt of resolvedOptions(); track opt.value) {
123
+ <label class="inline-flex items-center gap-sm text-body-sm text-fg">
124
+ <input
125
+ type="radio"
126
+ dgaRadio
127
+ [name]="field().name"
128
+ [value]="opt.value"
129
+ [formControl]="control"
130
+ [dgaInvalid]="!!error()"
131
+ />
132
+ {{ optionLabel(opt) }}
133
+ </label>
134
+ }
135
+ </div>
136
+ }
137
+ @case ('checkbox') {
138
+ <div
139
+ class="flex flex-wrap gap-lg"
140
+ [class.flex-col]="field().optionsLayout !== 'grid'"
141
+ >
142
+ @for (opt of resolvedOptions(); track opt.value) {
143
+ <label class="inline-flex items-center gap-sm text-body-sm text-fg">
144
+ <input
145
+ type="checkbox"
146
+ dgaCheckbox
147
+ [checked]="isChecked(opt.value)"
148
+ [disabled]="!!field().disabled"
149
+ [dgaInvalid]="!!error()"
150
+ (change)="toggleCheckbox(opt.value, $any($event.target).checked)"
151
+ />
152
+ {{ optionLabel(opt) }}
153
+ </label>
154
+ }
155
+ </div>
156
+ }
157
+ @default {
158
+ <input
159
+ dgaInput
160
+ type="text"
161
+ [id]="controlId"
162
+ [attr.placeholder]="placeholderText()"
163
+ [attr.maxlength]="field().maxLength || null"
164
+ [readonly]="field().readonly || null"
165
+ [formControl]="control"
166
+ [dgaInvalid]="!!error()"
167
+ (input)="onRestrict($event)"
168
+ />
169
+ }
170
+ }
171
+ </dga-field>
172
+ }
173
+ `,isInline:!0,dependencies:[{kind:"ngmodule",type:k},{kind:"directive",type:p.DefaultValueAccessor,selector:"input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]"},{kind:"directive",type:p.RadioControlValueAccessor,selector:"input[type=radio]:not([ngNoCva])[formControlName],input[type=radio]:not([ngNoCva])[formControl],input[type=radio]:not([ngNoCva])[ngModel]",inputs:["name","formControlName","value"]},{kind:"directive",type:p.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{kind:"directive",type:p.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{kind:"directive",type:p.FormControlDirective,selector:"[formControl]",inputs:["formControl","disabled","ngModel"],outputs:["ngModelChange"],exportAs:["ngForm"]},{kind:"ngmodule",type:G},{kind:"component",type:j,selector:"dga-field",inputs:["label","labelHint","hint","error","status","statusMessage","controlId","required"]},{kind:"directive",type:W,selector:"input[dgaInput], textarea[dgaInput]",inputs:["size","dgaInvalid","status"]},{kind:"component",type:H,selector:"dga-select",inputs:["options","value","multiple","searchable","openMode","clearable","size","status","placeholder","label","removeLabel","clearLabel","emptyLabel","disabled","dgaInvalid"],outputs:["valueChange"]},{kind:"directive",type:K,selector:'input[type="checkbox"][dgaCheckbox]',inputs:["size","dgaInvalid"]},{kind:"directive",type:U,selector:'input[type="radio"][dgaRadio]',inputs:["size","dgaInvalid"]},{kind:"component",type:Y,selector:"dga-phone-input",inputs:["countries","countryCode","nationalNumber","controlId","name","placeholder","autocomplete","codeLabel","disabled","required","invalid","value"],outputs:["countryCodeChange","nationalNumberChange","valueChange"]},{kind:"component",type:J,selector:"dga-date-picker",inputs:["value","startValue","endValue","calendar","range","format","mode","locale","minDate","maxDate","yearBefore","yearAfter","placeholder","controlId","name","size","disabled","required","clearable","editable","dgaInvalid","status","rangeSeparator","todayLabel","clearLabel","closeLabel","saveLabel","ariaLabel"],outputs:["startValueChange","endValueChange","valueChange","rangeChange"]},{kind:"component",type:X,selector:"dga-chip-input",inputs:["value","placeholder","label","removeLabel","disabled","dgaInvalid","status"],outputs:["valueChange"]},{kind:"component",type:Z,selector:"dga-switch",inputs:["checked","size","label","disabled"],outputs:["checkedChange","changed"]},{kind:"component",type:Q,selector:"dga-upload",inputs:["files","accept","multiple","appearance","hint","browseLabel","removeLabel","disabled","maxSize","dragging"],outputs:["filesChange","rejected","draggingChange"]},{kind:"component",type:ee,selector:"dga-number-input",inputs:["value","min","max","step","size","label","controlId","name","incrementLabel","decrementLabel","disabled","dgaInvalid","status"],outputs:["valueChange"]},{kind:"component",type:te,selector:"dga-otp",inputs:["length","value","label","alphanumeric","disabled","dgaInvalid"],outputs:["valueChange","completed"]}],changeDetection:i.ChangeDetectionStrategy.OnPush})}i.\u0275\u0275ngDeclareClassMetadata({minVersion:"12.0.0",version:"22.0.7",ngImport:i,type:f,decorators:[{type:z,args:[{selector:"dga-dynamic-form-field",standalone:!0,imports:[k,G,j,W,H,K,U,Y,J,X,Z,Q,ee,te],template:`
174
+ @if (field().type !== 'hidden') {
175
+ <dga-field
176
+ [label]="labelText()"
177
+ [hint]="hintText()"
178
+ [error]="error()"
179
+ [required]="required()"
180
+ [controlId]="controlId"
181
+ [status]="error() ? 'error' : 'default'"
182
+ >
183
+ @switch (field().type) {
184
+ @case ('textarea') {
185
+ <textarea
186
+ dgaInput
187
+ [id]="controlId"
188
+ [attr.rows]="field().rows || 4"
189
+ [attr.placeholder]="placeholderText()"
190
+ [attr.maxlength]="field().maxLength || null"
191
+ [readonly]="field().readonly || null"
192
+ [formControl]="control"
193
+ [dgaInvalid]="!!error()"
194
+ ></textarea>
195
+ }
196
+ @case ('email') {
197
+ <input
198
+ dgaInput
199
+ type="email"
200
+ [id]="controlId"
201
+ [attr.placeholder]="placeholderText()"
202
+ [attr.maxlength]="field().maxLength || null"
203
+ [readonly]="field().readonly || null"
204
+ [formControl]="control"
205
+ [dgaInvalid]="!!error()"
206
+ (input)="onRestrict($event)"
207
+ />
208
+ }
209
+ @case ('number') {
210
+ <dga-number-input
211
+ [formControl]="control"
212
+ [disabled]="!!field().disabled"
213
+ />
214
+ }
215
+ @case ('otp') {
216
+ <dga-otp [formControl]="control" />
217
+ }
218
+ @case ('phone') {
219
+ <dga-phone-input
220
+ [formControl]="control"
221
+ [placeholder]="placeholderText()"
222
+ [invalid]="!!error()"
223
+ [disabled]="!!field().disabled"
224
+ [required]="required()"
225
+ [controlId]="controlId"
226
+ />
227
+ }
228
+ @case ('date') {
229
+ <dga-date-picker
230
+ [formControl]="control"
231
+ [placeholder]="placeholderText()"
232
+ [dgaInvalid]="!!error()"
233
+ [disabled]="!!field().disabled"
234
+ [locale]="locale()"
235
+ [mode]="field().monthPicker ? 'month' : 'day'"
236
+ />
237
+ }
238
+ @case ('chips') {
239
+ <dga-chip-input
240
+ [formControl]="control"
241
+ [placeholder]="placeholderText()"
242
+ [dgaInvalid]="!!error()"
243
+ [disabled]="!!field().disabled"
244
+ [label]="labelText()"
245
+ />
246
+ }
247
+ @case ('toggle') {
248
+ <dga-switch
249
+ [formControl]="control"
250
+ [label]="labelText()"
251
+ [disabled]="!!field().disabled"
252
+ />
253
+ }
254
+ @case ('file') {
255
+ <dga-upload
256
+ [formControl]="control"
257
+ [accept]="field().acceptedFileTypes || ''"
258
+ [multiple]="(field().maxFiles ?? 1) > 1"
259
+ [maxSize]="field().maxFileSize || 0"
260
+ [disabled]="!!field().disabled"
261
+ />
262
+ }
263
+ @case ('select') {
264
+ <dga-select
265
+ [formControl]="control"
266
+ [options]="selectOptions()"
267
+ [searchable]="!!field().searchLookup || (selectOptions().length > 8)"
268
+ [placeholder]="placeholderText()"
269
+ [label]="labelText()"
270
+ [dgaInvalid]="!!error()"
271
+ [disabled]="!!field().disabled"
272
+ (valueChange)="onSelectSearch($event)"
273
+ />
274
+ }
275
+ @case ('multiselect') {
276
+ <dga-select
277
+ multiple
278
+ [formControl]="control"
279
+ [options]="selectOptions()"
280
+ [searchable]="true"
281
+ [placeholder]="placeholderText()"
282
+ [label]="labelText()"
283
+ [dgaInvalid]="!!error()"
284
+ [disabled]="!!field().disabled"
285
+ />
286
+ }
287
+ @case ('radio') {
288
+ <div
289
+ class="flex flex-wrap gap-lg"
290
+ [class.flex-col]="field().optionsLayout !== 'grid'"
291
+ role="radiogroup"
292
+ [attr.aria-labelledby]="controlId"
293
+ >
294
+ @for (opt of resolvedOptions(); track opt.value) {
295
+ <label class="inline-flex items-center gap-sm text-body-sm text-fg">
296
+ <input
297
+ type="radio"
298
+ dgaRadio
299
+ [name]="field().name"
300
+ [value]="opt.value"
301
+ [formControl]="control"
302
+ [dgaInvalid]="!!error()"
303
+ />
304
+ {{ optionLabel(opt) }}
305
+ </label>
306
+ }
307
+ </div>
308
+ }
309
+ @case ('checkbox') {
310
+ <div
311
+ class="flex flex-wrap gap-lg"
312
+ [class.flex-col]="field().optionsLayout !== 'grid'"
313
+ >
314
+ @for (opt of resolvedOptions(); track opt.value) {
315
+ <label class="inline-flex items-center gap-sm text-body-sm text-fg">
316
+ <input
317
+ type="checkbox"
318
+ dgaCheckbox
319
+ [checked]="isChecked(opt.value)"
320
+ [disabled]="!!field().disabled"
321
+ [dgaInvalid]="!!error()"
322
+ (change)="toggleCheckbox(opt.value, $any($event.target).checked)"
323
+ />
324
+ {{ optionLabel(opt) }}
325
+ </label>
326
+ }
327
+ </div>
328
+ }
329
+ @default {
330
+ <input
331
+ dgaInput
332
+ type="text"
333
+ [id]="controlId"
334
+ [attr.placeholder]="placeholderText()"
335
+ [attr.maxlength]="field().maxLength || null"
336
+ [readonly]="field().readonly || null"
337
+ [formControl]="control"
338
+ [dgaInvalid]="!!error()"
339
+ (input)="onRestrict($event)"
340
+ />
341
+ }
342
+ }
343
+ </dga-field>
344
+ }
345
+ `,changeDetection:q.OnPush,host:{class:"block w-full","[class.hidden]":'field().type === "hidden"'}}]}],propDecorators:{field:[{type:i.Input,args:[{isSignal:!0,alias:"field",required:!0}]}],form:[{type:i.Input,args:[{isSignal:!0,alias:"form",required:!0}]}],locale:[{type:i.Input,args:[{isSignal:!0,alias:"locale",required:!1}]}],required:[{type:i.Input,args:[{isSignal:!0,alias:"required",required:!1}]}],error:[{type:i.Input,args:[{isSignal:!0,alias:"error",required:!1}]}],runtimeOptions:[{type:i.Input,args:[{isSignal:!0,alias:"runtimeOptions",required:!1}]}],optionsLoaded:[{type:i.Output,args:["optionsLoaded"]}]}});const we={1:"col-span-1",2:"col-span-2",3:"col-span-3",4:"col-span-4",5:"col-span-5",6:"col-span-6",7:"col-span-7",8:"col-span-8",9:"col-span-9",10:"col-span-10",11:"col-span-11",12:"col-span-12"},Me={1:"md:col-span-1",2:"md:col-span-2",3:"md:col-span-3",4:"md:col-span-4",5:"md:col-span-5",6:"md:col-span-6",7:"md:col-span-7",8:"md:col-span-8",9:"md:col-span-9",10:"md:col-span-10",11:"md:col-span-11",12:"md:col-span-12"},Ve={1:"lg:col-span-1",2:"lg:col-span-2",3:"lg:col-span-3",4:"lg:col-span-4",5:"lg:col-span-5",6:"lg:col-span-6",7:"lg:col-span-7",8:"lg:col-span-8",9:"lg:col-span-9",10:"lg:col-span-10",11:"lg:col-span-11",12:"lg:col-span-12"};function R(a){return Number.isFinite(a)?Math.min(12,Math.max(1,Math.round(a))):12}class I{config=u.required(...ngDevMode?[{debugName:"config"}]:[]);localeInput=u(null,{...ngDevMode?{debugName:"localeInput"}:{},alias:"locale"});formSubmitted=S();formError=S();clearButtonClick=S();draftSaved=S();fb=s(Ce);formService=s(v);toast=s(L,{optional:!0});i18n=s(N,{optional:!0});destroyRef=s(E);registry=s(y);form;activeStep=C(0,...ngDevMode?[{debugName:"activeStep"}]:[]);submitting=C(!1,...ngDevMode?[{debugName:"submitting"}]:[]);optionsMap=C({},...ngDevMode?[{debugName:"optionsMap"}]:[]);formValues=C({},...ngDevMode?[{debugName:"formValues"}]:[]);locale=d(()=>this.localeInput()??pe(),...ngDevMode?[{debugName:"locale"}]:[]);wizardEnabled=d(()=>!!this.config().wizard?.enabled&&!!this.config().wizard?.steps?.length,...ngDevMode?[{debugName:"wizardEnabled"}]:[]);allFields=d(()=>ce(this.config().fields,this.config().wizard?.steps),...ngDevMode?[{debugName:"allFields"}]:[]);currentStepFields=d(()=>this.wizardEnabled()?this.config().wizard.steps[this.activeStep()]?.fields??[]:this.config().fields,...ngDevMode?[{debugName:"currentStepFields"}]:[]);visibleFields=d(()=>{const e=this.formValues(),t=this.optionsMap();return this.currentStepFields().filter(o=>M(o,e,r=>t[r]))},...ngDevMode?[{debugName:"visibleFields"}]:[]);stepperSteps=d(()=>(this.config().wizard?.steps??[]).map(t=>({label:this.labelOf(t.title),description:t.description?this.labelOf(t.description):void 0})),...ngDevMode?[{debugName:"stepperSteps"}]:[]);descriptionText=d(()=>this.labelOf(this.config().description),...ngDevMode?[{debugName:"descriptionText"}]:[]);constructor(){xe(()=>{const e=this.formValues(),t=this.optionsMap();if(this.form)for(const o of this.allFields()){const r=this.form.get(o.name);if(!r||!o.requiredWhen)continue;const n=V(o,e,l=>t[l]);ue(r,o,n)}})}ngOnInit(){this.form=de(this.fb,this.allFields());const e=this.config().formId;if(e&&this.config().enableLocalStorageSave){const t=this.formService.loadDraft(e);t&&this.form.patchValue(t,{emitEvent:!1})}this.form.valueChanges.pipe(ke(this.form.getRawValue()),_(this.destroyRef)).subscribe(t=>this.formValues.set(t))}labelOf(e){return e?this.i18n?this.i18n.resolveLabel(e,this.locale()):b(e,this.locale()):""}submitLabel(){const e=this.config();return this.wizardEnabled()?this.labelOf(e.wizard?.submitButtonText??e.submitButtonLabel)||(this.locale()==="en"?"Submit":"\u0625\u0631\u0633\u0627\u0644"):this.labelOf(e.submitButtonLabel)||(this.locale()==="en"?"Submit":"\u0625\u0631\u0633\u0627\u0644")}nextLabel(){return this.labelOf(this.config().wizard?.nextButtonText)||(this.locale()==="en"?"Next":"\u0627\u0644\u062A\u0627\u0644\u064A")}previousLabel(){return this.labelOf(this.config().wizard?.previousButtonText)||(this.locale()==="en"?"Previous":"\u0627\u0644\u0633\u0627\u0628\u0642")}clearLabel(){return this.labelOf(this.config().clearButtonLabel)}saveLabel(){return this.labelOf(this.config().saveButtonLabel??this.config().wizard?.saveButtonText)||(this.locale()==="en"?"Save":"\u062D\u0641\u0638")}showSave(){const e=this.config();return!!(e.enableLocalStorageSave||e.wizard?.showSaveButton)}isLastStep(){return this.wizardEnabled()?this.activeStep()>=this.config().wizard.steps.length-1:!0}isRequired(e){return V(e,this.formValues(),t=>this.optionsMap()[t])}fieldError(e){const t=this.form?.get(e.name);if(!t||!(t.touched||t.dirty)||!t.errors)return"";const o=t.errors,r=e.validation?.errorMessages,n=this.locale(),l=x=>r?.[x]?this.labelOf(r[x]):"";return o.required?l("required")||(n==="en"?"Required":"\u0645\u0637\u0644\u0648\u0628"):o.email?l("email")||(n==="en"?"Invalid email":"\u0628\u0631\u064A\u062F \u063A\u064A\u0631 \u0635\u0627\u0644\u062D"):o.minlength?l("minLength")||(n==="en"?"Too short":"\u0642\u0635\u064A\u0631 \u062C\u062F\u0627\u064B"):o.maxlength?l("maxLength")||(n==="en"?"Too long":"\u0637\u0648\u064A\u0644 \u062C\u062F\u0627\u064B"):o.pattern?l("pattern")||(n==="en"?"Invalid format":"\u0635\u064A\u063A\u0629 \u063A\u064A\u0631 \u0635\u0627\u0644\u062D\u0629"):o.maxChips?l("maxChips")||(n==="en"?"Too many items":"\u0639\u062F\u062F \u0627\u0644\u0639\u0646\u0627\u0635\u0631 \u0643\u0628\u064A\u0631"):n==="en"?"Invalid":"\u063A\u064A\u0631 \u0635\u0627\u0644\u062D"}columnClass(e){const t=e.columns,o=R(t?.sm??t?.md??12),r=R(t?.md??o),n=R(t?.lg??r);return["w-full",we[o],Me[r],Ve[n]].join(" ")}customComponent(e){return this.registry.hasCustom(e.type)?this.registry.get(e.type):null}customInputs(e){return{field:e,form:this.form,locale:this.locale(),required:this.isRequired(e),error:this.fieldError(e)}}onOptionsLoaded(e){this.optionsMap.update(t=>({...t,[e.name]:e.options}))}previousStep(){this.activeStep()>0&&this.activeStep.update(e=>e-1)}nextStep(){this.config().wizard?.validateOnStepChange!==!1&&(this.markStepTouched(),!this.stepValid())||this.isLastStep()||this.activeStep.update(t=>t+1)}onClear(){this.form.reset();for(const t of this.allFields()){const o=this.form.get(t.name);o&&o.setValue(t.value!==void 0?t.value:["checkbox","multiselect","chips","file"].includes(t.type)?[]:t.type==="toggle"?!1:"")}this.activeStep.set(0);const e=this.config().formId;e&&this.formService.clearDraft(e),this.clearButtonClick.emit()}onSaveDraft(){const e=this.form.getRawValue(),t=this.config().formId;t&&this.formService.saveDraft(t,e),this.draftSaved.emit(e),this.toast?.show({variant:"success",message:this.locale()==="en"?"Draft saved":"\u062A\u0645 \u062D\u0641\u0638 \u0627\u0644\u0645\u0633\u0648\u062F\u0629"})}onSubmit(){if(this.wizardEnabled()&&!this.isLastStep()){this.nextStep();return}if(this.markVisibleTouched(),!this.visibleValid()){this.toast?.show({variant:"error",message:this.locale()==="en"?"Please fix the highlighted fields":"\u064A\u0631\u062C\u0649 \u062A\u0635\u062D\u064A\u062D \u0627\u0644\u062D\u0642\u0648\u0644 \u0627\u0644\u0645\u062D\u062F\u062F\u0629"});return}const e=this.config(),t=this.form.getRawValue(),o=new Set(this.collectVisibleFieldNames()),r={};for(const n of this.allFields())!o.has(n.name)&&n.type!=="hidden"||(r[n.name]=t[n.name]);if(e.emitOnly||!e.endpoint){this.formSubmitted.emit(r),this.toast?.show({variant:"success",message:this.labelOf(e.successMessage)||(this.locale()==="en"?"Submitted":"\u062A\u0645 \u0627\u0644\u0625\u0631\u0633\u0627\u0644")});return}this.submitting.set(!0),this.formService.submitForm(e,r,this.allFields()).subscribe({next:n=>{this.submitting.set(!1),this.formSubmitted.emit(n),e.formId&&this.formService.clearDraft(e.formId),this.toast?.show({variant:"success",message:this.labelOf(e.successMessage)||(this.locale()==="en"?"Submitted successfully":"\u062A\u0645 \u0627\u0644\u0625\u0631\u0633\u0627\u0644 \u0628\u0646\u062C\u0627\u062D")})},error:n=>{this.submitting.set(!1),this.formError.emit(n),this.toast?.show({variant:"error",message:this.labelOf(e.errorMessage)||(this.locale()==="en"?"Submission failed":"\u0641\u0634\u0644 \u0627\u0644\u0625\u0631\u0633\u0627\u0644")})}})}patchFormValues(e){this.form.patchValue(e)}getForm(){return this.form}markStepTouched(){for(const e of this.visibleFields())this.form.get(e.name)?.markAsTouched()}markVisibleTouched(){for(const e of this.collectVisibleFieldNames())this.form.get(e)?.markAsTouched()}stepValid(){return this.visibleFields().every(e=>{const t=this.form.get(e.name);return!t||t.disabled||t.valid})}visibleValid(){return this.collectVisibleFieldNames().every(e=>{const t=this.form.get(e);return!t||t.disabled||t.valid})}collectVisibleFieldNames(){const e=this.formValues(),t=this.optionsMap();return(this.wizardEnabled()?this.allFields():this.config().fields).filter(r=>r.type==="hidden"||M(r,e,n=>t[n])).map(r=>r.name)}static \u0275fac=i.\u0275\u0275ngDeclareFactory({minVersion:"12.0.0",version:"22.0.7",ngImport:i,type:I,deps:[],target:i.\u0275\u0275FactoryTarget.Component});static \u0275cmp=i.\u0275\u0275ngDeclareComponent({minVersion:"17.0.0",version:"22.0.7",type:I,isStandalone:!0,selector:"dga-dynamic-form",inputs:{config:{classPropertyName:"config",publicName:"config",isSignal:!0,isRequired:!0,transformFunction:null},localeInput:{classPropertyName:"localeInput",publicName:"locale",isSignal:!0,isRequired:!1,transformFunction:null}},outputs:{formSubmitted:"formSubmitted",formError:"formError",clearButtonClick:"clearButtonClick",draftSaved:"draftSaved"},host:{classAttribute:"block w-full"},ngImport:i,template:`
346
+ <form class="flex w-full flex-col gap-3xl" [formGroup]="form" (ngSubmit)="onSubmit()">
347
+ @if (descriptionText()) {
348
+ <p class="text-body-md text-paragraph">{{ descriptionText() }}</p>
349
+ }
350
+
351
+ @if (wizardEnabled()) {
352
+ <dga-stepper
353
+ [steps]="stepperSteps()"
354
+ [(active)]="activeStep"
355
+ [label]="locale() === 'en' ? 'Steps' : '\u0627\u0644\u062E\u0637\u0648\u0627\u062A'"
356
+ />
357
+ }
358
+
359
+ <div class="grid grid-cols-12 gap-xl">
360
+ @for (field of visibleFields(); track field.name) {
361
+ <div [class]="columnClass(field)">
362
+ @if (customComponent(field); as cmp) {
363
+ <ng-container *ngComponentOutlet="cmp; inputs: customInputs(field)" />
364
+ } @else {
365
+ <dga-dynamic-form-field
366
+ [field]="field"
367
+ [form]="form"
368
+ [locale]="locale()"
369
+ [required]="isRequired(field)"
370
+ [error]="fieldError(field)"
371
+ [runtimeOptions]="optionsMap()[field.name] || []"
372
+ (optionsLoaded)="onOptionsLoaded($event)"
373
+ />
374
+ }
375
+ </div>
376
+ }
377
+ </div>
378
+
379
+ <div class="flex flex-wrap items-center gap-md">
380
+ @if (wizardEnabled() && activeStep() > 0) {
381
+ <button
382
+ dgaButton
383
+ type="button"
384
+ variant="secondary"
385
+ (click)="previousStep()"
386
+ >
387
+ {{ previousLabel() }}
388
+ </button>
389
+ }
390
+
391
+ @if (wizardEnabled() && !isLastStep()) {
392
+ <button dgaButton type="button" (click)="nextStep()">
393
+ {{ nextLabel() }}
394
+ </button>
395
+ } @else {
396
+ <button dgaButton type="submit" [disabled]="submitting()">
397
+ {{ submitLabel() }}
398
+ </button>
399
+ }
400
+
401
+ @if (config().clearButtonLabel) {
402
+ <button
403
+ dgaButton
404
+ type="button"
405
+ variant="ghost"
406
+ (click)="onClear()"
407
+ >
408
+ {{ clearLabel() }}
409
+ </button>
410
+ }
411
+
412
+ @if (showSave()) {
413
+ <button
414
+ dgaButton
415
+ type="button"
416
+ variant="secondary"
417
+ (click)="onSaveDraft()"
418
+ >
419
+ {{ saveLabel() }}
420
+ </button>
421
+ }
422
+ </div>
423
+ </form>
424
+ `,isInline:!0,dependencies:[{kind:"ngmodule",type:k},{kind:"directive",type:p.\u0275NgNoValidate,selector:"form:not([ngNoForm]):not([ngNativeValidate])"},{kind:"directive",type:p.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]"},{kind:"directive",type:p.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{kind:"component",type:f,selector:"dga-dynamic-form-field",inputs:["field","form","locale","required","error","runtimeOptions"],outputs:["optionsLoaded"]},{kind:"directive",type:oe,selector:"button[dgaButton], a[dgaButton]",inputs:["variant","size","iconOnly","cooldownMs","loading"]},{kind:"component",type:ae,selector:"dga-stepper",inputs:["steps","active","label"],outputs:["activeChange"]},{kind:"directive",type:B,selector:"[ngComponentOutlet]",inputs:["ngComponentOutlet","ngComponentOutletInputs","ngComponentOutletInjector","ngComponentOutletEnvironmentInjector","ngComponentOutletContent","ngComponentOutletNgModule"],exportAs:["ngComponentOutlet"]}],changeDetection:i.ChangeDetectionStrategy.OnPush})}i.\u0275\u0275ngDeclareClassMetadata({minVersion:"12.0.0",version:"22.0.7",ngImport:i,type:I,decorators:[{type:z,args:[{selector:"dga-dynamic-form",standalone:!0,imports:[k,f,oe,ae,B],template:`
425
+ <form class="flex w-full flex-col gap-3xl" [formGroup]="form" (ngSubmit)="onSubmit()">
426
+ @if (descriptionText()) {
427
+ <p class="text-body-md text-paragraph">{{ descriptionText() }}</p>
428
+ }
429
+
430
+ @if (wizardEnabled()) {
431
+ <dga-stepper
432
+ [steps]="stepperSteps()"
433
+ [(active)]="activeStep"
434
+ [label]="locale() === 'en' ? 'Steps' : '\u0627\u0644\u062E\u0637\u0648\u0627\u062A'"
435
+ />
436
+ }
437
+
438
+ <div class="grid grid-cols-12 gap-xl">
439
+ @for (field of visibleFields(); track field.name) {
440
+ <div [class]="columnClass(field)">
441
+ @if (customComponent(field); as cmp) {
442
+ <ng-container *ngComponentOutlet="cmp; inputs: customInputs(field)" />
443
+ } @else {
444
+ <dga-dynamic-form-field
445
+ [field]="field"
446
+ [form]="form"
447
+ [locale]="locale()"
448
+ [required]="isRequired(field)"
449
+ [error]="fieldError(field)"
450
+ [runtimeOptions]="optionsMap()[field.name] || []"
451
+ (optionsLoaded)="onOptionsLoaded($event)"
452
+ />
453
+ }
454
+ </div>
455
+ }
456
+ </div>
457
+
458
+ <div class="flex flex-wrap items-center gap-md">
459
+ @if (wizardEnabled() && activeStep() > 0) {
460
+ <button
461
+ dgaButton
462
+ type="button"
463
+ variant="secondary"
464
+ (click)="previousStep()"
465
+ >
466
+ {{ previousLabel() }}
467
+ </button>
468
+ }
469
+
470
+ @if (wizardEnabled() && !isLastStep()) {
471
+ <button dgaButton type="button" (click)="nextStep()">
472
+ {{ nextLabel() }}
473
+ </button>
474
+ } @else {
475
+ <button dgaButton type="submit" [disabled]="submitting()">
476
+ {{ submitLabel() }}
477
+ </button>
478
+ }
479
+
480
+ @if (config().clearButtonLabel) {
481
+ <button
482
+ dgaButton
483
+ type="button"
484
+ variant="ghost"
485
+ (click)="onClear()"
486
+ >
487
+ {{ clearLabel() }}
488
+ </button>
489
+ }
490
+
491
+ @if (showSave()) {
492
+ <button
493
+ dgaButton
494
+ type="button"
495
+ variant="secondary"
496
+ (click)="onSaveDraft()"
497
+ >
498
+ {{ saveLabel() }}
499
+ </button>
500
+ }
501
+ </div>
502
+ </form>
503
+ `,changeDetection:q.OnPush,host:{class:"block w-full"}}]}],ctorParameters:()=>[],propDecorators:{config:[{type:i.Input,args:[{isSignal:!0,alias:"config",required:!0}]}],localeInput:[{type:i.Input,args:[{isSignal:!0,alias:"locale",required:!1}]}],formSubmitted:[{type:i.Output,args:["formSubmitted"]}],formError:[{type:i.Output,args:["formError"]}],clearButtonClick:[{type:i.Output,args:["clearButtonClick"]}],draftSaved:[{type:i.Output,args:["draftSaved"]}]}});const me={resolveLabel(a,e){return e==="en"?a.en:a.ar}},fe={getToken:()=>h(null)},ge={lookup:()=>h([])};function he(a=s(re)){return{lookup(e){const o=`${e.domain.replace(/\/$/,"")}/lookup/${e.name}`,r={};if(e.parentId!=null&&e.parentId!==""&&(r.parentId=String(e.parentId)),e.search){const n=e.searchParamName||"q";r[n]=e.search}return a.get(o,{params:r}).pipe(De(n=>Array.isArray(n)?n.map(Re):[]))}}}function Re(a){const e=a??{},t=e.value??e.id??e.Id??"",o=String(e.labelEn??e.titleEn??e.en??e.TitleEn??t),r=String(e.labelAr??e.titleAr??e.ar??e.TitleAr??o);return{value:t,label:{en:o,ar:r},...e}}function be(a=s(re)){return{submit(e){const t=new Le(e.headers??{}),o=e.method.toUpperCase();return o==="GET"?a.get(e.endpoint,{headers:t}):o==="DELETE"?a.delete(e.endpoint,{headers:t,body:e.body}):o==="PUT"?a.put(e.endpoint,e.body,{headers:t}):o==="PATCH"?a.patch(e.endpoint,e.body,{headers:t}):a.post(e.endpoint,e.body,{headers:t})}}}function ve(a=s(Oe)){return{show(e){a.show({title:e.title,message:e.message,variant:e.variant??"info"})}}}function Pe(a={}){const e=a.httpLookup??!1,t=a.httpSubmit??!0,o=a.dgaToast??!0;return Se([{provide:N,useValue:me},{provide:T,useValue:fe},{provide:D,useFactory:()=>e?he():ge},{provide:O,useFactory:()=>t?be():{submit:()=>h(null)}},{provide:L,useFactory:()=>o?ve():{show:()=>{}}}])}function Ee(a,e,t=""){return a?e==="en"?a.en:a.ar:t}export{T as DGA_CAPTCHA_ADAPTER,ne as DGA_DYNAMIC_FORM_FIELD_REGISTRY,N as DGA_FORM_I18N_ADAPTER,L as DGA_FORM_TOAST_ADAPTER,D as DGA_LOOKUP_ADAPTER,O as DGA_SUBMIT_ADAPTER,I as DgaDynamicForm,f as DgaDynamicFormField,y as DgaDynamicFormFieldRegistry,v as DgaDynamicFormService,ve as createDgaToastAdapter,he as createHttpLookupAdapter,be as createHttpSubmitAdapter,me as defaultFormI18nAdapter,de as dgaBuildFormGroup,A as dgaBuildValidators,ce as dgaCollectFields,se as dgaCreateControl,le as dgaDefaultValueForField,pe as dgaDetectLocale,w as dgaFindSelectedOption,M as dgaIsFieldVisible,V as dgaIsRequiredWhen,b as dgaResolveLabel,ue as dgaSetControlValidators,ge as emptyLookupAdapter,fe as noopCaptchaAdapter,Pe as provideDgaDynamicForm,Ee as resolveFormLabel};
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@a.nemreen/dga-dynamic-form",
3
+ "version": "0.1.0",
4
+ "description": "Schema-driven dynamic forms for Angular, rendered with @a.nemreen/dga-ui controls",
5
+ "license": "MIT",
6
+ "author": {
7
+ "name": "Ahmed Nemreen",
8
+ "email": "a.nemreen@gmail.com",
9
+ "url": "https://nemreen.info"
10
+ },
11
+ "homepage": "https://nemreen.info/dga/",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/nemreen/dga-angular.git",
15
+ "directory": "projects/dga-dynamic-form"
16
+ },
17
+ "bugs": {
18
+ "url": "https://github.com/nemreen/dga-angular/issues"
19
+ },
20
+ "keywords": [
21
+ "angular",
22
+ "dynamic-form",
23
+ "schema-form",
24
+ "reactive-forms",
25
+ "dga",
26
+ "design-system",
27
+ "rtl",
28
+ "arabic",
29
+ "saudi",
30
+ "government",
31
+ "forms"
32
+ ],
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "peerDependencies": {
37
+ "@angular/common": "^19.0.0 || ^20.0.0 || ^21.0.0 || ^22.0.0",
38
+ "@angular/core": "^19.0.0 || ^20.0.0 || ^21.0.0 || ^22.0.0",
39
+ "@angular/forms": "^19.0.0 || ^20.0.0 || ^21.0.0 || ^22.0.0",
40
+ "@a.nemreen/dga-tokens": "^0.2.0",
41
+ "@a.nemreen/dga-ui": "^0.2.0"
42
+ },
43
+ "dependencies": {
44
+ "tslib": "^2.3.0"
45
+ },
46
+ "sideEffects": false,
47
+ "module": "fesm2022/a.nemreen-dga-dynamic-form.mjs",
48
+ "typings": "types/a.nemreen-dga-dynamic-form.d.ts",
49
+ "exports": {
50
+ "./package.json": {
51
+ "default": "./package.json"
52
+ },
53
+ ".": {
54
+ "types": "./types/a.nemreen-dga-dynamic-form.d.ts",
55
+ "default": "./fesm2022/a.nemreen-dga-dynamic-form.mjs"
56
+ }
57
+ },
58
+ "type": "module"
59
+ }
@@ -0,0 +1,381 @@
1
+ import * as _angular_core from '@angular/core';
2
+ import { InjectionToken, Type, OnInit, EnvironmentProviders } from '@angular/core';
3
+ import { FormGroup, FormControl, FormBuilder, ValidatorFn, AbstractControl } from '@angular/forms';
4
+ import { DgaStep, DgaSelectOption, DgaToastService } from '@a.nemreen/dga-ui';
5
+ import { Observable } from 'rxjs';
6
+ import { HttpClient } from '@angular/common/http';
7
+
8
+ /** Bilingual label used across the schema. */
9
+ interface DgaFormLabel {
10
+ en: string;
11
+ ar: string;
12
+ }
13
+ interface DgaFormFieldColumns {
14
+ xl?: number;
15
+ lg?: number;
16
+ md?: number;
17
+ sm?: number;
18
+ }
19
+ interface DgaFormFieldOption {
20
+ value: string | number | boolean;
21
+ label: DgaFormLabel;
22
+ requiresTextInput?: boolean;
23
+ description?: DgaFormLabel;
24
+ disabled?: boolean;
25
+ [key: string]: unknown;
26
+ }
27
+ type DgaFormFieldType = 'text' | 'email' | 'textarea' | 'select' | 'multiselect' | 'checkbox' | 'radio' | 'phone' | 'file' | 'date' | 'chips' | 'toggle' | 'hidden' | 'number' | 'otp';
28
+ type DgaInputRestriction = 'numbers' | 'english' | 'arabic';
29
+ type DgaHttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
30
+ interface DgaFormField {
31
+ name: string;
32
+ type: DgaFormFieldType;
33
+ label: DgaFormLabel;
34
+ required?: boolean;
35
+ options?: DgaFormFieldOption[];
36
+ groupedOptions?: Array<{
37
+ groupLabel: DgaFormLabel;
38
+ options: DgaFormFieldOption[];
39
+ }>;
40
+ hidden?: boolean;
41
+ dependsOn?: string;
42
+ showOnValues?: unknown[];
43
+ showWhenOptionProperty?: {
44
+ property: string;
45
+ value: unknown;
46
+ };
47
+ lookupDomain?: string;
48
+ lookupName?: string;
49
+ lookupFilter?: {
50
+ property: string;
51
+ value: unknown;
52
+ };
53
+ useParentId?: boolean;
54
+ searchLookup?: boolean;
55
+ searchParamName?: string;
56
+ minSearchLength?: number;
57
+ maxLength?: number;
58
+ placeholder?: DgaFormLabel;
59
+ hint?: DgaFormLabel;
60
+ acceptedFileTypes?: string;
61
+ maxFileSize?: number;
62
+ maxFiles?: number;
63
+ rows?: number;
64
+ minDate?: string;
65
+ maxDate?: string;
66
+ monthPicker?: boolean;
67
+ maxChips?: number;
68
+ allowDuplicates?: boolean;
69
+ disabled?: boolean;
70
+ readonly?: boolean;
71
+ value?: unknown;
72
+ columns?: DgaFormFieldColumns;
73
+ optionsLayout?: 'stack' | 'grid';
74
+ excludeFromPayload?: boolean;
75
+ inputRestriction?: DgaInputRestriction;
76
+ requiredWhen?: {
77
+ field: string;
78
+ value?: unknown;
79
+ values?: unknown[];
80
+ optionProperty?: string;
81
+ optionPropertyValue?: unknown;
82
+ };
83
+ validation?: {
84
+ pattern?: string;
85
+ minLength?: number;
86
+ maxLength?: number;
87
+ required?: boolean;
88
+ min?: number;
89
+ max?: number;
90
+ errorMessages?: Partial<Record<'required' | 'pattern' | 'minLength' | 'maxLength' | 'email' | 'min' | 'max' | 'minDate' | 'maxDate' | 'maxChips' | 'invalidFormat', DgaFormLabel>>;
91
+ };
92
+ }
93
+ interface DgaFormStep {
94
+ id: string;
95
+ title: DgaFormLabel;
96
+ description?: DgaFormLabel;
97
+ fields: DgaFormField[];
98
+ optional?: boolean;
99
+ }
100
+ interface DgaWizardConfig {
101
+ enabled: boolean;
102
+ validateOnStepChange?: boolean;
103
+ allowSkipSteps?: boolean;
104
+ showSaveButton?: boolean;
105
+ autoSave?: boolean;
106
+ saveEndpoint?: string;
107
+ saveMethod?: DgaHttpMethod;
108
+ saveButtonText?: DgaFormLabel;
109
+ nextButtonText?: DgaFormLabel;
110
+ previousButtonText?: DgaFormLabel;
111
+ submitButtonText?: DgaFormLabel;
112
+ steps: DgaFormStep[];
113
+ }
114
+ type DgaDynamicFormPayloadTransformer = (data: Record<string, unknown>) => Record<string, unknown> | FormData;
115
+ interface DgaDynamicFormConfig {
116
+ /** Submit URL. Optional when the consumer handles `formSubmitted` only. */
117
+ endpoint?: string;
118
+ method?: DgaHttpMethod;
119
+ fields: DgaFormField[];
120
+ description?: DgaFormLabel;
121
+ wizard?: DgaWizardConfig;
122
+ submitButtonLabel?: DgaFormLabel;
123
+ clearButtonLabel?: DgaFormLabel;
124
+ saveButtonLabel?: DgaFormLabel;
125
+ successMessage?: DgaFormLabel;
126
+ errorMessage?: DgaFormLabel;
127
+ fieldMapping?: Record<string, string>;
128
+ payloadTransformer?: DgaDynamicFormPayloadTransformer;
129
+ formId?: string;
130
+ enableLocalStorageSave?: boolean;
131
+ idempotencyKey?: string;
132
+ /** When true, HTTP submit is skipped and only `formSubmitted` is emitted. */
133
+ emitOnly?: boolean;
134
+ }
135
+ type DgaFormLocale = 'ar' | 'en';
136
+
137
+ interface DgaLookupRequest {
138
+ domain: string;
139
+ name: string;
140
+ parentId?: string | number | null;
141
+ search?: string;
142
+ searchParamName?: string;
143
+ }
144
+ interface DgaLookupAdapter {
145
+ lookup(request: DgaLookupRequest): Observable<DgaFormFieldOption[]>;
146
+ }
147
+ interface DgaSubmitRequest {
148
+ endpoint: string;
149
+ method: DgaHttpMethod;
150
+ body: Record<string, unknown> | FormData;
151
+ headers?: Record<string, string>;
152
+ }
153
+ interface DgaSubmitAdapter {
154
+ submit<T = unknown>(request: DgaSubmitRequest): Observable<T>;
155
+ }
156
+ type DgaToastVariant = 'neutral' | 'success' | 'info' | 'warning' | 'error';
157
+ interface DgaFormToastAdapter {
158
+ show(options: {
159
+ title?: string;
160
+ message?: string;
161
+ variant?: DgaToastVariant;
162
+ }): void;
163
+ }
164
+ interface DgaCaptchaAdapter {
165
+ /** Returns a captcha token (or null when captcha is disabled). */
166
+ getToken(action?: string): Observable<string | null>;
167
+ }
168
+ interface DgaFormI18nAdapter {
169
+ /** Resolve a bilingual label for the active locale. */
170
+ resolveLabel(label: {
171
+ en: string;
172
+ ar: string;
173
+ }, locale: DgaFormLocale): string;
174
+ /** Optional free-form key lookup (e.g. ngx-translate). */
175
+ translate?(key: string, locale: DgaFormLocale): string;
176
+ }
177
+ /** Custom field renderer registered via `DGA_DYNAMIC_FORM_FIELD_REGISTRY`. */
178
+ interface DgaDynamicFieldRenderer {
179
+ /** Unique type id matching `DgaFormField.type` (or a custom string). */
180
+ type: string;
181
+ component: Type<unknown>;
182
+ }
183
+ declare const DGA_LOOKUP_ADAPTER: InjectionToken<DgaLookupAdapter>;
184
+ declare const DGA_SUBMIT_ADAPTER: InjectionToken<DgaSubmitAdapter>;
185
+ declare const DGA_FORM_TOAST_ADAPTER: InjectionToken<DgaFormToastAdapter>;
186
+ declare const DGA_CAPTCHA_ADAPTER: InjectionToken<DgaCaptchaAdapter>;
187
+ declare const DGA_FORM_I18N_ADAPTER: InjectionToken<DgaFormI18nAdapter>;
188
+ declare const DGA_DYNAMIC_FORM_FIELD_REGISTRY: InjectionToken<DgaDynamicFieldRenderer[]>;
189
+ /** Context passed into custom field renderer components (optional inject). */
190
+ interface DgaDynamicFieldHostContext {
191
+ field: DgaFormField;
192
+ controlName: string;
193
+ locale: DgaFormLocale;
194
+ invalid: boolean;
195
+ error: string;
196
+ }
197
+
198
+ /**
199
+ * Resolves built-in + consumer-registered field renderers by `type`.
200
+ * Built-in types are rendered by `DgaDynamicFormField` directly;
201
+ * custom types use the registered component via `NgComponentOutlet`.
202
+ */
203
+ declare class DgaDynamicFormFieldRegistry {
204
+ private readonly extras;
205
+ private readonly map;
206
+ constructor();
207
+ register(entry: DgaDynamicFieldRenderer): void;
208
+ get(type: string): Type<unknown> | null;
209
+ hasCustom(type: string): boolean;
210
+ isBuiltIn(type: string): type is DgaFormFieldType;
211
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<DgaDynamicFormFieldRegistry, never>;
212
+ static ɵprov: _angular_core.ɵɵInjectableDeclaration<DgaDynamicFormFieldRegistry>;
213
+ }
214
+
215
+ declare class DgaDynamicForm implements OnInit {
216
+ readonly config: _angular_core.InputSignal<DgaDynamicFormConfig>;
217
+ readonly localeInput: _angular_core.InputSignal<DgaFormLocale | null>;
218
+ readonly formSubmitted: _angular_core.OutputEmitterRef<unknown>;
219
+ readonly formError: _angular_core.OutputEmitterRef<unknown>;
220
+ readonly clearButtonClick: _angular_core.OutputEmitterRef<void>;
221
+ readonly draftSaved: _angular_core.OutputEmitterRef<Record<string, unknown>>;
222
+ private readonly fb;
223
+ private readonly formService;
224
+ private readonly toast;
225
+ private readonly i18n;
226
+ private readonly destroyRef;
227
+ readonly registry: DgaDynamicFormFieldRegistry;
228
+ form: FormGroup;
229
+ readonly activeStep: _angular_core.WritableSignal<number>;
230
+ readonly submitting: _angular_core.WritableSignal<boolean>;
231
+ readonly optionsMap: _angular_core.WritableSignal<Record<string, DgaFormFieldOption[]>>;
232
+ private readonly formValues;
233
+ readonly locale: _angular_core.Signal<DgaFormLocale>;
234
+ readonly wizardEnabled: _angular_core.Signal<boolean>;
235
+ readonly allFields: _angular_core.Signal<DgaFormField[]>;
236
+ readonly currentStepFields: _angular_core.Signal<DgaFormField[]>;
237
+ readonly visibleFields: _angular_core.Signal<DgaFormField[]>;
238
+ readonly stepperSteps: _angular_core.Signal<DgaStep[]>;
239
+ readonly descriptionText: _angular_core.Signal<string>;
240
+ constructor();
241
+ ngOnInit(): void;
242
+ labelOf(label: {
243
+ en: string;
244
+ ar: string;
245
+ } | undefined): string;
246
+ submitLabel(): string;
247
+ nextLabel(): string;
248
+ previousLabel(): string;
249
+ clearLabel(): string;
250
+ saveLabel(): string;
251
+ showSave(): boolean;
252
+ isLastStep(): boolean;
253
+ isRequired(field: DgaFormField): boolean;
254
+ fieldError(field: DgaFormField): string;
255
+ columnClass(field: DgaFormField): string;
256
+ customComponent(field: DgaFormField): _angular_core.Type<unknown> | null;
257
+ customInputs(field: DgaFormField): Record<string, unknown>;
258
+ onOptionsLoaded(event: {
259
+ name: string;
260
+ options: DgaFormFieldOption[];
261
+ }): void;
262
+ previousStep(): void;
263
+ nextStep(): void;
264
+ onClear(): void;
265
+ onSaveDraft(): void;
266
+ onSubmit(): void;
267
+ /** Imperative helpers for host apps. */
268
+ patchFormValues(values: Record<string, unknown>): void;
269
+ getForm(): FormGroup;
270
+ private markStepTouched;
271
+ private markVisibleTouched;
272
+ private stepValid;
273
+ private visibleValid;
274
+ private collectVisibleFieldNames;
275
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<DgaDynamicForm, never>;
276
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<DgaDynamicForm, "dga-dynamic-form", never, { "config": { "alias": "config"; "required": true; "isSignal": true; }; "localeInput": { "alias": "locale"; "required": false; "isSignal": true; }; }, { "formSubmitted": "formSubmitted"; "formError": "formError"; "clearButtonClick": "clearButtonClick"; "draftSaved": "draftSaved"; }, never, never, true, never>;
277
+ }
278
+
279
+ declare class DgaDynamicFormField implements OnInit {
280
+ readonly field: _angular_core.InputSignal<DgaFormField>;
281
+ readonly form: _angular_core.InputSignal<FormGroup<any>>;
282
+ readonly locale: _angular_core.InputSignal<DgaFormLocale>;
283
+ readonly required: _angular_core.InputSignalWithTransform<boolean, unknown>;
284
+ readonly error: _angular_core.InputSignal<string>;
285
+ readonly runtimeOptions: _angular_core.InputSignal<DgaFormFieldOption[]>;
286
+ readonly optionsLoaded: _angular_core.OutputEmitterRef<{
287
+ name: string;
288
+ options: DgaFormFieldOption[];
289
+ }>;
290
+ private readonly lookup;
291
+ private readonly destroyRef;
292
+ readonly loadedOptions: _angular_core.WritableSignal<DgaFormFieldOption[]>;
293
+ readonly controlId: string;
294
+ get control(): FormControl;
295
+ readonly labelText: _angular_core.Signal<string>;
296
+ readonly hintText: _angular_core.Signal<string>;
297
+ readonly placeholderText: _angular_core.Signal<string>;
298
+ readonly resolvedOptions: _angular_core.Signal<DgaFormFieldOption[]>;
299
+ readonly selectOptions: _angular_core.Signal<DgaSelectOption[]>;
300
+ ngOnInit(): void;
301
+ optionLabel(opt: DgaFormFieldOption): string;
302
+ isChecked(value: unknown): boolean;
303
+ toggleCheckbox(value: unknown, checked: boolean): void;
304
+ onSelectSearch(_value: string | string[]): void;
305
+ onRestrict(event: Event): void;
306
+ private applyLookupFilter;
307
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<DgaDynamicFormField, never>;
308
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<DgaDynamicFormField, "dga-dynamic-form-field", never, { "field": { "alias": "field"; "required": true; "isSignal": true; }; "form": { "alias": "form"; "required": true; "isSignal": true; }; "locale": { "alias": "locale"; "required": false; "isSignal": true; }; "required": { "alias": "required"; "required": false; "isSignal": true; }; "error": { "alias": "error"; "required": false; "isSignal": true; }; "runtimeOptions": { "alias": "runtimeOptions"; "required": false; "isSignal": true; }; }, { "optionsLoaded": "optionsLoaded"; }, never, never, true, never>;
309
+ }
310
+
311
+ declare class DgaDynamicFormService {
312
+ private readonly submitAdapter;
313
+ private readonly captcha;
314
+ /**
315
+ * Build a JSON payload from form values, applying fieldMapping / transformer / exclusions.
316
+ */
317
+ buildPayload(config: DgaDynamicFormConfig, values: Record<string, unknown>, fields: DgaFormField[]): Record<string, unknown> | FormData;
318
+ hasFileValues(data: Record<string, unknown>): boolean;
319
+ toFormData(data: Record<string, unknown>): FormData;
320
+ submitForm(config: DgaDynamicFormConfig, values: Record<string, unknown>, fields: DgaFormField[]): Observable<unknown>;
321
+ saveDraft(formId: string, values: Record<string, unknown>): void;
322
+ loadDraft(formId: string): Record<string, unknown> | null;
323
+ clearDraft(formId: string): void;
324
+ private storageKey;
325
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<DgaDynamicFormService, never>;
326
+ static ɵprov: _angular_core.ɵɵInjectableDeclaration<DgaDynamicFormService>;
327
+ }
328
+
329
+ declare function dgaDefaultValueForField(field: DgaFormField): unknown;
330
+ declare function dgaBuildValidators(field: DgaFormField, includeRequired: boolean): ValidatorFn[];
331
+ declare function dgaCreateControl(fb: FormBuilder, field: DgaFormField, includeRequired?: boolean): FormControl;
332
+ declare function dgaBuildFormGroup(fb: FormBuilder, fields: DgaFormField[]): FormGroup;
333
+ declare function dgaCollectFields(fields: DgaFormField[], wizardSteps?: {
334
+ fields: DgaFormField[];
335
+ }[]): DgaFormField[];
336
+ declare function dgaSetControlValidators(control: AbstractControl, field: DgaFormField, includeRequired: boolean): void;
337
+
338
+ declare function dgaFindSelectedOption(field: DgaFormField | undefined, value: unknown, runtimeOptions?: DgaFormFieldOption[]): DgaFormFieldOption | undefined;
339
+ /**
340
+ * Whether a field should be visible given the current form values.
341
+ */
342
+ declare function dgaIsFieldVisible(field: DgaFormField, values: Record<string, unknown>, getOptions?: (name: string) => DgaFormFieldOption[] | undefined): boolean;
343
+ declare function dgaIsRequiredWhen(field: DgaFormField, values: Record<string, unknown>, getOptions?: (name: string) => DgaFormFieldOption[] | undefined): boolean;
344
+
345
+ /** Default bilingual label resolver. */
346
+ declare const defaultFormI18nAdapter: DgaFormI18nAdapter;
347
+ /** No-op captcha (always null token). */
348
+ declare const noopCaptchaAdapter: DgaCaptchaAdapter;
349
+ /** Empty lookup (returns []). Override in the host app. */
350
+ declare const emptyLookupAdapter: DgaLookupAdapter;
351
+ /**
352
+ * HTTP lookup that expects `{domain}/lookup/{name}` JSON arrays.
353
+ * Maps common `{ id|value, titleEn|labelEn|en, titleAr|labelAr|ar }` shapes.
354
+ */
355
+ declare function createHttpLookupAdapter(http?: HttpClient): DgaLookupAdapter;
356
+ declare function createHttpSubmitAdapter(http?: HttpClient): DgaSubmitAdapter;
357
+ declare function createDgaToastAdapter(toasts?: DgaToastService): DgaFormToastAdapter;
358
+ interface ProvideDgaDynamicFormOptions {
359
+ /** Use HttpClient-backed lookup (default: empty). */
360
+ httpLookup?: boolean;
361
+ /** Use HttpClient-backed submit (default: true). */
362
+ httpSubmit?: boolean;
363
+ /** Wire DgaToastService (default: true). */
364
+ dgaToast?: boolean;
365
+ }
366
+ /**
367
+ * Registers default adapters for lookup, submit, toast, captcha, and i18n.
368
+ * Call from `app.config.ts` providers.
369
+ */
370
+ declare function provideDgaDynamicForm(options?: ProvideDgaDynamicFormOptions): EnvironmentProviders;
371
+ /** Helper for templates / host apps. */
372
+ declare function resolveFormLabel(label: {
373
+ en: string;
374
+ ar: string;
375
+ } | undefined, locale: DgaFormLocale, fallback?: string): string;
376
+
377
+ declare function dgaResolveLabel(label: DgaFormLabel | undefined, locale: DgaFormLocale, fallback?: string): string;
378
+ declare function dgaDetectLocale(): DgaFormLocale;
379
+
380
+ export { DGA_CAPTCHA_ADAPTER, DGA_DYNAMIC_FORM_FIELD_REGISTRY, DGA_FORM_I18N_ADAPTER, DGA_FORM_TOAST_ADAPTER, DGA_LOOKUP_ADAPTER, DGA_SUBMIT_ADAPTER, DgaDynamicForm, DgaDynamicFormField, DgaDynamicFormFieldRegistry, DgaDynamicFormService, createDgaToastAdapter, createHttpLookupAdapter, createHttpSubmitAdapter, defaultFormI18nAdapter, dgaBuildFormGroup, dgaBuildValidators, dgaCollectFields, dgaCreateControl, dgaDefaultValueForField, dgaDetectLocale, dgaFindSelectedOption, dgaIsFieldVisible, dgaIsRequiredWhen, dgaResolveLabel, dgaSetControlValidators, emptyLookupAdapter, noopCaptchaAdapter, provideDgaDynamicForm, resolveFormLabel };
381
+ export type { DgaCaptchaAdapter, DgaDynamicFieldHostContext, DgaDynamicFieldRenderer, DgaDynamicFormConfig, DgaDynamicFormPayloadTransformer, DgaFormField, DgaFormFieldColumns, DgaFormFieldOption, DgaFormFieldType, DgaFormI18nAdapter, DgaFormLabel, DgaFormLocale, DgaFormStep, DgaFormToastAdapter, DgaHttpMethod, DgaInputRestriction, DgaLookupAdapter, DgaLookupRequest, DgaSubmitAdapter, DgaSubmitRequest, DgaToastVariant, DgaWizardConfig, ProvideDgaDynamicFormOptions };