ngx-dynamic-entity 1.0.0 → 1.2.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 +129 -139
- package/esm2022/lib/field-types/array-field.component.mjs +112 -104
- package/esm2022/lib/field-types/boolean-field.component.mjs +62 -61
- package/esm2022/lib/field-types/checkbox-field.component.mjs +48 -47
- package/esm2022/lib/field-types/currency-field.component.mjs +54 -53
- package/esm2022/lib/field-types/date-field.component.mjs +46 -45
- package/esm2022/lib/field-types/dropdown-field.component.mjs +89 -87
- package/esm2022/lib/field-types/email-field.component.mjs +64 -63
- package/esm2022/lib/field-types/entity-ref-field.component.mjs +85 -71
- package/esm2022/lib/field-types/field-component.contract.mjs +2 -0
- package/esm2022/lib/field-types/file-field.component.mjs +88 -87
- package/esm2022/lib/field-types/group-field.component.mjs +50 -50
- package/esm2022/lib/field-types/image-field.component.mjs +92 -91
- package/esm2022/lib/field-types/month-year-field.component.mjs +80 -79
- package/esm2022/lib/field-types/multi-select-field.component.mjs +66 -65
- package/esm2022/lib/field-types/number-field.component.mjs +71 -75
- package/esm2022/lib/field-types/password-field.component.mjs +68 -67
- package/esm2022/lib/field-types/radio-field.component.mjs +66 -65
- package/esm2022/lib/field-types/text-field.component.mjs +81 -74
- package/esm2022/lib/field-types/textarea-field.component.mjs +48 -47
- package/esm2022/lib/form/dynamic-field/dynamic-field.component.mjs +53 -6
- package/esm2022/lib/form/dynamic-form.component.mjs +309 -20
- package/esm2022/lib/form/dynamic-record-form.component.mjs +36 -11
- package/esm2022/lib/providers/provide-field-types.mjs +1 -1
- package/esm2022/lib/providers/provide-ngx-dynamic-entity.mjs +8 -2
- package/esm2022/lib/services/cascade-data.service.mjs +2 -2
- package/esm2022/lib/services/entity-ref-cache.mjs +1 -1
- package/esm2022/lib/services/entity-ref-registry.service.mjs +2 -2
- package/esm2022/lib/services/entity-ref-selection.service.mjs +1 -1
- package/esm2022/lib/services/entity-reference.service.mjs +1 -1
- package/esm2022/lib/services/field-registry.service.mjs +1 -1
- package/esm2022/lib/services/file-upload.service.mjs +1 -1
- package/esm2022/lib/services/lookup-registry.service.mjs +2 -2
- package/esm2022/lib/services/rbac.service.mjs +1 -1
- package/esm2022/lib/services/validation-messages.service.mjs +56 -0
- package/esm2022/lib/services/validator-registry.service.mjs +35 -2
- package/esm2022/lib/tokens/injection-tokens.mjs +35 -6
- package/esm2022/public-api.mjs +23 -2
- package/fesm2022/ngx-dynamic-entity.mjs +1754 -1236
- package/fesm2022/ngx-dynamic-entity.mjs.map +1 -1
- package/lib/field-types/array-field.component.d.ts +7 -1
- package/lib/field-types/dropdown-field.component.d.ts +2 -1
- package/lib/field-types/entity-ref-field.component.d.ts +12 -2
- package/lib/field-types/field-component.contract.d.ts +49 -0
- package/lib/field-types/group-field.component.d.ts +1 -1
- package/lib/field-types/multi-select-field.component.d.ts +1 -1
- package/lib/field-types/number-field.component.d.ts +1 -0
- package/lib/field-types/radio-field.component.d.ts +1 -1
- package/lib/field-types/text-field.component.d.ts +10 -2
- package/lib/form/dynamic-field/dynamic-field.component.d.ts +39 -4
- package/lib/form/dynamic-form.component.d.ts +155 -8
- package/lib/form/dynamic-record-form.component.d.ts +13 -2
- package/lib/providers/provide-ngx-dynamic-entity.d.ts +25 -3
- package/lib/services/cascade-data.service.d.ts +1 -1
- package/lib/services/entity-ref-registry.service.d.ts +1 -1
- package/lib/services/lookup-registry.service.d.ts +1 -1
- package/lib/services/validation-messages.service.d.ts +43 -0
- package/lib/services/validator-registry.service.d.ts +13 -1
- package/lib/tokens/injection-tokens.d.ts +42 -7
- package/package.json +17 -18
- package/public-api.d.ts +12 -0
- package/styles.css +579 -0
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { AbstractControl, FormArray } from '@angular/forms';
|
|
2
2
|
import type { NestedFieldConfig, EntityFormConfig } from '@dynamic-entity/core';
|
|
3
3
|
import * as i0 from "@angular/core";
|
|
4
|
-
/**
|
|
4
|
+
/** Renders repeating rows for a FormArray, with Add Item and Remove. */
|
|
5
5
|
export declare class ArrayFieldComponent {
|
|
6
|
+
/**
|
|
7
|
+
* addItem/removeItem are public and may be called from outside this component's template,
|
|
8
|
+
* which under OnPush would not re-render. The host also watches the control, but this
|
|
9
|
+
* keeps the component correct on its own.
|
|
10
|
+
*/
|
|
11
|
+
private readonly cdr;
|
|
6
12
|
field: NestedFieldConfig;
|
|
7
13
|
control: AbstractControl;
|
|
8
14
|
config: EntityFormConfig;
|
|
@@ -3,6 +3,7 @@ import type { NestedFieldConfig } from '@dynamic-entity/core';
|
|
|
3
3
|
import * as i0 from "@angular/core";
|
|
4
4
|
export declare class DropdownFieldComponent {
|
|
5
5
|
private readonly lookups;
|
|
6
|
+
private readonly messages;
|
|
6
7
|
private _field;
|
|
7
8
|
private _language;
|
|
8
9
|
/** Setter-based so options resolve however the input is set — see `refreshChoiceOptions`. */
|
|
@@ -14,7 +15,7 @@ export declare class DropdownFieldComponent {
|
|
|
14
15
|
readonly: boolean;
|
|
15
16
|
masked: boolean;
|
|
16
17
|
/** Inline `options`, or the field's named list resolved through the registry (§6.3). */
|
|
17
|
-
readonly options: import("@angular/core").WritableSignal<
|
|
18
|
+
readonly options: import("@angular/core").WritableSignal<DropdownOption[]>;
|
|
18
19
|
readonly compareFn: (o1: any, o2: any) => boolean;
|
|
19
20
|
isObjectVal(option: any): boolean;
|
|
20
21
|
get label(): string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { OnInit } from '@angular/core';
|
|
2
2
|
import { AbstractControl } from '@angular/forms';
|
|
3
|
-
import type { NestedFieldConfig
|
|
3
|
+
import type { NestedFieldConfig } from '@dynamic-entity/core';
|
|
4
4
|
import * as i0 from "@angular/core";
|
|
5
5
|
/**
|
|
6
6
|
* EntityRefFieldComponent — a select populated from a consumer-registered loader.
|
|
@@ -29,7 +29,17 @@ export declare class EntityRefFieldComponent implements OnInit {
|
|
|
29
29
|
ngOnInit(): void;
|
|
30
30
|
/** Reload options against the parent's current value. */
|
|
31
31
|
reload(): Promise<void>;
|
|
32
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Publish the selected option so autoPatch can copy from its record.
|
|
34
|
+
*
|
|
35
|
+
* Driven from `valueChanges` rather than the select's `change` event: that event is the
|
|
36
|
+
* same one the `formControl` directive listens to, so reading the control inside a
|
|
37
|
+
* template `(change)` handler can see the previous value. `valueChanges` fires after the
|
|
38
|
+
* model has updated — including Playwright `selectOption`, which updates the control
|
|
39
|
+
* through the value accessor.
|
|
40
|
+
*/
|
|
41
|
+
onSelectionChange(value?: string): void;
|
|
42
|
+
private publishSelection;
|
|
33
43
|
getLabel(value: any): string;
|
|
34
44
|
private parentValue;
|
|
35
45
|
/** Reload (and drop a stale selection) whenever the parent control changes. */
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { AbstractControl } from '@angular/forms';
|
|
2
|
+
import type { NestedFieldConfig } from '@dynamic-entity/core';
|
|
3
|
+
/**
|
|
4
|
+
* What a field component must accept to be registered with `provideFieldTypes`.
|
|
5
|
+
*
|
|
6
|
+
* The renderer instantiates a field component dynamically and assigns exactly these five
|
|
7
|
+
* inputs — no more, no less — so any component satisfying this interface can stand in for a
|
|
8
|
+
* built-in type or add a new one.
|
|
9
|
+
*
|
|
10
|
+
* Implement it explicitly and TypeScript will tell you when you have missed one:
|
|
11
|
+
*
|
|
12
|
+
* ```typescript
|
|
13
|
+
* @Component({
|
|
14
|
+
* selector: 'app-signature-field',
|
|
15
|
+
* standalone: true,
|
|
16
|
+
* template: `<canvas [attr.aria-label]="field.id"></canvas>`,
|
|
17
|
+
* })
|
|
18
|
+
* export class SignatureFieldComponent implements DynamicFieldComponentContract {
|
|
19
|
+
* @Input() field!: NestedFieldConfig;
|
|
20
|
+
* @Input() control!: AbstractControl;
|
|
21
|
+
* @Input() language = 'en';
|
|
22
|
+
* @Input() readonly = false;
|
|
23
|
+
* @Input() masked = false;
|
|
24
|
+
* }
|
|
25
|
+
*
|
|
26
|
+
* provideFieldTypes({ signature: SignatureFieldComponent });
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* Two implementation notes that are easy to get wrong:
|
|
30
|
+
*
|
|
31
|
+
* - Declare the inputs with definite assignment (`field!: …`) or an initialiser. The
|
|
32
|
+
* renderer sets them through `ComponentRef.setInput()` after construction, so they are
|
|
33
|
+
* genuinely undefined until the first assignment.
|
|
34
|
+
* - `masked` means the value must not be displayed. Render a placeholder rather than the
|
|
35
|
+
* control's value. This is presentational: the real value stays in the control and is
|
|
36
|
+
* included in the submitted record, so it is not an access-control boundary.
|
|
37
|
+
*/
|
|
38
|
+
export interface DynamicFieldComponentContract {
|
|
39
|
+
/** The field's schema entry: id, type, label, validators, options, and so on. */
|
|
40
|
+
field: NestedFieldConfig;
|
|
41
|
+
/** The reactive control backing this field. A `FormGroup` for `group`, `FormArray` for `array`. */
|
|
42
|
+
control: AbstractControl;
|
|
43
|
+
/** Active language, for resolving `LocalizedText` labels, placeholders and options. */
|
|
44
|
+
language: string;
|
|
45
|
+
/** Render the value as text rather than an editable control. */
|
|
46
|
+
readonly: boolean;
|
|
47
|
+
/** Hide the value from display. See the note above — presentational only. */
|
|
48
|
+
masked: boolean;
|
|
49
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AbstractControl } from '@angular/forms';
|
|
2
2
|
import type { NestedFieldConfig, EntityFormConfig } from '@dynamic-entity/core';
|
|
3
3
|
import * as i0 from "@angular/core";
|
|
4
|
-
/**
|
|
4
|
+
/** Renders a nested fieldset container for a group field's children. */
|
|
5
5
|
export declare class GroupFieldComponent {
|
|
6
6
|
field: NestedFieldConfig;
|
|
7
7
|
control: AbstractControl;
|
|
@@ -14,7 +14,7 @@ export declare class MultiSelectFieldComponent {
|
|
|
14
14
|
readonly: boolean;
|
|
15
15
|
masked: boolean;
|
|
16
16
|
/** Inline `options`, or the field's named list resolved through the registry (§6.3). */
|
|
17
|
-
readonly options: import("@angular/core").WritableSignal<
|
|
17
|
+
readonly options: import("@angular/core").WritableSignal<DropdownOption[]>;
|
|
18
18
|
readonly compareFn: (o1: any, o2: any) => boolean;
|
|
19
19
|
isObjectVal(option: any): boolean;
|
|
20
20
|
get label(): string;
|
|
@@ -2,6 +2,7 @@ import { AbstractControl } from '@angular/forms';
|
|
|
2
2
|
import type { NestedFieldConfig } from '@dynamic-entity/core';
|
|
3
3
|
import * as i0 from "@angular/core";
|
|
4
4
|
export declare class NumberFieldComponent {
|
|
5
|
+
private readonly messages;
|
|
5
6
|
field: NestedFieldConfig;
|
|
6
7
|
control: AbstractControl;
|
|
7
8
|
language: string;
|
|
@@ -15,7 +15,7 @@ export declare class RadioFieldComponent {
|
|
|
15
15
|
readonly: boolean;
|
|
16
16
|
masked: boolean;
|
|
17
17
|
/** Inline `options`, or the field's named list resolved through the registry (§6.3). */
|
|
18
|
-
readonly options: import("@angular/core").WritableSignal<
|
|
18
|
+
readonly options: import("@angular/core").WritableSignal<DropdownOption[]>;
|
|
19
19
|
get label(): string;
|
|
20
20
|
getOptStoredVal(option: any): any;
|
|
21
21
|
/**
|
|
@@ -1,12 +1,20 @@
|
|
|
1
|
+
import { OnDestroy } from '@angular/core';
|
|
1
2
|
import { AbstractControl } from '@angular/forms';
|
|
2
3
|
import type { NestedFieldConfig } from '@dynamic-entity/core';
|
|
3
4
|
import * as i0 from "@angular/core";
|
|
4
|
-
export declare class TextFieldComponent {
|
|
5
|
+
export declare class TextFieldComponent implements OnDestroy {
|
|
6
|
+
private readonly messages;
|
|
7
|
+
private displaySub?;
|
|
5
8
|
field: NestedFieldConfig;
|
|
6
|
-
control: AbstractControl;
|
|
9
|
+
set control(value: AbstractControl);
|
|
10
|
+
get control(): AbstractControl;
|
|
11
|
+
private _control;
|
|
7
12
|
language: string;
|
|
8
13
|
readonly: boolean;
|
|
9
14
|
masked: boolean;
|
|
15
|
+
/** Readonly markup reads this signal so an external patch (autoPatch) is visible under OnPush. */
|
|
16
|
+
readonly displayValue: import("@angular/core").WritableSignal<unknown>;
|
|
17
|
+
ngOnDestroy(): void;
|
|
10
18
|
get label(): string;
|
|
11
19
|
get placeholder(): string;
|
|
12
20
|
get errorMessage(): string;
|
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
import { OnChanges, SimpleChanges } from '@angular/core';
|
|
1
|
+
import { OnChanges, OnDestroy, SimpleChanges } from '@angular/core';
|
|
2
2
|
import { AbstractControl } from '@angular/forms';
|
|
3
3
|
import type { NestedFieldConfig, EntityFormConfig } from '@dynamic-entity/core';
|
|
4
4
|
import * as i0 from "@angular/core";
|
|
5
5
|
/**
|
|
6
6
|
* DynamicFieldComponent — mounts the correct field component for a given NestedFieldConfig.
|
|
7
|
-
* Uses ViewContainerRef.createComponent() and passes inputs via setInput()
|
|
7
|
+
* Uses ViewContainerRef.createComponent() and passes inputs via setInput(); see
|
|
8
|
+
* DynamicFieldComponentContract for the five inputs every field component receives.
|
|
8
9
|
* Never add field-type-specific logic here — this component must remain generic.
|
|
9
10
|
*
|
|
10
11
|
* The 5 inputs (field, control, language, readonly, masked) are passed to every mounted
|
|
11
12
|
* component uniformly — no special-casing per type.
|
|
12
13
|
*/
|
|
13
|
-
export declare class DynamicFieldComponent implements OnChanges {
|
|
14
|
+
export declare class DynamicFieldComponent implements OnChanges, OnDestroy {
|
|
14
15
|
field: NestedFieldConfig;
|
|
15
16
|
control: AbstractControl;
|
|
16
17
|
config: EntityFormConfig;
|
|
@@ -21,6 +22,12 @@ export declare class DynamicFieldComponent implements OnChanges {
|
|
|
21
22
|
private readonly fieldHost;
|
|
22
23
|
private readonly fieldRegistry;
|
|
23
24
|
private readonly rbacService;
|
|
25
|
+
/**
|
|
26
|
+
* Passed into `createComponent` so a hosted field sees this host's injector — including
|
|
27
|
+
* `DynamicFormComponent`'s scoped `EntityRefSelectionService`. Without it, a standalone
|
|
28
|
+
* field resolves the root bus and `autoPatch` never runs.
|
|
29
|
+
*/
|
|
30
|
+
private readonly injector;
|
|
24
31
|
private componentRef;
|
|
25
32
|
ngOnChanges(_changes: SimpleChanges): void;
|
|
26
33
|
private mountField;
|
|
@@ -32,7 +39,35 @@ export declare class DynamicFieldComponent implements OnChanges {
|
|
|
32
39
|
private warnUnresolved;
|
|
33
40
|
private static readonly warnedTypes;
|
|
34
41
|
/**
|
|
35
|
-
*
|
|
42
|
+
* Re-check the hosted field component.
|
|
43
|
+
*
|
|
44
|
+
* The field components are OnPush, so they re-render on an input change, an event from
|
|
45
|
+
* their own template, or a signal they read. `form.markAllAsTouched()` is none of those —
|
|
46
|
+
* it flips `touched` from outside, and every field's error message depends on it. Without
|
|
47
|
+
* this, a blocked submit would mark the form touched and no error would appear.
|
|
48
|
+
*/
|
|
49
|
+
refresh(): void;
|
|
50
|
+
/**
|
|
51
|
+
* Re-check the hosted component whenever its control changes from outside.
|
|
52
|
+
*
|
|
53
|
+
* The field components are OnPush, so they re-render on an input change or an event from
|
|
54
|
+
* their own template. A control mutated externally is neither — and that is not an edge
|
|
55
|
+
* case: patchForm, reset, autoPatch and patchOnTrue all do exactly that. Without this, a
|
|
56
|
+
* field patched from a record would keep rendering its previous value.
|
|
57
|
+
*
|
|
58
|
+
* `detectChanges()` rather than `markForCheck()`: a selection that arrives from a native
|
|
59
|
+
* `change` event (or Playwright) may not be followed by another Angular tick, so a dirty
|
|
60
|
+
* flag would never be flushed. Running CD on the hosted component now is the copy showing up.
|
|
61
|
+
*
|
|
62
|
+
* Done here rather than in each of the eighteen field components: the host owns the
|
|
63
|
+
* component reference, so one subscription covers every type including custom ones.
|
|
64
|
+
*/
|
|
65
|
+
private watchControl;
|
|
66
|
+
private controlSub?;
|
|
67
|
+
ngOnDestroy(): void;
|
|
68
|
+
/**
|
|
69
|
+
* Pass all 5 contract inputs via ComponentRef.setInput() — uniform for all types.
|
|
70
|
+
* The public shape is DynamicFieldComponentContract.
|
|
36
71
|
* Must use setInput(), not property assignment: the field components declare inputs with
|
|
37
72
|
* definite assignment and no initializer, and the lib compiles with
|
|
38
73
|
* useDefineForClassFields:false, so `'field' in instance` is false until first set.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { EventEmitter, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core';
|
|
2
2
|
import { AbstractControl, FormArray, FormGroup } from '@angular/forms';
|
|
3
3
|
import type { EntityFormConfig, FormRule, NestedFieldConfig, NestedTabConfig } from '@dynamic-entity/core';
|
|
4
|
+
import { RbacService } from '../services/rbac.service';
|
|
4
5
|
import * as i0 from "@angular/core";
|
|
5
6
|
/**
|
|
6
7
|
* DynamicFormComponent — the main form component.
|
|
@@ -9,7 +10,21 @@ import * as i0 from "@angular/core";
|
|
|
9
10
|
* keyboard shortcuts (Ctrl+S, Esc), and RBAC-gated submission.
|
|
10
11
|
*/
|
|
11
12
|
export declare class DynamicFormComponent implements OnInit, OnChanges, OnDestroy {
|
|
12
|
-
|
|
13
|
+
/**
|
|
14
|
+
* The schema to render.
|
|
15
|
+
*
|
|
16
|
+
* An accessor pair rather than a plain field: `ngOnChanges` normalises the option shape,
|
|
17
|
+
* and that result is kept here instead of being written back over the input. Reassigning
|
|
18
|
+
* an `@Input` works, but it mutates what the parent handed us — and a parent using OnPush
|
|
19
|
+
* or signals would never observe the replacement anyway.
|
|
20
|
+
*
|
|
21
|
+
* Reads of `this.config` get the normalised copy once it exists, so every internal caller
|
|
22
|
+
* is unaffected.
|
|
23
|
+
*/
|
|
24
|
+
set config(value: EntityFormConfig);
|
|
25
|
+
get config(): EntityFormConfig;
|
|
26
|
+
private rawConfig;
|
|
27
|
+
private normalizedConfig?;
|
|
13
28
|
rules?: FormRule[];
|
|
14
29
|
initialData?: Record<string, any>;
|
|
15
30
|
/** Session-original values for `VALUE_CHANGED` rules. Captured from the first build when omitted. */
|
|
@@ -42,6 +57,14 @@ export declare class DynamicFormComponent implements OnInit, OnChanges, OnDestro
|
|
|
42
57
|
formReset: EventEmitter<void>;
|
|
43
58
|
/** The visible tab changed. Lets a host track it without reaching into this component. */
|
|
44
59
|
activeTabChange: EventEmitter<string>;
|
|
60
|
+
/**
|
|
61
|
+
* Emitted when a `beforeSave` hook aborted the save — by returning false or throwing.
|
|
62
|
+
* `formSubmit` does not fire in that case.
|
|
63
|
+
*/
|
|
64
|
+
saveRejected: EventEmitter<{
|
|
65
|
+
reason: string;
|
|
66
|
+
error?: unknown;
|
|
67
|
+
}>;
|
|
45
68
|
private readonly fb;
|
|
46
69
|
private readonly validatorRegistry;
|
|
47
70
|
private readonly hookRegistry;
|
|
@@ -49,6 +72,7 @@ export declare class DynamicFormComponent implements OnInit, OnChanges, OnDestro
|
|
|
49
72
|
private readonly rulesEvaluation;
|
|
50
73
|
private readonly entityRefSelection;
|
|
51
74
|
private readonly commonModulesRegistry;
|
|
75
|
+
private readonly migrations;
|
|
52
76
|
protected readonly Object: ObjectConstructor;
|
|
53
77
|
readonly activeTab: import("@angular/core").WritableSignal<string>;
|
|
54
78
|
readonly activeSubTab: import("@angular/core").WritableSignal<string>;
|
|
@@ -58,13 +82,17 @@ export declare class DynamicFormComponent implements OnInit, OnChanges, OnDestro
|
|
|
58
82
|
readonly sessionBaseline: import("@angular/core").WritableSignal<Record<string, any>>;
|
|
59
83
|
/** Critical fields the user has explicitly unlocked for editing. */
|
|
60
84
|
readonly unlockedFields: import("@angular/core").WritableSignal<ReadonlySet<string>>;
|
|
85
|
+
/** The active tab's field panel, focused after a tab switch. */
|
|
86
|
+
private formPanel?;
|
|
87
|
+
/** The hosted field components, so an external control-state change can reach them. */
|
|
88
|
+
private fieldHosts?;
|
|
61
89
|
form: FormGroup;
|
|
62
90
|
private valueSub?;
|
|
63
91
|
private selectionSub?;
|
|
64
92
|
/** Previous values, for detecting `patchOnTrue` false→true transitions. */
|
|
65
93
|
private previousValues;
|
|
66
94
|
handleKeyboardEvent(event: KeyboardEvent): void;
|
|
67
|
-
readonly ruleResult: import("@angular/core").Signal<
|
|
95
|
+
readonly ruleResult: import("@angular/core").Signal<RuleEvaluationResult>;
|
|
68
96
|
/**
|
|
69
97
|
* Critical fields whose value differs from the session baseline.
|
|
70
98
|
* These drive the deferred `VALUE_CHANGED` banner — a critical edit is announced once,
|
|
@@ -78,12 +106,57 @@ export declare class DynamicFormComponent implements OnInit, OnChanges, OnDestro
|
|
|
78
106
|
get activeSubTabConfig(): NestedTabConfig | null;
|
|
79
107
|
get activeTabModuleComponent(): any | null;
|
|
80
108
|
get fieldsForActiveTab(): NestedFieldConfig[];
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
109
|
+
/**
|
|
110
|
+
* Entity-level permissions for the current roles.
|
|
111
|
+
*
|
|
112
|
+
* Cached rather than recomputed: this is read from the template several times per change
|
|
113
|
+
* detection pass, and a fresh object each time is both wasted allocation and a new
|
|
114
|
+
* identity for anything comparing by reference. Invalidated in `ngOnChanges` when
|
|
115
|
+
* `config` or `userRoles` change, which are its only inputs.
|
|
116
|
+
*/
|
|
117
|
+
get permissions(): ReturnType<RbacService['getPermissions']>;
|
|
118
|
+
private permissionsCache;
|
|
119
|
+
/**
|
|
120
|
+
* Whether the current roles may see this record at all.
|
|
121
|
+
*
|
|
122
|
+
* `permissions.view` used to be computed and thrown away — a user whose roles failed it
|
|
123
|
+
* still received the complete form with every value in the DOM. It is honoured now.
|
|
124
|
+
*
|
|
125
|
+
* As with masking, this is presentational: it stops the browser rendering data, it does
|
|
126
|
+
* not stop the data reaching the browser. Authorize on the server.
|
|
127
|
+
*/
|
|
128
|
+
get canView(): boolean;
|
|
129
|
+
/**
|
|
130
|
+
* Whether the current roles may delete this record.
|
|
131
|
+
*
|
|
132
|
+
* The library ships no delete affordance, so this is surfaced for the consumer to gate
|
|
133
|
+
* their own — exposed rather than dropped, because a permission the schema declares
|
|
134
|
+
* should be answerable.
|
|
135
|
+
*/
|
|
136
|
+
get canDelete(): boolean;
|
|
86
137
|
get canSubmit(): boolean;
|
|
138
|
+
/**
|
|
139
|
+
* Errors raised by `validation` rules, keyed by the target they point at.
|
|
140
|
+
*
|
|
141
|
+
* A rule can declare the record invalid independently of Angular's validators, so form
|
|
142
|
+
* validity alone is not the whole picture. `DynamicRecordFormComponent.saveSection()`
|
|
143
|
+
* has always honoured these; `submit()` did not, which meant the same rule blocked one
|
|
144
|
+
* save path and merely painted a banner on the other — and anything wiring
|
|
145
|
+
* `(formSubmit)` to persistence wrote records the rules engine had already rejected.
|
|
146
|
+
*/
|
|
147
|
+
get ruleValidationErrors(): Record<string, string>;
|
|
148
|
+
/** True when a `validation` rule is currently blocking submission. */
|
|
149
|
+
get hasRuleErrors(): boolean;
|
|
150
|
+
/**
|
|
151
|
+
* True while an async validator is still running.
|
|
152
|
+
*
|
|
153
|
+
* Angular reports a control with an outstanding async check as `pending`, and `invalid` is
|
|
154
|
+
* false until it settles — so without this a form could be submitted in the gap before a
|
|
155
|
+
* uniqueness check came back.
|
|
156
|
+
*/
|
|
157
|
+
get isValidating(): boolean;
|
|
158
|
+
/** Single source of truth for both the submit guard and the button's disabled state. */
|
|
159
|
+
get submitBlocked(): boolean;
|
|
87
160
|
ngOnInit(): void;
|
|
88
161
|
ngOnChanges(changes: SimpleChanges): void;
|
|
89
162
|
ngOnDestroy(): void;
|
|
@@ -113,7 +186,38 @@ export declare class DynamicFormComponent implements OnInit, OnChanges, OnDestro
|
|
|
113
186
|
*/
|
|
114
187
|
createArrayRow(field: NestedFieldConfig, value?: unknown): AbstractControl;
|
|
115
188
|
private buildArrayRow;
|
|
189
|
+
/**
|
|
190
|
+
* Mark every control touched, then re-check the field components.
|
|
191
|
+
*
|
|
192
|
+
* They are OnPush and `touched` is flipped from outside their own template, so the second
|
|
193
|
+
* half is what makes the error messages actually appear.
|
|
194
|
+
*/
|
|
195
|
+
private markAllTouched;
|
|
196
|
+
/**
|
|
197
|
+
* Bring a record up to the config's `version` before it reaches the form.
|
|
198
|
+
*
|
|
199
|
+
* Done here because this is the one place a record enters the renderer, so a consumer
|
|
200
|
+
* cannot forget it. Migration is a no-op unless the consumer registered steps *and* the
|
|
201
|
+
* record is behind — an unstamped record is deliberately left alone (see `migrateRecord`).
|
|
202
|
+
*
|
|
203
|
+
* A missing or broken step throws from core. That is not swallowed: rendering a
|
|
204
|
+
* half-understood record would write it back in a shape matching neither schema.
|
|
205
|
+
*/
|
|
206
|
+
private upgradeRecord;
|
|
116
207
|
private patchForm;
|
|
208
|
+
/**
|
|
209
|
+
* A record is nested by tab id (`{ tabId: { fieldId: value } }`) unless the tab sets
|
|
210
|
+
* `flatData: true`. Handing a flat record to a nested tab finds nothing, so the fields
|
|
211
|
+
* stay empty — with no error and no clue. That silent miss is the most expensive way to
|
|
212
|
+
* lose an hour with this library, so name it in dev builds.
|
|
213
|
+
*
|
|
214
|
+
* Only top-level keys that match a known field id are reported: anything else is assumed
|
|
215
|
+
* to be the consumer's own record metadata (ids, timestamps, `_configVersion`) and is not
|
|
216
|
+
* our business.
|
|
217
|
+
*/
|
|
218
|
+
private warnUnconsumedInitialData;
|
|
219
|
+
/** Warn once per key per form instance — patchForm re-runs on every initialData change. */
|
|
220
|
+
private readonly warnedUnconsumedKeys;
|
|
117
221
|
/** Assemble full nested record from per-tab FormGroups, respecting flatData, refererField & arrays. */
|
|
118
222
|
extractRecord(): Record<string, any>;
|
|
119
223
|
private flattenFormValues;
|
|
@@ -130,15 +234,58 @@ export declare class DynamicFormComponent implements OnInit, OnChanges, OnDestro
|
|
|
130
234
|
private runPatchOnTrue;
|
|
131
235
|
/** Prefer a control declared on the configured target tab; fall back to a top-level control. */
|
|
132
236
|
private resolveTargetControl;
|
|
237
|
+
/**
|
|
238
|
+
* The fields the render filter operates on — a tab's own fields, at every tab depth.
|
|
239
|
+
* Deliberately not `allFields()`: `group` and `array` children are shown or hidden with
|
|
240
|
+
* their parent, and toggling a child's state independently would fight the parent's.
|
|
241
|
+
*/
|
|
242
|
+
private tabFields;
|
|
243
|
+
/**
|
|
244
|
+
* Keep a hidden field's control out of form validity.
|
|
245
|
+
*
|
|
246
|
+
* A field hidden by a rule or a `showWhen` condition is filtered out of the render, but
|
|
247
|
+
* its validators stay attached — so a hidden *required* field holds `form.invalid` true
|
|
248
|
+
* forever, permanently disabling Save with nothing on screen to explain why.
|
|
249
|
+
*
|
|
250
|
+
* Disabling is the fix rather than stripping validators: Angular excludes disabled
|
|
251
|
+
* controls from validity, and the field's own validators survive intact for when it
|
|
252
|
+
* comes back. Values are untouched — `flattenFormValues` and `extractRecord` both read
|
|
253
|
+
* `control.value`, which a disabled control still carries, so rule evaluation and the
|
|
254
|
+
* emitted record see exactly what they saw before. That also keeps this from feeding
|
|
255
|
+
* back on itself: hiding a field cannot change the values the rules are evaluated over.
|
|
256
|
+
*
|
|
257
|
+
* The visibility predicate is the same one `fieldsForActiveTab` filters with, so what is
|
|
258
|
+
* rendered and what counts toward validity cannot drift apart.
|
|
259
|
+
*/
|
|
260
|
+
private syncHiddenFieldState;
|
|
133
261
|
/** Every field in the config, including `group`/`array` children. */
|
|
134
262
|
private allFields;
|
|
135
263
|
/** An empty `originalData` counts as "not supplied" — fall back to the captured baseline. */
|
|
136
264
|
private baseline;
|
|
137
265
|
private sameValue;
|
|
138
266
|
getControl(fieldId: string, currentTabId?: string): AbstractControl | null;
|
|
267
|
+
/**
|
|
268
|
+
* Move focus into the newly shown panel.
|
|
269
|
+
*
|
|
270
|
+
* Activating a tab replaces everything below it while focus stays on the tab button, so a
|
|
271
|
+
* screen-reader or keyboard user is given no indication that the content changed and must
|
|
272
|
+
* tab back through the strip to reach it. Deferred a frame because the panel's contents
|
|
273
|
+
* are rendered by the change detection this call is part of.
|
|
274
|
+
*/
|
|
275
|
+
private focusActivePanel;
|
|
276
|
+
/** Accessible name for the field panel — it is the tabpanel for the active tab. */
|
|
277
|
+
get activeTabLabel(): string;
|
|
278
|
+
/**
|
|
279
|
+
* Why a referenced field is flagged.
|
|
280
|
+
*
|
|
281
|
+
* `hasDrift` was written by the builder and read by nothing at runtime, so a field whose
|
|
282
|
+
* source definition had changed since it was linked looked entirely normal to the person
|
|
283
|
+
* filling the form in.
|
|
284
|
+
*/
|
|
285
|
+
driftHint(field: NestedFieldConfig): string;
|
|
139
286
|
resolveTabLabel(tab: NestedTabConfig): string;
|
|
140
287
|
submit(): Promise<void>;
|
|
141
288
|
reset(): void;
|
|
142
289
|
static ɵfac: i0.ɵɵFactoryDeclaration<DynamicFormComponent, never>;
|
|
143
|
-
static ɵcmp: i0.ɵɵComponentDeclaration<DynamicFormComponent, "ngx-dynamic-form", never, { "config": { "alias": "config"; "required": false; }; "rules": { "alias": "rules"; "required": false; }; "initialData": { "alias": "initialData"; "required": false; }; "originalData": { "alias": "originalData"; "required": false; }; "userRoles": { "alias": "userRoles"; "required": false; }; "language": { "alias": "language"; "required": false; }; "readonly": { "alias": "readonly"; "required": false; }; "readOnlyFields": { "alias": "readOnlyFields"; "required": false; }; "showInfoBanners": { "alias": "showInfoBanners"; "required": false; }; "loading": { "alias": "loading"; "required": false; }; "error": { "alias": "error"; "required": false; }; "changeDebounceMs": { "alias": "changeDebounceMs"; "required": false; }; "preview": { "alias": "preview"; "required": false; }; }, { "formSubmit": "formSubmit"; "formChange": "formChange"; "formReset": "formReset"; "activeTabChange": "activeTabChange"; }, never, never, true, never>;
|
|
290
|
+
static ɵcmp: i0.ɵɵComponentDeclaration<DynamicFormComponent, "ngx-dynamic-form", never, { "config": { "alias": "config"; "required": false; }; "rules": { "alias": "rules"; "required": false; }; "initialData": { "alias": "initialData"; "required": false; }; "originalData": { "alias": "originalData"; "required": false; }; "userRoles": { "alias": "userRoles"; "required": false; }; "language": { "alias": "language"; "required": false; }; "readonly": { "alias": "readonly"; "required": false; }; "readOnlyFields": { "alias": "readOnlyFields"; "required": false; }; "showInfoBanners": { "alias": "showInfoBanners"; "required": false; }; "loading": { "alias": "loading"; "required": false; }; "error": { "alias": "error"; "required": false; }; "changeDebounceMs": { "alias": "changeDebounceMs"; "required": false; }; "preview": { "alias": "preview"; "required": false; }; }, { "formSubmit": "formSubmit"; "formChange": "formChange"; "formReset": "formReset"; "activeTabChange": "activeTabChange"; "saveRejected": "saveRejected"; }, never, never, true, never>;
|
|
144
291
|
}
|
|
@@ -70,7 +70,7 @@ export declare class DynamicRecordFormComponent implements OnChanges {
|
|
|
70
70
|
* building a row's FormGroup inside the tab view races the tab's own initialisation, so
|
|
71
71
|
* the row form is kept detached and only pushed into the FormArray on save.
|
|
72
72
|
*/
|
|
73
|
-
readonly inlineRowField: import("@angular/core").WritableSignal<
|
|
73
|
+
readonly inlineRowField: import("@angular/core").WritableSignal<any>;
|
|
74
74
|
readonly inlineRowIndex: import("@angular/core").WritableSignal<number>;
|
|
75
75
|
inlineRowForm: FormGroup | null;
|
|
76
76
|
/** `array` fields on the tab currently shown, so the template can list their rows. */
|
|
@@ -153,7 +153,18 @@ export declare class DynamicRecordFormComponent implements OnChanges {
|
|
|
153
153
|
toggleHeaderStatus(): void;
|
|
154
154
|
/** Locate a field's control wherever it lives, since header fields may be on any tab. */
|
|
155
155
|
private controlFor;
|
|
156
|
-
/**
|
|
156
|
+
/**
|
|
157
|
+
* A field's current value, read from the record so it is stable during render.
|
|
158
|
+
*
|
|
159
|
+
* Resolved through `getTabData` so it honours exactly the nesting contract the form
|
|
160
|
+
* patches with — `{ tabId: { fieldId } }`, or the record root when the tab sets
|
|
161
|
+
* `flatData`. Reading the record flat instead is not a shortcut: it makes the summary
|
|
162
|
+
* display values the form never loaded, which reads as "the data is there" while every
|
|
163
|
+
* control behind it is empty.
|
|
164
|
+
*
|
|
165
|
+
* Walks sub-tabs as well: a summary, header, or profile field may sit on any tab at any
|
|
166
|
+
* depth.
|
|
167
|
+
*/
|
|
157
168
|
private fieldValue;
|
|
158
169
|
readonly isModified: import("@angular/core").Signal<boolean>;
|
|
159
170
|
readonly summaryFields: import("@angular/core").Signal<NestedFieldConfig[]>;
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { EnvironmentProviders, Type } from '@angular/core';
|
|
2
|
-
import type { EntityReferenceLoader, LookupListSource } from '@dynamic-entity/core';
|
|
2
|
+
import type { EntityReferenceLoader, LookupListSource, RecordMigration } from '@dynamic-entity/core';
|
|
3
|
+
import { type HookFn } from '../tokens/injection-tokens';
|
|
3
4
|
export interface NgxDynamicEntityConfig {
|
|
4
5
|
/** Roles that see masked field values as XXXXXXXXX */
|
|
5
6
|
maskedRoles?: string[];
|
|
6
7
|
/** Custom field type components keyed by type string */
|
|
7
8
|
fieldTypes?: Record<string, Type<any>>;
|
|
8
9
|
/**
|
|
9
|
-
* Entity-ref option loaders keyed by entity key
|
|
10
|
+
* Entity-ref option loaders keyed by entity key.
|
|
10
11
|
* Each receives `{ parentValue, filters, lang }` and may return an array, Promise, or Observable.
|
|
11
12
|
*/
|
|
12
13
|
entityRefs?: Record<string, EntityReferenceLoader>;
|
|
@@ -16,10 +17,31 @@ export interface NgxDynamicEntityConfig {
|
|
|
16
17
|
* a loader for anything fetched, so an unused list is never loaded.
|
|
17
18
|
*/
|
|
18
19
|
lookups?: Record<string, LookupListSource>;
|
|
20
|
+
/**
|
|
21
|
+
* Override the messages shown under invalid fields, keyed by Angular error key. A value is
|
|
22
|
+
* a string, or a function receiving `(language, error)` when it needs the error's detail.
|
|
23
|
+
* Unlisted keys keep their English default.
|
|
24
|
+
*/
|
|
25
|
+
validationMessages?: Record<string, string | ((language: string, error: any) => string)>;
|
|
26
|
+
/**
|
|
27
|
+
* Async validator functions keyed by name, for checks that need a server — uniqueness,
|
|
28
|
+
* availability, a remote rule. Name them from a schema with
|
|
29
|
+
* `validators: { customAsync: ['uniqueEmail'] }`.
|
|
30
|
+
*
|
|
31
|
+
* Each is an Angular `AsyncValidatorFn`: it receives the control and returns a Promise or
|
|
32
|
+
* Observable of `ValidationErrors | null`. The control is `pending` while it runs, and the
|
|
33
|
+
* form cannot be submitted until every pending check settles.
|
|
34
|
+
*/
|
|
35
|
+
asyncValidators?: Record<string, any>;
|
|
19
36
|
/** Custom validator functions keyed by validator name */
|
|
20
37
|
validators?: Record<string, any>;
|
|
21
38
|
/** Hook functions keyed by hook name */
|
|
22
|
-
hooks?: Record<string,
|
|
39
|
+
hooks?: Record<string, HookFn>;
|
|
40
|
+
/**
|
|
41
|
+
* Ordered steps that upgrade a saved record when `EntityFormConfig.version` moves ahead
|
|
42
|
+
* of the record's `_configVersion`. Applied to `initialData` before the form is patched.
|
|
43
|
+
*/
|
|
44
|
+
migrations?: RecordMigration[];
|
|
23
45
|
}
|
|
24
46
|
/**
|
|
25
47
|
* Provider function for ngx-dynamic-entity.
|
|
@@ -3,7 +3,7 @@ import * as i0 from "@angular/core";
|
|
|
3
3
|
/**
|
|
4
4
|
* CascadeDataService — loads entity-reference options and applies parent→child cascades.
|
|
5
5
|
*
|
|
6
|
-
* Loaders come from the registry only
|
|
6
|
+
* Loaders come from the registry only. This service owns the three things a
|
|
7
7
|
* field component should not: resolving the loader, coercing whatever it returned
|
|
8
8
|
* (array | Promise | Observable) into a Promise, and filtering by the parent's value.
|
|
9
9
|
*/
|
|
@@ -2,7 +2,7 @@ import type { EntityReferenceLoader } from '@dynamic-entity/core';
|
|
|
2
2
|
import * as i0 from "@angular/core";
|
|
3
3
|
/**
|
|
4
4
|
* EntityRefRegistryService — resolves entity-ref option loader functions.
|
|
5
|
-
*
|
|
5
|
+
* Entity-ref options come ONLY from this registry.
|
|
6
6
|
* Never pass loaders via @Input() on field components.
|
|
7
7
|
*
|
|
8
8
|
* Consumers register loaders via provideNgxDynamicEntity({ entityRefs: { clients: () => [...] } }).
|
|
@@ -81,7 +81,7 @@ export interface ChoiceOptionHost {
|
|
|
81
81
|
* Point a choice component's `options` signal at the right source (parity plan §6.3).
|
|
82
82
|
*
|
|
83
83
|
* Called from the `field` / `language` input setters of `dropdown`, `radio` and `multiSelect` —
|
|
84
|
-
* the wiring is theirs, the decision is the service's, which is what keeps the
|
|
84
|
+
* the wiring is theirs, the decision is the service's, which is what keeps the field
|
|
85
85
|
* five-input contract intact: options are never pushed in as a sixth input.
|
|
86
86
|
*
|
|
87
87
|
* Setters rather than `ngOnChanges` on purpose. `ngOnChanges` only fires for inputs Angular
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { ValidationErrors } from '@angular/forms';
|
|
2
|
+
import * as i0 from "@angular/core";
|
|
3
|
+
/**
|
|
4
|
+
* Resolves an Angular `ValidationErrors` object to the message shown under a field.
|
|
5
|
+
*
|
|
6
|
+
* These strings used to be hardcoded English literals inside each field component, so a
|
|
7
|
+
* German form meant re-implementing every field type — the labels honoured `language` while
|
|
8
|
+
* the errors underneath them did not.
|
|
9
|
+
*
|
|
10
|
+
* A message is a plain string, or a function when it needs the error's own detail
|
|
11
|
+
* (`minlength.requiredLength`, `min.min`, and so on). The active `language` is passed
|
|
12
|
+
* through, so one registration can serve every locale:
|
|
13
|
+
*
|
|
14
|
+
* ```typescript
|
|
15
|
+
* provideNgxDynamicEntity({
|
|
16
|
+
* validationMessages: {
|
|
17
|
+
* required: lang => (lang === 'de' ? 'Pflichtfeld.' : 'This field is required.'),
|
|
18
|
+
* minlength: (lang, err) => `Mindestens ${err['requiredLength']} Zeichen.`,
|
|
19
|
+
* },
|
|
20
|
+
* });
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* Unregistered keys fall back to the defaults below, so overriding one message does not
|
|
24
|
+
* mean supplying them all.
|
|
25
|
+
*/
|
|
26
|
+
export type ValidationMessage = string | ((language: string, error: any) => string);
|
|
27
|
+
/** The built-in English messages. Every key here can be overridden individually. */
|
|
28
|
+
export declare const DEFAULT_VALIDATION_MESSAGES: Record<string, ValidationMessage>;
|
|
29
|
+
export declare class ValidationMessagesService {
|
|
30
|
+
private readonly overrides;
|
|
31
|
+
/**
|
|
32
|
+
* The message for the first error present, in the order the caller lists.
|
|
33
|
+
*
|
|
34
|
+
* Order is the caller's because it is field-specific: a dropdown wants
|
|
35
|
+
* `requiredSelection` where a text input wants `required`, and the first match wins so the
|
|
36
|
+
* most specific message is the one shown.
|
|
37
|
+
*/
|
|
38
|
+
resolve(errors: ValidationErrors | null | undefined, language: string, order: readonly (string | readonly [errorKey: string, messageKey: string])[], fallbackKey?: string): string;
|
|
39
|
+
/** One message by key, honouring an override before the default. */
|
|
40
|
+
messageFor(key: string, language: string, error: unknown): string;
|
|
41
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<ValidationMessagesService, never>;
|
|
42
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<ValidationMessagesService>;
|
|
43
|
+
}
|