@a.nemreen/dga-dynamic-form 0.1.6 → 0.1.8

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 CHANGED
@@ -2,12 +2,12 @@
2
2
 
3
3
  Schema-driven dynamic forms for Angular, rendered with [`@a.nemreen/dga-ui`](https://www.npmjs.com/package/@a.nemreen/dga-ui) controls.
4
4
 
5
- Version **0.1.1** — API may move until freeze.
5
+ **Version 0.1.8** — password match, staged uploads, lookups, wizard polish, and cross-field validators.
6
6
 
7
7
  ## Install
8
8
 
9
9
  ```bash
10
- npm install @a.nemreen/dga-dynamic-form @a.nemreen/dga-ui @a.nemreen/dga-tokens @platformscode/icons
10
+ npm install @a.nemreen/dga-dynamic-form @a.nemreen/dga-ui @a.nemreen/dga-tokens
11
11
  ```
12
12
 
13
13
  **Peers:** `@angular/core` / `common` / `forms` ^19–22, `@a.nemreen/dga-ui` ^0.2, `@a.nemreen/dga-tokens` ^0.2.
@@ -25,16 +25,86 @@ export const appConfig = {
25
25
  httpSubmit: true,
26
26
  httpLookup: true,
27
27
  dgaToast: true,
28
- // Recommended when schemas/endpoints come from CMS or untrusted config:
29
- // allowedOrigins: ['https://api.example.com'],
28
+ allowedOrigins: ['https://api.example.com'],
30
29
  }),
31
30
  ],
32
31
  };
33
32
  ```
34
33
 
35
- **Security note:** Treat form schemas as trusted app config unless you set `allowedOrigins`.
36
- HTTP adapters reject non-`http(s)` URLs; path-absolute URLs (`/api/...`) stay same-origin.
37
- Always validate uploads and payloads on the server.
34
+ **Security:** Treat CMS schemas as untrusted unless `allowedOrigins` is set. HTTP adapters reject non-`http(s)` URLs; same-origin paths (`/api/...`) are always allowed. Validate uploads server-side.
35
+
36
+ ### reCAPTCHA (production HTTP submits)
37
+
38
+ Use **reCAPTCHA v3** (invisible). There is **no** `type: 'captcha'` field in the form schema.
39
+
40
+ | Audience | What happens |
41
+ |----------|----------------|
42
+ | **End user** | No checkbox. Submit works like a normal form; Google scores in the background. |
43
+ | **Developer** | Wire `ng-recaptcha` + override `DGA_CAPTCHA_ADAPTER` in `app.config`. |
44
+ | **Backend** | Read `X-Recaptcha-Token` and verify with Google's secret key. |
45
+
46
+ ```bash
47
+ npm install ng-recaptcha
48
+ ```
49
+
50
+ ```ts
51
+ // environments/environment.ts
52
+ export const environment = {
53
+ recaptchaSiteKey: '6Lc...your-v3-site-key...',
54
+ };
55
+ ```
56
+
57
+ `provideDgaDynamicForm` registers a **no-op** `DGA_CAPTCHA_ADAPTER` (token is always `null`). Override it after `provideDgaDynamicForm`. On submit the package calls `getToken('submit')` and sends header `X-Recaptcha-Token`.
58
+
59
+ ```ts
60
+ import { importProvidersFrom } from '@angular/core';
61
+ import {
62
+ RECAPTCHA_V3_SITE_KEY,
63
+ RecaptchaV3Module,
64
+ ReCaptchaV3Service,
65
+ } from 'ng-recaptcha';
66
+ import { catchError, of } from 'rxjs';
67
+ import {
68
+ DGA_CAPTCHA_ADAPTER,
69
+ provideDgaDynamicForm,
70
+ } from '@a.nemreen/dga-dynamic-form';
71
+
72
+ export const appConfig = {
73
+ providers: [
74
+ provideHttpClient(),
75
+ importProvidersFrom(RecaptchaV3Module),
76
+ { provide: RECAPTCHA_V3_SITE_KEY, useValue: environment.recaptchaSiteKey },
77
+ provideDgaDynamicForm({ httpSubmit: true }),
78
+ {
79
+ provide: DGA_CAPTCHA_ADAPTER,
80
+ useFactory: (recaptcha: ReCaptchaV3Service) => ({
81
+ getToken: (action?: string) => {
82
+ const googleAction = action === 'submit' ? 'form_submit' : action ?? 'form_submit';
83
+ return recaptcha.execute(googleAction).pipe(catchError(() => of(null)));
84
+ },
85
+ }),
86
+ deps: [ReCaptchaV3Service],
87
+ },
88
+ ],
89
+ };
90
+ ```
91
+
92
+ Form config — set `endpoint` (do **not** use `emitOnly` for live POSTs):
93
+
94
+ ```ts
95
+ readonly config: DgaDynamicFormConfig = {
96
+ endpoint: 'https://api.example.com/contact',
97
+ method: 'POST',
98
+ fields: [/* no captcha field */],
99
+ };
100
+ ```
101
+
102
+ **Backend:** verify token via `https://www.google.com/recaptcha/api/siteverify` with secret key; check `action === 'form_submit'` and score threshold.
103
+
104
+ **Troubleshooting:** token null → provide site key + `RecaptchaV3Module`; captcha skipped → form uses `emitOnly`; script blocked → CSP must allow `www.google.com` and `www.gstatic.com`. Use `RecaptchaV3Module` only (not v2 `RecaptchaModule` checkbox).
105
+
106
+ Full guide: demo `/dynamic-form` → reCAPTCHA section.
107
+
38
108
  ```css
39
109
  @source "../node_modules/@a.nemreen/dga-dynamic-form/**/*.{mjs,js}";
40
110
  @source "../node_modules/@a.nemreen/dga-ui/**/*.{mjs,js}";
@@ -51,31 +121,46 @@ readonly config: DgaDynamicFormConfig = {
51
121
  submitButtonLabel: { en: 'Send', ar: 'إرسال' },
52
122
  fields: [
53
123
  {
54
- name: 'fullName',
55
- type: 'text',
56
- label: { en: 'Full name', ar: 'الاسم الكامل' },
124
+ name: 'password',
125
+ type: 'password',
126
+ label: { en: 'Password', ar: 'كلمة المرور' },
57
127
  required: true,
58
- columns: { md: 6 },
128
+ },
129
+ {
130
+ name: 'confirmPassword',
131
+ type: 'password',
132
+ label: { en: 'Confirm', ar: 'تأكيد' },
133
+ required: true,
134
+ validation: {
135
+ errorMessages: {
136
+ passwordMismatch: { en: 'Passwords must match', ar: 'يجب أن تتطابق كلمات المرور' },
137
+ },
138
+ },
59
139
  },
60
140
  ],
61
141
  };
62
142
  ```
