@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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@a.nemreen/dga-dynamic-form",
3
- "version": "0.1.6",
4
- "description": "Schema-driven dynamic forms for Angular, rendered with @a.nemreen/dga-ui controls",
3
+ "version": "0.1.8",
4
+ "description": "Schema-driven Angular dynamic forms 18 field types, wizard, cross-field validators (password, Saudi phone, date range), staged uploads, lookup, reCAPTCHA v3, and conditional visibility. Built on @a.nemreen/dga-ui.",
5
5
  "license": "MIT",
6
6
  "author": {
7
7
  "name": "Ahmed Nemreen",
@@ -22,6 +22,10 @@
22
22
  "dynamic-form",
23
23
  "schema-form",
24
24
  "reactive-forms",
25
+ "wizard",
26
+ "validators",
27
+ "file-upload",
28
+ "recaptcha",
25
29
  "dga",
26
30
  "design-system",
27
31
  "rtl",
@@ -33,6 +37,12 @@
33
37
  "publishConfig": {
34
38
  "access": "public"
35
39
  },
40
+ "files": [
41
+ "fesm2022",
42
+ "types",
43
+ "LICENSE",
44
+ "README.md"
45
+ ],
36
46
  "engines": {
37
47
  "node": ">=18"
38
48
  },
@@ -40,8 +50,8 @@
40
50
  "@angular/common": "^19.0.0 || ^20.0.0 || ^21.0.0 || ^22.0.0",
41
51
  "@angular/core": "^19.0.0 || ^20.0.0 || ^21.0.0 || ^22.0.0",
42
52
  "@angular/forms": "^19.0.0 || ^20.0.0 || ^21.0.0 || ^22.0.0",
43
- "@a.nemreen/dga-tokens": "^0.2.0",
44
- "@a.nemreen/dga-ui": "^0.2.0"
53
+ "@a.nemreen/dga-tokens": "^0.2.0 || ^0.3.0",
54
+ "@a.nemreen/dga-ui": "^0.2.0 || ^0.3.0"
45
55
  },