63
143
 
64
144
  ```html
65
- <dga-dynamic-form [config]="config" (formSubmitted)="onSubmit($event)" />
145
+ <dga-dynamic-form
146
+ [config]="config"
147
+ [locale]="'ar'"
148
+ (formSubmitted)="onSubmit($event)"
149
+ (fileDownload)="onDownload($event)"
150
+ />
66
151
  ```
67
152
 
153
+ When both `password` and `confirmPassword` fields exist, `passwordMismatch` validation is applied automatically.
154
+
68
155
  ---
69
156
 
70
157
  ## Config reference (`DgaDynamicFormConfig`)
71
158
 
72
- Passed as `[config]` on `<dga-dynamic-form>`.
73
-
74
159
  | Key | Type | Default | Description |
75
160
  |-----|------|---------|-------------|
76
- | `endpoint` | `string?` | — | Submit URL. Omit (or use `emitOnly`) to only emit `formSubmitted`. |
77
- | `method` | `'GET' \| 'POST' \| 'PUT' \| 'PATCH' \| 'DELETE'` | `'POST'` | HTTP method for the submit adapter. |
78
- | `emitOnly` | `boolean?` | `false` | Skip HTTP; emit the payload on `formSubmitted`. |
161
+ | `endpoint` | `string?` | — | Submit URL. Omit or use `emitOnly` to only emit `formSubmitted`. |
162
+ | `method` | `DgaHttpMethod` | `'POST'` | HTTP method for submit adapter. |
163
+ | `emitOnly` | `boolean?` | `false` | Skip HTTP; emit payload on `formSubmitted`. |
79
164
  | `fields` | `DgaFormField[]` | **required** | Field list. Can be `[]` when using `wizard.steps` only. |
80
165
  | `description` | `DgaFormLabel?` | — | Bilingual text above the form. |