46
56
  "dependencies": {
47
57
  "tslib": "^2.3.0"
@@ -1,6 +1,6 @@
1
1
  import * as _angular_core from '@angular/core';
2
2
  import { InjectionToken, Type, OnInit, EnvironmentProviders } from '@angular/core';
3
- import { FormGroup, FormControl, FormBuilder, ValidatorFn, AbstractControl } from '@angular/forms';
3
+ import { FormGroup, FormControl, FormBuilder, ValidatorFn, AbstractControl, ValidationErrors } from '@angular/forms';
4
4
  import { DgaStep, DgaSelectOption, DgaToastService } from '@a.nemreen/dga-ui';
5
5
  import { Observable } from 'rxjs';
6
6
  import { HttpClient } from '@angular/common/http';
@@ -24,7 +24,24 @@ interface DgaFormFieldOption {
24
24
  disabled?: boolean;
25
25
  [key: string]: unknown;
26
26
  }
27
- type DgaFormFieldType = 'text' | 'email' | 'textarea' | 'select' | 'multiselect' | 'checkbox' | 'radio' | 'phone' | 'file' | 'date' | 'chips' | 'toggle' | 'hidden' | 'number' | 'otp';
27
+ type DgaFormFieldType = 'text' | 'email' | 'textarea' | 'select' | 'multiselect' | 'checkbox' | 'radio' | 'phone' | 'file' | 'multifile' | 'multifilestaged' | 'date' | 'chips' | 'toggle' | 'hidden' | 'number' | 'otp' | 'password';
28
+ /** Pre-populated attachment from the server (edit mode). */
29
+ interface DgaExistingAttachment {
30
+ Id: number;
31
+ FileName: string;
32
+ ContentType: string;
33
+ }
34
+ type DgaStagedFileStatus = 'uploading' | 'success' | 'error';
35
+ /** Value shape for `multifilestaged` fields. */
36
+ interface DgaStagedFileRef {
37
+ fileId: string;
38
+ fileName: string;
39
+ sizeBytes?: number;
40
+ status?: DgaStagedFileStatus;
41
+ errorMessage?: string;
42
+ /** Client-only list identity when server reuses fileId. */
43
+ listKey?: string;
44
+ }
28
45
  type DgaInputRestriction = 'numbers' | 'english' | 'arabic';
29
46
  type DgaHttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
30
47
  interface DgaFormField {
@@ -40,10 +57,22 @@ interface DgaFormField {
40
57
  hidden?: boolean;
41
58
  dependsOn?: string;
42
59
  showOnValues?: unknown[];
60
+ /** Show when selected option English label matches. */
61
+ showOnTitleEn?: string;
62
+ /** Show when selected option Arabic label matches. */
63
+ showOnTitleAr?: string;
64
+ /** Extra text field shown when selected option has `requiresTextInput`. */
65
+ extraField?: boolean;
66
+ /** Trigger field for `extraField` (defaults to `dependsOn`). */
67
+ extraFieldTrigger?: string;
43
68
  showWhenOptionProperty?: {
44
69
  property: string;
45
70
  value: unknown;
46
71
  };
72
+ /** CMS consent key — fetched from `consentBaseUrl` on the form config. */
73
+ consentServiceName?: string;
74
+ /** Keep select dropdown open (DGA select `openMode="always"`). */
75
+ alwaysOpen?: boolean;
47
76
  lookupDomain?: string;
48
77
  lookupName?: string;
49
78
  lookupFilter?: {
@@ -60,6 +89,11 @@ interface DgaFormField {
60
89
  acceptedFileTypes?: string;
61
90
  maxFileSize?: number;
62
91
  maxFiles?: number;
92
+ /** POST URL for `multifilestaged` staging uploads. Response: `{ fileId, fileName? }`. */
93
+ uploadEndpoint?: string;
94
+ /** DELETE/POST URL for staged file removal. Use `:id` or `:fileId` placeholder. */
95
+ deleteEndpoint?: string;
96
+ deleteMethod?: 'DELETE' | 'POST';
63
97
  rows?: number;
64
98
  minDate?: string;
65
99
  maxDate?: string;
@@ -72,6 +106,13 @@ interface DgaFormField {
72
106
  columns?: DgaFormFieldColumns;
73
107
  optionsLayout?: 'stack' | 'grid';
74
108
  excludeFromPayload?: boolean;
109
+ /**
110
+ * When true, the field value is never written to draft storage.
111
+ * OTP / file fields are always excluded even without this flag.
112
+ */
113
+ excludeFromDraft?: boolean;
114
+ /** Treat as sensitive in drafts (same effect as excludeFromDraft). */
115
+ sensitive?: boolean;
75
116
  inputRestriction?: DgaInputRestriction;
76
117
  requiredWhen?: {
77
118
  field: string;
@@ -87,7 +128,9 @@ interface DgaFormField {
87
128
  required?: boolean;
88
129
  min?: number;
89
130
  max?: number;
90
- errorMessages?: Partial<Record<'required' | 'pattern' | 'minLength' | 'maxLength' | 'email' | 'min' | 'max' | 'minDate' | 'maxDate' | 'maxChips' | 'invalidFormat', DgaFormLabel>>;
131
+ minDate?: string;
132
+ maxDate?: string;
133
+ errorMessages?: Partial<Record<'required' | 'pattern' | 'minLength' | 'maxLength' | 'email' | 'min' | 'max' | 'minDate' | 'maxDate' | 'maxChips' | 'invalidFormat' | 'invalidPrefix' | 'invalidLength' | 'passwordMismatch' | 'endDateAfterStartDate' | 'uploadInProgress' | 'maxFiles', DgaFormLabel>>;
91
134
  };
92
135
  }
93
136
  interface DgaFormStep {
@@ -96,6 +139,8 @@ interface DgaFormStep {
96
139
  description?: DgaFormLabel;
97
140
  fields: DgaFormField[];
98
141
  optional?: boolean;
142
+ /** Pre-fill values when entering this wizard step. */
143
+ initialData?: Record<string, unknown>;
99
144
  }
100
145
  interface DgaWizardConfig {
101
146
  enabled: boolean;
@@ -131,6 +176,11 @@ interface DgaDynamicFormConfig {
131
176
  idempotencyKey?: string;
132
177
  /** When true, HTTP submit is skipped and only `formSubmitted` is emitted. */
133
178
  emitOnly?: boolean;
179
+ /**
180
+ * Base URL for CMS consent fetch (`consentServiceName` fields).
181
+ * e.g. `https://cms.example.com/api` → GET `{base}/consents/{name}`.
182
+ */
183
+ consentBaseUrl?: string;
134
184
  }
135
185
  type DgaFormLocale = 'ar' | 'en';
136
186
 
@@ -184,6 +234,10 @@ declare const DGA_LOOKUP_ADAPTER: InjectionToken<DgaLookupAdapter>;
184
234
  declare const DGA_SUBMIT_ADAPTER: InjectionToken<DgaSubmitAdapter>;
185
235
  declare const DGA_FORM_TOAST_ADAPTER: InjectionToken<DgaFormToastAdapter>;
186
236
  declare const DGA_CAPTCHA_ADAPTER: InjectionToken<DgaCaptchaAdapter>;
237
+ interface DgaHttpSecurityOptions {
238
+ allowedOrigins?: string[];
239
+ }
240
+ declare const DGA_HTTP_SECURITY: InjectionToken<DgaHttpSecurityOptions>;
187
241
  declare const DGA_FORM_I18N_ADAPTER: InjectionToken<DgaFormI18nAdapter>;
188
242
  declare const DGA_DYNAMIC_FORM_FIELD_REGISTRY: InjectionToken<DgaDynamicFieldRenderer[]>;
189
243
  /** Context passed into custom field renderer components (optional inject). */
@@ -219,8 +273,10 @@ declare class DgaDynamicForm {
219
273
  readonly formError: _angular_core.OutputEmitterRef<unknown>;
220
274
  readonly clearButtonClick: _angular_core.OutputEmitterRef<void>;
221
275
  readonly draftSaved: _angular_core.OutputEmitterRef<Record<string, unknown>>;
276
+ readonly fileDownload: _angular_core.OutputEmitterRef<DgaExistingAttachment>;
222
277
  private readonly fb;
223
278
  private readonly formService;
279
+ private readonly submitAdapter;
224
280
  private readonly toast;
225
281
  private readonly i18n;
226
282
  private readonly destroyRef;
@@ -261,7 +317,10 @@ declare class DgaDynamicForm {
261
317
  options: DgaFormFieldOption[];
262
318
  }): void;
263
319
  previousStep(): void;
320
+ /** Keep stepper clicks aligned with Next validation / allowSkip rules. */
321
+ readonly canActivateStep: (next: number, current: number) => boolean;
264
322
  nextStep(): void;
323
+ private skipEmptyStepsForward;
265
324
  onClear(): void;
266
325
  onSaveDraft(): void;
267
326
  onSubmit(): void;
@@ -274,7 +333,7 @@ declare class DgaDynamicForm {
274
333
  private visibleValid;
275
334
  private collectVisibleFieldNames;
276
335
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<DgaDynamicForm, never>;
277
- 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>;
336
+ 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"; "fileDownload": "fileDownload"; }, never, never, true, never>;
278
337
  }
279
338
 
280
339
  declare class DgaDynamicFormField implements OnInit {
@@ -284,11 +343,14 @@ declare class DgaDynamicFormField implements OnInit {
284
343
  readonly required: _angular_core.InputSignalWithTransform<boolean, unknown>;
285
344
  readonly error: _angular_core.InputSignal<string>;
286
345
  readonly runtimeOptions: _angular_core.InputSignal<DgaFormFieldOption[]>;
346
+ readonly consentBaseUrl: _angular_core.InputSignal<string | undefined>;
287
347
  readonly optionsLoaded: _angular_core.OutputEmitterRef<{
288
348
  name: string;
289
349
  options: DgaFormFieldOption[];
290
350
  }>;
351
+ readonly fileDownload: _angular_core.OutputEmitterRef<DgaExistingAttachment>;
291
352
  private readonly lookup;
353
+ private readonly http;
292
354
  private readonly destroyRef;
293
355
  readonly loadedOptions: _angular_core.WritableSignal<DgaFormFieldOption[]>;
294
356
  readonly controlId: string;
@@ -299,14 +361,19 @@ declare class DgaDynamicFormField implements OnInit {
299
361
  readonly resolvedOptions: _angular_core.Signal<DgaFormFieldOption[]>;
300
362
  readonly selectOptions: _angular_core.Signal<DgaSelectOption[]>;
301
363
  ngOnInit(): void;
364
+ private mapExistingFileValue;
365
+ private fetchConsent;
366
+ private fetchLookup;
302
367
  optionLabel(opt: DgaFormFieldOption): string;
368
+ isPasswordField(): boolean;
303
369
  isChecked(value: unknown): boolean;
304
370
  toggleCheckbox(value: unknown, checked: boolean): void;
305
- onSelectSearch(_value: string | string[]): void;
371
+ onSelectSearchQuery(query: string): void;
372
+ onStagedFileDownload(event: DgaExistingAttachment): void;
306
373
  onRestrict(event: Event): void;
307
374
  private applyLookupFilter;
308
375
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<DgaDynamicFormField, never>;
309
- 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>;
376
+ 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; }; "consentBaseUrl": { "alias": "consentBaseUrl"; "required": false; "isSignal": true; }; }, { "optionsLoaded": "optionsLoaded"; "fileDownload": "fileDownload"; }, never, never, true, never>;
310
377
  }
311
378
 
312
379
  declare class DgaDynamicFormService {
@@ -319,9 +386,14 @@ declare class DgaDynamicFormService {
319
386
  hasFileValues(data: Record<string, unknown>): boolean;
320
387
  toFormData(data: Record<string, unknown>): FormData;
321
388
  submitForm(config: DgaDynamicFormConfig, values: Record<string, unknown>, fields: DgaFormField[]): Observable<unknown>;
322
- saveDraft(formId: string, values: Record<string, unknown>): void;
389
+ /**
390
+ * Persist a draft. Sensitive fields (otp, file, password-like names,
391
+ * `excludeFromDraft` / `sensitive`) are stripped before writing.
392
+ */
393
+ saveDraft(formId: string, values: Record<string, unknown>, fields?: DgaFormField[]): void;
323
394
  loadDraft(formId: string): Record<string, unknown> | null;
324
395
  clearDraft(formId: string): void;
396
+ private isDraftBlocked;
325
397
  private storageKey;
326
398
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<DgaDynamicFormService, never>;
327
399
  static ɵprov: _angular_core.ɵɵInjectableDeclaration<DgaDynamicFormService>;
@@ -340,8 +412,19 @@ declare function dgaFindSelectedOption(field: DgaFormField | undefined, value: u
340
412
  /**
341
413
  * Whether a field should be visible given the current form values.
342
414
  */
343
- declare function dgaIsFieldVisible(field: DgaFormField, values: Record<string, unknown>, getOptions?: (name: string) => DgaFormFieldOption[] | undefined): boolean;
344
- declare function dgaIsRequiredWhen(field: DgaFormField, values: Record<string, unknown>, getOptions?: (name: string) => DgaFormFieldOption[] | undefined): boolean;
415
+ declare function dgaIsFieldVisible(field: DgaFormField, values: Record<string, unknown>, getOptions?: (name: string) => DgaFormFieldOption[] | undefined, getField?: (name: string) => DgaFormField | undefined): boolean;
416
+ declare function dgaIsRequiredWhen(field: DgaFormField, values: Record<string, unknown>, getOptions?: (name: string) => DgaFormFieldOption[] | undefined, getField?: (name: string) => DgaFormField | undefined): boolean;
417
+
418
+ /** Known start/end date field pairs used by cross-field validators. */
419
+ declare const DGA_DATE_RANGE_PAIRS: ReadonlyArray<[string, string]>;
420
+ declare function dgaPasswordMatchValidator(passwordControl: AbstractControl): ValidatorFn;
421
+ /**
422
+ * Saudi mobile validation for type: phone fields.
423
+ * Accepts E.164 (+966…) or national digits (0xxxxxxxx or 5xxxxxxxx).
424
+ */
425
+ declare function dgaSaudiMobileValidator(control: AbstractControl): ValidationErrors | null;
426
+ /** Wire password match + date-range validators after form group is built. */
427
+ declare function dgaApplyCrossFieldValidators(form: FormGroup, fields: DgaFormField[]): void;
345
428
 
346
429
  /** Default bilingual label resolver. */
347
430
  declare const defaultFormI18nAdapter: DgaFormI18nAdapter;
@@ -349,13 +432,7 @@ declare const defaultFormI18nAdapter: DgaFormI18nAdapter;
349
432
  declare const noopCaptchaAdapter: DgaCaptchaAdapter;
350
433
  /** Empty lookup (returns []). Override in the host app. */
351
434
  declare const emptyLookupAdapter: DgaLookupAdapter;
352
- interface DgaHttpAdapterSecurityOptions {
353
- /**
354
- * Allowed URL origins for schema-driven HTTP (e.g. `https://api.example.com`).
355
- * When set, requests outside the list are rejected.
356
- * When omitted, only `http:` / `https:` are accepted (no `file:` / opaque URLs).
357
- */
358
- allowedOrigins?: string[];
435
+ interface DgaHttpAdapterSecurityOptions extends DgaHttpSecurityOptions {
359
436
  }
360
437
  /**
361
438
  * Validates schema-supplied URLs before HttpClient calls.
@@ -378,6 +455,11 @@ interface ProvideDgaDynamicFormOptions {
378
455
  dgaToast?: boolean;
379
456
  /** Optional origin allowlist for HTTP adapters (recommended for CMS schemas). */
380
457
  allowedOrigins?: string[];
458
+ /**
459
+ * When true, `allowedOrigins` is required if httpLookup or httpSubmit is enabled.
460
+ * Defaults to false (warn in dev). Set true for CMS / untrusted schemas.
461
+ */
462
+ requireAllowedOrigins?: boolean;
381
463
  }
382
464
  /**
383
465
  * Registers default adapters for lookup, submit, toast, captcha, and i18n.
@@ -393,5 +475,5 @@ declare function resolveFormLabel(label: {
393
475
  declare function dgaResolveLabel(label: DgaFormLabel | undefined, locale: DgaFormLocale, fallback?: string): string;
394
476
  declare function dgaDetectLocale(): DgaFormLocale;
395
477
 
396
- 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, dgaAssertSafeHttpUrl, dgaBuildFormGroup, dgaBuildValidators, dgaCollectFields, dgaCreateControl, dgaDefaultValueForField, dgaDetectLocale, dgaFindSelectedOption, dgaIsFieldVisible, dgaIsRequiredWhen, dgaResolveLabel, dgaSetControlValidators, emptyLookupAdapter, noopCaptchaAdapter, provideDgaDynamicForm, resolveFormLabel };
397
- export type { DgaCaptchaAdapter, DgaDynamicFieldHostContext, DgaDynamicFieldRenderer, DgaDynamicFormConfig, DgaDynamicFormPayloadTransformer, DgaFormField, DgaFormFieldColumns, DgaFormFieldOption, DgaFormFieldType, DgaFormI18nAdapter, DgaFormLabel, DgaFormLocale, DgaFormStep, DgaFormToastAdapter, DgaHttpAdapterSecurityOptions, DgaHttpMethod, DgaInputRestriction, DgaLookupAdapter, DgaLookupRequest, DgaSubmitAdapter, DgaSubmitRequest, DgaToastVariant, DgaWizardConfig, ProvideDgaDynamicFormOptions };
478
+ export { DGA_CAPTCHA_ADAPTER, DGA_DATE_RANGE_PAIRS, DGA_DYNAMIC_FORM_FIELD_REGISTRY, DGA_FORM_I18N_ADAPTER, DGA_FORM_TOAST_ADAPTER, DGA_HTTP_SECURITY, DGA_LOOKUP_ADAPTER, DGA_SUBMIT_ADAPTER, DgaDynamicForm, DgaDynamicFormField, DgaDynamicFormFieldRegistry, DgaDynamicFormService, createDgaToastAdapter, createHttpLookupAdapter, createHttpSubmitAdapter, defaultFormI18nAdapter, dgaApplyCrossFieldValidators, dgaAssertSafeHttpUrl, dgaBuildFormGroup, dgaBuildValidators, dgaCollectFields, dgaCreateControl, dgaDefaultValueForField, dgaDetectLocale, dgaFindSelectedOption, dgaIsFieldVisible, dgaIsRequiredWhen, dgaPasswordMatchValidator, dgaResolveLabel, dgaSaudiMobileValidator, dgaSetControlValidators, emptyLookupAdapter, noopCaptchaAdapter, provideDgaDynamicForm, resolveFormLabel };
479
+ export type { DgaCaptchaAdapter, DgaDynamicFieldHostContext, DgaDynamicFieldRenderer, DgaDynamicFormConfig, DgaDynamicFormPayloadTransformer, DgaExistingAttachment, DgaFormField, DgaFormFieldColumns, DgaFormFieldOption, DgaFormFieldType, DgaFormI18nAdapter, DgaFormLabel, DgaFormLocale, DgaFormStep, DgaFormToastAdapter, DgaHttpAdapterSecurityOptions, DgaHttpMethod, DgaHttpSecurityOptions, DgaInputRestriction, DgaLookupAdapter, DgaLookupRequest, DgaStagedFileRef, DgaSubmitAdapter, DgaSubmitRequest, DgaToastVariant, DgaWizardConfig, ProvideDgaDynamicFormOptions };