81
166
  | `submitButtonLabel` | `DgaFormLabel?` | Submit / إرسال | Primary button label. |
@@ -83,47 +168,31 @@ Passed as `[config]` on `<dga-dynamic-form>`.
83
168
  | `saveButtonLabel` | `DgaFormLabel?` | — | Draft save button label. |
84
169
  | `successMessage` | `DgaFormLabel?` | — | Toast on success. |
85
170
  | `errorMessage` | `DgaFormLabel?` | — | Toast on failure. |
86
- | `fieldMapping` | `Record<string, string>?` | — | Rename field keys in the submit payload. |
171
+ | `fieldMapping` | `Record<string, string>?` | — | Rename field keys in submit payload. |
87
172
  | `payloadTransformer` | `(data) => object \| FormData` | — | Final reshape before submit. |
88
173
  | `formId` | `string?` | — | localStorage namespace for drafts. |
89
174
  | `enableLocalStorageSave` | `boolean?` | `false` | Enable Save draft + restore on init. |
90
175
  | `idempotencyKey` | `string?` | — | Sent as `Idempotency-Key` header. |
176
+ | `consentBaseUrl` | `string?` | — | CMS base for `consentServiceName` fields → GET `{base}/consents/{name}`. |
91
177
  | `wizard` | `DgaWizardConfig?` | — | Multi-step mode (see below). |
92
178
 
93
179
  `DgaFormLabel` is always `{ en: string; ar: string }`.
94
180
 
95
- ### Example with endpoint
96
-
97
- ```ts
98
- const EDIT_STUDY: DgaDynamicFormConfig = {
99
- endpoint: `${apiUrl}/consulting-studies/{id}`,
100
- method: 'PUT',
101
- successMessage: { en: 'Saved', ar: 'تم الحفظ' },
102
- errorMessage: { en: 'Failed', ar: 'فشل' },
103
- fields: [ /* ... */ ],
104
- };
105
- ```
106
-
107
181
  ---
108
182
 
109
- ## Wizard (`config.wizard` → `DgaWizardConfig`)
110
-
111
- When `wizard.enabled` is true, fields are taken from `wizard.steps[].fields` (root `fields` can be empty).
183
+ ## Wizard (`config.wizard`)
112
184
 
113
185
  | Key | Type | Default | Description |
114
186
  |-----|------|---------|-------------|
115
187
  | `enabled` | `boolean` | — | Turn on stepper UI. |
116
188
  | `steps` | `DgaFormStep[]` | — | Ordered steps. |
117
189
  | `validateOnStepChange` | `boolean?` | `true` | Block Next if current step invalid. |
118
- | `allowSkipSteps` | `boolean?` | `false` | Allow jumping ahead (reserved). |
190
+ | `allowSkipSteps` | `boolean?` | `false` | Allow jumping ahead in stepper. |
119
191
  | `showSaveButton` | `boolean?` | — | Show draft save in wizard chrome. |
120
- | `autoSave` | `boolean?` | — | Auto-save drafts (host / reserved). |
121
- | `saveEndpoint` | `string?` | — | Optional draft HTTP URL. |
122
- | `saveMethod` | `DgaHttpMethod?` | | Method for `saveEndpoint`. |
123
- | `nextButtonText` | `DgaFormLabel?` | Next / التالي | |
124
- | `previousButtonText` | `DgaFormLabel?` | Previous / السابق | |
125
- | `submitButtonText` | `DgaFormLabel?` | Finish / إنهاء | Final step. |
126
- | `saveButtonText` | `DgaFormLabel?` | Save / حفظ | |
192
+ | `autoSave` | `boolean?` | — | When true + `saveEndpoint`, HTTP draft save on Save. |
193
+ | `saveEndpoint` | `string?` | — | Draft HTTP URL. |
194
+ | `saveMethod` | `DgaHttpMethod?` | `'POST'` | Method for `saveEndpoint`. |
195
+ | `nextButtonText` / `previousButtonText` / `submitButtonText` / `saveButtonText` | `DgaFormLabel?` | | Wizard chrome labels. |
127
196
 
128
197
  ### Step (`DgaFormStep`)
129
198
 
@@ -133,113 +202,93 @@ When `wizard.enabled` is true, fields are taken from `wizard.steps[].fields` (ro
133
202
  | `title` | `DgaFormLabel` | Stepper title. |
134
203
  | `description` | `DgaFormLabel?` | Stepper subtitle. |
135
204
  | `fields` | `DgaFormField[]` | Fields in this step. |
136
- | `optional` | `boolean?` | Mark step optional. |
205
+ | `optional` | `boolean?` | Skip validation for this step. |
206
+ | `initialData` | `Record<string, unknown>?` | Pre-fill when entering the step. |
137
207
 
138
208
  ---
139
209
 
140
210
  ## Field (`DgaFormField`)
141
211
 
142
- ### Core
143
-
144
- | Key | Type | Description |
145
- |-----|------|-------------|
146
- | `name` | `string` | FormControl name / payload key. |
147
- | `type` | `DgaFormFieldType` | See built-in types below. |
148
- | `label` | `DgaFormLabel` | Bilingual label. |
149
- | `required` | `boolean?` | Always-required. |
150
- | `placeholder` | `DgaFormLabel?` | |
151
- | `hint` | `DgaFormLabel?` | Helper under the field. |
152
- | `value` | `unknown?` | Initial value. |
153
- | `disabled` | `boolean?` | |
154
- | `readonly` | `boolean?` | Text-like inputs. |
155
- | `hidden` | `boolean?` | Hide from layout. |
156
- | `excludeFromPayload` | `boolean?` | Keep in UI; drop from submit body. |
157
- | `columns` | `{ sm?, md?, lg?, xl? }` | 12-column grid spans (default 12). |
158
- | `maxLength` | `number?` | |
159
- | `rows` | `number?` | Textarea rows. |
160
- | `inputRestriction` | `'numbers' \| 'english' \| 'arabic'` | Filter keystrokes. |
161
-
162
212
  ### Built-in `type` values
163
213
 
164
- `text` · `email` · `textarea` · `number` · `otp` · `phone` · `select` · `multiselect` · `checkbox` · `radio` · `date` · `chips` · `toggle` · `file` · `hidden`
214
+ `text` · `email` · `password` · `textarea` · `number` · `otp` · `phone` · `select` · `multiselect` · `checkbox` · `radio` · `date` · `chips` · `toggle` · `file` · `multifile` · `multifilestaged` · `hidden`
165
215
 
166
- ### Options (select / radio / checkbox)
216
+ Fields named `password` or `confirmPassword` with `type: 'text'` are rendered as password inputs.
167
217
 
168
- | Key | Type | Description |
169
- |-----|------|-------------|
170
- | `options` | `DgaFormFieldOption[]?` | Static options. |
171
- | `groupedOptions` | `{ groupLabel, options }[]?` | **Planned** — flattened for visibility only today. |
172
- | `optionsLayout` | `'stack' \| 'grid'` | Radio/checkbox layout (default `stack`). |
218
+ ### Visibility & conditional required
173
219
 
174
- **Option object:** `value`, `label`, optional `requiresTextInput`, `description`, `disabled`.
220
+ | Key | Description |
221
+ |-----|-------------|
222
+ | `dependsOn` | Other field that controls visibility. |
223
+ | `showOnValues` | Show when `dependsOn` value is in list. |
224
+ | `showOnTitleEn` / `showOnTitleAr` | Show when selected option label matches. |
225
+ | `showWhenOptionProperty` | Show when selected option has `property === value`. |
226
+ | `extraField` | Show when selected option has `requiresTextInput: true`. |
227
+ | `extraFieldTrigger` | Trigger field for `extraField` (defaults to `dependsOn`). |
228
+ | `requiredWhen` | `{ field, value?, values?, optionProperty?, optionPropertyValue? }` |
175
229
 
176
- ### Visibility & conditional required
230
+ ### Lookup (remote options)
177
231
 
178
- | Key | Type | Description |
179
- |-----|------|-------------|
180
- | `dependsOn` | `string?` | Other field that controls visibility. |
181
- | `showOnValues` | `unknown[]?` | Show when `dependsOn` value is in list. |
182
- | `showWhenOptionProperty` | `{ property, value }?` | Show when selected option has `property === value`. |
183
- | `requiredWhen` | object? | Conditional required (see below). |
232
+ | Key | Description |
233
+ |-----|-------------|
234
+ | `lookupDomain` | Base URL for lookup adapter. |
235
+ | `lookupName` | Resource `{domain}/lookup/{name}`. |
236
+ | `lookupFilter` | Client-side filter on loaded options. |
237
+ | `useParentId` | Pass `dependsOn` value as `parentId`; reloads when parent changes. |
238
+ | `searchLookup` | Searchable select; remote search via `DGA_LOOKUP_ADAPTER`. |
239
+ | `searchParamName` | Query param (default `q`). |
240
+ | `minSearchLength` | Min chars before search (default `3` when `searchLookup`). |
184
241
 
185
- ```ts
186
- requiredWhen: {
187
- field: 'topic',
188
- value?: 'other', // single match
189
- values?: ['a', 'b'], // any-of match
190
- optionProperty?: 'requiresTextInput',
191
- optionPropertyValue?: true,
192
- }
193
- ```
242
+ HTTP adapter maps `Id/Title`, `Code/Name`, and `{UserName,Email,DisplayName}` shapes.
194
243
 
195
- ### Lookup (remote options)
244
+ ### Files
196
245
 
197
- | Key | Type | Description |
198
- |-----|------|-------------|
199
- | `lookupDomain` | `string?` | Base URL for lookup adapter. |
200
- | `lookupName` | `string?` | Resource `{domain}/lookup/{name}`. |
201
- | `lookupFilter` | `{ property, value }?` | Client-side filter on loaded options. |
202
- | `useParentId` | `boolean?` | Pass `dependsOn` value as `parentId` for cascading lookups. |
203
- | `searchLookup` | `boolean?` | **Planned** searchable UI only; remote search adapter not wired yet. |
204
- | `searchParamName` | `string?` | Query param (default `q`) when search is implemented. |
205
- | `minSearchLength` | `number?` | Min chars before search (default 3) — **planned**. |
246
+ | Key | Types | Description |
247
+ |-----|-------|-------------|
248
+ | `acceptedFileTypes` | file, multifile, multifilestaged | e.g. `.pdf,.png`. |
249
+ | `maxFileSize` | all file types | Max bytes per file. |
250
+ | `maxFiles` | all file types | Max file count. |
251
+ | `uploadEndpoint` | multifilestaged | POST staging URL `{ fileId, fileName? }`. |
252
+ | `deleteEndpoint` | multifilestaged | DELETE/POST removal URL (`:id` / `:fileId`). |
253
+ | `deleteMethod` | multifilestaged | `'DELETE' \| 'POST'`. |
254
+
255
+ `multifilestaged` value: `DgaStagedFileRef[]`. Edit mode: `DgaExistingAttachment[]` on file fields.
206
256
 
207
257
  ### Type-specific
208
258
 
209
259
  | Key | Types | Description |
210
260
  |-----|-------|-------------|
211
- | `minDate` / `maxDate` | `date` | ISO date bounds. |
212
- | `monthPicker` | `date` | Month-only picker. |
213
- | `acceptedFileTypes` | `file` | e.g. `.pdf,.png`. |
214
- | `maxFileSize` | `file` | Max bytes. |
215
- | `maxFiles` | `file` | Max files (`>1` → multiple; enforced client-side). |
216
- | `maxChips` | `chips` | Max chip count. |
217
- | `allowDuplicates` | `chips` | **Planned** not yet bound to `dga-chip-input`. |
261
+ | `minDate` / `maxDate` | date | ISO bounds on picker. |
262
+ | `monthPicker` | date | Month-only mode. |
263
+ | `maxChips` | chips | Max chip count validator. |
264
+ | `allowDuplicates` | chips | Allow duplicate chip values. |
265
+ | `alwaysOpen` | select | `openMode="always"` on `dga-select`. |
266
+ | `consentServiceName` | checkbox | Fetch label from CMS (`consentBaseUrl`). |
267
+ | `excludeFromDraft` / `sensitive` | any | Strip from localStorage drafts. |
218
268
 
219
269
  ### Validation (`field.validation`)
220
270
 
221
- | Key | Description |
222
- |-----|-------------|
223
- | `pattern` | Regex string, or `'email'`. |
224
- | `minLength` / `maxLength` | Length validators. |
225
- | `min` / `max` | Numeric bounds. |
226
- | `required` | Same as `field.required`. |
227
- | `errorMessages.*` | Bilingual messages for `required`, `pattern`, `minLength`, `maxLength`, `email`, `min`, `max`, `minDate`, `maxDate`, `maxChips`, `invalidFormat`. |
271
+ Supports `pattern`, `minLength`, `maxLength`, `min`, `max`, `minDate`, `maxDate`, `required`.
272
+
273
+ **Error message keys:** `required`, `pattern`, `minLength`, `maxLength`, `email`, `min`, `max`, `minDate`, `maxDate`, `maxChips`, `invalidFormat`, `invalidPrefix`, `invalidLength`, `passwordMismatch`, `endDateAfterStartDate`, `uploadInProgress`, `maxFiles`.
274
+
275
+ **Auto cross-field rules:** `password` + `confirmPassword` match validator; known date pairs (`StartDate`/`EndDate`, `UseCaseStartDate`/`UseCaseEndDate`, `startDate`/`endDate`) → end-after-start.
228
276
 
229
277
  ---
230
278
 
231
279
  ## Component API
232
280
 
233
- | Member | Kind | Description |
234
- |--------|------|-------------|
235
- | `config` | input | `DgaDynamicFormConfig` (required). |
236
- | `locale` | input | `'ar' \| 'en' \| null` (default from `<html lang>`). |
237
- | `formSubmitted` | output | HTTP response, or payload when `emitOnly` / no endpoint. |
238
- | `formError` | output | Submit failure. |
239
- | `clearButtonClick` | output | After Clear. |
240
- | `draftSaved` | output | After localStorage save. |
241
- | `patchFormValues(values)` | method | Patch controls. |
242
- | `getForm()` | method | Returns `FormGroup`. |
281
+ | Member | Description |
282
+ |--------|-------------|
283
+ | `[config]` | `DgaDynamicFormConfig` (required). |
284
+ | `[locale]` | `'ar' \| 'en'` (default from `<html lang>`). |
285
+ | `(formSubmitted)` | HTTP response or payload when `emitOnly` / no endpoint. |
286
+ | `(formError)` | Submit failure. |
287
+ | `(clearButtonClick)` | After Clear. |
288
+ | `(draftSaved)` | After localStorage save. |
289
+ | `(fileDownload)` | Existing attachment download (`DgaExistingAttachment`). |
290
+ | `patchFormValues(values)` | Patch controls; config `value` changes also patch at runtime. |
291
+ | `getForm()` | Returns `FormGroup`. |
243
292
 
244
293
  ---
245
294
 
@@ -248,31 +297,17 @@ requiredWhen: {
248
297
  | Token | Role |
249
298
  |-------|------|
250
299
  | `DGA_SUBMIT_ADAPTER` | `submit({ endpoint, method, body, headers })` |
251
- | `DGA_LOOKUP_ADAPTER` | `lookup({ domain, name, parentId?, search? })` |
300
+ | `DGA_LOOKUP_ADAPTER` | `lookup({ domain, name, parentId?, search?, searchParamName? })` |
252
301
  | `DGA_FORM_TOAST_ADAPTER` | `show({ title?, message?, variant? })` |
253
302
  | `DGA_CAPTCHA_ADAPTER` | `getToken(action?) → Observable<string \| null>` |
254
303
  | `DGA_FORM_I18N_ADAPTER` | `resolveLabel(label, locale)` |
304
+ | `DGA_HTTP_SECURITY` | `{ allowedOrigins? }` for schema-driven URLs |
255
305
  | `DGA_DYNAMIC_FORM_FIELD_REGISTRY` | `{ type, component }[]` custom field renderers |
256
306
 
257
- ```ts
258
- provideDgaDynamicForm({
259
- httpSubmit: true,
260
- httpLookup: true,
261
- dgaToast: true,
262
- });
263
- ```
307
+ ### Exported utilities
264
308
 
265
- ### Custom field type
266
-
267
- ```ts
268
- {
269
- provide: DGA_DYNAMIC_FORM_FIELD_REGISTRY,
270
- useValue: [{ type: 'nationalId', component: NationalIdField }],
271
- }
272
- ```
309
+ `dgaBuildFormGroup`, `dgaIsFieldVisible`, `dgaIsRequiredWhen`, `dgaApplyCrossFieldValidators`, `dgaSaudiMobileValidator`, `dgaPasswordMatchValidator`, `dgaAssertSafeHttpUrl`, `createHttpLookupAdapter`, `createHttpSubmitAdapter`.
273
310
 
274
311
  ## License
275
312
 
276
313
  MIT — see [LICENSE](./LICENSE). Copyright (c) 2026 Ahmed Nemreen.
277
-
278
- Use it in any project. **Do not rebrand and sell it as your own unique product.**