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,8 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken, makeEnvironmentProviders,
|
|
2
|
+
import { InjectionToken, makeEnvironmentProviders, inject, Injectable, signal, Input, ChangeDetectionStrategy, Component, DestroyRef, Injector, isDevMode, ViewContainerRef, ViewChild, forwardRef, ChangeDetectorRef, Optional, Inject, EventEmitter, computed, HostListener, ViewChildren, Output } from '@angular/core';
|
|
3
3
|
import * as i1 from '@angular/forms';
|
|
4
4
|
import { ReactiveFormsModule, Validators, FormBuilder, FormArray, FormGroup } from '@angular/forms';
|
|
5
|
-
import { resolveLabel, normalizeLookupValues, lookupValuesToOptions, valuesMatch, getOptionStoredValue, resolveOptionLabel, resolveOptionValue, buildReferenceCacheKey, normalizeReferenceOptions, entityKeyFromCacheKey, applyCascadeFilter, resolveEffectiveMask, shouldMaskField, findTab, fileRefName, evaluateFormRules, filterRulesForTab, evaluateFieldVisibility, normalizeConfigOptions, getTabData, getValueByPath, setTabData, setValueByPath, normalizeArrayStructures, getTabPath, applyAutoPatch, applyPatchOnTrue, formatDisplayValue } from '@dynamic-entity/core';
|
|
5
|
+
import { resolveLabel, normalizeLookupValues, lookupValuesToOptions, valuesMatch, getOptionStoredValue, resolveOptionLabel, resolveOptionValue, buildReferenceCacheKey, normalizeReferenceOptions, entityKeyFromCacheKey, applyCascadeFilter, resolveEffectiveMask, shouldMaskField, findTab, fileRefName, evaluateFormRules, filterRulesForTab, evaluateFieldVisibility, normalizeConfigOptions, migrateRecord, getTabData, getValueByPath, setTabData, setValueByPath, normalizeArrayStructures, getTabPath, applyAutoPatch, applyPatchOnTrue, formatDisplayValue } from '@dynamic-entity/core';
|
|
6
6
|
export { formatDisplayValue, normalizeConfig, normalizeOption, resolveLabel, resolveOptionLabel, resolveOptionValue, valuesMatch } from '@dynamic-entity/core';
|
|
7
7
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
8
8
|
import { Subject, isObservable, firstValueFrom } from 'rxjs';
|
|
@@ -29,7 +29,7 @@ class InMemoryEntityRefCacheStore {
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
/** Roles that see XXXXXXXXX for masked fields
|
|
32
|
+
/** Roles that see XXXXXXXXX for masked fields. Presentational only — enforce authz server-side. */
|
|
33
33
|
const MASKED_ROLES = new InjectionToken('MASKED_ROLES');
|
|
34
34
|
/**
|
|
35
35
|
* Consumer **overrides**: fieldType string → Angular component class.
|
|
@@ -70,8 +70,41 @@ const ENTITY_REF_REGISTRY = new InjectionToken('ENTITY_REF_REGISTRY');
|
|
|
70
70
|
* })
|
|
71
71
|
*/
|
|
72
72
|
const LOOKUP_REGISTRY = new InjectionToken('LOOKUP_REGISTRY');
|
|
73
|
+
/**
|
|
74
|
+
* Ordered steps that move a saved record forward when a config's `version` increases.
|
|
75
|
+
*
|
|
76
|
+
* Without these, `EntityFormConfig.version` and `VersionedRecord._configVersion` are
|
|
77
|
+
* declarations nothing acts on: a schema can be edited freely while records keep their old
|
|
78
|
+
* shape, and nothing reconciles the two. Registering migrations makes the version numbers
|
|
79
|
+
* mean something.
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* provideNgxDynamicEntity({
|
|
83
|
+
* migrations: [
|
|
84
|
+
* { from: 1, to: 2, description: 'split name', migrate: r => ({ ...r, firstName: r['name'] }) },
|
|
85
|
+
* ],
|
|
86
|
+
* })
|
|
87
|
+
*/
|
|
88
|
+
const RECORD_MIGRATIONS = new InjectionToken('RECORD_MIGRATIONS');
|
|
89
|
+
/**
|
|
90
|
+
* Overrides for the messages shown under an invalid field, keyed by Angular error key
|
|
91
|
+
* (`required`, `minlength`, `pattern`, …) plus a few field-specific keys.
|
|
92
|
+
*
|
|
93
|
+
* The built-in messages are English literals. Registering here is what makes a non-English
|
|
94
|
+
* form possible without re-implementing every field component; unlisted keys keep their
|
|
95
|
+
* default, so overriding one does not mean supplying them all.
|
|
96
|
+
*/
|
|
97
|
+
const VALIDATION_MESSAGES = new InjectionToken('VALIDATION_MESSAGES');
|
|
73
98
|
/** Registry: validator key → ValidatorFn */
|
|
74
99
|
const VALIDATOR_REGISTRY = new InjectionToken('VALIDATOR_REGISTRY');
|
|
100
|
+
/**
|
|
101
|
+
* Registry: validator key → AsyncValidatorFn.
|
|
102
|
+
*
|
|
103
|
+
* Kept separate from VALIDATOR_REGISTRY because Angular applies the two differently: an async
|
|
104
|
+
* validator runs only once the synchronous ones pass, and puts the control into `pending`
|
|
105
|
+
* while it does. Name them from a schema with `validators: { customAsync: ['uniqueEmail'] }`.
|
|
106
|
+
*/
|
|
107
|
+
const ASYNC_VALIDATOR_REGISTRY = new InjectionToken('ASYNC_VALIDATOR_REGISTRY');
|
|
75
108
|
/** Registry: hook key → async (data, context) => data */
|
|
76
109
|
const HOOK_REGISTRY = new InjectionToken('HOOK_REGISTRY');
|
|
77
110
|
/**
|
|
@@ -109,10 +142,6 @@ const UPLOAD_HANDLER = new InjectionToken('UPLOAD_HANDLER');
|
|
|
109
142
|
* { provide: ENTITY_REF_CACHE_STORE, useClass: SessionStorageRefCache }
|
|
110
143
|
*/
|
|
111
144
|
const ENTITY_REF_CACHE_STORE = new InjectionToken('ENTITY_REF_CACHE_STORE', { providedIn: 'root', factory: () => new InMemoryEntityRefCacheStore() });
|
|
112
|
-
/**
|
|
113
|
-
* Consumer-provided predicate function to check whether user roles can edit system default tabs or fields.
|
|
114
|
-
* Defaults to allowing edits if not provided.
|
|
115
|
-
*/
|
|
116
145
|
/**
|
|
117
146
|
* Consumer-provided predicate function to check whether user roles can edit system default tabs or fields.
|
|
118
147
|
* Defaults to allowing edits if not provided.
|
|
@@ -140,15 +169,90 @@ const provideNgxDynamicEntity = (config = {}) => makeEnvironmentProviders([
|
|
|
140
169
|
{ provide: ENTITY_REF_REGISTRY, useValue: new Map(Object.entries(config.entityRefs ?? {})) },
|
|
141
170
|
{ provide: LOOKUP_REGISTRY, useValue: new Map(Object.entries(config.lookups ?? {})) },
|
|
142
171
|
{ provide: VALIDATOR_REGISTRY, useValue: new Map(Object.entries(config.validators ?? {})) },
|
|
172
|
+
{
|
|
173
|
+
provide: ASYNC_VALIDATOR_REGISTRY,
|
|
174
|
+
useValue: new Map(Object.entries(config.asyncValidators ?? {})),
|
|
175
|
+
},
|
|
143
176
|
{ provide: HOOK_REGISTRY, useValue: new Map(Object.entries(config.hooks ?? {})) },
|
|
177
|
+
{ provide: RECORD_MIGRATIONS, useValue: config.migrations ?? [] },
|
|
178
|
+
{ provide: VALIDATION_MESSAGES, useValue: config.validationMessages ?? {} },
|
|
144
179
|
]);
|
|
145
180
|
|
|
181
|
+
/** The built-in English messages. Every key here can be overridden individually. */
|
|
182
|
+
const DEFAULT_VALIDATION_MESSAGES = {
|
|
183
|
+
required: 'This field is required.',
|
|
184
|
+
requiredSelection: 'Please select an option.',
|
|
185
|
+
email: 'Please enter a valid email address.',
|
|
186
|
+
pattern: 'Invalid format.',
|
|
187
|
+
minlength: (_lang, err) => `Minimum ${err?.requiredLength} characters required.`,
|
|
188
|
+
maxlength: (_lang, err) => `Maximum ${err?.requiredLength} characters allowed.`,
|
|
189
|
+
min: (_lang, err) => `Value must be at least ${err?.min}.`,
|
|
190
|
+
max: (_lang, err) => `Value must not exceed ${err?.max}.`,
|
|
191
|
+
invalid: 'Invalid value.',
|
|
192
|
+
invalidNumber: 'Invalid number.',
|
|
193
|
+
invalidSelection: 'Invalid selection.',
|
|
194
|
+
};
|
|
195
|
+
class ValidationMessagesService {
|
|
196
|
+
overrides = inject(VALIDATION_MESSAGES, { optional: true }) ?? {};
|
|
197
|
+
/**
|
|
198
|
+
* The message for the first error present, in the order the caller lists.
|
|
199
|
+
*
|
|
200
|
+
* Order is the caller's because it is field-specific: a dropdown wants
|
|
201
|
+
* `requiredSelection` where a text input wants `required`, and the first match wins so the
|
|
202
|
+
* most specific message is the one shown.
|
|
203
|
+
*/
|
|
204
|
+
resolve(errors, language, order, fallbackKey = 'invalid') {
|
|
205
|
+
if (!errors)
|
|
206
|
+
return '';
|
|
207
|
+
for (const entry of order) {
|
|
208
|
+
// A tuple maps an Angular error key to a different message key: a dropdown raises the
|
|
209
|
+
// standard `required` error but reads better as "Please select an option", and both
|
|
210
|
+
// messages stay independently overridable.
|
|
211
|
+
const [errorKey, messageKey] = typeof entry === 'string' ? [entry, entry] : entry;
|
|
212
|
+
// `errors[errorKey]` is usually an object of detail, but `required` is simply `true`.
|
|
213
|
+
if (errors[errorKey] === undefined)
|
|
214
|
+
continue;
|
|
215
|
+
return this.messageFor(messageKey, language, errors[errorKey]);
|
|
216
|
+
}
|
|
217
|
+
return this.messageFor(fallbackKey, language, null);
|
|
218
|
+
}
|
|
219
|
+
/** One message by key, honouring an override before the default. */
|
|
220
|
+
messageFor(key, language, error) {
|
|
221
|
+
const message = this.overrides[key] ?? DEFAULT_VALIDATION_MESSAGES[key];
|
|
222
|
+
if (message === undefined)
|
|
223
|
+
return '';
|
|
224
|
+
return typeof message === 'function' ? message(language, error) : message;
|
|
225
|
+
}
|
|
226
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ValidationMessagesService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
227
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ValidationMessagesService, providedIn: 'root' });
|
|
228
|
+
}
|
|
229
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ValidationMessagesService, decorators: [{
|
|
230
|
+
type: Injectable,
|
|
231
|
+
args: [{ providedIn: 'root' }]
|
|
232
|
+
}] });
|
|
233
|
+
|
|
146
234
|
class TextFieldComponent {
|
|
235
|
+
messages = inject(ValidationMessagesService);
|
|
236
|
+
displaySub;
|
|
147
237
|
field;
|
|
148
|
-
control
|
|
238
|
+
set control(value) {
|
|
239
|
+
this._control = value;
|
|
240
|
+
this.displaySub?.unsubscribe();
|
|
241
|
+
this.displayValue.set(value?.value ?? null);
|
|
242
|
+
this.displaySub = value?.valueChanges.subscribe(v => this.displayValue.set(v));
|
|
243
|
+
}
|
|
244
|
+
get control() {
|
|
245
|
+
return this._control;
|
|
246
|
+
}
|
|
247
|
+
_control;
|
|
149
248
|
language = 'en';
|
|
150
249
|
readonly = false;
|
|
151
250
|
masked = false;
|
|
251
|
+
/** Readonly markup reads this signal so an external patch (autoPatch) is visible under OnPush. */
|
|
252
|
+
displayValue = signal(null);
|
|
253
|
+
ngOnDestroy() {
|
|
254
|
+
this.displaySub?.unsubscribe();
|
|
255
|
+
}
|
|
152
256
|
get label() {
|
|
153
257
|
return resolveLabel(this.field?.label, this.language);
|
|
154
258
|
}
|
|
@@ -158,86 +262,76 @@ class TextFieldComponent {
|
|
|
158
262
|
get errorMessage() {
|
|
159
263
|
if (!this.control || !this.control.errors || !this.control.touched)
|
|
160
264
|
return '';
|
|
161
|
-
|
|
162
|
-
if (errs['required'])
|
|
163
|
-
return 'This field is required.';
|
|
164
|
-
if (errs['email'])
|
|
165
|
-
return 'Please enter a valid email address.';
|
|
166
|
-
if (errs['minlength'])
|
|
167
|
-
return `Minimum ${errs['minlength'].requiredLength} characters required.`;
|
|
168
|
-
if (errs['maxlength'])
|
|
169
|
-
return `Maximum ${errs['maxlength'].requiredLength} characters allowed.`;
|
|
170
|
-
if (errs['pattern'])
|
|
171
|
-
return 'Invalid format.';
|
|
172
|
-
return 'Invalid value.';
|
|
265
|
+
return this.messages.resolve(this.control.errors, this.language, ['required', 'email', 'minlength', 'maxlength', 'pattern']);
|
|
173
266
|
}
|
|
174
267
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: TextFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
175
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: TextFieldComponent, isStandalone: true, selector: "ngx-text-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
176
|
-
<div
|
|
177
|
-
class="ngx-field ngx-field--text"
|
|
178
|
-
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type"
|
|
179
|
-
[class.ngx-field--readonly]="readonly"
|
|
180
|
-
[class.ngx-field--masked]="masked"
|
|
181
|
-
[class.ngx-field--invalid]="control && control.invalid && control.touched"
|
|
182
|
-
>
|
|
183
|
-
<label class="ngx-field__label" [attr.for]="field.id">{{ label }}@if (field.validators?.required) { <span class="ngx-field__req">*</span> }</label>
|
|
184
|
-
@if (masked) {
|
|
185
|
-
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
186
|
-
} @else if (readonly) {
|
|
187
|
-
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{
|
|
188
|
-
} @else {
|
|
189
|
-
<input
|
|
190
|
-
[id]="field.id"
|
|
191
|
-
type="text"
|
|
192
|
-
class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
|
|
193
|
-
[formControl]="$any(control)"
|
|
194
|
-
[placeholder]="placeholder"
|
|
195
|
-
[attr.disabled]="field.disabled ? true : null"
|
|
196
|
-
[attr.aria-invalid]="control.invalid && control.touched"
|
|
197
|
-
[attr.aria-describedby]="errorMessage ? field.id + '-error' : null"
|
|
198
|
-
/>
|
|
199
|
-
@if (errorMessage) {
|
|
200
|
-
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'" [id]="field.id + '-error'" role="alert">{{ errorMessage }}</span>
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
</div>
|
|
204
|
-
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }] });
|
|
268
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: TextFieldComponent, isStandalone: true, selector: "ngx-text-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
269
|
+
<div
|
|
270
|
+
class="ngx-field ngx-field--text"
|
|
271
|
+
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type"
|
|
272
|
+
[class.ngx-field--readonly]="readonly"
|
|
273
|
+
[class.ngx-field--masked]="masked"
|
|
274
|
+
[class.ngx-field--invalid]="control && control.invalid && control.touched"
|
|
275
|
+
>
|
|
276
|
+
<label class="ngx-field__label" [attr.for]="field.id">{{ label }}@if (field.validators?.required) { <span class="ngx-field__req">*</span> }</label>
|
|
277
|
+
@if (masked) {
|
|
278
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
279
|
+
} @else if (readonly) {
|
|
280
|
+
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ displayValue() ?? '—' }}</span>
|
|
281
|
+
} @else {
|
|
282
|
+
<input
|
|
283
|
+
[id]="field.id"
|
|
284
|
+
type="text"
|
|
285
|
+
class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
|
|
286
|
+
[formControl]="$any(control)"
|
|
287
|
+
[placeholder]="placeholder"
|
|
288
|
+
[attr.disabled]="field.disabled ? true : null"
|
|
289
|
+
[attr.aria-invalid]="control.invalid && control.touched"
|
|
290
|
+
[attr.aria-describedby]="errorMessage ? field.id + '-error' : null"
|
|
291
|
+
/>
|
|
292
|
+
@if (errorMessage) {
|
|
293
|
+
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'" [id]="field.id + '-error'" role="alert">{{ errorMessage }}</span>
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
</div>
|
|
297
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
205
298
|
}
|
|
206
299
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: TextFieldComponent, decorators: [{
|
|
207
300
|
type: Component,
|
|
208
301
|
args: [{
|
|
302
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
209
303
|
selector: 'ngx-text-field',
|
|
210
304
|
standalone: true,
|
|
211
305
|
imports: [ReactiveFormsModule],
|
|
212
|
-
template: `
|
|
213
|
-
<div
|
|
214
|
-
class="ngx-field ngx-field--text"
|
|
215
|
-
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type"
|
|
216
|
-
[class.ngx-field--readonly]="readonly"
|
|
217
|
-
[class.ngx-field--masked]="masked"
|
|
218
|
-
[class.ngx-field--invalid]="control && control.invalid && control.touched"
|
|
219
|
-
>
|
|
220
|
-
<label class="ngx-field__label" [attr.for]="field.id">{{ label }}@if (field.validators?.required) { <span class="ngx-field__req">*</span> }</label>
|
|
221
|
-
@if (masked) {
|
|
222
|
-
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
223
|
-
} @else if (readonly) {
|
|
224
|
-
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{
|
|
225
|
-
} @else {
|
|
226
|
-
<input
|
|
227
|
-
[id]="field.id"
|
|
228
|
-
type="text"
|
|
229
|
-
class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
|
|
230
|
-
[formControl]="$any(control)"
|
|
231
|
-
[placeholder]="placeholder"
|
|
232
|
-
[attr.disabled]="field.disabled ? true : null"
|
|
233
|
-
[attr.aria-invalid]="control.invalid && control.touched"
|
|
234
|
-
[attr.aria-describedby]="errorMessage ? field.id + '-error' : null"
|
|
235
|
-
/>
|
|
236
|
-
@if (errorMessage) {
|
|
237
|
-
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'" [id]="field.id + '-error'" role="alert">{{ errorMessage }}</span>
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
</div>
|
|
306
|
+
template: `
|
|
307
|
+
<div
|
|
308
|
+
class="ngx-field ngx-field--text"
|
|
309
|
+
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type"
|
|
310
|
+
[class.ngx-field--readonly]="readonly"
|
|
311
|
+
[class.ngx-field--masked]="masked"
|
|
312
|
+
[class.ngx-field--invalid]="control && control.invalid && control.touched"
|
|
313
|
+
>
|
|
314
|
+
<label class="ngx-field__label" [attr.for]="field.id">{{ label }}@if (field.validators?.required) { <span class="ngx-field__req">*</span> }</label>
|
|
315
|
+
@if (masked) {
|
|
316
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
317
|
+
} @else if (readonly) {
|
|
318
|
+
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ displayValue() ?? '—' }}</span>
|
|
319
|
+
} @else {
|
|
320
|
+
<input
|
|
321
|
+
[id]="field.id"
|
|
322
|
+
type="text"
|
|
323
|
+
class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
|
|
324
|
+
[formControl]="$any(control)"
|
|
325
|
+
[placeholder]="placeholder"
|
|
326
|
+
[attr.disabled]="field.disabled ? true : null"
|
|
327
|
+
[attr.aria-invalid]="control.invalid && control.touched"
|
|
328
|
+
[attr.aria-describedby]="errorMessage ? field.id + '-error' : null"
|
|
329
|
+
/>
|
|
330
|
+
@if (errorMessage) {
|
|
331
|
+
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'" [id]="field.id + '-error'" role="alert">{{ errorMessage }}</span>
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
</div>
|
|
241
335
|
`,
|
|
242
336
|
}]
|
|
243
337
|
}], propDecorators: { field: [{
|
|
@@ -265,58 +359,59 @@ class TextareaFieldComponent {
|
|
|
265
359
|
return resolveLabel(this.field?.placeholder, this.language);
|
|
266
360
|
}
|
|
267
361
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: TextareaFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
268
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: TextareaFieldComponent, isStandalone: true, selector: "ngx-textarea-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
269
|
-
<div class="ngx-field ngx-field--textarea"
|
|
270
|
-
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
271
|
-
<label class="ngx-field__label" [attr.for]="field.id">{{ label }}</label>
|
|
272
|
-
@if (masked) {
|
|
273
|
-
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
274
|
-
} @else if (readonly) {
|
|
275
|
-
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ control.value }}</span>
|
|
276
|
-
} @else {
|
|
277
|
-
<textarea
|
|
278
|
-
[id]="field.id"
|
|
279
|
-
class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
|
|
280
|
-
[formControl]="$any(control)"
|
|
281
|
-
[placeholder]="placeholder"
|
|
282
|
-
[attr.disabled]="field.disabled ? true : null"
|
|
283
|
-
rows="3"
|
|
284
|
-
></textarea>
|
|
285
|
-
@if (control.invalid && control.touched) {
|
|
286
|
-
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
</div>
|
|
290
|
-
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }] });
|
|
362
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: TextareaFieldComponent, isStandalone: true, selector: "ngx-textarea-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
363
|
+
<div class="ngx-field ngx-field--textarea"
|
|
364
|
+
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
365
|
+
<label class="ngx-field__label" [attr.for]="field.id">{{ label }}</label>
|
|
366
|
+
@if (masked) {
|
|
367
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
368
|
+
} @else if (readonly) {
|
|
369
|
+
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ control.value }}</span>
|
|
370
|
+
} @else {
|
|
371
|
+
<textarea
|
|
372
|
+
[id]="field.id"
|
|
373
|
+
class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
|
|
374
|
+
[formControl]="$any(control)"
|
|
375
|
+
[placeholder]="placeholder"
|
|
376
|
+
[attr.disabled]="field.disabled ? true : null"
|
|
377
|
+
rows="3"
|
|
378
|
+
></textarea>
|
|
379
|
+
@if (control.invalid && control.touched) {
|
|
380
|
+
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
</div>
|
|
384
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
291
385
|
}
|
|
292
386
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: TextareaFieldComponent, decorators: [{
|
|
293
387
|
type: Component,
|
|
294
388
|
args: [{
|
|
389
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
295
390
|
selector: 'ngx-textarea-field',
|
|
296
391
|
standalone: true,
|
|
297
392
|
imports: [ReactiveFormsModule],
|
|
298
|
-
template: `
|
|
299
|
-
<div class="ngx-field ngx-field--textarea"
|
|
300
|
-
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
301
|
-
<label class="ngx-field__label" [attr.for]="field.id">{{ label }}</label>
|
|
302
|
-
@if (masked) {
|
|
303
|
-
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
304
|
-
} @else if (readonly) {
|
|
305
|
-
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ control.value }}</span>
|
|
306
|
-
} @else {
|
|
307
|
-
<textarea
|
|
308
|
-
[id]="field.id"
|
|
309
|
-
class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
|
|
310
|
-
[formControl]="$any(control)"
|
|
311
|
-
[placeholder]="placeholder"
|
|
312
|
-
[attr.disabled]="field.disabled ? true : null"
|
|
313
|
-
rows="3"
|
|
314
|
-
></textarea>
|
|
315
|
-
@if (control.invalid && control.touched) {
|
|
316
|
-
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
</div>
|
|
393
|
+
template: `
|
|
394
|
+
<div class="ngx-field ngx-field--textarea"
|
|
395
|
+
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
396
|
+
<label class="ngx-field__label" [attr.for]="field.id">{{ label }}</label>
|
|
397
|
+
@if (masked) {
|
|
398
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
399
|
+
} @else if (readonly) {
|
|
400
|
+
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ control.value }}</span>
|
|
401
|
+
} @else {
|
|
402
|
+
<textarea
|
|
403
|
+
[id]="field.id"
|
|
404
|
+
class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
|
|
405
|
+
[formControl]="$any(control)"
|
|
406
|
+
[placeholder]="placeholder"
|
|
407
|
+
[attr.disabled]="field.disabled ? true : null"
|
|
408
|
+
rows="3"
|
|
409
|
+
></textarea>
|
|
410
|
+
@if (control.invalid && control.touched) {
|
|
411
|
+
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
</div>
|
|
320
415
|
`,
|
|
321
416
|
}]
|
|
322
417
|
}], propDecorators: { field: [{
|
|
@@ -332,6 +427,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
332
427
|
}] } });
|
|
333
428
|
|
|
334
429
|
class NumberFieldComponent {
|
|
430
|
+
messages = inject(ValidationMessagesService);
|
|
335
431
|
field;
|
|
336
432
|
control;
|
|
337
433
|
language = 'en';
|
|
@@ -346,88 +442,82 @@ class NumberFieldComponent {
|
|
|
346
442
|
get errorMessage() {
|
|
347
443
|
if (!this.control || !this.control.errors || !this.control.touched)
|
|
348
444
|
return '';
|
|
349
|
-
|
|
350
|
-
if (errs['required'])
|
|
351
|
-
return 'This field is required.';
|
|
352
|
-
if (errs['min'])
|
|
353
|
-
return `Value must be at least ${errs['min'].min}.`;
|
|
354
|
-
if (errs['max'])
|
|
355
|
-
return `Value must not exceed ${errs['max'].max}.`;
|
|
356
|
-
return 'Invalid number.';
|
|
445
|
+
return this.messages.resolve(this.control.errors, this.language, ['required', 'min', 'max'], 'invalidNumber');
|
|
357
446
|
}
|
|
358
447
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: NumberFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
359
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: NumberFieldComponent, isStandalone: true, selector: "ngx-number-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
360
|
-
<div
|
|
361
|
-
class="ngx-field ngx-field--number"
|
|
362
|
-
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type"
|
|
363
|
-
[class.ngx-field--readonly]="readonly"
|
|
364
|
-
[class.ngx-field--masked]="masked"
|
|
365
|
-
[class.ngx-field--invalid]="control && control.invalid && control.touched"
|
|
366
|
-
>
|
|
367
|
-
<label class="ngx-field__label" [attr.for]="field.id">
|
|
368
|
-
{{ label }}
|
|
369
|
-
@if (field.validators?.required) { <span class="ngx-field__req">*</span> }
|
|
370
|
-
</label>
|
|
371
|
-
@if (masked) {
|
|
372
|
-
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
373
|
-
} @else if (readonly) {
|
|
374
|
-
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ control.value ?? '—' }}</span>
|
|
375
|
-
} @else {
|
|
376
|
-
<input
|
|
377
|
-
[id]="field.id"
|
|
378
|
-
type="number"
|
|
379
|
-
class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
|
|
380
|
-
[formControl]="$any(control)"
|
|
381
|
-
[placeholder]="placeholder"
|
|
382
|
-
[attr.disabled]="field.disabled ? true : null"
|
|
383
|
-
[attr.aria-invalid]="control.invalid && control.touched"
|
|
384
|
-
[attr.aria-describedby]="errorMessage ? field.id + '-error' : null"
|
|
385
|
-
/>
|
|
386
|
-
@if (errorMessage) {
|
|
387
|
-
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'" [id]="field.id + '-error'" role="alert">{{ errorMessage }}</span>
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
</div>
|
|
391
|
-
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }] });
|
|
448
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: NumberFieldComponent, isStandalone: true, selector: "ngx-number-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
449
|
+
<div
|
|
450
|
+
class="ngx-field ngx-field--number"
|
|
451
|
+
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type"
|
|
452
|
+
[class.ngx-field--readonly]="readonly"
|
|
453
|
+
[class.ngx-field--masked]="masked"
|
|
454
|
+
[class.ngx-field--invalid]="control && control.invalid && control.touched"
|
|
455
|
+
>
|
|
456
|
+
<label class="ngx-field__label" [attr.for]="field.id">
|
|
457
|
+
{{ label }}
|
|
458
|
+
@if (field.validators?.required) { <span class="ngx-field__req">*</span> }
|
|
459
|
+
</label>
|
|
460
|
+
@if (masked) {
|
|
461
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
462
|
+
} @else if (readonly) {
|
|
463
|
+
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ control.value ?? '—' }}</span>
|
|
464
|
+
} @else {
|
|
465
|
+
<input
|
|
466
|
+
[id]="field.id"
|
|
467
|
+
type="number"
|
|
468
|
+
class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
|
|
469
|
+
[formControl]="$any(control)"
|
|
470
|
+
[placeholder]="placeholder"
|
|
471
|
+
[attr.disabled]="field.disabled ? true : null"
|
|
472
|
+
[attr.aria-invalid]="control.invalid && control.touched"
|
|
473
|
+
[attr.aria-describedby]="errorMessage ? field.id + '-error' : null"
|
|
474
|
+
/>
|
|
475
|
+
@if (errorMessage) {
|
|
476
|
+
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'" [id]="field.id + '-error'" role="alert">{{ errorMessage }}</span>
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
</div>
|
|
480
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
392
481
|
}
|
|
393
482
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: NumberFieldComponent, decorators: [{
|
|
394
483
|
type: Component,
|
|
395
484
|
args: [{
|
|
485
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
396
486
|
selector: 'ngx-number-field',
|
|
397
487
|
standalone: true,
|
|
398
488
|
imports: [ReactiveFormsModule],
|
|
399
|
-
template: `
|
|
400
|
-
<div
|
|
401
|
-
class="ngx-field ngx-field--number"
|
|
402
|
-
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type"
|
|
403
|
-
[class.ngx-field--readonly]="readonly"
|
|
404
|
-
[class.ngx-field--masked]="masked"
|
|
405
|
-
[class.ngx-field--invalid]="control && control.invalid && control.touched"
|
|
406
|
-
>
|
|
407
|
-
<label class="ngx-field__label" [attr.for]="field.id">
|
|
408
|
-
{{ label }}
|
|
409
|
-
@if (field.validators?.required) { <span class="ngx-field__req">*</span> }
|
|
410
|
-
</label>
|
|
411
|
-
@if (masked) {
|
|
412
|
-
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
413
|
-
} @else if (readonly) {
|
|
414
|
-
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ control.value ?? '—' }}</span>
|
|
415
|
-
} @else {
|
|
416
|
-
<input
|
|
417
|
-
[id]="field.id"
|
|
418
|
-
type="number"
|
|
419
|
-
class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
|
|
420
|
-
[formControl]="$any(control)"
|
|
421
|
-
[placeholder]="placeholder"
|
|
422
|
-
[attr.disabled]="field.disabled ? true : null"
|
|
423
|
-
[attr.aria-invalid]="control.invalid && control.touched"
|
|
424
|
-
[attr.aria-describedby]="errorMessage ? field.id + '-error' : null"
|
|
425
|
-
/>
|
|
426
|
-
@if (errorMessage) {
|
|
427
|
-
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'" [id]="field.id + '-error'" role="alert">{{ errorMessage }}</span>
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
</div>
|
|
489
|
+
template: `
|
|
490
|
+
<div
|
|
491
|
+
class="ngx-field ngx-field--number"
|
|
492
|
+
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type"
|
|
493
|
+
[class.ngx-field--readonly]="readonly"
|
|
494
|
+
[class.ngx-field--masked]="masked"
|
|
495
|
+
[class.ngx-field--invalid]="control && control.invalid && control.touched"
|
|
496
|
+
>
|
|
497
|
+
<label class="ngx-field__label" [attr.for]="field.id">
|
|
498
|
+
{{ label }}
|
|
499
|
+
@if (field.validators?.required) { <span class="ngx-field__req">*</span> }
|
|
500
|
+
</label>
|
|
501
|
+
@if (masked) {
|
|
502
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
503
|
+
} @else if (readonly) {
|
|
504
|
+
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ control.value ?? '—' }}</span>
|
|
505
|
+
} @else {
|
|
506
|
+
<input
|
|
507
|
+
[id]="field.id"
|
|
508
|
+
type="number"
|
|
509
|
+
class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
|
|
510
|
+
[formControl]="$any(control)"
|
|
511
|
+
[placeholder]="placeholder"
|
|
512
|
+
[attr.disabled]="field.disabled ? true : null"
|
|
513
|
+
[attr.aria-invalid]="control.invalid && control.touched"
|
|
514
|
+
[attr.aria-describedby]="errorMessage ? field.id + '-error' : null"
|
|
515
|
+
/>
|
|
516
|
+
@if (errorMessage) {
|
|
517
|
+
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'" [id]="field.id + '-error'" role="alert">{{ errorMessage }}</span>
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
</div>
|
|
431
521
|
`,
|
|
432
522
|
}]
|
|
433
523
|
}], propDecorators: { field: [{
|
|
@@ -466,64 +556,65 @@ class CurrencyFieldComponent {
|
|
|
466
556
|
}
|
|
467
557
|
}
|
|
468
558
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: CurrencyFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
469
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: CurrencyFieldComponent, isStandalone: true, selector: "ngx-currency-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
470
|
-
<div class="ngx-field ngx-field--currency"
|
|
471
|
-
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
472
|
-
<label class="ngx-field__label">{{ label }}</label>
|
|
473
|
-
@if (masked) {
|
|
474
|
-
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
475
|
-
} @else if (readonly) {
|
|
476
|
-
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ symbol }}{{ control.value }}</span>
|
|
477
|
-
} @else {
|
|
478
|
-
<div class="ngx-field__currency-wrap">
|
|
479
|
-
<span class="ngx-field__currency-symbol">{{ symbol }}</span>
|
|
480
|
-
<input
|
|
481
|
-
class="ngx-field__input ngx-field__input--currency" [attr.data-testid]="'field-' + field.id + '-input'"
|
|
482
|
-
type="number"
|
|
483
|
-
step="0.01"
|
|
484
|
-
[formControl]="$any(control)"
|
|
485
|
-
[placeholder]="placeholder || '0.00'"
|
|
486
|
-
[attr.disabled]="field.disabled ? true : null"
|
|
487
|
-
/>
|
|
488
|
-
</div>
|
|
489
|
-
@if (control.invalid && control.touched) {
|
|
490
|
-
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
</div>
|
|
494
|
-
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }] });
|
|
559
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: CurrencyFieldComponent, isStandalone: true, selector: "ngx-currency-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
560
|
+
<div class="ngx-field ngx-field--currency"
|
|
561
|
+
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
562
|
+
<label class="ngx-field__label">{{ label }}</label>
|
|
563
|
+
@if (masked) {
|
|
564
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
565
|
+
} @else if (readonly) {
|
|
566
|
+
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ symbol }}{{ control.value }}</span>
|
|
567
|
+
} @else {
|
|
568
|
+
<div class="ngx-field__currency-wrap">
|
|
569
|
+
<span class="ngx-field__currency-symbol">{{ symbol }}</span>
|
|
570
|
+
<input
|
|
571
|
+
class="ngx-field__input ngx-field__input--currency" [attr.data-testid]="'field-' + field.id + '-input'"
|
|
572
|
+
type="number"
|
|
573
|
+
step="0.01"
|
|
574
|
+
[formControl]="$any(control)"
|
|
575
|
+
[placeholder]="placeholder || '0.00'"
|
|
576
|
+
[attr.disabled]="field.disabled ? true : null"
|
|
577
|
+
/>
|
|
578
|
+
</div>
|
|
579
|
+
@if (control.invalid && control.touched) {
|
|
580
|
+
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
</div>
|
|
584
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
495
585
|
}
|
|
496
586
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: CurrencyFieldComponent, decorators: [{
|
|
497
587
|
type: Component,
|
|
498
588
|
args: [{
|
|
589
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
499
590
|
selector: 'ngx-currency-field',
|
|
500
591
|
standalone: true,
|
|
501
592
|
imports: [ReactiveFormsModule],
|
|
502
|
-
template: `
|
|
503
|
-
<div class="ngx-field ngx-field--currency"
|
|
504
|
-
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
505
|
-
<label class="ngx-field__label">{{ label }}</label>
|
|
506
|
-
@if (masked) {
|
|
507
|
-
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
508
|
-
} @else if (readonly) {
|
|
509
|
-
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ symbol }}{{ control.value }}</span>
|
|
510
|
-
} @else {
|
|
511
|
-
<div class="ngx-field__currency-wrap">
|
|
512
|
-
<span class="ngx-field__currency-symbol">{{ symbol }}</span>
|
|
513
|
-
<input
|
|
514
|
-
class="ngx-field__input ngx-field__input--currency" [attr.data-testid]="'field-' + field.id + '-input'"
|
|
515
|
-
type="number"
|
|
516
|
-
step="0.01"
|
|
517
|
-
[formControl]="$any(control)"
|
|
518
|
-
[placeholder]="placeholder || '0.00'"
|
|
519
|
-
[attr.disabled]="field.disabled ? true : null"
|
|
520
|
-
/>
|
|
521
|
-
</div>
|
|
522
|
-
@if (control.invalid && control.touched) {
|
|
523
|
-
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
</div>
|
|
593
|
+
template: `
|
|
594
|
+
<div class="ngx-field ngx-field--currency"
|
|
595
|
+
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
596
|
+
<label class="ngx-field__label">{{ label }}</label>
|
|
597
|
+
@if (masked) {
|
|
598
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
599
|
+
} @else if (readonly) {
|
|
600
|
+
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ symbol }}{{ control.value }}</span>
|
|
601
|
+
} @else {
|
|
602
|
+
<div class="ngx-field__currency-wrap">
|
|
603
|
+
<span class="ngx-field__currency-symbol">{{ symbol }}</span>
|
|
604
|
+
<input
|
|
605
|
+
class="ngx-field__input ngx-field__input--currency" [attr.data-testid]="'field-' + field.id + '-input'"
|
|
606
|
+
type="number"
|
|
607
|
+
step="0.01"
|
|
608
|
+
[formControl]="$any(control)"
|
|
609
|
+
[placeholder]="placeholder || '0.00'"
|
|
610
|
+
[attr.disabled]="field.disabled ? true : null"
|
|
611
|
+
/>
|
|
612
|
+
</div>
|
|
613
|
+
@if (control.invalid && control.touched) {
|
|
614
|
+
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
</div>
|
|
527
618
|
`,
|
|
528
619
|
}]
|
|
529
620
|
}], propDecorators: { field: [{
|
|
@@ -552,74 +643,75 @@ class EmailFieldComponent {
|
|
|
552
643
|
return resolveLabel(this.field?.placeholder, this.language);
|
|
553
644
|
}
|
|
554
645
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: EmailFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
555
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: EmailFieldComponent, isStandalone: true, selector: "ngx-email-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
556
|
-
<div class="ngx-field ngx-field--email"
|
|
557
|
-
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
558
|
-
<label class="ngx-field__label">{{ label }}</label>
|
|
559
|
-
@if (masked) {
|
|
560
|
-
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
561
|
-
} @else if (readonly) {
|
|
562
|
-
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">
|
|
563
|
-
<a [href]="'mailto:' + control.value" class="ngx-field__email-link">{{ control.value || '—' }}</a>
|
|
564
|
-
</span>
|
|
565
|
-
} @else {
|
|
566
|
-
<input
|
|
567
|
-
class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
|
|
568
|
-
type="email"
|
|
569
|
-
autocomplete="email"
|
|
570
|
-
[formControl]="$any(control)"
|
|
571
|
-
[placeholder]="placeholder || 'you@example.com'"
|
|
572
|
-
[attr.disabled]="field.disabled ? true : null"
|
|
573
|
-
/>
|
|
574
|
-
@if (control.invalid && control.touched) {
|
|
575
|
-
@if (control.errors?.['required']) {
|
|
576
|
-
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">Email is required</span>
|
|
577
|
-
} @else if (control.errors?.['email']) {
|
|
578
|
-
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">Enter a valid email address</span>
|
|
579
|
-
} @else {
|
|
580
|
-
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
</div>
|
|
585
|
-
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }] });
|
|
646
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: EmailFieldComponent, isStandalone: true, selector: "ngx-email-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
647
|
+
<div class="ngx-field ngx-field--email"
|
|
648
|
+
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
649
|
+
<label class="ngx-field__label">{{ label }}</label>
|
|
650
|
+
@if (masked) {
|
|
651
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
652
|
+
} @else if (readonly) {
|
|
653
|
+
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">
|
|
654
|
+
<a [href]="'mailto:' + control.value" class="ngx-field__email-link">{{ control.value || '—' }}</a>
|
|
655
|
+
</span>
|
|
656
|
+
} @else {
|
|
657
|
+
<input
|
|
658
|
+
class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
|
|
659
|
+
type="email"
|
|
660
|
+
autocomplete="email"
|
|
661
|
+
[formControl]="$any(control)"
|
|
662
|
+
[placeholder]="placeholder || 'you@example.com'"
|
|
663
|
+
[attr.disabled]="field.disabled ? true : null"
|
|
664
|
+
/>
|
|
665
|
+
@if (control.invalid && control.touched) {
|
|
666
|
+
@if (control.errors?.['required']) {
|
|
667
|
+
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">Email is required</span>
|
|
668
|
+
} @else if (control.errors?.['email']) {
|
|
669
|
+
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">Enter a valid email address</span>
|
|
670
|
+
} @else {
|
|
671
|
+
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
</div>
|
|
676
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
586
677
|
}
|
|
587
678
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: EmailFieldComponent, decorators: [{
|
|
588
679
|
type: Component,
|
|
589
680
|
args: [{
|
|
681
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
590
682
|
selector: 'ngx-email-field',
|
|
591
683
|
standalone: true,
|
|
592
684
|
imports: [ReactiveFormsModule],
|
|
593
|
-
template: `
|
|
594
|
-
<div class="ngx-field ngx-field--email"
|
|
595
|
-
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
596
|
-
<label class="ngx-field__label">{{ label }}</label>
|
|
597
|
-
@if (masked) {
|
|
598
|
-
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
599
|
-
} @else if (readonly) {
|
|
600
|
-
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">
|
|
601
|
-
<a [href]="'mailto:' + control.value" class="ngx-field__email-link">{{ control.value || '—' }}</a>
|
|
602
|
-
</span>
|
|
603
|
-
} @else {
|
|
604
|
-
<input
|
|
605
|
-
class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
|
|
606
|
-
type="email"
|
|
607
|
-
autocomplete="email"
|
|
608
|
-
[formControl]="$any(control)"
|
|
609
|
-
[placeholder]="placeholder || 'you@example.com'"
|
|
610
|
-
[attr.disabled]="field.disabled ? true : null"
|
|
611
|
-
/>
|
|
612
|
-
@if (control.invalid && control.touched) {
|
|
613
|
-
@if (control.errors?.['required']) {
|
|
614
|
-
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">Email is required</span>
|
|
615
|
-
} @else if (control.errors?.['email']) {
|
|
616
|
-
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">Enter a valid email address</span>
|
|
617
|
-
} @else {
|
|
618
|
-
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
</div>
|
|
685
|
+
template: `
|
|
686
|
+
<div class="ngx-field ngx-field--email"
|
|
687
|
+
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
688
|
+
<label class="ngx-field__label">{{ label }}</label>
|
|
689
|
+
@if (masked) {
|
|
690
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
691
|
+
} @else if (readonly) {
|
|
692
|
+
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">
|
|
693
|
+
<a [href]="'mailto:' + control.value" class="ngx-field__email-link">{{ control.value || '—' }}</a>
|
|
694
|
+
</span>
|
|
695
|
+
} @else {
|
|
696
|
+
<input
|
|
697
|
+
class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
|
|
698
|
+
type="email"
|
|
699
|
+
autocomplete="email"
|
|
700
|
+
[formControl]="$any(control)"
|
|
701
|
+
[placeholder]="placeholder || 'you@example.com'"
|
|
702
|
+
[attr.disabled]="field.disabled ? true : null"
|
|
703
|
+
/>
|
|
704
|
+
@if (control.invalid && control.touched) {
|
|
705
|
+
@if (control.errors?.['required']) {
|
|
706
|
+
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">Email is required</span>
|
|
707
|
+
} @else if (control.errors?.['email']) {
|
|
708
|
+
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">Enter a valid email address</span>
|
|
709
|
+
} @else {
|
|
710
|
+
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
</div>
|
|
623
715
|
`,
|
|
624
716
|
}]
|
|
625
717
|
}], propDecorators: { field: [{
|
|
@@ -652,78 +744,79 @@ class PasswordFieldComponent {
|
|
|
652
744
|
this.visible.update(v => !v);
|
|
653
745
|
}
|
|
654
746
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: PasswordFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
655
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: PasswordFieldComponent, isStandalone: true, selector: "ngx-password-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
656
|
-
<div class="ngx-field ngx-field--password"
|
|
657
|
-
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
658
|
-
<label class="ngx-field__label">{{ label }}</label>
|
|
659
|
-
@if (masked) {
|
|
660
|
-
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
661
|
-
} @else if (readonly) {
|
|
662
|
-
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">••••••••</span>
|
|
663
|
-
} @else {
|
|
664
|
-
<div class="ngx-field__password-wrap">
|
|
665
|
-
<input
|
|
666
|
-
class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
|
|
667
|
-
[type]="visible() ? 'text' : 'password'"
|
|
668
|
-
autocomplete="current-password"
|
|
669
|
-
[formControl]="$any(control)"
|
|
670
|
-
[placeholder]="placeholder || '••••••••'"
|
|
671
|
-
[attr.disabled]="field.disabled ? true : null"
|
|
672
|
-
/>
|
|
673
|
-
<button
|
|
674
|
-
type="button"
|
|
675
|
-
class="ngx-field__eye-btn"
|
|
676
|
-
(click)="toggleVisibility()"
|
|
677
|
-
[attr.aria-label]="visible() ? 'Hide password' : 'Show password'"
|
|
678
|
-
>
|
|
679
|
-
{{ visible() ? '🙈' : '👁' }}
|
|
680
|
-
</button>
|
|
681
|
-
</div>
|
|
682
|
-
@if (control.invalid && control.touched) {
|
|
683
|
-
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
</div>
|
|
687
|
-
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }] });
|
|
747
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: PasswordFieldComponent, isStandalone: true, selector: "ngx-password-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
748
|
+
<div class="ngx-field ngx-field--password"
|
|
749
|
+
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
750
|
+
<label class="ngx-field__label">{{ label }}</label>
|
|
751
|
+
@if (masked) {
|
|
752
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
753
|
+
} @else if (readonly) {
|
|
754
|
+
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">••••••••</span>
|
|
755
|
+
} @else {
|
|
756
|
+
<div class="ngx-field__password-wrap">
|
|
757
|
+
<input
|
|
758
|
+
class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
|
|
759
|
+
[type]="visible() ? 'text' : 'password'"
|
|
760
|
+
autocomplete="current-password"
|
|
761
|
+
[formControl]="$any(control)"
|
|
762
|
+
[placeholder]="placeholder || '••••••••'"
|
|
763
|
+
[attr.disabled]="field.disabled ? true : null"
|
|
764
|
+
/>
|
|
765
|
+
<button
|
|
766
|
+
type="button"
|
|
767
|
+
class="ngx-field__eye-btn"
|
|
768
|
+
(click)="toggleVisibility()"
|
|
769
|
+
[attr.aria-label]="visible() ? 'Hide password' : 'Show password'"
|
|
770
|
+
>
|
|
771
|
+
{{ visible() ? '🙈' : '👁' }}
|
|
772
|
+
</button>
|
|
773
|
+
</div>
|
|
774
|
+
@if (control.invalid && control.touched) {
|
|
775
|
+
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
</div>
|
|
779
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
688
780
|
}
|
|
689
781
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: PasswordFieldComponent, decorators: [{
|
|
690
782
|
type: Component,
|
|
691
783
|
args: [{
|
|
784
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
692
785
|
selector: 'ngx-password-field',
|
|
693
786
|
standalone: true,
|
|
694
787
|
imports: [ReactiveFormsModule],
|
|
695
|
-
template: `
|
|
696
|
-
<div class="ngx-field ngx-field--password"
|
|
697
|
-
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
698
|
-
<label class="ngx-field__label">{{ label }}</label>
|
|
699
|
-
@if (masked) {
|
|
700
|
-
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
701
|
-
} @else if (readonly) {
|
|
702
|
-
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">••••••••</span>
|
|
703
|
-
} @else {
|
|
704
|
-
<div class="ngx-field__password-wrap">
|
|
705
|
-
<input
|
|
706
|
-
class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
|
|
707
|
-
[type]="visible() ? 'text' : 'password'"
|
|
708
|
-
autocomplete="current-password"
|
|
709
|
-
[formControl]="$any(control)"
|
|
710
|
-
[placeholder]="placeholder || '••••••••'"
|
|
711
|
-
[attr.disabled]="field.disabled ? true : null"
|
|
712
|
-
/>
|
|
713
|
-
<button
|
|
714
|
-
type="button"
|
|
715
|
-
class="ngx-field__eye-btn"
|
|
716
|
-
(click)="toggleVisibility()"
|
|
717
|
-
[attr.aria-label]="visible() ? 'Hide password' : 'Show password'"
|
|
718
|
-
>
|
|
719
|
-
{{ visible() ? '🙈' : '👁' }}
|
|
720
|
-
</button>
|
|
721
|
-
</div>
|
|
722
|
-
@if (control.invalid && control.touched) {
|
|
723
|
-
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
</div>
|
|
788
|
+
template: `
|
|
789
|
+
<div class="ngx-field ngx-field--password"
|
|
790
|
+
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
791
|
+
<label class="ngx-field__label">{{ label }}</label>
|
|
792
|
+
@if (masked) {
|
|
793
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
794
|
+
} @else if (readonly) {
|
|
795
|
+
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">••••••••</span>
|
|
796
|
+
} @else {
|
|
797
|
+
<div class="ngx-field__password-wrap">
|
|
798
|
+
<input
|
|
799
|
+
class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
|
|
800
|
+
[type]="visible() ? 'text' : 'password'"
|
|
801
|
+
autocomplete="current-password"
|
|
802
|
+
[formControl]="$any(control)"
|
|
803
|
+
[placeholder]="placeholder || '••••••••'"
|
|
804
|
+
[attr.disabled]="field.disabled ? true : null"
|
|
805
|
+
/>
|
|
806
|
+
<button
|
|
807
|
+
type="button"
|
|
808
|
+
class="ngx-field__eye-btn"
|
|
809
|
+
(click)="toggleVisibility()"
|
|
810
|
+
[attr.aria-label]="visible() ? 'Hide password' : 'Show password'"
|
|
811
|
+
>
|
|
812
|
+
{{ visible() ? '🙈' : '👁' }}
|
|
813
|
+
</button>
|
|
814
|
+
</div>
|
|
815
|
+
@if (control.invalid && control.touched) {
|
|
816
|
+
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
</div>
|
|
727
820
|
`,
|
|
728
821
|
}]
|
|
729
822
|
}], propDecorators: { field: [{
|
|
@@ -748,58 +841,59 @@ class CheckboxFieldComponent {
|
|
|
748
841
|
return resolveLabel(this.field?.label, this.language);
|
|
749
842
|
}
|
|
750
843
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: CheckboxFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
751
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: CheckboxFieldComponent, isStandalone: true, selector: "ngx-checkbox-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
752
|
-
<div class="ngx-field ngx-field--checkbox"
|
|
753
|
-
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
754
|
-
@if (masked) {
|
|
755
|
-
<label class="ngx-field__label">{{ label }}</label>
|
|
756
|
-
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
757
|
-
} @else if (readonly) {
|
|
758
|
-
<label class="ngx-field__label">{{ label }}</label>
|
|
759
|
-
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ control.value ? 'Yes' : 'No' }}</span>
|
|
760
|
-
} @else {
|
|
761
|
-
<label class="ngx-field__label" [attr.for]="field.id">
|
|
762
|
-
<input
|
|
763
|
-
[id]="field.id"
|
|
764
|
-
class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
|
|
765
|
-
type="checkbox"
|
|
766
|
-
[formControl]="$any(control)"
|
|
767
|
-
[attr.disabled]="field.disabled ? true : null"
|
|
768
|
-
/>
|
|
769
|
-
{{ label }}
|
|
770
|
-
</label>
|
|
771
|
-
}
|
|
772
|
-
</div>
|
|
773
|
-
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }] });
|
|
844
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: CheckboxFieldComponent, isStandalone: true, selector: "ngx-checkbox-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
845
|
+
<div class="ngx-field ngx-field--checkbox"
|
|
846
|
+
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
847
|
+
@if (masked) {
|
|
848
|
+
<label class="ngx-field__label">{{ label }}</label>
|
|
849
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
850
|
+
} @else if (readonly) {
|
|
851
|
+
<label class="ngx-field__label">{{ label }}</label>
|
|
852
|
+
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ control.value ? 'Yes' : 'No' }}</span>
|
|
853
|
+
} @else {
|
|
854
|
+
<label class="ngx-field__label" [attr.for]="field.id">
|
|
855
|
+
<input
|
|
856
|
+
[id]="field.id"
|
|
857
|
+
class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
|
|
858
|
+
type="checkbox"
|
|
859
|
+
[formControl]="$any(control)"
|
|
860
|
+
[attr.disabled]="field.disabled ? true : null"
|
|
861
|
+
/>
|
|
862
|
+
{{ label }}
|
|
863
|
+
</label>
|
|
864
|
+
}
|
|
865
|
+
</div>
|
|
866
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
774
867
|
}
|
|
775
868
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: CheckboxFieldComponent, decorators: [{
|
|
776
869
|
type: Component,
|
|
777
870
|
args: [{
|
|
871
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
778
872
|
selector: 'ngx-checkbox-field',
|
|
779
873
|
standalone: true,
|
|
780
874
|
imports: [ReactiveFormsModule],
|
|
781
|
-
template: `
|
|
782
|
-
<div class="ngx-field ngx-field--checkbox"
|
|
783
|
-
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
784
|
-
@if (masked) {
|
|
785
|
-
<label class="ngx-field__label">{{ label }}</label>
|
|
786
|
-
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
787
|
-
} @else if (readonly) {
|
|
788
|
-
<label class="ngx-field__label">{{ label }}</label>
|
|
789
|
-
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ control.value ? 'Yes' : 'No' }}</span>
|
|
790
|
-
} @else {
|
|
791
|
-
<label class="ngx-field__label" [attr.for]="field.id">
|
|
792
|
-
<input
|
|
793
|
-
[id]="field.id"
|
|
794
|
-
class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
|
|
795
|
-
type="checkbox"
|
|
796
|
-
[formControl]="$any(control)"
|
|
797
|
-
[attr.disabled]="field.disabled ? true : null"
|
|
798
|
-
/>
|
|
799
|
-
{{ label }}
|
|
800
|
-
</label>
|
|
801
|
-
}
|
|
802
|
-
</div>
|
|
875
|
+
template: `
|
|
876
|
+
<div class="ngx-field ngx-field--checkbox"
|
|
877
|
+
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
878
|
+
@if (masked) {
|
|
879
|
+
<label class="ngx-field__label">{{ label }}</label>
|
|
880
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
881
|
+
} @else if (readonly) {
|
|
882
|
+
<label class="ngx-field__label">{{ label }}</label>
|
|
883
|
+
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ control.value ? 'Yes' : 'No' }}</span>
|
|
884
|
+
} @else {
|
|
885
|
+
<label class="ngx-field__label" [attr.for]="field.id">
|
|
886
|
+
<input
|
|
887
|
+
[id]="field.id"
|
|
888
|
+
class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
|
|
889
|
+
type="checkbox"
|
|
890
|
+
[formControl]="$any(control)"
|
|
891
|
+
[attr.disabled]="field.disabled ? true : null"
|
|
892
|
+
/>
|
|
893
|
+
{{ label }}
|
|
894
|
+
</label>
|
|
895
|
+
}
|
|
896
|
+
</div>
|
|
803
897
|
`,
|
|
804
898
|
}]
|
|
805
899
|
}], propDecorators: { field: [{
|
|
@@ -829,72 +923,73 @@ class BooleanFieldComponent {
|
|
|
829
923
|
return resolveLabel(this.field?.label, this.language);
|
|
830
924
|
}
|
|
831
925
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: BooleanFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
832
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: BooleanFieldComponent, isStandalone: true, selector: "ngx-boolean-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
833
|
-
<div class="ngx-field ngx-field--boolean"
|
|
834
|
-
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
835
|
-
@if (masked) {
|
|
836
|
-
<label class="ngx-field__label">{{ label }}</label>
|
|
837
|
-
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
838
|
-
} @else if (readonly) {
|
|
839
|
-
<label class="ngx-field__label">{{ label }}</label>
|
|
840
|
-
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ control.value ? 'Yes' : 'No' }}</span>
|
|
841
|
-
} @else {
|
|
842
|
-
<div class="ngx-field__toggle-wrap">
|
|
843
|
-
<label class="ngx-field__toggle-label">{{ label }}</label>
|
|
844
|
-
<label class="ngx-field__toggle-switch" [class.ngx-field__toggle-switch--checked]="control.value">
|
|
845
|
-
<input
|
|
846
|
-
class="ngx-field__toggle-input"
|
|
847
|
-
type="checkbox"
|
|
848
|
-
role="switch"
|
|
849
|
-
[formControl]="$any(control)"
|
|
850
|
-
[attr.disabled]="field.disabled ? true : null"
|
|
851
|
-
[attr.aria-checked]="control.value"
|
|
852
|
-
/>
|
|
853
|
-
<span class="ngx-field__toggle-track">
|
|
854
|
-
<span class="ngx-field__toggle-thumb"></span>
|
|
855
|
-
</span>
|
|
856
|
-
<span class="ngx-field__toggle-text">{{ control.value ? 'Yes' : 'No' }}</span>
|
|
857
|
-
</label>
|
|
858
|
-
</div>
|
|
859
|
-
}
|
|
860
|
-
</div>
|
|
861
|
-
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }] });
|
|
926
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: BooleanFieldComponent, isStandalone: true, selector: "ngx-boolean-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
927
|
+
<div class="ngx-field ngx-field--boolean"
|
|
928
|
+
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
929
|
+
@if (masked) {
|
|
930
|
+
<label class="ngx-field__label">{{ label }}</label>
|
|
931
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
932
|
+
} @else if (readonly) {
|
|
933
|
+
<label class="ngx-field__label">{{ label }}</label>
|
|
934
|
+
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ control.value ? 'Yes' : 'No' }}</span>
|
|
935
|
+
} @else {
|
|
936
|
+
<div class="ngx-field__toggle-wrap">
|
|
937
|
+
<label class="ngx-field__toggle-label">{{ label }}</label>
|
|
938
|
+
<label class="ngx-field__toggle-switch" [class.ngx-field__toggle-switch--checked]="control.value">
|
|
939
|
+
<input
|
|
940
|
+
class="ngx-field__toggle-input"
|
|
941
|
+
type="checkbox"
|
|
942
|
+
role="switch"
|
|
943
|
+
[formControl]="$any(control)"
|
|
944
|
+
[attr.disabled]="field.disabled ? true : null"
|
|
945
|
+
[attr.aria-checked]="control.value"
|
|
946
|
+
/>
|
|
947
|
+
<span class="ngx-field__toggle-track">
|
|
948
|
+
<span class="ngx-field__toggle-thumb"></span>
|
|
949
|
+
</span>
|
|
950
|
+
<span class="ngx-field__toggle-text">{{ control.value ? 'Yes' : 'No' }}</span>
|
|
951
|
+
</label>
|
|
952
|
+
</div>
|
|
953
|
+
}
|
|
954
|
+
</div>
|
|
955
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
862
956
|
}
|
|
863
957
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: BooleanFieldComponent, decorators: [{
|
|
864
958
|
type: Component,
|
|
865
959
|
args: [{
|
|
960
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
866
961
|
selector: 'ngx-boolean-field',
|
|
867
962
|
standalone: true,
|
|
868
963
|
imports: [ReactiveFormsModule],
|
|
869
|
-
template: `
|
|
870
|
-
<div class="ngx-field ngx-field--boolean"
|
|
871
|
-
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
872
|
-
@if (masked) {
|
|
873
|
-
<label class="ngx-field__label">{{ label }}</label>
|
|
874
|
-
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
875
|
-
} @else if (readonly) {
|
|
876
|
-
<label class="ngx-field__label">{{ label }}</label>
|
|
877
|
-
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ control.value ? 'Yes' : 'No' }}</span>
|
|
878
|
-
} @else {
|
|
879
|
-
<div class="ngx-field__toggle-wrap">
|
|
880
|
-
<label class="ngx-field__toggle-label">{{ label }}</label>
|
|
881
|
-
<label class="ngx-field__toggle-switch" [class.ngx-field__toggle-switch--checked]="control.value">
|
|
882
|
-
<input
|
|
883
|
-
class="ngx-field__toggle-input"
|
|
884
|
-
type="checkbox"
|
|
885
|
-
role="switch"
|
|
886
|
-
[formControl]="$any(control)"
|
|
887
|
-
[attr.disabled]="field.disabled ? true : null"
|
|
888
|
-
[attr.aria-checked]="control.value"
|
|
889
|
-
/>
|
|
890
|
-
<span class="ngx-field__toggle-track">
|
|
891
|
-
<span class="ngx-field__toggle-thumb"></span>
|
|
892
|
-
</span>
|
|
893
|
-
<span class="ngx-field__toggle-text">{{ control.value ? 'Yes' : 'No' }}</span>
|
|
894
|
-
</label>
|
|
895
|
-
</div>
|
|
896
|
-
}
|
|
897
|
-
</div>
|
|
964
|
+
template: `
|
|
965
|
+
<div class="ngx-field ngx-field--boolean"
|
|
966
|
+
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
967
|
+
@if (masked) {
|
|
968
|
+
<label class="ngx-field__label">{{ label }}</label>
|
|
969
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
970
|
+
} @else if (readonly) {
|
|
971
|
+
<label class="ngx-field__label">{{ label }}</label>
|
|
972
|
+
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ control.value ? 'Yes' : 'No' }}</span>
|
|
973
|
+
} @else {
|
|
974
|
+
<div class="ngx-field__toggle-wrap">
|
|
975
|
+
<label class="ngx-field__toggle-label">{{ label }}</label>
|
|
976
|
+
<label class="ngx-field__toggle-switch" [class.ngx-field__toggle-switch--checked]="control.value">
|
|
977
|
+
<input
|
|
978
|
+
class="ngx-field__toggle-input"
|
|
979
|
+
type="checkbox"
|
|
980
|
+
role="switch"
|
|
981
|
+
[formControl]="$any(control)"
|
|
982
|
+
[attr.disabled]="field.disabled ? true : null"
|
|
983
|
+
[attr.aria-checked]="control.value"
|
|
984
|
+
/>
|
|
985
|
+
<span class="ngx-field__toggle-track">
|
|
986
|
+
<span class="ngx-field__toggle-thumb"></span>
|
|
987
|
+
</span>
|
|
988
|
+
<span class="ngx-field__toggle-text">{{ control.value ? 'Yes' : 'No' }}</span>
|
|
989
|
+
</label>
|
|
990
|
+
</div>
|
|
991
|
+
}
|
|
992
|
+
</div>
|
|
898
993
|
`,
|
|
899
994
|
}]
|
|
900
995
|
}], propDecorators: { field: [{
|
|
@@ -930,56 +1025,57 @@ class DateFieldComponent {
|
|
|
930
1025
|
}
|
|
931
1026
|
}
|
|
932
1027
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: DateFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
933
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: DateFieldComponent, isStandalone: true, selector: "ngx-date-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
934
|
-
<div class="ngx-field ngx-field--date"
|
|
935
|
-
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
936
|
-
<label class="ngx-field__label" [attr.for]="field.id">{{ label }}</label>
|
|
937
|
-
@if (masked) {
|
|
938
|
-
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
939
|
-
} @else if (readonly) {
|
|
940
|
-
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ formatDate(control.value) }}</span>
|
|
941
|
-
} @else {
|
|
942
|
-
<input
|
|
943
|
-
[id]="field.id"
|
|
944
|
-
class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
|
|
945
|
-
type="date"
|
|
946
|
-
[formControl]="$any(control)"
|
|
947
|
-
[attr.disabled]="field.disabled ? true : null"
|
|
948
|
-
/>
|
|
949
|
-
@if (control.invalid && control.touched) {
|
|
950
|
-
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
</div>
|
|
954
|
-
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }] });
|
|
1028
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: DateFieldComponent, isStandalone: true, selector: "ngx-date-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
1029
|
+
<div class="ngx-field ngx-field--date"
|
|
1030
|
+
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
1031
|
+
<label class="ngx-field__label" [attr.for]="field.id">{{ label }}</label>
|
|
1032
|
+
@if (masked) {
|
|
1033
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
1034
|
+
} @else if (readonly) {
|
|
1035
|
+
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ formatDate(control.value) }}</span>
|
|
1036
|
+
} @else {
|
|
1037
|
+
<input
|
|
1038
|
+
[id]="field.id"
|
|
1039
|
+
class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
|
|
1040
|
+
type="date"
|
|
1041
|
+
[formControl]="$any(control)"
|
|
1042
|
+
[attr.disabled]="field.disabled ? true : null"
|
|
1043
|
+
/>
|
|
1044
|
+
@if (control.invalid && control.touched) {
|
|
1045
|
+
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
</div>
|
|
1049
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
955
1050
|
}
|
|
956
1051
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: DateFieldComponent, decorators: [{
|
|
957
1052
|
type: Component,
|
|
958
1053
|
args: [{
|
|
1054
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
959
1055
|
selector: 'ngx-date-field',
|
|
960
1056
|
standalone: true,
|
|
961
1057
|
imports: [ReactiveFormsModule],
|
|
962
|
-
template: `
|
|
963
|
-
<div class="ngx-field ngx-field--date"
|
|
964
|
-
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
965
|
-
<label class="ngx-field__label" [attr.for]="field.id">{{ label }}</label>
|
|
966
|
-
@if (masked) {
|
|
967
|
-
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
968
|
-
} @else if (readonly) {
|
|
969
|
-
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ formatDate(control.value) }}</span>
|
|
970
|
-
} @else {
|
|
971
|
-
<input
|
|
972
|
-
[id]="field.id"
|
|
973
|
-
class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
|
|
974
|
-
type="date"
|
|
975
|
-
[formControl]="$any(control)"
|
|
976
|
-
[attr.disabled]="field.disabled ? true : null"
|
|
977
|
-
/>
|
|
978
|
-
@if (control.invalid && control.touched) {
|
|
979
|
-
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
|
|
980
|
-
}
|
|
981
|
-
}
|
|
982
|
-
</div>
|
|
1058
|
+
template: `
|
|
1059
|
+
<div class="ngx-field ngx-field--date"
|
|
1060
|
+
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
1061
|
+
<label class="ngx-field__label" [attr.for]="field.id">{{ label }}</label>
|
|
1062
|
+
@if (masked) {
|
|
1063
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
1064
|
+
} @else if (readonly) {
|
|
1065
|
+
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ formatDate(control.value) }}</span>
|
|
1066
|
+
} @else {
|
|
1067
|
+
<input
|
|
1068
|
+
[id]="field.id"
|
|
1069
|
+
class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
|
|
1070
|
+
type="date"
|
|
1071
|
+
[formControl]="$any(control)"
|
|
1072
|
+
[attr.disabled]="field.disabled ? true : null"
|
|
1073
|
+
/>
|
|
1074
|
+
@if (control.invalid && control.touched) {
|
|
1075
|
+
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
</div>
|
|
983
1079
|
`,
|
|
984
1080
|
}]
|
|
985
1081
|
}], propDecorators: { field: [{
|
|
@@ -1042,90 +1138,91 @@ class MonthYearFieldComponent {
|
|
|
1042
1138
|
return monthName && year ? `${monthName} ${year}` : String(value);
|
|
1043
1139
|
}
|
|
1044
1140
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: MonthYearFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1045
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: MonthYearFieldComponent, isStandalone: true, selector: "ngx-month-year-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
1046
|
-
<div class="ngx-field ngx-field--month-year"
|
|
1047
|
-
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
1048
|
-
<label class="ngx-field__label">{{ label }}</label>
|
|
1049
|
-
@if (masked) {
|
|
1050
|
-
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
1051
|
-
} @else if (readonly) {
|
|
1052
|
-
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ formatValue(control.value) }}</span>
|
|
1053
|
-
} @else {
|
|
1054
|
-
<div class="ngx-field__month-year-wrap">
|
|
1055
|
-
<select
|
|
1056
|
-
class="ngx-field__input ngx-field__input--month" [attr.data-testid]="'field-' + field.id + '-month'"
|
|
1057
|
-
[value]="selectedMonth"
|
|
1058
|
-
(change)="onMonthChange($any($event.target).value)"
|
|
1059
|
-
[attr.disabled]="field.disabled ? true : null"
|
|
1060
|
-
>
|
|
1061
|
-
<option value="">Month</option>
|
|
1062
|
-
@for (m of months; track m.value) {
|
|
1063
|
-
<option [value]="m.value">{{ m.label }}</option>
|
|
1064
|
-
}
|
|
1065
|
-
</select>
|
|
1066
|
-
<select
|
|
1067
|
-
class="ngx-field__input ngx-field__input--year" [attr.data-testid]="'field-' + field.id + '-year'"
|
|
1068
|
-
[value]="selectedYear"
|
|
1069
|
-
(change)="onYearChange($any($event.target).value)"
|
|
1070
|
-
[attr.disabled]="field.disabled ? true : null"
|
|
1071
|
-
>
|
|
1072
|
-
<option value="">Year</option>
|
|
1073
|
-
@for (y of years; track y) {
|
|
1074
|
-
<option [value]="y">{{ y }}</option>
|
|
1075
|
-
}
|
|
1076
|
-
</select>
|
|
1077
|
-
</div>
|
|
1078
|
-
@if (control.invalid && control.touched) {
|
|
1079
|
-
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
|
|
1080
|
-
}
|
|
1081
|
-
}
|
|
1082
|
-
</div>
|
|
1083
|
-
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }] });
|
|
1141
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: MonthYearFieldComponent, isStandalone: true, selector: "ngx-month-year-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
1142
|
+
<div class="ngx-field ngx-field--month-year"
|
|
1143
|
+
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
1144
|
+
<label class="ngx-field__label">{{ label }}</label>
|
|
1145
|
+
@if (masked) {
|
|
1146
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
1147
|
+
} @else if (readonly) {
|
|
1148
|
+
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ formatValue(control.value) }}</span>
|
|
1149
|
+
} @else {
|
|
1150
|
+
<div class="ngx-field__month-year-wrap">
|
|
1151
|
+
<select
|
|
1152
|
+
class="ngx-field__input ngx-field__input--month" [attr.data-testid]="'field-' + field.id + '-month'"
|
|
1153
|
+
[value]="selectedMonth"
|
|
1154
|
+
(change)="onMonthChange($any($event.target).value)"
|
|
1155
|
+
[attr.disabled]="field.disabled ? true : null"
|
|
1156
|
+
>
|
|
1157
|
+
<option value="">Month</option>
|
|
1158
|
+
@for (m of months; track m.value) {
|
|
1159
|
+
<option [value]="m.value">{{ m.label }}</option>
|
|
1160
|
+
}
|
|
1161
|
+
</select>
|
|
1162
|
+
<select
|
|
1163
|
+
class="ngx-field__input ngx-field__input--year" [attr.data-testid]="'field-' + field.id + '-year'"
|
|
1164
|
+
[value]="selectedYear"
|
|
1165
|
+
(change)="onYearChange($any($event.target).value)"
|
|
1166
|
+
[attr.disabled]="field.disabled ? true : null"
|
|
1167
|
+
>
|
|
1168
|
+
<option value="">Year</option>
|
|
1169
|
+
@for (y of years; track y) {
|
|
1170
|
+
<option [value]="y">{{ y }}</option>
|
|
1171
|
+
}
|
|
1172
|
+
</select>
|
|
1173
|
+
</div>
|
|
1174
|
+
@if (control.invalid && control.touched) {
|
|
1175
|
+
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
</div>
|
|
1179
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1084
1180
|
}
|
|
1085
1181
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: MonthYearFieldComponent, decorators: [{
|
|
1086
1182
|
type: Component,
|
|
1087
1183
|
args: [{
|
|
1184
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1088
1185
|
selector: 'ngx-month-year-field',
|
|
1089
1186
|
standalone: true,
|
|
1090
1187
|
imports: [ReactiveFormsModule],
|
|
1091
|
-
template: `
|
|
1092
|
-
<div class="ngx-field ngx-field--month-year"
|
|
1093
|
-
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
1094
|
-
<label class="ngx-field__label">{{ label }}</label>
|
|
1095
|
-
@if (masked) {
|
|
1096
|
-
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
1097
|
-
} @else if (readonly) {
|
|
1098
|
-
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ formatValue(control.value) }}</span>
|
|
1099
|
-
} @else {
|
|
1100
|
-
<div class="ngx-field__month-year-wrap">
|
|
1101
|
-
<select
|
|
1102
|
-
class="ngx-field__input ngx-field__input--month" [attr.data-testid]="'field-' + field.id + '-month'"
|
|
1103
|
-
[value]="selectedMonth"
|
|
1104
|
-
(change)="onMonthChange($any($event.target).value)"
|
|
1105
|
-
[attr.disabled]="field.disabled ? true : null"
|
|
1106
|
-
>
|
|
1107
|
-
<option value="">Month</option>
|
|
1108
|
-
@for (m of months; track m.value) {
|
|
1109
|
-
<option [value]="m.value">{{ m.label }}</option>
|
|
1110
|
-
}
|
|
1111
|
-
</select>
|
|
1112
|
-
<select
|
|
1113
|
-
class="ngx-field__input ngx-field__input--year" [attr.data-testid]="'field-' + field.id + '-year'"
|
|
1114
|
-
[value]="selectedYear"
|
|
1115
|
-
(change)="onYearChange($any($event.target).value)"
|
|
1116
|
-
[attr.disabled]="field.disabled ? true : null"
|
|
1117
|
-
>
|
|
1118
|
-
<option value="">Year</option>
|
|
1119
|
-
@for (y of years; track y) {
|
|
1120
|
-
<option [value]="y">{{ y }}</option>
|
|
1121
|
-
}
|
|
1122
|
-
</select>
|
|
1123
|
-
</div>
|
|
1124
|
-
@if (control.invalid && control.touched) {
|
|
1125
|
-
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
|
|
1126
|
-
}
|
|
1127
|
-
}
|
|
1128
|
-
</div>
|
|
1188
|
+
template: `
|
|
1189
|
+
<div class="ngx-field ngx-field--month-year"
|
|
1190
|
+
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
1191
|
+
<label class="ngx-field__label">{{ label }}</label>
|
|
1192
|
+
@if (masked) {
|
|
1193
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
1194
|
+
} @else if (readonly) {
|
|
1195
|
+
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ formatValue(control.value) }}</span>
|
|
1196
|
+
} @else {
|
|
1197
|
+
<div class="ngx-field__month-year-wrap">
|
|
1198
|
+
<select
|
|
1199
|
+
class="ngx-field__input ngx-field__input--month" [attr.data-testid]="'field-' + field.id + '-month'"
|
|
1200
|
+
[value]="selectedMonth"
|
|
1201
|
+
(change)="onMonthChange($any($event.target).value)"
|
|
1202
|
+
[attr.disabled]="field.disabled ? true : null"
|
|
1203
|
+
>
|
|
1204
|
+
<option value="">Month</option>
|
|
1205
|
+
@for (m of months; track m.value) {
|
|
1206
|
+
<option [value]="m.value">{{ m.label }}</option>
|
|
1207
|
+
}
|
|
1208
|
+
</select>
|
|
1209
|
+
<select
|
|
1210
|
+
class="ngx-field__input ngx-field__input--year" [attr.data-testid]="'field-' + field.id + '-year'"
|
|
1211
|
+
[value]="selectedYear"
|
|
1212
|
+
(change)="onYearChange($any($event.target).value)"
|
|
1213
|
+
[attr.disabled]="field.disabled ? true : null"
|
|
1214
|
+
>
|
|
1215
|
+
<option value="">Year</option>
|
|
1216
|
+
@for (y of years; track y) {
|
|
1217
|
+
<option [value]="y">{{ y }}</option>
|
|
1218
|
+
}
|
|
1219
|
+
</select>
|
|
1220
|
+
</div>
|
|
1221
|
+
@if (control.invalid && control.touched) {
|
|
1222
|
+
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
</div>
|
|
1129
1226
|
`,
|
|
1130
1227
|
}]
|
|
1131
1228
|
}], propDecorators: { field: [{
|
|
@@ -1355,7 +1452,7 @@ const hostLastConfig = new WeakMap();
|
|
|
1355
1452
|
* Point a choice component's `options` signal at the right source (parity plan §6.3).
|
|
1356
1453
|
*
|
|
1357
1454
|
* Called from the `field` / `language` input setters of `dropdown`, `radio` and `multiSelect` —
|
|
1358
|
-
* the wiring is theirs, the decision is the service's, which is what keeps the
|
|
1455
|
+
* the wiring is theirs, the decision is the service's, which is what keeps the field
|
|
1359
1456
|
* five-input contract intact: options are never pushed in as a sixth input.
|
|
1360
1457
|
*
|
|
1361
1458
|
* Setters rather than `ngOnChanges` on purpose. `ngOnChanges` only fires for inputs Angular
|
|
@@ -1384,6 +1481,7 @@ function refreshChoiceOptions(host, lookups) {
|
|
|
1384
1481
|
|
|
1385
1482
|
class DropdownFieldComponent {
|
|
1386
1483
|
lookups = inject(LookupRegistryService);
|
|
1484
|
+
messages = inject(ValidationMessagesService);
|
|
1387
1485
|
_field;
|
|
1388
1486
|
_language = 'en';
|
|
1389
1487
|
/** Setter-based so options resolve however the input is set — see `refreshChoiceOptions`. */
|
|
@@ -1442,100 +1540,100 @@ class DropdownFieldComponent {
|
|
|
1442
1540
|
get errorMessage() {
|
|
1443
1541
|
if (!this.control || !this.control.errors || !this.control.touched)
|
|
1444
1542
|
return '';
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
return 'Invalid selection.';
|
|
1543
|
+
// `requiredSelection` rather than `required`: "Please select an option" reads better on a
|
|
1544
|
+
// dropdown than "This field is required", and both stay independently overridable.
|
|
1545
|
+
return this.messages.resolve(this.control.errors, this.language, [['required', 'requiredSelection']], 'invalidSelection');
|
|
1449
1546
|
}
|
|
1450
1547
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: DropdownFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1451
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: DropdownFieldComponent, isStandalone: true, selector: "ngx-dropdown-field", inputs: { field: "field", language: "language", control: "control", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
1452
|
-
<div
|
|
1453
|
-
class="ngx-field ngx-field--dropdown"
|
|
1454
|
-
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type"
|
|
1455
|
-
[class.ngx-field--readonly]="readonly"
|
|
1456
|
-
[class.ngx-field--masked]="masked"
|
|
1457
|
-
[class.ngx-field--invalid]="control && control.invalid && control.touched"
|
|
1458
|
-
>
|
|
1459
|
-
<label class="ngx-field__label" [attr.for]="field.id">
|
|
1460
|
-
{{ label }}
|
|
1461
|
-
@if (field.validators?.required) { <span class="ngx-field__req">*</span> }
|
|
1462
|
-
</label>
|
|
1463
|
-
@if (masked) {
|
|
1464
|
-
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
1465
|
-
} @else if (readonly) {
|
|
1466
|
-
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ getLabel(control.value) }}</span>
|
|
1467
|
-
} @else {
|
|
1468
|
-
<select
|
|
1469
|
-
[id]="field.id"
|
|
1470
|
-
class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
|
|
1471
|
-
[formControl]="$any(control)"
|
|
1472
|
-
[compareWith]="compareFn"
|
|
1473
|
-
[attr.disabled]="field.disabled ? true : null"
|
|
1474
|
-
[attr.aria-invalid]="control.invalid && control.touched"
|
|
1475
|
-
[attr.aria-describedby]="errorMessage ? field.id + '-error' : null"
|
|
1476
|
-
>
|
|
1477
|
-
<option [value]="''">{{ placeholder || 'Select...' }}</option>
|
|
1478
|
-
@for (option of options(); track getOptLabel(option)) {
|
|
1479
|
-
@if (isObjectVal(option)) {
|
|
1480
|
-
<option [ngValue]="getOptStoredVal(option)">{{ getOptLabel(option) }}</option>
|
|
1481
|
-
} @else {
|
|
1482
|
-
<option [value]="getOptStoredVal(option)">{{ getOptLabel(option) }}</option>
|
|
1483
|
-
}
|
|
1484
|
-
}
|
|
1485
|
-
</select>
|
|
1486
|
-
@if (errorMessage) {
|
|
1487
|
-
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'" [id]="field.id + '-error'" role="alert">{{ errorMessage }}</span>
|
|
1488
|
-
}
|
|
1489
|
-
}
|
|
1490
|
-
</div>
|
|
1491
|
-
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }] });
|
|
1548
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: DropdownFieldComponent, isStandalone: true, selector: "ngx-dropdown-field", inputs: { field: "field", language: "language", control: "control", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
1549
|
+
<div
|
|
1550
|
+
class="ngx-field ngx-field--dropdown"
|
|
1551
|
+
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type"
|
|
1552
|
+
[class.ngx-field--readonly]="readonly"
|
|
1553
|
+
[class.ngx-field--masked]="masked"
|
|
1554
|
+
[class.ngx-field--invalid]="control && control.invalid && control.touched"
|
|
1555
|
+
>
|
|
1556
|
+
<label class="ngx-field__label" [attr.for]="field.id">
|
|
1557
|
+
{{ label }}
|
|
1558
|
+
@if (field.validators?.required) { <span class="ngx-field__req">*</span> }
|
|
1559
|
+
</label>
|
|
1560
|
+
@if (masked) {
|
|
1561
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
1562
|
+
} @else if (readonly) {
|
|
1563
|
+
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ getLabel(control.value) }}</span>
|
|
1564
|
+
} @else {
|
|
1565
|
+
<select
|
|
1566
|
+
[id]="field.id"
|
|
1567
|
+
class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
|
|
1568
|
+
[formControl]="$any(control)"
|
|
1569
|
+
[compareWith]="compareFn"
|
|
1570
|
+
[attr.disabled]="field.disabled ? true : null"
|
|
1571
|
+
[attr.aria-invalid]="control.invalid && control.touched"
|
|
1572
|
+
[attr.aria-describedby]="errorMessage ? field.id + '-error' : null"
|
|
1573
|
+
>
|
|
1574
|
+
<option [value]="''">{{ placeholder || 'Select...' }}</option>
|
|
1575
|
+
@for (option of options(); track getOptLabel(option)) {
|
|
1576
|
+
@if (isObjectVal(option)) {
|
|
1577
|
+
<option [ngValue]="getOptStoredVal(option)">{{ getOptLabel(option) }}</option>
|
|
1578
|
+
} @else {
|
|
1579
|
+
<option [value]="getOptStoredVal(option)">{{ getOptLabel(option) }}</option>
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
</select>
|
|
1583
|
+
@if (errorMessage) {
|
|
1584
|
+
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'" [id]="field.id + '-error'" role="alert">{{ errorMessage }}</span>
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
</div>
|
|
1588
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1492
1589
|
}
|
|
1493
1590
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: DropdownFieldComponent, decorators: [{
|
|
1494
1591
|
type: Component,
|
|
1495
1592
|
args: [{
|
|
1593
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1496
1594
|
selector: 'ngx-dropdown-field',
|
|
1497
1595
|
standalone: true,
|
|
1498
1596
|
imports: [ReactiveFormsModule],
|
|
1499
|
-
template: `
|
|
1500
|
-
<div
|
|
1501
|
-
class="ngx-field ngx-field--dropdown"
|
|
1502
|
-
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type"
|
|
1503
|
-
[class.ngx-field--readonly]="readonly"
|
|
1504
|
-
[class.ngx-field--masked]="masked"
|
|
1505
|
-
[class.ngx-field--invalid]="control && control.invalid && control.touched"
|
|
1506
|
-
>
|
|
1507
|
-
<label class="ngx-field__label" [attr.for]="field.id">
|
|
1508
|
-
{{ label }}
|
|
1509
|
-
@if (field.validators?.required) { <span class="ngx-field__req">*</span> }
|
|
1510
|
-
</label>
|
|
1511
|
-
@if (masked) {
|
|
1512
|
-
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
1513
|
-
} @else if (readonly) {
|
|
1514
|
-
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ getLabel(control.value) }}</span>
|
|
1515
|
-
} @else {
|
|
1516
|
-
<select
|
|
1517
|
-
[id]="field.id"
|
|
1518
|
-
class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
|
|
1519
|
-
[formControl]="$any(control)"
|
|
1520
|
-
[compareWith]="compareFn"
|
|
1521
|
-
[attr.disabled]="field.disabled ? true : null"
|
|
1522
|
-
[attr.aria-invalid]="control.invalid && control.touched"
|
|
1523
|
-
[attr.aria-describedby]="errorMessage ? field.id + '-error' : null"
|
|
1524
|
-
>
|
|
1525
|
-
<option [value]="''">{{ placeholder || 'Select...' }}</option>
|
|
1526
|
-
@for (option of options(); track getOptLabel(option)) {
|
|
1527
|
-
@if (isObjectVal(option)) {
|
|
1528
|
-
<option [ngValue]="getOptStoredVal(option)">{{ getOptLabel(option) }}</option>
|
|
1529
|
-
} @else {
|
|
1530
|
-
<option [value]="getOptStoredVal(option)">{{ getOptLabel(option) }}</option>
|
|
1531
|
-
}
|
|
1532
|
-
}
|
|
1533
|
-
</select>
|
|
1534
|
-
@if (errorMessage) {
|
|
1535
|
-
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'" [id]="field.id + '-error'" role="alert">{{ errorMessage }}</span>
|
|
1536
|
-
}
|
|
1537
|
-
}
|
|
1538
|
-
</div>
|
|
1597
|
+
template: `
|
|
1598
|
+
<div
|
|
1599
|
+
class="ngx-field ngx-field--dropdown"
|
|
1600
|
+
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type"
|
|
1601
|
+
[class.ngx-field--readonly]="readonly"
|
|
1602
|
+
[class.ngx-field--masked]="masked"
|
|
1603
|
+
[class.ngx-field--invalid]="control && control.invalid && control.touched"
|
|
1604
|
+
>
|
|
1605
|
+
<label class="ngx-field__label" [attr.for]="field.id">
|
|
1606
|
+
{{ label }}
|
|
1607
|
+
@if (field.validators?.required) { <span class="ngx-field__req">*</span> }
|
|
1608
|
+
</label>
|
|
1609
|
+
@if (masked) {
|
|
1610
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
1611
|
+
} @else if (readonly) {
|
|
1612
|
+
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ getLabel(control.value) }}</span>
|
|
1613
|
+
} @else {
|
|
1614
|
+
<select
|
|
1615
|
+
[id]="field.id"
|
|
1616
|
+
class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
|
|
1617
|
+
[formControl]="$any(control)"
|
|
1618
|
+
[compareWith]="compareFn"
|
|
1619
|
+
[attr.disabled]="field.disabled ? true : null"
|
|
1620
|
+
[attr.aria-invalid]="control.invalid && control.touched"
|
|
1621
|
+
[attr.aria-describedby]="errorMessage ? field.id + '-error' : null"
|
|
1622
|
+
>
|
|
1623
|
+
<option [value]="''">{{ placeholder || 'Select...' }}</option>
|
|
1624
|
+
@for (option of options(); track getOptLabel(option)) {
|
|
1625
|
+
@if (isObjectVal(option)) {
|
|
1626
|
+
<option [ngValue]="getOptStoredVal(option)">{{ getOptLabel(option) }}</option>
|
|
1627
|
+
} @else {
|
|
1628
|
+
<option [value]="getOptStoredVal(option)">{{ getOptLabel(option) }}</option>
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
</select>
|
|
1632
|
+
@if (errorMessage) {
|
|
1633
|
+
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'" [id]="field.id + '-error'" role="alert">{{ errorMessage }}</span>
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
</div>
|
|
1539
1637
|
`,
|
|
1540
1638
|
}]
|
|
1541
1639
|
}], propDecorators: { field: [{
|
|
@@ -1612,76 +1710,77 @@ class RadioFieldComponent {
|
|
|
1612
1710
|
return typeof value === 'object' ? resolveLabel(value, this.language) : (value ?? '—');
|
|
1613
1711
|
}
|
|
1614
1712
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: RadioFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1615
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: RadioFieldComponent, isStandalone: true, selector: "ngx-radio-field", inputs: { field: "field", language: "language", control: "control", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
1616
|
-
<div class="ngx-field ngx-field--radio"
|
|
1617
|
-
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
1618
|
-
<fieldset class="ngx-field__fieldset">
|
|
1619
|
-
<legend class="ngx-field__label">{{ label }}</legend>
|
|
1620
|
-
@if (masked) {
|
|
1621
|
-
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
1622
|
-
} @else if (readonly) {
|
|
1623
|
-
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ getSelectedLabel() }}</span>
|
|
1624
|
-
} @else {
|
|
1625
|
-
<div class="ngx-field__radio-group">
|
|
1626
|
-
@for (option of options(); track getOptLabel(option)) {
|
|
1627
|
-
<label class="ngx-field__radio-option" [attr.for]="getRadioId(option)">
|
|
1628
|
-
<input
|
|
1629
|
-
[id]="getRadioId(option)"
|
|
1630
|
-
type="radio"
|
|
1631
|
-
class="ngx-field__radio-input"
|
|
1632
|
-
[formControl]="$any(control)"
|
|
1633
|
-
[value]="getOptStoredVal(option)"
|
|
1634
|
-
[attr.disabled]="field.disabled ? true : null"
|
|
1635
|
-
/>
|
|
1636
|
-
<span class="ngx-field__radio-label">{{ getOptLabel(option) }}</span>
|
|
1637
|
-
</label>
|
|
1638
|
-
}
|
|
1639
|
-
</div>
|
|
1640
|
-
@if (control.invalid && control.touched) {
|
|
1641
|
-
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
|
|
1642
|
-
}
|
|
1643
|
-
}
|
|
1644
|
-
</fieldset>
|
|
1645
|
-
</div>
|
|
1646
|
-
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.RadioControlValueAccessor, selector: "input[type=radio][formControlName],input[type=radio][formControl],input[type=radio][ngModel]", inputs: ["name", "formControlName", "value"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }] });
|
|
1713
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: RadioFieldComponent, isStandalone: true, selector: "ngx-radio-field", inputs: { field: "field", language: "language", control: "control", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
1714
|
+
<div class="ngx-field ngx-field--radio"
|
|
1715
|
+
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
1716
|
+
<fieldset class="ngx-field__fieldset">
|
|
1717
|
+
<legend class="ngx-field__label">{{ label }}</legend>
|
|
1718
|
+
@if (masked) {
|
|
1719
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
1720
|
+
} @else if (readonly) {
|
|
1721
|
+
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ getSelectedLabel() }}</span>
|
|
1722
|
+
} @else {
|
|
1723
|
+
<div class="ngx-field__radio-group">
|
|
1724
|
+
@for (option of options(); track getOptLabel(option)) {
|
|
1725
|
+
<label class="ngx-field__radio-option" [attr.for]="getRadioId(option)">
|
|
1726
|
+
<input
|
|
1727
|
+
[id]="getRadioId(option)"
|
|
1728
|
+
type="radio"
|
|
1729
|
+
class="ngx-field__radio-input"
|
|
1730
|
+
[formControl]="$any(control)"
|
|
1731
|
+
[value]="getOptStoredVal(option)"
|
|
1732
|
+
[attr.disabled]="field.disabled ? true : null"
|
|
1733
|
+
/>
|
|
1734
|
+
<span class="ngx-field__radio-label">{{ getOptLabel(option) }}</span>
|
|
1735
|
+
</label>
|
|
1736
|
+
}
|
|
1737
|
+
</div>
|
|
1738
|
+
@if (control.invalid && control.touched) {
|
|
1739
|
+
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
</fieldset>
|
|
1743
|
+
</div>
|
|
1744
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.RadioControlValueAccessor, selector: "input[type=radio][formControlName],input[type=radio][formControl],input[type=radio][ngModel]", inputs: ["name", "formControlName", "value"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1647
1745
|
}
|
|
1648
1746
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: RadioFieldComponent, decorators: [{
|
|
1649
1747
|
type: Component,
|
|
1650
1748
|
args: [{
|
|
1749
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1651
1750
|
selector: 'ngx-radio-field',
|
|
1652
1751
|
standalone: true,
|
|
1653
1752
|
imports: [ReactiveFormsModule],
|
|
1654
|
-
template: `
|
|
1655
|
-
<div class="ngx-field ngx-field--radio"
|
|
1656
|
-
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
1657
|
-
<fieldset class="ngx-field__fieldset">
|
|
1658
|
-
<legend class="ngx-field__label">{{ label }}</legend>
|
|
1659
|
-
@if (masked) {
|
|
1660
|
-
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
1661
|
-
} @else if (readonly) {
|
|
1662
|
-
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ getSelectedLabel() }}</span>
|
|
1663
|
-
} @else {
|
|
1664
|
-
<div class="ngx-field__radio-group">
|
|
1665
|
-
@for (option of options(); track getOptLabel(option)) {
|
|
1666
|
-
<label class="ngx-field__radio-option" [attr.for]="getRadioId(option)">
|
|
1667
|
-
<input
|
|
1668
|
-
[id]="getRadioId(option)"
|
|
1669
|
-
type="radio"
|
|
1670
|
-
class="ngx-field__radio-input"
|
|
1671
|
-
[formControl]="$any(control)"
|
|
1672
|
-
[value]="getOptStoredVal(option)"
|
|
1673
|
-
[attr.disabled]="field.disabled ? true : null"
|
|
1674
|
-
/>
|
|
1675
|
-
<span class="ngx-field__radio-label">{{ getOptLabel(option) }}</span>
|
|
1676
|
-
</label>
|
|
1677
|
-
}
|
|
1678
|
-
</div>
|
|
1679
|
-
@if (control.invalid && control.touched) {
|
|
1680
|
-
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
|
|
1681
|
-
}
|
|
1682
|
-
}
|
|
1683
|
-
</fieldset>
|
|
1684
|
-
</div>
|
|
1753
|
+
template: `
|
|
1754
|
+
<div class="ngx-field ngx-field--radio"
|
|
1755
|
+
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
1756
|
+
<fieldset class="ngx-field__fieldset">
|
|
1757
|
+
<legend class="ngx-field__label">{{ label }}</legend>
|
|
1758
|
+
@if (masked) {
|
|
1759
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
1760
|
+
} @else if (readonly) {
|
|
1761
|
+
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ getSelectedLabel() }}</span>
|
|
1762
|
+
} @else {
|
|
1763
|
+
<div class="ngx-field__radio-group">
|
|
1764
|
+
@for (option of options(); track getOptLabel(option)) {
|
|
1765
|
+
<label class="ngx-field__radio-option" [attr.for]="getRadioId(option)">
|
|
1766
|
+
<input
|
|
1767
|
+
[id]="getRadioId(option)"
|
|
1768
|
+
type="radio"
|
|
1769
|
+
class="ngx-field__radio-input"
|
|
1770
|
+
[formControl]="$any(control)"
|
|
1771
|
+
[value]="getOptStoredVal(option)"
|
|
1772
|
+
[attr.disabled]="field.disabled ? true : null"
|
|
1773
|
+
/>
|
|
1774
|
+
<span class="ngx-field__radio-label">{{ getOptLabel(option) }}</span>
|
|
1775
|
+
</label>
|
|
1776
|
+
}
|
|
1777
|
+
</div>
|
|
1778
|
+
@if (control.invalid && control.touched) {
|
|
1779
|
+
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
</fieldset>
|
|
1783
|
+
</div>
|
|
1685
1784
|
`,
|
|
1686
1785
|
}]
|
|
1687
1786
|
}], propDecorators: { field: [{
|
|
@@ -1751,76 +1850,77 @@ class MultiSelectFieldComponent {
|
|
|
1751
1850
|
.join(', ');
|
|
1752
1851
|
}
|
|
1753
1852
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: MultiSelectFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1754
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: MultiSelectFieldComponent, isStandalone: true, selector: "ngx-multi-select-field", inputs: { field: "field", language: "language", control: "control", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
1755
|
-
<div class="ngx-field ngx-field--multiSelect"
|
|
1756
|
-
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
1757
|
-
<label class="ngx-field__label">{{ label }}</label>
|
|
1758
|
-
@if (masked) {
|
|
1759
|
-
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
1760
|
-
} @else if (readonly) {
|
|
1761
|
-
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ getLabels(control.value) }}</span>
|
|
1762
|
-
} @else {
|
|
1763
|
-
<select
|
|
1764
|
-
[id]="field.id"
|
|
1765
|
-
class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
|
|
1766
|
-
[formControl]="$any(control)"
|
|
1767
|
-
[compareWith]="compareFn"
|
|
1768
|
-
[attr.disabled]="field.disabled ? true : null"
|
|
1769
|
-
multiple
|
|
1770
|
-
size="4"
|
|
1771
|
-
>
|
|
1772
|
-
@for (option of options(); track getOptLabel(option)) {
|
|
1773
|
-
@if (isObjectVal(option)) {
|
|
1774
|
-
<option [ngValue]="getOptStoredVal(option)">{{ getOptLabel(option) }}</option>
|
|
1775
|
-
} @else {
|
|
1776
|
-
<option [value]="getOptStoredVal(option)">{{ getOptLabel(option) }}</option>
|
|
1777
|
-
}
|
|
1778
|
-
}
|
|
1779
|
-
</select>
|
|
1780
|
-
@if (control.invalid && control.touched) {
|
|
1781
|
-
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
|
|
1782
|
-
}
|
|
1783
|
-
}
|
|
1784
|
-
</div>
|
|
1785
|
-
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.SelectMultipleControlValueAccessor, selector: "select[multiple][formControlName],select[multiple][formControl],select[multiple][ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }] });
|
|
1853
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: MultiSelectFieldComponent, isStandalone: true, selector: "ngx-multi-select-field", inputs: { field: "field", language: "language", control: "control", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
1854
|
+
<div class="ngx-field ngx-field--multiSelect"
|
|
1855
|
+
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
1856
|
+
<label class="ngx-field__label">{{ label }}</label>
|
|
1857
|
+
@if (masked) {
|
|
1858
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
1859
|
+
} @else if (readonly) {
|
|
1860
|
+
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ getLabels(control.value) }}</span>
|
|
1861
|
+
} @else {
|
|
1862
|
+
<select
|
|
1863
|
+
[id]="field.id"
|
|
1864
|
+
class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
|
|
1865
|
+
[formControl]="$any(control)"
|
|
1866
|
+
[compareWith]="compareFn"
|
|
1867
|
+
[attr.disabled]="field.disabled ? true : null"
|
|
1868
|
+
multiple
|
|
1869
|
+
size="4"
|
|
1870
|
+
>
|
|
1871
|
+
@for (option of options(); track getOptLabel(option)) {
|
|
1872
|
+
@if (isObjectVal(option)) {
|
|
1873
|
+
<option [ngValue]="getOptStoredVal(option)">{{ getOptLabel(option) }}</option>
|
|
1874
|
+
} @else {
|
|
1875
|
+
<option [value]="getOptStoredVal(option)">{{ getOptLabel(option) }}</option>
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
</select>
|
|
1879
|
+
@if (control.invalid && control.touched) {
|
|
1880
|
+
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
</div>
|
|
1884
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.SelectMultipleControlValueAccessor, selector: "select[multiple][formControlName],select[multiple][formControl],select[multiple][ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1786
1885
|
}
|
|
1787
1886
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: MultiSelectFieldComponent, decorators: [{
|
|
1788
1887
|
type: Component,
|
|
1789
1888
|
args: [{
|
|
1889
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1790
1890
|
selector: 'ngx-multi-select-field',
|
|
1791
1891
|
standalone: true,
|
|
1792
1892
|
imports: [ReactiveFormsModule],
|
|
1793
|
-
template: `
|
|
1794
|
-
<div class="ngx-field ngx-field--multiSelect"
|
|
1795
|
-
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
1796
|
-
<label class="ngx-field__label">{{ label }}</label>
|
|
1797
|
-
@if (masked) {
|
|
1798
|
-
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
1799
|
-
} @else if (readonly) {
|
|
1800
|
-
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ getLabels(control.value) }}</span>
|
|
1801
|
-
} @else {
|
|
1802
|
-
<select
|
|
1803
|
-
[id]="field.id"
|
|
1804
|
-
class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
|
|
1805
|
-
[formControl]="$any(control)"
|
|
1806
|
-
[compareWith]="compareFn"
|
|
1807
|
-
[attr.disabled]="field.disabled ? true : null"
|
|
1808
|
-
multiple
|
|
1809
|
-
size="4"
|
|
1810
|
-
>
|
|
1811
|
-
@for (option of options(); track getOptLabel(option)) {
|
|
1812
|
-
@if (isObjectVal(option)) {
|
|
1813
|
-
<option [ngValue]="getOptStoredVal(option)">{{ getOptLabel(option) }}</option>
|
|
1814
|
-
} @else {
|
|
1815
|
-
<option [value]="getOptStoredVal(option)">{{ getOptLabel(option) }}</option>
|
|
1816
|
-
}
|
|
1817
|
-
}
|
|
1818
|
-
</select>
|
|
1819
|
-
@if (control.invalid && control.touched) {
|
|
1820
|
-
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
|
|
1821
|
-
}
|
|
1822
|
-
}
|
|
1823
|
-
</div>
|
|
1893
|
+
template: `
|
|
1894
|
+
<div class="ngx-field ngx-field--multiSelect"
|
|
1895
|
+
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
1896
|
+
<label class="ngx-field__label">{{ label }}</label>
|
|
1897
|
+
@if (masked) {
|
|
1898
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
1899
|
+
} @else if (readonly) {
|
|
1900
|
+
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ getLabels(control.value) }}</span>
|
|
1901
|
+
} @else {
|
|
1902
|
+
<select
|
|
1903
|
+
[id]="field.id"
|
|
1904
|
+
class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
|
|
1905
|
+
[formControl]="$any(control)"
|
|
1906
|
+
[compareWith]="compareFn"
|
|
1907
|
+
[attr.disabled]="field.disabled ? true : null"
|
|
1908
|
+
multiple
|
|
1909
|
+
size="4"
|
|
1910
|
+
>
|
|
1911
|
+
@for (option of options(); track getOptLabel(option)) {
|
|
1912
|
+
@if (isObjectVal(option)) {
|
|
1913
|
+
<option [ngValue]="getOptStoredVal(option)">{{ getOptLabel(option) }}</option>
|
|
1914
|
+
} @else {
|
|
1915
|
+
<option [value]="getOptStoredVal(option)">{{ getOptLabel(option) }}</option>
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
</select>
|
|
1919
|
+
@if (control.invalid && control.touched) {
|
|
1920
|
+
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
</div>
|
|
1824
1924
|
`,
|
|
1825
1925
|
}]
|
|
1826
1926
|
}], propDecorators: { field: [{
|
|
@@ -1837,7 +1937,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
1837
1937
|
|
|
1838
1938
|
/**
|
|
1839
1939
|
* EntityRefRegistryService — resolves entity-ref option loader functions.
|
|
1840
|
-
*
|
|
1940
|
+
* Entity-ref options come ONLY from this registry.
|
|
1841
1941
|
* Never pass loaders via @Input() on field components.
|
|
1842
1942
|
*
|
|
1843
1943
|
* Consumers register loaders via provideNgxDynamicEntity({ entityRefs: { clients: () => [...] } }).
|
|
@@ -2029,7 +2129,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
2029
2129
|
/**
|
|
2030
2130
|
* CascadeDataService — loads entity-reference options and applies parent→child cascades.
|
|
2031
2131
|
*
|
|
2032
|
-
* Loaders come from the registry only
|
|
2132
|
+
* Loaders come from the registry only. This service owns the three things a
|
|
2033
2133
|
* field component should not: resolving the loader, coercing whatever it returned
|
|
2034
2134
|
* (array | Promise | Observable) into a Promise, and filtering by the parent's value.
|
|
2035
2135
|
*/
|
|
@@ -2209,6 +2309,9 @@ class EntityRefFieldComponent {
|
|
|
2209
2309
|
return;
|
|
2210
2310
|
void this.reload();
|
|
2211
2311
|
this.watchParent();
|
|
2312
|
+
this.control?.valueChanges
|
|
2313
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
2314
|
+
.subscribe(value => this.publishSelection(String(value ?? '')));
|
|
2212
2315
|
}
|
|
2213
2316
|
/** Reload options against the parent's current value. */
|
|
2214
2317
|
async reload() {
|
|
@@ -2226,8 +2329,20 @@ class EntityRefFieldComponent {
|
|
|
2226
2329
|
this.loading.set(false);
|
|
2227
2330
|
}
|
|
2228
2331
|
}
|
|
2229
|
-
|
|
2230
|
-
|
|
2332
|
+
/**
|
|
2333
|
+
* Publish the selected option so autoPatch can copy from its record.
|
|
2334
|
+
*
|
|
2335
|
+
* Driven from `valueChanges` rather than the select's `change` event: that event is the
|
|
2336
|
+
* same one the `formControl` directive listens to, so reading the control inside a
|
|
2337
|
+
* template `(change)` handler can see the previous value. `valueChanges` fires after the
|
|
2338
|
+
* model has updated — including Playwright `selectOption`, which updates the control
|
|
2339
|
+
* through the value accessor.
|
|
2340
|
+
*/
|
|
2341
|
+
onSelectionChange(value) {
|
|
2342
|
+
this.publishSelection(value ?? String(this.control?.value ?? ''));
|
|
2343
|
+
}
|
|
2344
|
+
publishSelection(current) {
|
|
2345
|
+
const selected = this.options().find(o => String(o.value) === current);
|
|
2231
2346
|
this.selectionBus.emit(this.field.id, selected ?? null);
|
|
2232
2347
|
}
|
|
2233
2348
|
getLabel(value) {
|
|
@@ -2259,80 +2374,79 @@ class EntityRefFieldComponent {
|
|
|
2259
2374
|
});
|
|
2260
2375
|
}
|
|
2261
2376
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: EntityRefFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2262
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: EntityRefFieldComponent, isStandalone: true, selector: "ngx-entity-ref-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
2263
|
-
<div class="ngx-field ngx-field--entity-ref"
|
|
2264
|
-
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
2265
|
-
<label class="ngx-field__label" [attr.for]="'field-' + field.id">{{ label }}</label>
|
|
2266
|
-
@if (masked) {
|
|
2267
|
-
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
2268
|
-
} @else if (readonly) {
|
|
2269
|
-
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ getLabel(control.value) }}</span>
|
|
2270
|
-
} @else {
|
|
2271
|
-
@if (loading()) {
|
|
2272
|
-
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-loading'" role="status">Loading…</span>
|
|
2273
|
-
} @else {
|
|
2274
|
-
<select
|
|
2275
|
-
class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
|
|
2276
|
-
[id]="'field-' + field.id"
|
|
2277
|
-
[formControl]="$any(control)"
|
|
2278
|
-
[attr.disabled]="field.disabled ? true : null"
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }] });
|
|
2377
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: EntityRefFieldComponent, isStandalone: true, selector: "ngx-entity-ref-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
2378
|
+
<div class="ngx-field ngx-field--entity-ref"
|
|
2379
|
+
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
2380
|
+
<label class="ngx-field__label" [attr.for]="'field-' + field.id">{{ label }}</label>
|
|
2381
|
+
@if (masked) {
|
|
2382
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
2383
|
+
} @else if (readonly) {
|
|
2384
|
+
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ getLabel(control.value) }}</span>
|
|
2385
|
+
} @else {
|
|
2386
|
+
@if (loading()) {
|
|
2387
|
+
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-loading'" role="status">Loading…</span>
|
|
2388
|
+
} @else {
|
|
2389
|
+
<select
|
|
2390
|
+
class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
|
|
2391
|
+
[id]="'field-' + field.id"
|
|
2392
|
+
[formControl]="$any(control)"
|
|
2393
|
+
[attr.disabled]="field.disabled ? true : null"
|
|
2394
|
+
>
|
|
2395
|
+
<option value="">{{ placeholder || 'Select...' }}</option>
|
|
2396
|
+
@for (option of options(); track option.value) {
|
|
2397
|
+
<option [value]="option.value">{{ option.label }}</option>
|
|
2398
|
+
}
|
|
2399
|
+
</select>
|
|
2400
|
+
@if (awaitingParent()) {
|
|
2401
|
+
<span class="ngx-field__hint" [attr.data-testid]="'field-' + field.id + '-hint'">Select {{ parentFieldId }} first.</span>
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
@if (control.invalid && control.touched) {
|
|
2405
|
+
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
</div>
|
|
2409
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2296
2410
|
}
|
|
2297
2411
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: EntityRefFieldComponent, decorators: [{
|
|
2298
2412
|
type: Component,
|
|
2299
2413
|
args: [{
|
|
2414
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
2300
2415
|
selector: 'ngx-entity-ref-field',
|
|
2301
2416
|
standalone: true,
|
|
2302
2417
|
imports: [ReactiveFormsModule],
|
|
2303
|
-
template: `
|
|
2304
|
-
<div class="ngx-field ngx-field--entity-ref"
|
|
2305
|
-
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
2306
|
-
<label class="ngx-field__label" [attr.for]="'field-' + field.id">{{ label }}</label>
|
|
2307
|
-
@if (masked) {
|
|
2308
|
-
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
2309
|
-
} @else if (readonly) {
|
|
2310
|
-
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ getLabel(control.value) }}</span>
|
|
2311
|
-
} @else {
|
|
2312
|
-
@if (loading()) {
|
|
2313
|
-
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-loading'" role="status">Loading…</span>
|
|
2314
|
-
} @else {
|
|
2315
|
-
<select
|
|
2316
|
-
class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
|
|
2317
|
-
[id]="'field-' + field.id"
|
|
2318
|
-
[formControl]="$any(control)"
|
|
2319
|
-
[attr.disabled]="field.disabled ? true : null"
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
</div>
|
|
2418
|
+
template: `
|
|
2419
|
+
<div class="ngx-field ngx-field--entity-ref"
|
|
2420
|
+
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
2421
|
+
<label class="ngx-field__label" [attr.for]="'field-' + field.id">{{ label }}</label>
|
|
2422
|
+
@if (masked) {
|
|
2423
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
2424
|
+
} @else if (readonly) {
|
|
2425
|
+
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ getLabel(control.value) }}</span>
|
|
2426
|
+
} @else {
|
|
2427
|
+
@if (loading()) {
|
|
2428
|
+
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-loading'" role="status">Loading…</span>
|
|
2429
|
+
} @else {
|
|
2430
|
+
<select
|
|
2431
|
+
class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
|
|
2432
|
+
[id]="'field-' + field.id"
|
|
2433
|
+
[formControl]="$any(control)"
|
|
2434
|
+
[attr.disabled]="field.disabled ? true : null"
|
|
2435
|
+
>
|
|
2436
|
+
<option value="">{{ placeholder || 'Select...' }}</option>
|
|
2437
|
+
@for (option of options(); track option.value) {
|
|
2438
|
+
<option [value]="option.value">{{ option.label }}</option>
|
|
2439
|
+
}
|
|
2440
|
+
</select>
|
|
2441
|
+
@if (awaitingParent()) {
|
|
2442
|
+
<span class="ngx-field__hint" [attr.data-testid]="'field-' + field.id + '-hint'">Select {{ parentFieldId }} first.</span>
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
@if (control.invalid && control.touched) {
|
|
2446
|
+
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
</div>
|
|
2336
2450
|
`,
|
|
2337
2451
|
}]
|
|
2338
2452
|
}], propDecorators: { field: [{
|
|
@@ -2443,7 +2557,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
2443
2557
|
|
|
2444
2558
|
/**
|
|
2445
2559
|
* DynamicFieldComponent — mounts the correct field component for a given NestedFieldConfig.
|
|
2446
|
-
* Uses ViewContainerRef.createComponent() and passes inputs via setInput()
|
|
2560
|
+
* Uses ViewContainerRef.createComponent() and passes inputs via setInput(); see
|
|
2561
|
+
* DynamicFieldComponentContract for the five inputs every field component receives.
|
|
2447
2562
|
* Never add field-type-specific logic here — this component must remain generic.
|
|
2448
2563
|
*
|
|
2449
2564
|
* The 5 inputs (field, control, language, readonly, masked) are passed to every mounted
|
|
@@ -2460,6 +2575,12 @@ class DynamicFieldComponent {
|
|
|
2460
2575
|
fieldHost;
|
|
2461
2576
|
fieldRegistry = inject(FieldRegistryService);
|
|
2462
2577
|
rbacService = inject(RbacService);
|
|
2578
|
+
/**
|
|
2579
|
+
* Passed into `createComponent` so a hosted field sees this host's injector — including
|
|
2580
|
+
* `DynamicFormComponent`'s scoped `EntityRefSelectionService`. Without it, a standalone
|
|
2581
|
+
* field resolves the root bus and `autoPatch` never runs.
|
|
2582
|
+
*/
|
|
2583
|
+
injector = inject(Injector);
|
|
2463
2584
|
componentRef = null;
|
|
2464
2585
|
ngOnChanges(_changes) {
|
|
2465
2586
|
this.mountField();
|
|
@@ -2481,9 +2602,12 @@ class DynamicFieldComponent {
|
|
|
2481
2602
|
return;
|
|
2482
2603
|
}
|
|
2483
2604
|
this.fieldHost.clear();
|
|
2484
|
-
this.componentRef = this.fieldHost.createComponent(ComponentClass
|
|
2605
|
+
this.componentRef = this.fieldHost.createComponent(ComponentClass, {
|
|
2606
|
+
injector: this.injector,
|
|
2607
|
+
});
|
|
2485
2608
|
this.setInputs(this.componentRef, masked);
|
|
2486
|
-
this.
|
|
2609
|
+
this.watchControl();
|
|
2610
|
+
this.componentRef.changeDetectorRef.detectChanges();
|
|
2487
2611
|
}
|
|
2488
2612
|
/**
|
|
2489
2613
|
* Field types are opt-in (see `provideFieldTypes` / `provideBuiltInFieldTypes`), so an
|
|
@@ -2499,7 +2623,44 @@ class DynamicFieldComponent {
|
|
|
2499
2623
|
}
|
|
2500
2624
|
static warnedTypes = new Set();
|
|
2501
2625
|
/**
|
|
2502
|
-
*
|
|
2626
|
+
* Re-check the hosted field component.
|
|
2627
|
+
*
|
|
2628
|
+
* The field components are OnPush, so they re-render on an input change, an event from
|
|
2629
|
+
* their own template, or a signal they read. `form.markAllAsTouched()` is none of those —
|
|
2630
|
+
* it flips `touched` from outside, and every field's error message depends on it. Without
|
|
2631
|
+
* this, a blocked submit would mark the form touched and no error would appear.
|
|
2632
|
+
*/
|
|
2633
|
+
refresh() {
|
|
2634
|
+
this.componentRef?.changeDetectorRef.detectChanges();
|
|
2635
|
+
}
|
|
2636
|
+
/**
|
|
2637
|
+
* Re-check the hosted component whenever its control changes from outside.
|
|
2638
|
+
*
|
|
2639
|
+
* The field components are OnPush, so they re-render on an input change or an event from
|
|
2640
|
+
* their own template. A control mutated externally is neither — and that is not an edge
|
|
2641
|
+
* case: patchForm, reset, autoPatch and patchOnTrue all do exactly that. Without this, a
|
|
2642
|
+
* field patched from a record would keep rendering its previous value.
|
|
2643
|
+
*
|
|
2644
|
+
* `detectChanges()` rather than `markForCheck()`: a selection that arrives from a native
|
|
2645
|
+
* `change` event (or Playwright) may not be followed by another Angular tick, so a dirty
|
|
2646
|
+
* flag would never be flushed. Running CD on the hosted component now is the copy showing up.
|
|
2647
|
+
*
|
|
2648
|
+
* Done here rather than in each of the eighteen field components: the host owns the
|
|
2649
|
+
* component reference, so one subscription covers every type including custom ones.
|
|
2650
|
+
*/
|
|
2651
|
+
watchControl() {
|
|
2652
|
+
this.controlSub?.unsubscribe();
|
|
2653
|
+
if (!this.control)
|
|
2654
|
+
return;
|
|
2655
|
+
this.controlSub = this.control.valueChanges.subscribe(() => this.refresh());
|
|
2656
|
+
}
|
|
2657
|
+
controlSub;
|
|
2658
|
+
ngOnDestroy() {
|
|
2659
|
+
this.controlSub?.unsubscribe();
|
|
2660
|
+
}
|
|
2661
|
+
/**
|
|
2662
|
+
* Pass all 5 contract inputs via ComponentRef.setInput() — uniform for all types.
|
|
2663
|
+
* The public shape is DynamicFieldComponentContract.
|
|
2503
2664
|
* Must use setInput(), not property assignment: the field components declare inputs with
|
|
2504
2665
|
* definite assignment and no initializer, and the lib compiles with
|
|
2505
2666
|
* useDefineForClassFields:false, so `'field' in instance` is false until first set.
|
|
@@ -2540,7 +2701,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
2540
2701
|
args: ['fieldHost', { read: ViewContainerRef, static: true }]
|
|
2541
2702
|
}] } });
|
|
2542
2703
|
|
|
2543
|
-
/**
|
|
2704
|
+
/** Renders a nested fieldset container for a group field's children. */
|
|
2544
2705
|
class GroupFieldComponent {
|
|
2545
2706
|
field;
|
|
2546
2707
|
control;
|
|
@@ -2557,56 +2718,56 @@ class GroupFieldComponent {
|
|
|
2557
2718
|
return group?.get(childId);
|
|
2558
2719
|
}
|
|
2559
2720
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: GroupFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2560
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: GroupFieldComponent, isStandalone: true, selector: "ngx-group-field", inputs: { field: "field", control: "control", config: "config", language: "language", readonly: "readonly", masked: "masked", userRoles: "userRoles" }, ngImport: i0, template: `
|
|
2561
|
-
<fieldset class="ngx-field ngx-field--group"
|
|
2562
|
-
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
2563
|
-
<legend class="ngx-field__legend">{{ label }}</legend>
|
|
2564
|
-
@if (masked) {
|
|
2565
|
-
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
2566
|
-
} @else {
|
|
2567
|
-
<div class="ngx-field__group-children">
|
|
2568
|
-
@for (child of field.children ?? []; track child.id) {
|
|
2569
|
-
@if (child.visibility !== false) {
|
|
2570
|
-
<ngx-dynamic-field
|
|
2571
|
-
[field]="child"
|
|
2572
|
-
[control]="getChildControl(child.id)"
|
|
2573
|
-
[config]="config"
|
|
2574
|
-
[language]="language"
|
|
2575
|
-
[readonly]="readonly || !!child.readonly"
|
|
2576
|
-
[userRoles]="userRoles"
|
|
2577
|
-
/>
|
|
2578
|
-
}
|
|
2579
|
-
}
|
|
2580
|
-
</div>
|
|
2581
|
-
}
|
|
2582
|
-
</fieldset>
|
|
2583
|
-
`, isInline: true, styles: [".ngx-field--group{border:1px solid #e2e8f0;border-radius:8px;padding:16px;margin-bottom:16px;background:#fafafa}.ngx-field__legend{font-weight:600;font-size:14px;padding:0 8px;color:#334155}.ngx-field__group-children{display:flex;flex-direction:column;gap:12px}\n"], dependencies: [{ kind: "ngmodule", type: i0.forwardRef(() => ReactiveFormsModule) }, { kind: "component", type: i0.forwardRef(() => DynamicFieldComponent), selector: "ngx-dynamic-field", inputs: ["field", "control", "config", "currentTabId", "language", "readonly", "userRoles"] }] });
|
|
2721
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: GroupFieldComponent, isStandalone: true, selector: "ngx-group-field", inputs: { field: "field", control: "control", config: "config", language: "language", readonly: "readonly", masked: "masked", userRoles: "userRoles" }, ngImport: i0, template: `
|
|
2722
|
+
<fieldset class="ngx-field ngx-field--group"
|
|
2723
|
+
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
2724
|
+
<legend class="ngx-field__legend">{{ label }}</legend>
|
|
2725
|
+
@if (masked) {
|
|
2726
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
2727
|
+
} @else {
|
|
2728
|
+
<div class="ngx-field__group-children">
|
|
2729
|
+
@for (child of field.children ?? []; track child.id) {
|
|
2730
|
+
@if (child.visibility !== false) {
|
|
2731
|
+
<ngx-dynamic-field
|
|
2732
|
+
[field]="child"
|
|
2733
|
+
[control]="getChildControl(child.id)"
|
|
2734
|
+
[config]="config"
|
|
2735
|
+
[language]="language"
|
|
2736
|
+
[readonly]="readonly || !!child.readonly"
|
|
2737
|
+
[userRoles]="userRoles"
|
|
2738
|
+
/>
|
|
2739
|
+
}
|
|
2740
|
+
}
|
|
2741
|
+
</div>
|
|
2742
|
+
}
|
|
2743
|
+
</fieldset>
|
|
2744
|
+
`, isInline: true, styles: [".ngx-field--group{border:1px solid #e2e8f0;border-radius:8px;padding:16px;margin-bottom:16px;background:#fafafa}.ngx-field__legend{font-weight:600;font-size:14px;padding:0 8px;color:#334155}.ngx-field__group-children{display:flex;flex-direction:column;gap:12px}\n"], dependencies: [{ kind: "ngmodule", type: i0.forwardRef(() => ReactiveFormsModule) }, { kind: "component", type: i0.forwardRef(() => DynamicFieldComponent), selector: "ngx-dynamic-field", inputs: ["field", "control", "config", "currentTabId", "language", "readonly", "userRoles"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2584
2745
|
}
|
|
2585
2746
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: GroupFieldComponent, decorators: [{
|
|
2586
2747
|
type: Component,
|
|
2587
|
-
args: [{ selector: 'ngx-group-field', standalone: true, imports: [ReactiveFormsModule, forwardRef(() => DynamicFieldComponent)], template: `
|
|
2588
|
-
<fieldset class="ngx-field ngx-field--group"
|
|
2589
|
-
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
2590
|
-
<legend class="ngx-field__legend">{{ label }}</legend>
|
|
2591
|
-
@if (masked) {
|
|
2592
|
-
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
2593
|
-
} @else {
|
|
2594
|
-
<div class="ngx-field__group-children">
|
|
2595
|
-
@for (child of field.children ?? []; track child.id) {
|
|
2596
|
-
@if (child.visibility !== false) {
|
|
2597
|
-
<ngx-dynamic-field
|
|
2598
|
-
[field]="child"
|
|
2599
|
-
[control]="getChildControl(child.id)"
|
|
2600
|
-
[config]="config"
|
|
2601
|
-
[language]="language"
|
|
2602
|
-
[readonly]="readonly || !!child.readonly"
|
|
2603
|
-
[userRoles]="userRoles"
|
|
2604
|
-
/>
|
|
2605
|
-
}
|
|
2606
|
-
}
|
|
2607
|
-
</div>
|
|
2608
|
-
}
|
|
2609
|
-
</fieldset>
|
|
2748
|
+
args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'ngx-group-field', standalone: true, imports: [ReactiveFormsModule, forwardRef(() => DynamicFieldComponent)], template: `
|
|
2749
|
+
<fieldset class="ngx-field ngx-field--group"
|
|
2750
|
+
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
2751
|
+
<legend class="ngx-field__legend">{{ label }}</legend>
|
|
2752
|
+
@if (masked) {
|
|
2753
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
2754
|
+
} @else {
|
|
2755
|
+
<div class="ngx-field__group-children">
|
|
2756
|
+
@for (child of field.children ?? []; track child.id) {
|
|
2757
|
+
@if (child.visibility !== false) {
|
|
2758
|
+
<ngx-dynamic-field
|
|
2759
|
+
[field]="child"
|
|
2760
|
+
[control]="getChildControl(child.id)"
|
|
2761
|
+
[config]="config"
|
|
2762
|
+
[language]="language"
|
|
2763
|
+
[readonly]="readonly || !!child.readonly"
|
|
2764
|
+
[userRoles]="userRoles"
|
|
2765
|
+
/>
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
</div>
|
|
2769
|
+
}
|
|
2770
|
+
</fieldset>
|
|
2610
2771
|
`, styles: [".ngx-field--group{border:1px solid #e2e8f0;border-radius:8px;padding:16px;margin-bottom:16px;background:#fafafa}.ngx-field__legend{font-weight:600;font-size:14px;padding:0 8px;color:#334155}.ngx-field__group-children{display:flex;flex-direction:column;gap:12px}\n"] }]
|
|
2611
2772
|
}], propDecorators: { field: [{
|
|
2612
2773
|
type: Input
|
|
@@ -2629,6 +2790,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
2629
2790
|
*/
|
|
2630
2791
|
class ValidatorRegistryService {
|
|
2631
2792
|
consumerRegistry = inject(VALIDATOR_REGISTRY, { optional: true }) ?? new Map();
|
|
2793
|
+
asyncRegistry = inject(ASYNC_VALIDATOR_REGISTRY, { optional: true }) ?? new Map();
|
|
2632
2794
|
/**
|
|
2633
2795
|
* Resolve a validator key string to a ValidatorFn.
|
|
2634
2796
|
* Supports parameterized built-ins like 'min:0', 'maxLength:255'.
|
|
@@ -2676,8 +2838,40 @@ class ValidatorRegistryService {
|
|
|
2676
2838
|
fnList.push(Validators.maxLength(config.maxLength));
|
|
2677
2839
|
if (config.pattern)
|
|
2678
2840
|
fnList.push(Validators.pattern(config.pattern));
|
|
2841
|
+
if (config.email)
|
|
2842
|
+
fnList.push(Validators.email);
|
|
2843
|
+
// Named validators from the consumer registry. This branch previously hardcoded the
|
|
2844
|
+
// built-ins and consulted the registry only for the `string[]` form of this config, so
|
|
2845
|
+
// a validator the consumer had registered could not be named from a typed schema
|
|
2846
|
+
// without casting it to `any`.
|
|
2847
|
+
for (const key of config.custom ?? []) {
|
|
2848
|
+
const fn = this.resolve(key);
|
|
2849
|
+
if (fn)
|
|
2850
|
+
fnList.push(fn);
|
|
2851
|
+
}
|
|
2679
2852
|
return fnList;
|
|
2680
2853
|
}
|
|
2854
|
+
/**
|
|
2855
|
+
* Resolve the async validators a field names, in `validators.customAsync`.
|
|
2856
|
+
*
|
|
2857
|
+
* Async validators are deliberately a separate registry and a separate config key: Angular
|
|
2858
|
+
* attaches them through `setAsyncValidators`, runs them only once the synchronous ones
|
|
2859
|
+
* pass, and holds the control in `pending` until they settle. Mixing them into the
|
|
2860
|
+
* synchronous list would silently never run them.
|
|
2861
|
+
*
|
|
2862
|
+
* An unknown name is skipped rather than throwing, matching the synchronous behaviour.
|
|
2863
|
+
*/
|
|
2864
|
+
resolveAsyncFromConfig(config) {
|
|
2865
|
+
if (!config || Array.isArray(config))
|
|
2866
|
+
return [];
|
|
2867
|
+
const out = [];
|
|
2868
|
+
for (const key of config.customAsync ?? []) {
|
|
2869
|
+
const fn = this.asyncRegistry.get(key);
|
|
2870
|
+
if (fn)
|
|
2871
|
+
out.push(fn);
|
|
2872
|
+
}
|
|
2873
|
+
return out;
|
|
2874
|
+
}
|
|
2681
2875
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ValidatorRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
2682
2876
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ValidatorRegistryService, providedIn: 'root' });
|
|
2683
2877
|
}
|
|
@@ -2686,8 +2880,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
2686
2880
|
args: [{ providedIn: 'root' }]
|
|
2687
2881
|
}] });
|
|
2688
2882
|
|
|
2689
|
-
/**
|
|
2883
|
+
/** Renders repeating rows for a FormArray, with Add Item and Remove. */
|
|
2690
2884
|
class ArrayFieldComponent {
|
|
2885
|
+
/**
|
|
2886
|
+
* addItem/removeItem are public and may be called from outside this component's template,
|
|
2887
|
+
* which under OnPush would not re-render. The host also watches the control, but this
|
|
2888
|
+
* keeps the component correct on its own.
|
|
2889
|
+
*/
|
|
2890
|
+
cdr = inject(ChangeDetectorRef);
|
|
2691
2891
|
field;
|
|
2692
2892
|
control;
|
|
2693
2893
|
config;
|
|
@@ -2714,115 +2914,117 @@ class ArrayFieldComponent {
|
|
|
2714
2914
|
}
|
|
2715
2915
|
const group = this.fb.group(controls);
|
|
2716
2916
|
this.formArray.push(group);
|
|
2917
|
+
this.cdr.markForCheck();
|
|
2717
2918
|
}
|
|
2718
2919
|
removeItem(index) {
|
|
2719
2920
|
this.formArray.removeAt(index);
|
|
2921
|
+
this.cdr.markForCheck();
|
|
2720
2922
|
}
|
|
2721
2923
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ArrayFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2722
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: ArrayFieldComponent, isStandalone: true, selector: "ngx-array-field", inputs: { field: "field", control: "control", config: "config", language: "language", readonly: "readonly", masked: "masked", userRoles: "userRoles" }, ngImport: i0, template: `
|
|
2723
|
-
<div class="ngx-field ngx-field--array"
|
|
2724
|
-
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
2725
|
-
<div class="ngx-field__array-header">
|
|
2726
|
-
<label class="ngx-field__label">{{ label }}</label>
|
|
2727
|
-
@if (!readonly && !masked) {
|
|
2728
|
-
<button type="button" class="ngx-field__array-add-btn"
|
|
2729
|
-
[attr.data-testid]="'field-' + field.id + '-add'" (click)="addItem()">
|
|
2730
|
-
+ Add Item
|
|
2731
|
-
</button>
|
|
2732
|
-
}
|
|
2733
|
-
</div>
|
|
2734
|
-
|
|
2735
|
-
@if (masked) {
|
|
2736
|
-
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
2737
|
-
} @else {
|
|
2738
|
-
<div class="ngx-field__array-list">
|
|
2739
|
-
@for (itemGroup of formArray.controls; track idx; let idx = $index) {
|
|
2740
|
-
<div class="ngx-field__array-item" [attr.data-testid]="'field-' + field.id + '-row'">
|
|
2741
|
-
<div class="ngx-field__array-item-header">
|
|
2742
|
-
<span class="ngx-field__array-item-title">Item #{{ idx + 1 }}</span>
|
|
2743
|
-
@if (!readonly) {
|
|
2744
|
-
<button type="button" class="ngx-field__array-remove-btn"
|
|
2745
|
-
[attr.data-testid]="'field-' + field.id + '-remove-' + idx" (click)="removeItem(idx)">
|
|
2746
|
-
Remove
|
|
2747
|
-
</button>
|
|
2748
|
-
}
|
|
2749
|
-
</div>
|
|
2750
|
-
<div class="ngx-field__array-item-fields">
|
|
2751
|
-
@for (child of field.children ?? []; track child.id) {
|
|
2752
|
-
@if (child.visibility !== false) {
|
|
2753
|
-
<ngx-dynamic-field
|
|
2754
|
-
[field]="child"
|
|
2755
|
-
[control]="getItemChildControl(itemGroup, child.id)"
|
|
2756
|
-
[config]="config"
|
|
2757
|
-
[language]="language"
|
|
2758
|
-
[readonly]="readonly || !!child.readonly"
|
|
2759
|
-
[userRoles]="userRoles"
|
|
2760
|
-
/>
|
|
2761
|
-
}
|
|
2762
|
-
}
|
|
2763
|
-
</div>
|
|
2764
|
-
</div>
|
|
2765
|
-
}
|
|
2766
|
-
@if (formArray.controls.length === 0) {
|
|
2767
|
-
<div class="ngx-field__array-empty" [attr.data-testid]="'field-' + field.id + '-empty'">No items added yet.</div>
|
|
2768
|
-
}
|
|
2769
|
-
</div>
|
|
2770
|
-
}
|
|
2771
|
-
</div>
|
|
2772
|
-
`, isInline: true, styles: [".ngx-field--array{border:1px solid #e2e8f0;border-radius:8px;padding:16px;margin-bottom:16px;background:#f8fafc}.ngx-field__array-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}.ngx-field__array-add-btn{background:#2563eb;color:#fff;border:none;padding:6px 12px;border-radius:4px;font-weight:500;cursor:pointer}.ngx-field__array-add-btn:hover{background:#1d4ed8}.ngx-field__array-list{display:flex;flex-direction:column;gap:12px}.ngx-field__array-item{background:#fff;border:1px solid #cbd5e1;border-radius:6px;padding:12px}.ngx-field__array-item-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;border-bottom:1px solid #f1f5f9;padding-bottom:6px}.ngx-field__array-item-title{font-weight:600;font-size:13px;color:#475569}.ngx-field__array-remove-btn{background:#ef4444;color:#fff;border:none;padding:4px 8px;border-radius:4px;font-size:12px;cursor:pointer}.ngx-field__array-empty{color:#94a3b8;font-style:italic;font-size:13px}\n"], dependencies: [{ kind: "ngmodule", type: i0.forwardRef(() => ReactiveFormsModule) }, { kind: "component", type: i0.forwardRef(() => DynamicFieldComponent), selector: "ngx-dynamic-field", inputs: ["field", "control", "config", "currentTabId", "language", "readonly", "userRoles"] }] });
|
|
2924
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: ArrayFieldComponent, isStandalone: true, selector: "ngx-array-field", inputs: { field: "field", control: "control", config: "config", language: "language", readonly: "readonly", masked: "masked", userRoles: "userRoles" }, ngImport: i0, template: `
|
|
2925
|
+
<div class="ngx-field ngx-field--array"
|
|
2926
|
+
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
2927
|
+
<div class="ngx-field__array-header">
|
|
2928
|
+
<label class="ngx-field__label">{{ label }}</label>
|
|
2929
|
+
@if (!readonly && !masked) {
|
|
2930
|
+
<button type="button" class="ngx-field__array-add-btn"
|
|
2931
|
+
[attr.data-testid]="'field-' + field.id + '-add'" (click)="addItem()">
|
|
2932
|
+
+ Add Item
|
|
2933
|
+
</button>
|
|
2934
|
+
}
|
|
2935
|
+
</div>
|
|
2936
|
+
|
|
2937
|
+
@if (masked) {
|
|
2938
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
2939
|
+
} @else {
|
|
2940
|
+
<div class="ngx-field__array-list">
|
|
2941
|
+
@for (itemGroup of formArray.controls; track idx; let idx = $index) {
|
|
2942
|
+
<div class="ngx-field__array-item" [attr.data-testid]="'field-' + field.id + '-row'">
|
|
2943
|
+
<div class="ngx-field__array-item-header">
|
|
2944
|
+
<span class="ngx-field__array-item-title">Item #{{ idx + 1 }}</span>
|
|
2945
|
+
@if (!readonly) {
|
|
2946
|
+
<button type="button" class="ngx-field__array-remove-btn"
|
|
2947
|
+
[attr.data-testid]="'field-' + field.id + '-remove-' + idx" (click)="removeItem(idx)">
|
|
2948
|
+
Remove
|
|
2949
|
+
</button>
|
|
2950
|
+
}
|
|
2951
|
+
</div>
|
|
2952
|
+
<div class="ngx-field__array-item-fields">
|
|
2953
|
+
@for (child of field.children ?? []; track child.id) {
|
|
2954
|
+
@if (child.visibility !== false) {
|
|
2955
|
+
<ngx-dynamic-field
|
|
2956
|
+
[field]="child"
|
|
2957
|
+
[control]="getItemChildControl(itemGroup, child.id)"
|
|
2958
|
+
[config]="config"
|
|
2959
|
+
[language]="language"
|
|
2960
|
+
[readonly]="readonly || !!child.readonly"
|
|
2961
|
+
[userRoles]="userRoles"
|
|
2962
|
+
/>
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2965
|
+
</div>
|
|
2966
|
+
</div>
|
|
2967
|
+
}
|
|
2968
|
+
@if (formArray.controls.length === 0) {
|
|
2969
|
+
<div class="ngx-field__array-empty" [attr.data-testid]="'field-' + field.id + '-empty'">No items added yet.</div>
|
|
2970
|
+
}
|
|
2971
|
+
</div>
|
|
2972
|
+
}
|
|
2973
|
+
</div>
|
|
2974
|
+
`, isInline: true, styles: [".ngx-field--array{border:1px solid #e2e8f0;border-radius:8px;padding:16px;margin-bottom:16px;background:#f8fafc}.ngx-field__array-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}.ngx-field__array-add-btn{background:#2563eb;color:#fff;border:none;padding:6px 12px;border-radius:4px;font-weight:500;cursor:pointer}.ngx-field__array-add-btn:hover{background:#1d4ed8}.ngx-field__array-list{display:flex;flex-direction:column;gap:12px}.ngx-field__array-item{background:#fff;border:1px solid #cbd5e1;border-radius:6px;padding:12px}.ngx-field__array-item-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;border-bottom:1px solid #f1f5f9;padding-bottom:6px}.ngx-field__array-item-title{font-weight:600;font-size:13px;color:#475569}.ngx-field__array-remove-btn{background:#ef4444;color:#fff;border:none;padding:4px 8px;border-radius:4px;font-size:12px;cursor:pointer}.ngx-field__array-empty{color:#94a3b8;font-style:italic;font-size:13px}\n"], dependencies: [{ kind: "ngmodule", type: i0.forwardRef(() => ReactiveFormsModule) }, { kind: "component", type: i0.forwardRef(() => DynamicFieldComponent), selector: "ngx-dynamic-field", inputs: ["field", "control", "config", "currentTabId", "language", "readonly", "userRoles"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2773
2975
|
}
|
|
2774
2976
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ArrayFieldComponent, decorators: [{
|
|
2775
2977
|
type: Component,
|
|
2776
|
-
args: [{ selector: 'ngx-array-field', standalone: true, imports: [ReactiveFormsModule, forwardRef(() => DynamicFieldComponent)], template: `
|
|
2777
|
-
<div class="ngx-field ngx-field--array"
|
|
2778
|
-
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
2779
|
-
<div class="ngx-field__array-header">
|
|
2780
|
-
<label class="ngx-field__label">{{ label }}</label>
|
|
2781
|
-
@if (!readonly && !masked) {
|
|
2782
|
-
<button type="button" class="ngx-field__array-add-btn"
|
|
2783
|
-
[attr.data-testid]="'field-' + field.id + '-add'" (click)="addItem()">
|
|
2784
|
-
+ Add Item
|
|
2785
|
-
</button>
|
|
2786
|
-
}
|
|
2787
|
-
</div>
|
|
2788
|
-
|
|
2789
|
-
@if (masked) {
|
|
2790
|
-
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
2791
|
-
} @else {
|
|
2792
|
-
<div class="ngx-field__array-list">
|
|
2793
|
-
@for (itemGroup of formArray.controls; track idx; let idx = $index) {
|
|
2794
|
-
<div class="ngx-field__array-item" [attr.data-testid]="'field-' + field.id + '-row'">
|
|
2795
|
-
<div class="ngx-field__array-item-header">
|
|
2796
|
-
<span class="ngx-field__array-item-title">Item #{{ idx + 1 }}</span>
|
|
2797
|
-
@if (!readonly) {
|
|
2798
|
-
<button type="button" class="ngx-field__array-remove-btn"
|
|
2799
|
-
[attr.data-testid]="'field-' + field.id + '-remove-' + idx" (click)="removeItem(idx)">
|
|
2800
|
-
Remove
|
|
2801
|
-
</button>
|
|
2802
|
-
}
|
|
2803
|
-
</div>
|
|
2804
|
-
<div class="ngx-field__array-item-fields">
|
|
2805
|
-
@for (child of field.children ?? []; track child.id) {
|
|
2806
|
-
@if (child.visibility !== false) {
|
|
2807
|
-
<ngx-dynamic-field
|
|
2808
|
-
[field]="child"
|
|
2809
|
-
[control]="getItemChildControl(itemGroup, child.id)"
|
|
2810
|
-
[config]="config"
|
|
2811
|
-
[language]="language"
|
|
2812
|
-
[readonly]="readonly || !!child.readonly"
|
|
2813
|
-
[userRoles]="userRoles"
|
|
2814
|
-
/>
|
|
2815
|
-
}
|
|
2816
|
-
}
|
|
2817
|
-
</div>
|
|
2818
|
-
</div>
|
|
2819
|
-
}
|
|
2820
|
-
@if (formArray.controls.length === 0) {
|
|
2821
|
-
<div class="ngx-field__array-empty" [attr.data-testid]="'field-' + field.id + '-empty'">No items added yet.</div>
|
|
2822
|
-
}
|
|
2823
|
-
</div>
|
|
2824
|
-
}
|
|
2825
|
-
</div>
|
|
2978
|
+
args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'ngx-array-field', standalone: true, imports: [ReactiveFormsModule, forwardRef(() => DynamicFieldComponent)], template: `
|
|
2979
|
+
<div class="ngx-field ngx-field--array"
|
|
2980
|
+
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
2981
|
+
<div class="ngx-field__array-header">
|
|
2982
|
+
<label class="ngx-field__label">{{ label }}</label>
|
|
2983
|
+
@if (!readonly && !masked) {
|
|
2984
|
+
<button type="button" class="ngx-field__array-add-btn"
|
|
2985
|
+
[attr.data-testid]="'field-' + field.id + '-add'" (click)="addItem()">
|
|
2986
|
+
+ Add Item
|
|
2987
|
+
</button>
|
|
2988
|
+
}
|
|
2989
|
+
</div>
|
|
2990
|
+
|
|
2991
|
+
@if (masked) {
|
|
2992
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
2993
|
+
} @else {
|
|
2994
|
+
<div class="ngx-field__array-list">
|
|
2995
|
+
@for (itemGroup of formArray.controls; track idx; let idx = $index) {
|
|
2996
|
+
<div class="ngx-field__array-item" [attr.data-testid]="'field-' + field.id + '-row'">
|
|
2997
|
+
<div class="ngx-field__array-item-header">
|
|
2998
|
+
<span class="ngx-field__array-item-title">Item #{{ idx + 1 }}</span>
|
|
2999
|
+
@if (!readonly) {
|
|
3000
|
+
<button type="button" class="ngx-field__array-remove-btn"
|
|
3001
|
+
[attr.data-testid]="'field-' + field.id + '-remove-' + idx" (click)="removeItem(idx)">
|
|
3002
|
+
Remove
|
|
3003
|
+
</button>
|
|
3004
|
+
}
|
|
3005
|
+
</div>
|
|
3006
|
+
<div class="ngx-field__array-item-fields">
|
|
3007
|
+
@for (child of field.children ?? []; track child.id) {
|
|
3008
|
+
@if (child.visibility !== false) {
|
|
3009
|
+
<ngx-dynamic-field
|
|
3010
|
+
[field]="child"
|
|
3011
|
+
[control]="getItemChildControl(itemGroup, child.id)"
|
|
3012
|
+
[config]="config"
|
|
3013
|
+
[language]="language"
|
|
3014
|
+
[readonly]="readonly || !!child.readonly"
|
|
3015
|
+
[userRoles]="userRoles"
|
|
3016
|
+
/>
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
3019
|
+
</div>
|
|
3020
|
+
</div>
|
|
3021
|
+
}
|
|
3022
|
+
@if (formArray.controls.length === 0) {
|
|
3023
|
+
<div class="ngx-field__array-empty" [attr.data-testid]="'field-' + field.id + '-empty'">No items added yet.</div>
|
|
3024
|
+
}
|
|
3025
|
+
</div>
|
|
3026
|
+
}
|
|
3027
|
+
</div>
|
|
2826
3028
|
`, styles: [".ngx-field--array{border:1px solid #e2e8f0;border-radius:8px;padding:16px;margin-bottom:16px;background:#f8fafc}.ngx-field__array-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}.ngx-field__array-add-btn{background:#2563eb;color:#fff;border:none;padding:6px 12px;border-radius:4px;font-weight:500;cursor:pointer}.ngx-field__array-add-btn:hover{background:#1d4ed8}.ngx-field__array-list{display:flex;flex-direction:column;gap:12px}.ngx-field__array-item{background:#fff;border:1px solid #cbd5e1;border-radius:6px;padding:12px}.ngx-field__array-item-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;border-bottom:1px solid #f1f5f9;padding-bottom:6px}.ngx-field__array-item-title{font-weight:600;font-size:13px;color:#475569}.ngx-field__array-remove-btn{background:#ef4444;color:#fff;border:none;padding:4px 8px;border-radius:4px;font-size:12px;cursor:pointer}.ngx-field__array-empty{color:#94a3b8;font-style:italic;font-size:13px}\n"] }]
|
|
2827
3029
|
}], propDecorators: { field: [{
|
|
2828
3030
|
type: Input
|
|
@@ -2980,102 +3182,103 @@ class ImageFieldComponent {
|
|
|
2980
3182
|
this.uploads.revokePreviewUrl(this.previewCache);
|
|
2981
3183
|
}
|
|
2982
3184
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ImageFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2983
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: ImageFieldComponent, isStandalone: true, selector: "ngx-image-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
2984
|
-
<div class="ngx-field ngx-field--image"
|
|
2985
|
-
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
2986
|
-
<label class="ngx-field__label" [attr.for]="'field-' + field.id">{{ label }}</label>
|
|
2987
|
-
@if (masked) {
|
|
2988
|
-
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
2989
|
-
} @else {
|
|
2990
|
-
<div class="ngx-field__image-wrap">
|
|
2991
|
-
@if (previewUrl()) {
|
|
2992
|
-
<img class="ngx-field__image-preview" [src]="previewUrl()" [alt]="label" />
|
|
2993
|
-
} @else {
|
|
2994
|
-
<div class="ngx-field__image-placeholder">
|
|
2995
|
-
<span>📷</span>
|
|
2996
|
-
<span>No image</span>
|
|
2997
|
-
</div>
|
|
2998
|
-
}
|
|
2999
|
-
@if (!readonly) {
|
|
3000
|
-
<div class="ngx-field__image-actions">
|
|
3001
|
-
<label class="ngx-field__upload-btn" [class.ngx-field__upload-btn--loading]="uploading()">
|
|
3002
|
-
@if (uploading()) {
|
|
3003
|
-
<span>Uploading…</span>
|
|
3004
|
-
} @else {
|
|
3005
|
-
<span>{{ previewUrl() ? 'Change' : 'Upload' }}</span>
|
|
3006
|
-
<input
|
|
3007
|
-
type="file"
|
|
3008
|
-
class="ngx-field__file-input"
|
|
3009
|
-
[id]="'field-' + field.id"
|
|
3010
|
-
accept="image/*"
|
|
3011
|
-
[disabled]="field.disabled || uploading()"
|
|
3012
|
-
(change)="onFileSelect($any($event.target).files[0])"
|
|
3013
|
-
/>
|
|
3014
|
-
}
|
|
3015
|
-
</label>
|
|
3016
|
-
@if (previewUrl()) {
|
|
3017
|
-
<button type="button" class="ngx-field__remove-btn" (click)="remove()">Remove</button>
|
|
3018
|
-
}
|
|
3019
|
-
</div>
|
|
3020
|
-
}
|
|
3021
|
-
</div>
|
|
3022
|
-
@if (uploadError()) {
|
|
3023
|
-
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">{{ uploadError() }}</span>
|
|
3024
|
-
}
|
|
3025
|
-
}
|
|
3026
|
-
</div>
|
|
3027
|
-
`, isInline: true });
|
|
3185
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: ImageFieldComponent, isStandalone: true, selector: "ngx-image-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
3186
|
+
<div class="ngx-field ngx-field--image"
|
|
3187
|
+
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
3188
|
+
<label class="ngx-field__label" [attr.for]="'field-' + field.id">{{ label }}</label>
|
|
3189
|
+
@if (masked) {
|
|
3190
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
3191
|
+
} @else {
|
|
3192
|
+
<div class="ngx-field__image-wrap">
|
|
3193
|
+
@if (previewUrl()) {
|
|
3194
|
+
<img class="ngx-field__image-preview" [src]="previewUrl()" [alt]="label" />
|
|
3195
|
+
} @else {
|
|
3196
|
+
<div class="ngx-field__image-placeholder">
|
|
3197
|
+
<span>📷</span>
|
|
3198
|
+
<span>No image</span>
|
|
3199
|
+
</div>
|
|
3200
|
+
}
|
|
3201
|
+
@if (!readonly) {
|
|
3202
|
+
<div class="ngx-field__image-actions">
|
|
3203
|
+
<label class="ngx-field__upload-btn" [class.ngx-field__upload-btn--loading]="uploading()">
|
|
3204
|
+
@if (uploading()) {
|
|
3205
|
+
<span>Uploading…</span>
|
|
3206
|
+
} @else {
|
|
3207
|
+
<span>{{ previewUrl() ? 'Change' : 'Upload' }}</span>
|
|
3208
|
+
<input
|
|
3209
|
+
type="file"
|
|
3210
|
+
class="ngx-field__file-input"
|
|
3211
|
+
[id]="'field-' + field.id"
|
|
3212
|
+
accept="image/*"
|
|
3213
|
+
[disabled]="field.disabled || uploading()"
|
|
3214
|
+
(change)="onFileSelect($any($event.target).files[0])"
|
|
3215
|
+
/>
|
|
3216
|
+
}
|
|
3217
|
+
</label>
|
|
3218
|
+
@if (previewUrl()) {
|
|
3219
|
+
<button type="button" class="ngx-field__remove-btn" (click)="remove()">Remove</button>
|
|
3220
|
+
}
|
|
3221
|
+
</div>
|
|
3222
|
+
}
|
|
3223
|
+
</div>
|
|
3224
|
+
@if (uploadError()) {
|
|
3225
|
+
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">{{ uploadError() }}</span>
|
|
3226
|
+
}
|
|
3227
|
+
}
|
|
3228
|
+
</div>
|
|
3229
|
+
`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3028
3230
|
}
|
|
3029
3231
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ImageFieldComponent, decorators: [{
|
|
3030
3232
|
type: Component,
|
|
3031
3233
|
args: [{
|
|
3234
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
3032
3235
|
selector: 'ngx-image-field',
|
|
3033
3236
|
standalone: true,
|
|
3034
3237
|
imports: [],
|
|
3035
|
-
template: `
|
|
3036
|
-
<div class="ngx-field ngx-field--image"
|
|
3037
|
-
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
3038
|
-
<label class="ngx-field__label" [attr.for]="'field-' + field.id">{{ label }}</label>
|
|
3039
|
-
@if (masked) {
|
|
3040
|
-
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
3041
|
-
} @else {
|
|
3042
|
-
<div class="ngx-field__image-wrap">
|
|
3043
|
-
@if (previewUrl()) {
|
|
3044
|
-
<img class="ngx-field__image-preview" [src]="previewUrl()" [alt]="label" />
|
|
3045
|
-
} @else {
|
|
3046
|
-
<div class="ngx-field__image-placeholder">
|
|
3047
|
-
<span>📷</span>
|
|
3048
|
-
<span>No image</span>
|
|
3049
|
-
</div>
|
|
3050
|
-
}
|
|
3051
|
-
@if (!readonly) {
|
|
3052
|
-
<div class="ngx-field__image-actions">
|
|
3053
|
-
<label class="ngx-field__upload-btn" [class.ngx-field__upload-btn--loading]="uploading()">
|
|
3054
|
-
@if (uploading()) {
|
|
3055
|
-
<span>Uploading…</span>
|
|
3056
|
-
} @else {
|
|
3057
|
-
<span>{{ previewUrl() ? 'Change' : 'Upload' }}</span>
|
|
3058
|
-
<input
|
|
3059
|
-
type="file"
|
|
3060
|
-
class="ngx-field__file-input"
|
|
3061
|
-
[id]="'field-' + field.id"
|
|
3062
|
-
accept="image/*"
|
|
3063
|
-
[disabled]="field.disabled || uploading()"
|
|
3064
|
-
(change)="onFileSelect($any($event.target).files[0])"
|
|
3065
|
-
/>
|
|
3066
|
-
}
|
|
3067
|
-
</label>
|
|
3068
|
-
@if (previewUrl()) {
|
|
3069
|
-
<button type="button" class="ngx-field__remove-btn" (click)="remove()">Remove</button>
|
|
3070
|
-
}
|
|
3071
|
-
</div>
|
|
3072
|
-
}
|
|
3073
|
-
</div>
|
|
3074
|
-
@if (uploadError()) {
|
|
3075
|
-
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">{{ uploadError() }}</span>
|
|
3076
|
-
}
|
|
3077
|
-
}
|
|
3078
|
-
</div>
|
|
3238
|
+
template: `
|
|
3239
|
+
<div class="ngx-field ngx-field--image"
|
|
3240
|
+
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
3241
|
+
<label class="ngx-field__label" [attr.for]="'field-' + field.id">{{ label }}</label>
|
|
3242
|
+
@if (masked) {
|
|
3243
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
3244
|
+
} @else {
|
|
3245
|
+
<div class="ngx-field__image-wrap">
|
|
3246
|
+
@if (previewUrl()) {
|
|
3247
|
+
<img class="ngx-field__image-preview" [src]="previewUrl()" [alt]="label" />
|
|
3248
|
+
} @else {
|
|
3249
|
+
<div class="ngx-field__image-placeholder">
|
|
3250
|
+
<span>📷</span>
|
|
3251
|
+
<span>No image</span>
|
|
3252
|
+
</div>
|
|
3253
|
+
}
|
|
3254
|
+
@if (!readonly) {
|
|
3255
|
+
<div class="ngx-field__image-actions">
|
|
3256
|
+
<label class="ngx-field__upload-btn" [class.ngx-field__upload-btn--loading]="uploading()">
|
|
3257
|
+
@if (uploading()) {
|
|
3258
|
+
<span>Uploading…</span>
|
|
3259
|
+
} @else {
|
|
3260
|
+
<span>{{ previewUrl() ? 'Change' : 'Upload' }}</span>
|
|
3261
|
+
<input
|
|
3262
|
+
type="file"
|
|
3263
|
+
class="ngx-field__file-input"
|
|
3264
|
+
[id]="'field-' + field.id"
|
|
3265
|
+
accept="image/*"
|
|
3266
|
+
[disabled]="field.disabled || uploading()"
|
|
3267
|
+
(change)="onFileSelect($any($event.target).files[0])"
|
|
3268
|
+
/>
|
|
3269
|
+
}
|
|
3270
|
+
</label>
|
|
3271
|
+
@if (previewUrl()) {
|
|
3272
|
+
<button type="button" class="ngx-field__remove-btn" (click)="remove()">Remove</button>
|
|
3273
|
+
}
|
|
3274
|
+
</div>
|
|
3275
|
+
}
|
|
3276
|
+
</div>
|
|
3277
|
+
@if (uploadError()) {
|
|
3278
|
+
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">{{ uploadError() }}</span>
|
|
3279
|
+
}
|
|
3280
|
+
}
|
|
3281
|
+
</div>
|
|
3079
3282
|
`,
|
|
3080
3283
|
}]
|
|
3081
3284
|
}], propDecorators: { field: [{
|
|
@@ -3136,98 +3339,99 @@ class FileFieldComponent {
|
|
|
3136
3339
|
this.control.markAsTouched();
|
|
3137
3340
|
}
|
|
3138
3341
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: FileFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3139
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: FileFieldComponent, isStandalone: true, selector: "ngx-file-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
3140
|
-
<div class="ngx-field ngx-field--file"
|
|
3141
|
-
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
3142
|
-
<label class="ngx-field__label" [attr.for]="'field-' + field.id">{{ label }}</label>
|
|
3143
|
-
@if (masked) {
|
|
3144
|
-
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
3145
|
-
} @else if (readonly) {
|
|
3146
|
-
@if (fileUrl()) {
|
|
3147
|
-
<a class="ngx-field__file-link" [href]="fileUrl()!" target="_blank" rel="noopener">
|
|
3148
|
-
📎 {{ fileName() || 'Download file' }}
|
|
3149
|
-
</a>
|
|
3150
|
-
} @else {
|
|
3151
|
-
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">—</span>
|
|
3152
|
-
}
|
|
3153
|
-
} @else {
|
|
3154
|
-
<div class="ngx-field__file-wrap">
|
|
3155
|
-
@if (fileName()) {
|
|
3156
|
-
<div class="ngx-field__file-selected">
|
|
3157
|
-
<span class="ngx-field__file-name">📎 {{ fileName() }}</span>
|
|
3158
|
-
<button type="button" class="ngx-field__remove-btn" (click)="remove()" aria-label="Remove file">✕</button>
|
|
3159
|
-
</div>
|
|
3160
|
-
}
|
|
3161
|
-
<label class="ngx-field__upload-btn" [class.ngx-field__upload-btn--loading]="uploading()">
|
|
3162
|
-
@if (uploading()) {
|
|
3163
|
-
<span>Uploading…</span>
|
|
3164
|
-
} @else {
|
|
3165
|
-
<span>{{ fileName() ? 'Replace file' : 'Choose file' }}</span>
|
|
3166
|
-
<input
|
|
3167
|
-
type="file"
|
|
3168
|
-
class="ngx-field__file-input"
|
|
3169
|
-
[id]="'field-' + field.id"
|
|
3170
|
-
[disabled]="field.disabled || uploading()"
|
|
3171
|
-
(change)="onFileSelect($any($event.target).files[0])"
|
|
3172
|
-
/>
|
|
3173
|
-
}
|
|
3174
|
-
</label>
|
|
3175
|
-
</div>
|
|
3176
|
-
@if (uploadError()) {
|
|
3177
|
-
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">{{ uploadError() }}</span>
|
|
3178
|
-
}
|
|
3179
|
-
}
|
|
3180
|
-
</div>
|
|
3181
|
-
`, isInline: true });
|
|
3342
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: FileFieldComponent, isStandalone: true, selector: "ngx-file-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
3343
|
+
<div class="ngx-field ngx-field--file"
|
|
3344
|
+
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
3345
|
+
<label class="ngx-field__label" [attr.for]="'field-' + field.id">{{ label }}</label>
|
|
3346
|
+
@if (masked) {
|
|
3347
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
3348
|
+
} @else if (readonly) {
|
|
3349
|
+
@if (fileUrl()) {
|
|
3350
|
+
<a class="ngx-field__file-link" [href]="fileUrl()!" target="_blank" rel="noopener">
|
|
3351
|
+
📎 {{ fileName() || 'Download file' }}
|
|
3352
|
+
</a>
|
|
3353
|
+
} @else {
|
|
3354
|
+
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">—</span>
|
|
3355
|
+
}
|
|
3356
|
+
} @else {
|
|
3357
|
+
<div class="ngx-field__file-wrap">
|
|
3358
|
+
@if (fileName()) {
|
|
3359
|
+
<div class="ngx-field__file-selected">
|
|
3360
|
+
<span class="ngx-field__file-name">📎 {{ fileName() }}</span>
|
|
3361
|
+
<button type="button" class="ngx-field__remove-btn" (click)="remove()" aria-label="Remove file">✕</button>
|
|
3362
|
+
</div>
|
|
3363
|
+
}
|
|
3364
|
+
<label class="ngx-field__upload-btn" [class.ngx-field__upload-btn--loading]="uploading()">
|
|
3365
|
+
@if (uploading()) {
|
|
3366
|
+
<span>Uploading…</span>
|
|
3367
|
+
} @else {
|
|
3368
|
+
<span>{{ fileName() ? 'Replace file' : 'Choose file' }}</span>
|
|
3369
|
+
<input
|
|
3370
|
+
type="file"
|
|
3371
|
+
class="ngx-field__file-input"
|
|
3372
|
+
[id]="'field-' + field.id"
|
|
3373
|
+
[disabled]="field.disabled || uploading()"
|
|
3374
|
+
(change)="onFileSelect($any($event.target).files[0])"
|
|
3375
|
+
/>
|
|
3376
|
+
}
|
|
3377
|
+
</label>
|
|
3378
|
+
</div>
|
|
3379
|
+
@if (uploadError()) {
|
|
3380
|
+
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">{{ uploadError() }}</span>
|
|
3381
|
+
}
|
|
3382
|
+
}
|
|
3383
|
+
</div>
|
|
3384
|
+
`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3182
3385
|
}
|
|
3183
3386
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: FileFieldComponent, decorators: [{
|
|
3184
3387
|
type: Component,
|
|
3185
3388
|
args: [{
|
|
3389
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
3186
3390
|
selector: 'ngx-file-field',
|
|
3187
3391
|
standalone: true,
|
|
3188
3392
|
imports: [],
|
|
3189
|
-
template: `
|
|
3190
|
-
<div class="ngx-field ngx-field--file"
|
|
3191
|
-
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
3192
|
-
<label class="ngx-field__label" [attr.for]="'field-' + field.id">{{ label }}</label>
|
|
3193
|
-
@if (masked) {
|
|
3194
|
-
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
3195
|
-
} @else if (readonly) {
|
|
3196
|
-
@if (fileUrl()) {
|
|
3197
|
-
<a class="ngx-field__file-link" [href]="fileUrl()!" target="_blank" rel="noopener">
|
|
3198
|
-
📎 {{ fileName() || 'Download file' }}
|
|
3199
|
-
</a>
|
|
3200
|
-
} @else {
|
|
3201
|
-
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">—</span>
|
|
3202
|
-
}
|
|
3203
|
-
} @else {
|
|
3204
|
-
<div class="ngx-field__file-wrap">
|
|
3205
|
-
@if (fileName()) {
|
|
3206
|
-
<div class="ngx-field__file-selected">
|
|
3207
|
-
<span class="ngx-field__file-name">📎 {{ fileName() }}</span>
|
|
3208
|
-
<button type="button" class="ngx-field__remove-btn" (click)="remove()" aria-label="Remove file">✕</button>
|
|
3209
|
-
</div>
|
|
3210
|
-
}
|
|
3211
|
-
<label class="ngx-field__upload-btn" [class.ngx-field__upload-btn--loading]="uploading()">
|
|
3212
|
-
@if (uploading()) {
|
|
3213
|
-
<span>Uploading…</span>
|
|
3214
|
-
} @else {
|
|
3215
|
-
<span>{{ fileName() ? 'Replace file' : 'Choose file' }}</span>
|
|
3216
|
-
<input
|
|
3217
|
-
type="file"
|
|
3218
|
-
class="ngx-field__file-input"
|
|
3219
|
-
[id]="'field-' + field.id"
|
|
3220
|
-
[disabled]="field.disabled || uploading()"
|
|
3221
|
-
(change)="onFileSelect($any($event.target).files[0])"
|
|
3222
|
-
/>
|
|
3223
|
-
}
|
|
3224
|
-
</label>
|
|
3225
|
-
</div>
|
|
3226
|
-
@if (uploadError()) {
|
|
3227
|
-
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">{{ uploadError() }}</span>
|
|
3228
|
-
}
|
|
3229
|
-
}
|
|
3230
|
-
</div>
|
|
3393
|
+
template: `
|
|
3394
|
+
<div class="ngx-field ngx-field--file"
|
|
3395
|
+
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
3396
|
+
<label class="ngx-field__label" [attr.for]="'field-' + field.id">{{ label }}</label>
|
|
3397
|
+
@if (masked) {
|
|
3398
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
3399
|
+
} @else if (readonly) {
|
|
3400
|
+
@if (fileUrl()) {
|
|
3401
|
+
<a class="ngx-field__file-link" [href]="fileUrl()!" target="_blank" rel="noopener">
|
|
3402
|
+
📎 {{ fileName() || 'Download file' }}
|
|
3403
|
+
</a>
|
|
3404
|
+
} @else {
|
|
3405
|
+
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">—</span>
|
|
3406
|
+
}
|
|
3407
|
+
} @else {
|
|
3408
|
+
<div class="ngx-field__file-wrap">
|
|
3409
|
+
@if (fileName()) {
|
|
3410
|
+
<div class="ngx-field__file-selected">
|
|
3411
|
+
<span class="ngx-field__file-name">📎 {{ fileName() }}</span>
|
|
3412
|
+
<button type="button" class="ngx-field__remove-btn" (click)="remove()" aria-label="Remove file">✕</button>
|
|
3413
|
+
</div>
|
|
3414
|
+
}
|
|
3415
|
+
<label class="ngx-field__upload-btn" [class.ngx-field__upload-btn--loading]="uploading()">
|
|
3416
|
+
@if (uploading()) {
|
|
3417
|
+
<span>Uploading…</span>
|
|
3418
|
+
} @else {
|
|
3419
|
+
<span>{{ fileName() ? 'Replace file' : 'Choose file' }}</span>
|
|
3420
|
+
<input
|
|
3421
|
+
type="file"
|
|
3422
|
+
class="ngx-field__file-input"
|
|
3423
|
+
[id]="'field-' + field.id"
|
|
3424
|
+
[disabled]="field.disabled || uploading()"
|
|
3425
|
+
(change)="onFileSelect($any($event.target).files[0])"
|
|
3426
|
+
/>
|
|
3427
|
+
}
|
|
3428
|
+
</label>
|
|
3429
|
+
</div>
|
|
3430
|
+
@if (uploadError()) {
|
|
3431
|
+
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">{{ uploadError() }}</span>
|
|
3432
|
+
}
|
|
3433
|
+
}
|
|
3434
|
+
</div>
|
|
3231
3435
|
`,
|
|
3232
3436
|
}]
|
|
3233
3437
|
}], propDecorators: { field: [{
|
|
@@ -3412,7 +3616,26 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
3412
3616
|
*/
|
|
3413
3617
|
class DynamicFormComponent {
|
|
3414
3618
|
// ─── Inputs ───────────────────────────────────────────────────────────────
|
|
3415
|
-
|
|
3619
|
+
/**
|
|
3620
|
+
* The schema to render.
|
|
3621
|
+
*
|
|
3622
|
+
* An accessor pair rather than a plain field: `ngOnChanges` normalises the option shape,
|
|
3623
|
+
* and that result is kept here instead of being written back over the input. Reassigning
|
|
3624
|
+
* an `@Input` works, but it mutates what the parent handed us — and a parent using OnPush
|
|
3625
|
+
* or signals would never observe the replacement anyway.
|
|
3626
|
+
*
|
|
3627
|
+
* Reads of `this.config` get the normalised copy once it exists, so every internal caller
|
|
3628
|
+
* is unaffected.
|
|
3629
|
+
*/
|
|
3630
|
+
set config(value) {
|
|
3631
|
+
this.rawConfig = value;
|
|
3632
|
+
this.normalizedConfig = undefined; // re-normalised in ngOnChanges
|
|
3633
|
+
}
|
|
3634
|
+
get config() {
|
|
3635
|
+
return this.normalizedConfig ?? this.rawConfig;
|
|
3636
|
+
}
|
|
3637
|
+
rawConfig;
|
|
3638
|
+
normalizedConfig;
|
|
3416
3639
|
rules;
|
|
3417
3640
|
initialData;
|
|
3418
3641
|
/** Session-original values for `VALUE_CHANGED` rules. Captured from the first build when omitted. */
|
|
@@ -3446,6 +3669,11 @@ class DynamicFormComponent {
|
|
|
3446
3669
|
formReset = new EventEmitter();
|
|
3447
3670
|
/** The visible tab changed. Lets a host track it without reaching into this component. */
|
|
3448
3671
|
activeTabChange = new EventEmitter();
|
|
3672
|
+
/**
|
|
3673
|
+
* Emitted when a `beforeSave` hook aborted the save — by returning false or throwing.
|
|
3674
|
+
* `formSubmit` does not fire in that case.
|
|
3675
|
+
*/
|
|
3676
|
+
saveRejected = new EventEmitter();
|
|
3449
3677
|
// ─── Services ─────────────────────────────────────────────────────────────
|
|
3450
3678
|
fb = inject(FormBuilder);
|
|
3451
3679
|
validatorRegistry = inject(ValidatorRegistryService);
|
|
@@ -3454,6 +3682,7 @@ class DynamicFormComponent {
|
|
|
3454
3682
|
rulesEvaluation = inject(RulesEvaluationService);
|
|
3455
3683
|
entityRefSelection = inject(EntityRefSelectionService);
|
|
3456
3684
|
commonModulesRegistry = inject(COMMON_MODULES_REGISTRY, { optional: true });
|
|
3685
|
+
migrations = inject(RECORD_MIGRATIONS, { optional: true }) ?? [];
|
|
3457
3686
|
Object = Object;
|
|
3458
3687
|
// ─── Signals (local reactive state) ───────────────────────────────────────
|
|
3459
3688
|
activeTab = signal('');
|
|
@@ -3464,6 +3693,10 @@ class DynamicFormComponent {
|
|
|
3464
3693
|
sessionBaseline = signal({});
|
|
3465
3694
|
/** Critical fields the user has explicitly unlocked for editing. */
|
|
3466
3695
|
unlockedFields = signal(new Set());
|
|
3696
|
+
/** The active tab's field panel, focused after a tab switch. */
|
|
3697
|
+
formPanel;
|
|
3698
|
+
/** The hosted field components, so an external control-state change can reach them. */
|
|
3699
|
+
fieldHosts;
|
|
3467
3700
|
// ─── Form ─────────────────────────────────────────────────────────────────
|
|
3468
3701
|
form;
|
|
3469
3702
|
valueSub;
|
|
@@ -3536,11 +3769,72 @@ class DynamicFormComponent {
|
|
|
3536
3769
|
const hiddenFields = this.ruleResult().hiddenFields;
|
|
3537
3770
|
return rawFields.filter(field => evaluateFieldVisibility(field, currentValues) && !hiddenFields.includes(field.id));
|
|
3538
3771
|
}
|
|
3772
|
+
/**
|
|
3773
|
+
* Entity-level permissions for the current roles.
|
|
3774
|
+
*
|
|
3775
|
+
* Cached rather than recomputed: this is read from the template several times per change
|
|
3776
|
+
* detection pass, and a fresh object each time is both wasted allocation and a new
|
|
3777
|
+
* identity for anything comparing by reference. Invalidated in `ngOnChanges` when
|
|
3778
|
+
* `config` or `userRoles` change, which are its only inputs.
|
|
3779
|
+
*/
|
|
3539
3780
|
get permissions() {
|
|
3540
|
-
return this.rbacService.getPermissions(this.config, this.userRoles);
|
|
3781
|
+
return (this.permissionsCache ??= this.rbacService.getPermissions(this.config, this.userRoles));
|
|
3782
|
+
}
|
|
3783
|
+
permissionsCache = null;
|
|
3784
|
+
/**
|
|
3785
|
+
* Whether the current roles may see this record at all.
|
|
3786
|
+
*
|
|
3787
|
+
* `permissions.view` used to be computed and thrown away — a user whose roles failed it
|
|
3788
|
+
* still received the complete form with every value in the DOM. It is honoured now.
|
|
3789
|
+
*
|
|
3790
|
+
* As with masking, this is presentational: it stops the browser rendering data, it does
|
|
3791
|
+
* not stop the data reaching the browser. Authorize on the server.
|
|
3792
|
+
*/
|
|
3793
|
+
get canView() {
|
|
3794
|
+
return this.permissions.canView;
|
|
3795
|
+
}
|
|
3796
|
+
/**
|
|
3797
|
+
* Whether the current roles may delete this record.
|
|
3798
|
+
*
|
|
3799
|
+
* The library ships no delete affordance, so this is surfaced for the consumer to gate
|
|
3800
|
+
* their own — exposed rather than dropped, because a permission the schema declares
|
|
3801
|
+
* should be answerable.
|
|
3802
|
+
*/
|
|
3803
|
+
get canDelete() {
|
|
3804
|
+
return this.permissions.canDelete;
|
|
3541
3805
|
}
|
|
3542
3806
|
get canSubmit() {
|
|
3543
|
-
return this.permissions.canEdit && !this.readonly;
|
|
3807
|
+
return this.canView && this.permissions.canEdit && !this.readonly;
|
|
3808
|
+
}
|
|
3809
|
+
/**
|
|
3810
|
+
* Errors raised by `validation` rules, keyed by the target they point at.
|
|
3811
|
+
*
|
|
3812
|
+
* A rule can declare the record invalid independently of Angular's validators, so form
|
|
3813
|
+
* validity alone is not the whole picture. `DynamicRecordFormComponent.saveSection()`
|
|
3814
|
+
* has always honoured these; `submit()` did not, which meant the same rule blocked one
|
|
3815
|
+
* save path and merely painted a banner on the other — and anything wiring
|
|
3816
|
+
* `(formSubmit)` to persistence wrote records the rules engine had already rejected.
|
|
3817
|
+
*/
|
|
3818
|
+
get ruleValidationErrors() {
|
|
3819
|
+
return this.ruleResult().validationErrors;
|
|
3820
|
+
}
|
|
3821
|
+
/** True when a `validation` rule is currently blocking submission. */
|
|
3822
|
+
get hasRuleErrors() {
|
|
3823
|
+
return Object.keys(this.ruleValidationErrors).length > 0;
|
|
3824
|
+
}
|
|
3825
|
+
/**
|
|
3826
|
+
* True while an async validator is still running.
|
|
3827
|
+
*
|
|
3828
|
+
* Angular reports a control with an outstanding async check as `pending`, and `invalid` is
|
|
3829
|
+
* false until it settles — so without this a form could be submitted in the gap before a
|
|
3830
|
+
* uniqueness check came back.
|
|
3831
|
+
*/
|
|
3832
|
+
get isValidating() {
|
|
3833
|
+
return this.form.pending;
|
|
3834
|
+
}
|
|
3835
|
+
/** Single source of truth for both the submit guard and the button's disabled state. */
|
|
3836
|
+
get submitBlocked() {
|
|
3837
|
+
return this.form.invalid || this.form.pending || this.hasRuleErrors;
|
|
3544
3838
|
}
|
|
3545
3839
|
ngOnInit() {
|
|
3546
3840
|
this.selectionSub = this.entityRefSelection.selection$.subscribe(({ fieldId, option }) => {
|
|
@@ -3548,12 +3842,18 @@ class DynamicFormComponent {
|
|
|
3548
3842
|
});
|
|
3549
3843
|
}
|
|
3550
3844
|
ngOnChanges(changes) {
|
|
3551
|
-
if (changes['config']
|
|
3845
|
+
if (changes['config'] || changes['userRoles']) {
|
|
3846
|
+
this.permissionsCache = null;
|
|
3847
|
+
}
|
|
3848
|
+
if (changes['config'] && this.rawConfig) {
|
|
3552
3849
|
// Configs arrive as plain JSON from storage or an API, where TypeScript cannot enforce
|
|
3553
3850
|
// the option shape. Normalise here, at the library boundary, so everything downstream
|
|
3554
3851
|
// can rely on an option being a LocalizedText. Returns the same object when it already
|
|
3555
3852
|
// is, so a well-formed config costs nothing.
|
|
3556
|
-
|
|
3853
|
+
// Kept in a field of our own rather than written back to the @Input. Reassigning an
|
|
3854
|
+
// input works, but it surprises a parent that holds the same object — and a parent
|
|
3855
|
+
// using OnPush or signals would not see the replacement anyway.
|
|
3856
|
+
this.normalizedConfig = normalizeConfigOptions(this.config);
|
|
3557
3857
|
}
|
|
3558
3858
|
if (changes['config'] || changes['initialData']) {
|
|
3559
3859
|
this.buildForm();
|
|
@@ -3567,9 +3867,12 @@ class DynamicFormComponent {
|
|
|
3567
3867
|
this.selectionSub?.unsubscribe();
|
|
3568
3868
|
}
|
|
3569
3869
|
setActiveTab(tabId) {
|
|
3570
|
-
|
|
3870
|
+
const changed = this.activeTab() !== tabId;
|
|
3871
|
+
if (changed)
|
|
3571
3872
|
this.activeTabChange.emit(tabId);
|
|
3572
3873
|
this.activeTab.set(tabId);
|
|
3874
|
+
if (changed)
|
|
3875
|
+
this.focusActivePanel();
|
|
3573
3876
|
const parent = findTab(this.tabs, tabId);
|
|
3574
3877
|
if (parent?.children?.length) {
|
|
3575
3878
|
this.activeSubTab.set(parent.children[0].id);
|
|
@@ -3579,7 +3882,10 @@ class DynamicFormComponent {
|
|
|
3579
3882
|
}
|
|
3580
3883
|
}
|
|
3581
3884
|
setActiveSubTab(subTabId) {
|
|
3885
|
+
const changed = this.activeSubTab() !== subTabId;
|
|
3582
3886
|
this.activeSubTab.set(subTabId);
|
|
3887
|
+
if (changed)
|
|
3888
|
+
this.focusActivePanel();
|
|
3583
3889
|
}
|
|
3584
3890
|
getFieldSpan(field) {
|
|
3585
3891
|
const span = field.colSpan ?? 12;
|
|
@@ -3641,13 +3947,14 @@ class DynamicFormComponent {
|
|
|
3641
3947
|
buildTabControls(this.config.tabs || [], group);
|
|
3642
3948
|
this.form = this.fb.group(group);
|
|
3643
3949
|
if (this.initialData) {
|
|
3644
|
-
this.patchForm(this.initialData);
|
|
3950
|
+
this.patchForm(this.upgradeRecord(this.initialData));
|
|
3645
3951
|
}
|
|
3646
3952
|
const initialFlattened = this.flattenFormValues();
|
|
3647
3953
|
this.formValues.set(initialFlattened);
|
|
3648
3954
|
this.previousValues = { ...initialFlattened };
|
|
3649
3955
|
this.sessionBaseline.set({ ...initialFlattened });
|
|
3650
3956
|
this.unlockedFields.set(new Set());
|
|
3957
|
+
this.syncHiddenFieldState(initialFlattened);
|
|
3651
3958
|
if (this.preview) {
|
|
3652
3959
|
// Show what an array field looks like rather than an empty slot, then freeze.
|
|
3653
3960
|
for (const field of this.allFields()) {
|
|
@@ -3668,6 +3975,7 @@ class DynamicFormComponent {
|
|
|
3668
3975
|
const flattened = this.flattenFormValues();
|
|
3669
3976
|
this.formValues.set(flattened);
|
|
3670
3977
|
this.runPatchOnTrue(flattened);
|
|
3978
|
+
this.syncHiddenFieldState(flattened);
|
|
3671
3979
|
this.previousValues = { ...flattened };
|
|
3672
3980
|
this.formChange.emit(this.extractRecord());
|
|
3673
3981
|
});
|
|
@@ -3685,7 +3993,8 @@ class DynamicFormComponent {
|
|
|
3685
3993
|
}
|
|
3686
3994
|
else {
|
|
3687
3995
|
const validators = this.validatorRegistry.resolveFromConfig(field.validators);
|
|
3688
|
-
|
|
3996
|
+
const asyncValidators = this.validatorRegistry.resolveAsyncFromConfig(field.validators);
|
|
3997
|
+
group[field.id] = this.fb.control({ value: field.defaultValue ?? null, disabled: field.disabled ?? false }, validators, asyncValidators);
|
|
3689
3998
|
}
|
|
3690
3999
|
}
|
|
3691
4000
|
/** One row of an `array` field: a FormGroup when the field declares columns, else a bare control. */
|
|
@@ -3716,10 +4025,41 @@ class DynamicFormComponent {
|
|
|
3716
4025
|
group.patchValue(item);
|
|
3717
4026
|
return group;
|
|
3718
4027
|
}
|
|
4028
|
+
/**
|
|
4029
|
+
* Mark every control touched, then re-check the field components.
|
|
4030
|
+
*
|
|
4031
|
+
* They are OnPush and `touched` is flipped from outside their own template, so the second
|
|
4032
|
+
* half is what makes the error messages actually appear.
|
|
4033
|
+
*/
|
|
4034
|
+
markAllTouched() {
|
|
4035
|
+
this.form.markAllAsTouched();
|
|
4036
|
+
this.fieldHosts?.forEach(host => host.refresh());
|
|
4037
|
+
}
|
|
4038
|
+
/**
|
|
4039
|
+
* Bring a record up to the config's `version` before it reaches the form.
|
|
4040
|
+
*
|
|
4041
|
+
* Done here because this is the one place a record enters the renderer, so a consumer
|
|
4042
|
+
* cannot forget it. Migration is a no-op unless the consumer registered steps *and* the
|
|
4043
|
+
* record is behind — an unstamped record is deliberately left alone (see `migrateRecord`).
|
|
4044
|
+
*
|
|
4045
|
+
* A missing or broken step throws from core. That is not swallowed: rendering a
|
|
4046
|
+
* half-understood record would write it back in a shape matching neither schema.
|
|
4047
|
+
*/
|
|
4048
|
+
upgradeRecord(data) {
|
|
4049
|
+
if (!this.migrations.length || !this.config)
|
|
4050
|
+
return data;
|
|
4051
|
+
const result = migrateRecord(data, this.config, this.migrations);
|
|
4052
|
+
if (result.applied.length && isDevMode()) {
|
|
4053
|
+
console.info(`[ngx-dynamic-entity] Migrated a "${this.config.entity}" record from config version ` +
|
|
4054
|
+
`${result.from} to ${result.to} (steps: ${result.applied.join(', ')}).`);
|
|
4055
|
+
}
|
|
4056
|
+
return result.record;
|
|
4057
|
+
}
|
|
3719
4058
|
patchForm(data) {
|
|
3720
4059
|
if (!data || !this.form)
|
|
3721
4060
|
return;
|
|
3722
4061
|
const fieldsById = new Map(this.allFields().map(f => [f.id, f]));
|
|
4062
|
+
const patchedFieldIds = new Set();
|
|
3723
4063
|
const walkTabs = (tabs) => {
|
|
3724
4064
|
for (const tab of tabs) {
|
|
3725
4065
|
const tabData = getTabData(tab.id, data, this.config);
|
|
@@ -3744,13 +4084,57 @@ class DynamicFormComponent {
|
|
|
3744
4084
|
else {
|
|
3745
4085
|
ctrl.patchValue(val, { emitEvent: false });
|
|
3746
4086
|
}
|
|
4087
|
+
patchedFieldIds.add(field.id);
|
|
3747
4088
|
}
|
|
3748
4089
|
if (tab.children)
|
|
3749
4090
|
walkTabs(tab.children);
|
|
3750
4091
|
}
|
|
3751
4092
|
};
|
|
3752
4093
|
walkTabs(this.config.tabs || []);
|
|
4094
|
+
this.warnUnconsumedInitialData(data, fieldsById, patchedFieldIds);
|
|
3753
4095
|
}
|
|
4096
|
+
/**
|
|
4097
|
+
* A record is nested by tab id (`{ tabId: { fieldId: value } }`) unless the tab sets
|
|
4098
|
+
* `flatData: true`. Handing a flat record to a nested tab finds nothing, so the fields
|
|
4099
|
+
* stay empty — with no error and no clue. That silent miss is the most expensive way to
|
|
4100
|
+
* lose an hour with this library, so name it in dev builds.
|
|
4101
|
+
*
|
|
4102
|
+
* Only top-level keys that match a known field id are reported: anything else is assumed
|
|
4103
|
+
* to be the consumer's own record metadata (ids, timestamps, `_configVersion`) and is not
|
|
4104
|
+
* our business.
|
|
4105
|
+
*/
|
|
4106
|
+
warnUnconsumedInitialData(data, fieldsById, patchedFieldIds) {
|
|
4107
|
+
if (!isDevMode() || !this.config)
|
|
4108
|
+
return;
|
|
4109
|
+
const tabIds = new Set();
|
|
4110
|
+
const collectTabIds = (tabs) => {
|
|
4111
|
+
for (const tab of tabs) {
|
|
4112
|
+
tabIds.add(tab.id);
|
|
4113
|
+
if (tab.children)
|
|
4114
|
+
collectTabIds(tab.children);
|
|
4115
|
+
}
|
|
4116
|
+
};
|
|
4117
|
+
collectTabIds(this.config.tabs || []);
|
|
4118
|
+
const unconsumed = Object.keys(data).filter(key => !tabIds.has(key) && // a tab id at the root is the nested container, not a stray field
|
|
4119
|
+
data[key] !== undefined &&
|
|
4120
|
+
fieldsById.has(key) &&
|
|
4121
|
+
!patchedFieldIds.has(key) &&
|
|
4122
|
+
!this.warnedUnconsumedKeys.has(key));
|
|
4123
|
+
if (!unconsumed.length)
|
|
4124
|
+
return;
|
|
4125
|
+
for (const key of unconsumed)
|
|
4126
|
+
this.warnedUnconsumedKeys.add(key);
|
|
4127
|
+
const shown = unconsumed.slice(0, 5).join(', ');
|
|
4128
|
+
const more = unconsumed.length > 5 ? ` (+${unconsumed.length - 5} more)` : '';
|
|
4129
|
+
const plural = unconsumed.length === 1;
|
|
4130
|
+
console.warn(`[ngx-dynamic-entity] initialData has top-level ${plural ? 'key' : 'keys'} matching ` +
|
|
4131
|
+
`${plural ? 'a field that was' : 'fields that were'} not populated: ${shown}${more}. ` +
|
|
4132
|
+
`A record is nested by tab id ({ tabId: { fieldId: value } }) unless the tab sets ` +
|
|
4133
|
+
`flatData: true. Either nest the value under its tab id, or set flatData: true on ` +
|
|
4134
|
+
`the tab holding ${plural ? 'that field' : 'those fields'}.`);
|
|
4135
|
+
}
|
|
4136
|
+
/** Warn once per key per form instance — patchForm re-runs on every initialData change. */
|
|
4137
|
+
warnedUnconsumedKeys = new Set();
|
|
3754
4138
|
/** Assemble full nested record from per-tab FormGroups, respecting flatData, refererField & arrays. */
|
|
3755
4139
|
extractRecord() {
|
|
3756
4140
|
if (!this.form || !this.config)
|
|
@@ -3820,6 +4204,10 @@ class DynamicFormComponent {
|
|
|
3820
4204
|
const control = this.resolveTargetControl(autoPatch, targetId);
|
|
3821
4205
|
control?.patchValue(value);
|
|
3822
4206
|
}
|
|
4207
|
+
// Readonly targets interpolate `control.value` under OnPush. `detectChanges` on the
|
|
4208
|
+
// hosted component flushes that write immediately — `markForCheck` waits for a later
|
|
4209
|
+
// tick that a Playwright selection never schedules.
|
|
4210
|
+
this.fieldHosts?.forEach(host => host.refresh());
|
|
3823
4211
|
}
|
|
3824
4212
|
/**
|
|
3825
4213
|
* `patchOnTrue` — when a boolean/checkbox field transitions to `true`,
|
|
@@ -3849,6 +4237,57 @@ class DynamicFormComponent {
|
|
|
3849
4237
|
return this.getControl(targetId, autoPatch.targetTab);
|
|
3850
4238
|
}
|
|
3851
4239
|
// ─── Helpers ──────────────────────────────────────────────────────────────
|
|
4240
|
+
/**
|
|
4241
|
+
* The fields the render filter operates on — a tab's own fields, at every tab depth.
|
|
4242
|
+
* Deliberately not `allFields()`: `group` and `array` children are shown or hidden with
|
|
4243
|
+
* their parent, and toggling a child's state independently would fight the parent's.
|
|
4244
|
+
*/
|
|
4245
|
+
tabFields() {
|
|
4246
|
+
const out = [];
|
|
4247
|
+
const walk = (tabs) => {
|
|
4248
|
+
for (const tab of tabs ?? []) {
|
|
4249
|
+
out.push(...(tab.fields ?? []));
|
|
4250
|
+
walk(tab.children);
|
|
4251
|
+
}
|
|
4252
|
+
};
|
|
4253
|
+
walk(this.config?.tabs);
|
|
4254
|
+
return out;
|
|
4255
|
+
}
|
|
4256
|
+
/**
|
|
4257
|
+
* Keep a hidden field's control out of form validity.
|
|
4258
|
+
*
|
|
4259
|
+
* A field hidden by a rule or a `showWhen` condition is filtered out of the render, but
|
|
4260
|
+
* its validators stay attached — so a hidden *required* field holds `form.invalid` true
|
|
4261
|
+
* forever, permanently disabling Save with nothing on screen to explain why.
|
|
4262
|
+
*
|
|
4263
|
+
* Disabling is the fix rather than stripping validators: Angular excludes disabled
|
|
4264
|
+
* controls from validity, and the field's own validators survive intact for when it
|
|
4265
|
+
* comes back. Values are untouched — `flattenFormValues` and `extractRecord` both read
|
|
4266
|
+
* `control.value`, which a disabled control still carries, so rule evaluation and the
|
|
4267
|
+
* emitted record see exactly what they saw before. That also keeps this from feeding
|
|
4268
|
+
* back on itself: hiding a field cannot change the values the rules are evaluated over.
|
|
4269
|
+
*
|
|
4270
|
+
* The visibility predicate is the same one `fieldsForActiveTab` filters with, so what is
|
|
4271
|
+
* rendered and what counts toward validity cannot drift apart.
|
|
4272
|
+
*/
|
|
4273
|
+
syncHiddenFieldState(values) {
|
|
4274
|
+
if (this.preview)
|
|
4275
|
+
return; // preview freezes the whole form on purpose
|
|
4276
|
+
const hiddenByRule = new Set(this.ruleResult().hiddenFields);
|
|
4277
|
+
for (const field of this.tabFields()) {
|
|
4278
|
+
const ctrl = this.getControl(field.id);
|
|
4279
|
+
if (!ctrl)
|
|
4280
|
+
continue;
|
|
4281
|
+
const hidden = hiddenByRule.has(field.id) || !evaluateFieldVisibility(field, values);
|
|
4282
|
+
if (hidden) {
|
|
4283
|
+
if (ctrl.enabled)
|
|
4284
|
+
ctrl.disable({ emitEvent: false });
|
|
4285
|
+
}
|
|
4286
|
+
else if (ctrl.disabled && !field.disabled) {
|
|
4287
|
+
ctrl.enable({ emitEvent: false });
|
|
4288
|
+
}
|
|
4289
|
+
}
|
|
4290
|
+
}
|
|
3852
4291
|
/** Every field in the config, including `group`/`array` children. */
|
|
3853
4292
|
allFields() {
|
|
3854
4293
|
const out = [];
|
|
@@ -3914,24 +4353,70 @@ class DynamicFormComponent {
|
|
|
3914
4353
|
};
|
|
3915
4354
|
return findInGroup(this.form);
|
|
3916
4355
|
}
|
|
4356
|
+
/**
|
|
4357
|
+
* Move focus into the newly shown panel.
|
|
4358
|
+
*
|
|
4359
|
+
* Activating a tab replaces everything below it while focus stays on the tab button, so a
|
|
4360
|
+
* screen-reader or keyboard user is given no indication that the content changed and must
|
|
4361
|
+
* tab back through the strip to reach it. Deferred a frame because the panel's contents
|
|
4362
|
+
* are rendered by the change detection this call is part of.
|
|
4363
|
+
*/
|
|
4364
|
+
focusActivePanel() {
|
|
4365
|
+
if (typeof requestAnimationFrame !== 'function')
|
|
4366
|
+
return;
|
|
4367
|
+
requestAnimationFrame(() => this.formPanel?.nativeElement?.focus?.());
|
|
4368
|
+
}
|
|
4369
|
+
/** Accessible name for the field panel — it is the tabpanel for the active tab. */
|
|
4370
|
+
get activeTabLabel() {
|
|
4371
|
+
const tab = this.activeSubTabConfig ?? this.activeTabConfig;
|
|
4372
|
+
return tab ? this.resolveTabLabel(tab) : '';
|
|
4373
|
+
}
|
|
4374
|
+
/**
|
|
4375
|
+
* Why a referenced field is flagged.
|
|
4376
|
+
*
|
|
4377
|
+
* `hasDrift` was written by the builder and read by nothing at runtime, so a field whose
|
|
4378
|
+
* source definition had changed since it was linked looked entirely normal to the person
|
|
4379
|
+
* filling the form in.
|
|
4380
|
+
*/
|
|
4381
|
+
driftHint(field) {
|
|
4382
|
+
const source = field.referencedEntityKey
|
|
4383
|
+
? `"${field.referencedEntityKey}"`
|
|
4384
|
+
: 'its source entity';
|
|
4385
|
+
return `This field is linked to ${source} and its definition there has changed since it was linked.`;
|
|
4386
|
+
}
|
|
3917
4387
|
resolveTabLabel(tab) {
|
|
3918
4388
|
return resolveLabel(tab.label, this.language);
|
|
3919
4389
|
}
|
|
3920
4390
|
async submit() {
|
|
3921
|
-
if (!this.canSubmit || this.
|
|
3922
|
-
this.
|
|
4391
|
+
if (!this.canSubmit || this.submitBlocked) {
|
|
4392
|
+
this.markAllTouched();
|
|
3923
4393
|
return;
|
|
3924
4394
|
}
|
|
3925
4395
|
const rawData = this.extractRecord();
|
|
3926
4396
|
let processedData = rawData;
|
|
3927
|
-
const hookKey = `${this.config?.entity}:beforeSave`;
|
|
3928
|
-
if (this.hookRegistry.has(hookKey)) {
|
|
3929
|
-
processedData = await this.hookRegistry.run(hookKey, processedData);
|
|
3930
|
-
}
|
|
3931
4397
|
this.isSaving.set(true);
|
|
3932
4398
|
try {
|
|
4399
|
+
// beforeSave can now abort the save: returning false, or throwing, stops it. Previously
|
|
4400
|
+
// its return value simply replaced the payload and the submit proceeded regardless, so
|
|
4401
|
+
// a hook that wanted to veto — a server-side check, a confirmation — had no way to.
|
|
4402
|
+
const hookKey = `${this.config?.entity}:beforeSave`;
|
|
4403
|
+
if (this.hookRegistry.has(hookKey)) {
|
|
4404
|
+
const result = await this.hookRegistry.run(hookKey, processedData);
|
|
4405
|
+
if (result === false) {
|
|
4406
|
+
this.saveRejected.emit({ reason: 'beforeSave returned false' });
|
|
4407
|
+
return;
|
|
4408
|
+
}
|
|
4409
|
+
// Anything else is the payload to submit; `undefined` means "unchanged".
|
|
4410
|
+
if (result !== undefined && result !== true)
|
|
4411
|
+
processedData = result;
|
|
4412
|
+
}
|
|
3933
4413
|
this.formSubmit.emit(processedData);
|
|
3934
4414
|
}
|
|
4415
|
+
catch (err) {
|
|
4416
|
+
// A hook that throws aborts the save and reports why, rather than the error escaping
|
|
4417
|
+
// into an unhandled rejection with the form left looking as though it saved.
|
|
4418
|
+
this.saveRejected.emit({ reason: err instanceof Error ? err.message : String(err), error: err });
|
|
4419
|
+
}
|
|
3935
4420
|
finally {
|
|
3936
4421
|
this.isSaving.set(false);
|
|
3937
4422
|
}
|
|
@@ -3947,11 +4432,11 @@ class DynamicFormComponent {
|
|
|
3947
4432
|
this.formReset.emit();
|
|
3948
4433
|
}
|
|
3949
4434
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: DynamicFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3950
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: DynamicFormComponent, isStandalone: true, selector: "ngx-dynamic-form", inputs: { config: "config", rules: "rules", initialData: "initialData", originalData: "originalData", userRoles: "userRoles", language: "language", readonly: "readonly", readOnlyFields: "readOnlyFields", showInfoBanners: "showInfoBanners", loading: "loading", error: "error", changeDebounceMs: "changeDebounceMs", preview: "preview" }, outputs: { formSubmit: "formSubmit", formChange: "formChange", formReset: "formReset", activeTabChange: "activeTabChange" }, host: { listeners: { "window:keydown": "handleKeyboardEvent($event)" } }, providers: [EntityRefSelectionService], usesOnChanges: true, ngImport: i0, template: "<form [formGroup]=\"form\" (ngSubmit)=\"submit()\" class=\"ngx-form\" novalidate>\
|
|
4435
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: DynamicFormComponent, isStandalone: true, selector: "ngx-dynamic-form", inputs: { config: "config", rules: "rules", initialData: "initialData", originalData: "originalData", userRoles: "userRoles", language: "language", readonly: "readonly", readOnlyFields: "readOnlyFields", showInfoBanners: "showInfoBanners", loading: "loading", error: "error", changeDebounceMs: "changeDebounceMs", preview: "preview" }, outputs: { formSubmit: "formSubmit", formChange: "formChange", formReset: "formReset", activeTabChange: "activeTabChange", saveRejected: "saveRejected" }, host: { listeners: { "window:keydown": "handleKeyboardEvent($event)" } }, providers: [EntityRefSelectionService], viewQueries: [{ propertyName: "formPanel", first: true, predicate: ["formPanel"], descendants: true }, { propertyName: "fieldHosts", predicate: DynamicFieldComponent, descendants: true }], usesOnChanges: true, ngImport: i0, template: "@if (canView) {\n <form [formGroup]=\"form\" (ngSubmit)=\"submit()\" class=\"ngx-form\" novalidate>\n\n <!-- Error message banner -->\n @if (error) {\n <div class=\"ngx-form__error-banner\" role=\"alert\" data-testid=\"form-error\">{{ error }}</div>\n }\n\n <!-- Rules Info Banners -->\n @for (infoKey of showInfoBanners ? Object.keys(ruleResult().infoBanners) : []; track infoKey) {\n <div class=\"ngx-form__info-banner\" role=\"status\" [attr.data-testid]=\"'info-banner-' + infoKey\">\n \u2139\uFE0F {{ ruleResult().infoBanners[infoKey] }}\n </div>\n }\n\n <!-- Rules Validation Errors & Warnings -->\n @for (errKey of Object.keys(ruleResult().validationErrors); track errKey) {\n <div class=\"ngx-form__error-banner\" role=\"alert\" [attr.data-testid]=\"'rule-error-' + errKey\">\n \u26A0\uFE0F {{ ruleResult().validationErrors[errKey] }}\n </div>\n }\n @for (warnKey of Object.keys(ruleResult().validationWarnings); track warnKey) {\n <div class=\"ngx-form__warning-banner\" role=\"status\" [attr.data-testid]=\"'rule-warning-' + warnKey\">\n \u26A1 {{ ruleResult().validationWarnings[warnKey] }}\n </div>\n }\n\n <!-- Deferred VALUE_CHANGED notice for criticalField edits (vs session baseline) -->\n @if (changedCriticalFields().length > 0) {\n <div class=\"ngx-form__warning-banner\" role=\"status\" data-testid=\"critical-change-banner\">\n \uD83D\uDD12 Critical field changed:\n @for (field of changedCriticalFields(); track field.id) {\n <span>{{ resolveFieldLabel(field) }}{{ $last ? '' : ', ' }}</span>\n }\n \u2014 this differs from the value at the start of this session.\n </div>\n }\n\n <!-- Primary Tab bar \u2014 only if visible tabs defined -->\n @if (visibleTabs.length > 0) {\n <div class=\"ngx-form__tabs\" role=\"tablist\" data-testid=\"tab-strip\">\n @for (tab of visibleTabs; track tab.id) {\n <button\n type=\"button\"\n class=\"ngx-form__tab\"\n [attr.data-testid]=\"'tab-' + tab.id\"\n [class.ngx-form__tab--active]=\"activeTab() === tab.id\"\n (click)=\"setActiveTab(tab.id)\"\n role=\"tab\"\n [attr.aria-selected]=\"activeTab() === tab.id\"\n >\n {{ resolveTabLabel(tab) }}\n </button>\n }\n </div>\n }\n\n <!-- Sub-tab bar \u2014 only if active tab has sub-tabs -->\n @if (visibleSubTabs.length > 0) {\n <div class=\"ngx-form__tabs ngx-form__tabs--sub\" role=\"tablist\" data-testid=\"subtab-strip\" style=\"margin-top: -12px; margin-bottom: 12px; font-size: 13px;\">\n @for (subTab of visibleSubTabs; track subTab.id) {\n <button\n type=\"button\"\n class=\"ngx-form__tab ngx-form__tab--sub\"\n [attr.data-testid]=\"'subtab-' + subTab.id\"\n [class.ngx-form__tab--active]=\"(activeSubTabConfig?.id) === subTab.id\"\n (click)=\"setActiveSubTab(subTab.id)\"\n role=\"tab\"\n [attr.aria-selected]=\"(activeSubTabConfig?.id) === subTab.id\"\n >\n {{ resolveTabLabel(subTab) }}\n </button>\n }\n </div>\n }\n\n <!--\n One tabpanel per tab, wrapping both branches. Focus moves here on a tab switch, and a\n module tab is still a tab \u2014 putting the panel semantics on only the fields branch left\n a module tab with nowhere for focus to land.\n -->\n <div\n role=\"tabpanel\"\n tabindex=\"-1\"\n #formPanel\n class=\"ngx-form__tabpanel\"\n [attr.aria-label]=\"activeTabLabel\"\n >\n @if (activeTabModuleComponent) {\n <div class=\"ngx-form__module-panel\" data-testid=\"module-panel\">\n <ng-container *ngComponentOutlet=\"activeTabModuleComponent; inputs: (activeSubTabConfig || activeTabConfig)?.moduleInputs\"></ng-container>\n </div>\n } @else {\n <!-- Fields for active tab (Responsive 12-column grid) -->\n <div class=\"ngx-form__panel\" data-testid=\"form-panel\">\n @for (field of fieldsForActiveTab; track field.id) {\n <div\n class=\"ngx-form__field-slot\"\n [style.gridColumn]=\"getFieldSpan(field)\"\n [attr.id]=\"'field-container-' + field.id\"\n >\n <ngx-dynamic-field\n [field]=\"field\"\n [control]=\"getControl(field.id, (activeSubTabConfig || activeTabConfig)?.id)!\"\n [config]=\"config\"\n [currentTabId]=\"(activeSubTabConfig || activeTabConfig)?.id\"\n [language]=\"language\"\n [readonly]=\"isFieldReadonly(field)\"\n [userRoles]=\"userRoles\"\n />\n @if (field.hasDrift) {\n <!--\n Drift was previously visible only in the builder, so a referenced field whose\n source had changed looked completely normal to the person filling the form in.\n role=\"status\" rather than \"alert\": it is information, not an error.\n -->\n <span\n class=\"ngx-field__drift\"\n role=\"status\"\n [attr.data-testid]=\"'field-' + field.id + '-drift'\"\n [title]=\"driftHint(field)\"\n >\u26A0 {{ driftHint(field) }}</span>\n }\n @if (field.criticalField && !readonly) {\n <button\n type=\"button\"\n class=\"ngx-form__lock\"\n [attr.data-testid]=\"'lock-' + field.id\"\n [attr.aria-pressed]=\"!isFieldLocked(field)\"\n [attr.aria-label]=\"\n (isFieldLocked(field) ? 'Unlock ' : 'Lock ') + resolveFieldLabel(field)\n \"\n [title]=\"\n isFieldLocked(field)\n ? 'Critical field \u2014 click to unlock for editing'\n : 'Critical field \u2014 click to lock'\n \"\n (click)=\"toggleFieldLock(field)\"\n >\n {{ isFieldLocked(field) ? '\uD83D\uDD12' : '\uD83D\uDD13' }}\n </button>\n }\n </div>\n }\n </div>\n }\n </div>\n\n <!-- Submit button \u2014 gated by RBAC and isSaving lock -->\n @if (!readonly && permissions.canEdit) {\n <div class=\"ngx-form__actions\" data-testid=\"form-actions\">\n <button\n type=\"button\"\n class=\"ngx-form__reset\"\n data-testid=\"form-reset\"\n (click)=\"reset()\"\n [disabled]=\"isSaving()\"\n >\n Reset\n </button>\n <button\n type=\"submit\"\n class=\"ngx-form__submit\"\n data-testid=\"form-submit\"\n [disabled]=\"submitBlocked\"\n title=\"Shortcut: Ctrl+S\"\n >\n @if (isSaving() || loading) {\n Saving\u2026\n } @else {\n Save\n }\n </button>\n </div>\n }\n\n </form>\n} @else {\n <!-- permissions.view denied: render nothing of the record.\n Presentational only \u2014 the server must not send data this user may not see. -->\n <div class=\"ngx-form__denied\" role=\"status\" data-testid=\"form-access-denied\">\n You do not have permission to view this record.\n </div>\n}\n", styles: [".ngx-form__panel{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:16px 20px;align-items:start}.ngx-form__field-slot{display:flex;align-items:flex-start;gap:6px;min-width:0}.ngx-form__field-slot ngx-dynamic-field{flex:1;min-width:0}.ngx-form__lock{flex:0 0 auto;margin-top:22px;background:none;border:1px solid transparent;border-radius:6px;cursor:pointer;font-size:14px;line-height:1;padding:4px 6px}.ngx-form__lock:hover{border-color:#d1d5db;background:#f9fafb}@media (max-width: 768px){.ngx-form__panel{grid-template-columns:1fr}.ngx-form__field-slot{grid-column:span 12!important}}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: DynamicFieldComponent, selector: "ngx-dynamic-field", inputs: ["field", "control", "config", "currentTabId", "language", "readonly", "userRoles"] }, { kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletContent", "ngComponentOutletNgModule", "ngComponentOutletNgModuleFactory"] }] });
|
|
3951
4436
|
}
|
|
3952
4437
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: DynamicFormComponent, decorators: [{
|
|
3953
4438
|
type: Component,
|
|
3954
|
-
args: [{ selector: 'ngx-dynamic-form', standalone: true, imports: [ReactiveFormsModule, DynamicFieldComponent, NgComponentOutlet], providers: [EntityRefSelectionService], template: "<form [formGroup]=\"form\" (ngSubmit)=\"submit()\" class=\"ngx-form\" novalidate>\
|
|
4439
|
+
args: [{ selector: 'ngx-dynamic-form', standalone: true, imports: [ReactiveFormsModule, DynamicFieldComponent, NgComponentOutlet], providers: [EntityRefSelectionService], template: "@if (canView) {\n <form [formGroup]=\"form\" (ngSubmit)=\"submit()\" class=\"ngx-form\" novalidate>\n\n <!-- Error message banner -->\n @if (error) {\n <div class=\"ngx-form__error-banner\" role=\"alert\" data-testid=\"form-error\">{{ error }}</div>\n }\n\n <!-- Rules Info Banners -->\n @for (infoKey of showInfoBanners ? Object.keys(ruleResult().infoBanners) : []; track infoKey) {\n <div class=\"ngx-form__info-banner\" role=\"status\" [attr.data-testid]=\"'info-banner-' + infoKey\">\n \u2139\uFE0F {{ ruleResult().infoBanners[infoKey] }}\n </div>\n }\n\n <!-- Rules Validation Errors & Warnings -->\n @for (errKey of Object.keys(ruleResult().validationErrors); track errKey) {\n <div class=\"ngx-form__error-banner\" role=\"alert\" [attr.data-testid]=\"'rule-error-' + errKey\">\n \u26A0\uFE0F {{ ruleResult().validationErrors[errKey] }}\n </div>\n }\n @for (warnKey of Object.keys(ruleResult().validationWarnings); track warnKey) {\n <div class=\"ngx-form__warning-banner\" role=\"status\" [attr.data-testid]=\"'rule-warning-' + warnKey\">\n \u26A1 {{ ruleResult().validationWarnings[warnKey] }}\n </div>\n }\n\n <!-- Deferred VALUE_CHANGED notice for criticalField edits (vs session baseline) -->\n @if (changedCriticalFields().length > 0) {\n <div class=\"ngx-form__warning-banner\" role=\"status\" data-testid=\"critical-change-banner\">\n \uD83D\uDD12 Critical field changed:\n @for (field of changedCriticalFields(); track field.id) {\n <span>{{ resolveFieldLabel(field) }}{{ $last ? '' : ', ' }}</span>\n }\n \u2014 this differs from the value at the start of this session.\n </div>\n }\n\n <!-- Primary Tab bar \u2014 only if visible tabs defined -->\n @if (visibleTabs.length > 0) {\n <div class=\"ngx-form__tabs\" role=\"tablist\" data-testid=\"tab-strip\">\n @for (tab of visibleTabs; track tab.id) {\n <button\n type=\"button\"\n class=\"ngx-form__tab\"\n [attr.data-testid]=\"'tab-' + tab.id\"\n [class.ngx-form__tab--active]=\"activeTab() === tab.id\"\n (click)=\"setActiveTab(tab.id)\"\n role=\"tab\"\n [attr.aria-selected]=\"activeTab() === tab.id\"\n >\n {{ resolveTabLabel(tab) }}\n </button>\n }\n </div>\n }\n\n <!-- Sub-tab bar \u2014 only if active tab has sub-tabs -->\n @if (visibleSubTabs.length > 0) {\n <div class=\"ngx-form__tabs ngx-form__tabs--sub\" role=\"tablist\" data-testid=\"subtab-strip\" style=\"margin-top: -12px; margin-bottom: 12px; font-size: 13px;\">\n @for (subTab of visibleSubTabs; track subTab.id) {\n <button\n type=\"button\"\n class=\"ngx-form__tab ngx-form__tab--sub\"\n [attr.data-testid]=\"'subtab-' + subTab.id\"\n [class.ngx-form__tab--active]=\"(activeSubTabConfig?.id) === subTab.id\"\n (click)=\"setActiveSubTab(subTab.id)\"\n role=\"tab\"\n [attr.aria-selected]=\"(activeSubTabConfig?.id) === subTab.id\"\n >\n {{ resolveTabLabel(subTab) }}\n </button>\n }\n </div>\n }\n\n <!--\n One tabpanel per tab, wrapping both branches. Focus moves here on a tab switch, and a\n module tab is still a tab \u2014 putting the panel semantics on only the fields branch left\n a module tab with nowhere for focus to land.\n -->\n <div\n role=\"tabpanel\"\n tabindex=\"-1\"\n #formPanel\n class=\"ngx-form__tabpanel\"\n [attr.aria-label]=\"activeTabLabel\"\n >\n @if (activeTabModuleComponent) {\n <div class=\"ngx-form__module-panel\" data-testid=\"module-panel\">\n <ng-container *ngComponentOutlet=\"activeTabModuleComponent; inputs: (activeSubTabConfig || activeTabConfig)?.moduleInputs\"></ng-container>\n </div>\n } @else {\n <!-- Fields for active tab (Responsive 12-column grid) -->\n <div class=\"ngx-form__panel\" data-testid=\"form-panel\">\n @for (field of fieldsForActiveTab; track field.id) {\n <div\n class=\"ngx-form__field-slot\"\n [style.gridColumn]=\"getFieldSpan(field)\"\n [attr.id]=\"'field-container-' + field.id\"\n >\n <ngx-dynamic-field\n [field]=\"field\"\n [control]=\"getControl(field.id, (activeSubTabConfig || activeTabConfig)?.id)!\"\n [config]=\"config\"\n [currentTabId]=\"(activeSubTabConfig || activeTabConfig)?.id\"\n [language]=\"language\"\n [readonly]=\"isFieldReadonly(field)\"\n [userRoles]=\"userRoles\"\n />\n @if (field.hasDrift) {\n <!--\n Drift was previously visible only in the builder, so a referenced field whose\n source had changed looked completely normal to the person filling the form in.\n role=\"status\" rather than \"alert\": it is information, not an error.\n -->\n <span\n class=\"ngx-field__drift\"\n role=\"status\"\n [attr.data-testid]=\"'field-' + field.id + '-drift'\"\n [title]=\"driftHint(field)\"\n >\u26A0 {{ driftHint(field) }}</span>\n }\n @if (field.criticalField && !readonly) {\n <button\n type=\"button\"\n class=\"ngx-form__lock\"\n [attr.data-testid]=\"'lock-' + field.id\"\n [attr.aria-pressed]=\"!isFieldLocked(field)\"\n [attr.aria-label]=\"\n (isFieldLocked(field) ? 'Unlock ' : 'Lock ') + resolveFieldLabel(field)\n \"\n [title]=\"\n isFieldLocked(field)\n ? 'Critical field \u2014 click to unlock for editing'\n : 'Critical field \u2014 click to lock'\n \"\n (click)=\"toggleFieldLock(field)\"\n >\n {{ isFieldLocked(field) ? '\uD83D\uDD12' : '\uD83D\uDD13' }}\n </button>\n }\n </div>\n }\n </div>\n }\n </div>\n\n <!-- Submit button \u2014 gated by RBAC and isSaving lock -->\n @if (!readonly && permissions.canEdit) {\n <div class=\"ngx-form__actions\" data-testid=\"form-actions\">\n <button\n type=\"button\"\n class=\"ngx-form__reset\"\n data-testid=\"form-reset\"\n (click)=\"reset()\"\n [disabled]=\"isSaving()\"\n >\n Reset\n </button>\n <button\n type=\"submit\"\n class=\"ngx-form__submit\"\n data-testid=\"form-submit\"\n [disabled]=\"submitBlocked\"\n title=\"Shortcut: Ctrl+S\"\n >\n @if (isSaving() || loading) {\n Saving\u2026\n } @else {\n Save\n }\n </button>\n </div>\n }\n\n </form>\n} @else {\n <!-- permissions.view denied: render nothing of the record.\n Presentational only \u2014 the server must not send data this user may not see. -->\n <div class=\"ngx-form__denied\" role=\"status\" data-testid=\"form-access-denied\">\n You do not have permission to view this record.\n </div>\n}\n", styles: [".ngx-form__panel{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:16px 20px;align-items:start}.ngx-form__field-slot{display:flex;align-items:flex-start;gap:6px;min-width:0}.ngx-form__field-slot ngx-dynamic-field{flex:1;min-width:0}.ngx-form__lock{flex:0 0 auto;margin-top:22px;background:none;border:1px solid transparent;border-radius:6px;cursor:pointer;font-size:14px;line-height:1;padding:4px 6px}.ngx-form__lock:hover{border-color:#d1d5db;background:#f9fafb}@media (max-width: 768px){.ngx-form__panel{grid-template-columns:1fr}.ngx-form__field-slot{grid-column:span 12!important}}\n"] }]
|
|
3955
4440
|
}], propDecorators: { config: [{
|
|
3956
4441
|
type: Input
|
|
3957
4442
|
}], rules: [{
|
|
@@ -3986,11 +4471,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
3986
4471
|
type: Output
|
|
3987
4472
|
}], activeTabChange: [{
|
|
3988
4473
|
type: Output
|
|
4474
|
+
}], saveRejected: [{
|
|
4475
|
+
type: Output
|
|
4476
|
+
}], formPanel: [{
|
|
4477
|
+
type: ViewChild,
|
|
4478
|
+
args: ['formPanel']
|
|
4479
|
+
}], fieldHosts: [{
|
|
4480
|
+
type: ViewChildren,
|
|
4481
|
+
args: [DynamicFieldComponent]
|
|
3989
4482
|
}], handleKeyboardEvent: [{
|
|
3990
4483
|
type: HostListener,
|
|
3991
4484
|
args: ['window:keydown', ['$event']]
|
|
3992
4485
|
}] } });
|
|
3993
4486
|
|
|
4487
|
+
/**
|
|
4488
|
+
* Distinguishes "no tab holds this field" from "the field holds undefined", so the tab walk
|
|
4489
|
+
* can keep searching in the first case and stop in the second.
|
|
4490
|
+
*/
|
|
4491
|
+
const FIELD_NOT_FOUND = Symbol('field-not-found');
|
|
3994
4492
|
/**
|
|
3995
4493
|
* DynamicRecordFormComponent — comprehensive tabbed record view & edit component.
|
|
3996
4494
|
* Supports cross-tab rule evaluation, profile header, summary drawer (showOnMinimize),
|
|
@@ -4340,14 +4838,34 @@ class DynamicRecordFormComponent {
|
|
|
4340
4838
|
controlFor(fieldId) {
|
|
4341
4839
|
return this.dynamicFormComp?.getControl(fieldId) ?? null;
|
|
4342
4840
|
}
|
|
4343
|
-
/**
|
|
4841
|
+
/**
|
|
4842
|
+
* A field's current value, read from the record so it is stable during render.
|
|
4843
|
+
*
|
|
4844
|
+
* Resolved through `getTabData` so it honours exactly the nesting contract the form
|
|
4845
|
+
* patches with — `{ tabId: { fieldId } }`, or the record root when the tab sets
|
|
4846
|
+
* `flatData`. Reading the record flat instead is not a shortcut: it makes the summary
|
|
4847
|
+
* display values the form never loaded, which reads as "the data is there" while every
|
|
4848
|
+
* control behind it is empty.
|
|
4849
|
+
*
|
|
4850
|
+
* Walks sub-tabs as well: a summary, header, or profile field may sit on any tab at any
|
|
4851
|
+
* depth.
|
|
4852
|
+
*/
|
|
4344
4853
|
fieldValue(fieldId) {
|
|
4345
|
-
|
|
4346
|
-
const
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4854
|
+
const visit = (tabs = []) => {
|
|
4855
|
+
for (const tab of tabs) {
|
|
4856
|
+
const data = getTabData(tab.id, this.currentData(), this.config);
|
|
4857
|
+
if (data && fieldId in data)
|
|
4858
|
+
return data[fieldId];
|
|
4859
|
+
if (tab.children) {
|
|
4860
|
+
const found = visit(tab.children);
|
|
4861
|
+
if (found !== FIELD_NOT_FOUND)
|
|
4862
|
+
return found;
|
|
4863
|
+
}
|
|
4864
|
+
}
|
|
4865
|
+
return FIELD_NOT_FOUND;
|
|
4866
|
+
};
|
|
4867
|
+
const result = visit(this.config?.tabs);
|
|
4868
|
+
return result === FIELD_NOT_FOUND ? undefined : result;
|
|
4351
4869
|
}
|
|
4352
4870
|
isModified = computed(() => {
|
|
4353
4871
|
const orig = this.originalBaseline();
|
|
@@ -4391,7 +4909,7 @@ class DynamicRecordFormComponent {
|
|
|
4391
4909
|
}
|
|
4392
4910
|
/** Summary values render through the shared core formatter, not a local stringifier. */
|
|
4393
4911
|
formatFieldValue(field) {
|
|
4394
|
-
return formatDisplayValue(field.type, field.options, this.
|
|
4912
|
+
return formatDisplayValue(field.type, field.options, this.fieldValue(field.id), this.language);
|
|
4395
4913
|
}
|
|
4396
4914
|
jumpToField(fieldId) {
|
|
4397
4915
|
// Find parent tab for field
|
|
@@ -4411,11 +4929,11 @@ class DynamicRecordFormComponent {
|
|
|
4411
4929
|
}
|
|
4412
4930
|
}
|
|
4413
4931
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: DynamicRecordFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
4414
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: DynamicRecordFormComponent, isStandalone: true, selector: "ngx-dynamic-record-form", inputs: { config: "config", rules: "rules", initialData: "initialData", userRoles: "userRoles", language: "language", readonly: "readonly", loading: "loading", error: "error", isReadOnly: "isReadOnly", readOnlyFields: "readOnlyFields", viewMode: "viewMode" }, outputs: { formSubmit: "formSubmit", formChange: "formChange", formReset: "formReset", sectionSave: "sectionSave" }, viewQueries: [{ propertyName: "dynamicFormComp", first: true, predicate: DynamicFormComponent, descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"ngx-record-editor\">\r\n <!-- Record Header -->\r\n <div class=\"ngx-record-editor__header\">\r\n <div class=\"ngx-record-editor__title-group\">\r\n @if (profileImageUrl) {\r\n <img\r\n class=\"ngx-record-editor__avatar\"\r\n data-testid=\"header-profile-image\"\r\n [src]=\"profileImageUrl\"\r\n [alt]=\"recordTitle\"\r\n />\r\n } @else {\r\n <div class=\"ngx-record-editor__avatar\" data-testid=\"header-avatar-initial\">\r\n {{ avatarLetter }}\r\n </div>\r\n }\r\n <div>\r\n <h2 class=\"ngx-record-editor__title\" data-testid=\"record-title\">\r\n {{ recordTitle }}\r\n </h2>\r\n <span class=\"ngx-record-editor__subtitle\">Entity: {{ config.entity }}</span>\r\n </div>\r\n </div>\r\n\r\n <!-- Record-level status switch, outside section editing -->\r\n @if (headerToggleField; as toggleField) {\r\n <label class=\"ngx-record-editor__header-toggle\">\r\n <span>{{ formatFieldLabel(toggleField) }}</span>\r\n <input\r\n type=\"checkbox\"\r\n role=\"switch\"\r\n data-testid=\"header-toggle\"\r\n [checked]=\"headerToggleValue\"\r\n [disabled]=\"recordReadOnly\"\r\n [attr.aria-checked]=\"headerToggleValue\"\r\n (change)=\"toggleHeaderStatus()\"\r\n />\r\n <span class=\"ngx-record-editor__header-toggle-text\">{{ headerToggleValue ? 'Yes' : 'No' }}</span>\r\n </label>\r\n }\r\n </div>\r\n\r\n <!-- Dismissible info banners (rules `info` action) -->\r\n @for (banner of infoBanners; track banner.fieldId) {\r\n <div\r\n class=\"ngx-record-editor__banner ngx-record-editor__banner--info\"\r\n role=\"status\"\r\n [attr.data-testid]=\"'info-banner-' + banner.fieldId\"\r\n >\r\n <span>\u2139\uFE0F {{ banner.message }}</span>\r\n <button\r\n type=\"button\"\r\n class=\"ngx-record-editor__banner-dismiss\"\r\n [attr.data-testid]=\"'dismiss-' + banner.fieldId\"\r\n [attr.aria-label]=\"'Dismiss: ' + banner.message\"\r\n (click)=\"dismissInfoBanner(banner.fieldId)\"\r\n >\u2715</button>\r\n </div>\r\n }\r\n\r\n <!-- Modification baseline banner -->\r\n @if (isModified()) {\r\n <div class=\"ngx-record-editor__banner ngx-record-editor__banner--warning\">\r\n \uD83D\uDD12 Record modified from session baseline \u2014 unsaved changes pending.\r\n </div>\r\n }\r\n\r\n <!-- Summary Panel (showOnMinimize fields with interactive jump) -->\r\n @if (summaryFields().length > 0) {\r\n <div class=\"ngx-record-editor__summary-panel\" data-testid=\"summary-panel\">\r\n @for (sf of summaryFields(); track sf.id) {\r\n <div\r\n class=\"ngx-record-editor__summary-item\"\r\n [attr.data-testid]=\"'summary-' + sf.id\"\r\n (click)=\"jumpToField(sf.id)\"\r\n [title]=\"'Click to jump to ' + formatFieldLabel(sf)\"\r\n >\r\n <span class=\"ngx-record-editor__summary-label\">{{ formatFieldLabel(sf) }}</span>\r\n <span class=\"ngx-record-editor__summary-val\">{{ formatFieldValue(sf) }}</span>\r\n </div>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Section edit bar: one tab is edited at a time, and validated on its own -->\r\n @if (viewMode && !recordReadOnly) {\r\n <div class=\"ngx-record-editor__section-bar\">\r\n @if (isEditingActiveTab) {\r\n <span class=\"ngx-record-editor__section-hint\">Editing this section</span>\r\n <button type=\"button\" class=\"ngx-form__reset\" data-testid=\"cancel-section\" (click)=\"cancelSection()\">\r\n Cancel\r\n </button>\r\n <button type=\"button\" class=\"ngx-form__submit\" data-testid=\"save-section\" (click)=\"saveSection()\">\r\n Save section\r\n </button>\r\n } @else {\r\n <button type=\"button\" class=\"ngx-form__reset\" data-testid=\"edit-section\" (click)=\"editSection()\">\r\n Edit section\r\n </button>\r\n }\r\n </div>\r\n }\r\n\r\n @if (Object.keys(sectionErrors()).length > 0) {\r\n <div class=\"ngx-record-editor__banner ngx-record-editor__banner--error\" role=\"alert\" data-testid=\"section-errors\">\r\n @for (key of Object.keys(sectionErrors()); track key) {\r\n <div>\u26A0\uFE0F {{ sectionErrors()[key] }}</div>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Array rows: listed here, edited in the drawer below -->\r\n @for (arrayField of arrayFieldsForActiveTab; track arrayField.id) {\r\n <div class=\"ngx-record-editor__rows\" [attr.data-testid]=\"'rows-' + arrayField.id\">\r\n <div class=\"ngx-record-editor__rows-head\">\r\n <span class=\"ngx-record-editor__summary-label\">{{ formatFieldLabel(arrayField) }}</span>\r\n @if (!sectionReadOnly) {\r\n <button\r\n type=\"button\"\r\n class=\"ngx-form__reset\"\r\n [attr.data-testid]=\"'add-row-' + arrayField.id\"\r\n (click)=\"openAddRow(arrayField)\"\r\n >+ Row</button>\r\n }\r\n </div>\r\n\r\n @for (row of rowsOf(arrayField); track $index) {\r\n <div class=\"ngx-record-editor__row\" [attr.data-testid]=\"'row-' + arrayField.id + '-' + $index\">\r\n <span class=\"ngx-record-editor__row-text\">{{ rowSummary(arrayField, row) }}</span>\r\n @if (!sectionReadOnly) {\r\n <button type=\"button\" class=\"ngx-record-editor__row-btn\"\r\n [attr.data-testid]=\"'edit-row-' + arrayField.id + '-' + $index\"\r\n (click)=\"openEditRow(arrayField, $index)\">Edit</button>\r\n <button type=\"button\" class=\"ngx-record-editor__row-btn\"\r\n [attr.data-testid]=\"'delete-row-' + arrayField.id + '-' + $index\"\r\n (click)=\"deleteRow(arrayField, $index)\">Delete</button>\r\n }\r\n </div>\r\n } @empty {\r\n <p class=\"ngx-record-editor__row-empty\">No rows yet.</p>\r\n }\r\n </div>\r\n }\r\n\r\n <!--\r\n Row drawer. Deliberately outside the tab panel: building a row's FormGroup inside the\r\n tab view races the tab's own initialisation (the reference hit this with FormArray).\r\n -->\r\n @if (inlineRowField(); as rowField) {\r\n <div class=\"ngx-record-editor__drawer\" data-testid=\"row-drawer\" [formGroup]=\"inlineRowForm!\">\r\n <div class=\"ngx-record-editor__rows-head\">\r\n <strong>{{ inlineRowIndex() === null ? 'Add' : 'Edit' }} {{ formatFieldLabel(rowField) }} row</strong>\r\n </div>\r\n\r\n <div class=\"ngx-form__panel\">\r\n @for (child of rowField.children ?? []; track child.id) {\r\n <ngx-dynamic-field\r\n [field]=\"child\"\r\n [control]=\"inlineRowForm!.get(child.id)!\"\r\n [config]=\"config\"\r\n [language]=\"language\"\r\n [readonly]=\"false\"\r\n [userRoles]=\"userRoles\"\r\n [style.gridColumn]=\"'span 12'\"\r\n />\r\n }\r\n </div>\r\n\r\n <div class=\"ngx-record-editor__section-bar\">\r\n <button type=\"button\" class=\"ngx-form__reset\" data-testid=\"cancel-row\" (click)=\"cancelRow()\">Cancel</button>\r\n <button type=\"button\" class=\"ngx-form__submit\" data-testid=\"save-row\" (click)=\"saveRow()\">Save row</button>\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- Main Dynamic Form -->\r\n <ngx-dynamic-form\r\n [config]=\"config\"\r\n [rules]=\"rules\"\r\n [initialData]=\"initialData\"\r\n [originalData]=\"originalBaseline()\"\r\n [userRoles]=\"userRoles\"\r\n [language]=\"language\"\r\n [readonly]=\"sectionReadOnly\"\r\n [readOnlyFields]=\"readOnlyFields\"\r\n [showInfoBanners]=\"false\"\r\n [loading]=\"loading\"\r\n [error]=\"error\"\r\n (formChange)=\"onFormChange($event)\"\r\n (formSubmit)=\"onFormSubmit($event)\"\r\n (formReset)=\"onFormReset()\"\r\n (activeTabChange)=\"onActiveTabChange($event)\"\r\n />\r\n</div>\r\n", styles: [".ngx-record-editor{display:flex;flex-direction:column;gap:16px;background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:20px;box-shadow:0 4px 6px -1px #0000000d}.ngx-record-editor__header{display:flex;align-items:center;justify-content:space-between;padding-bottom:16px;border-bottom:1px solid #f3f4f6}.ngx-record-editor__title-group{display:flex;align-items:center;gap:12px}.ngx-record-editor__avatar{width:48px;height:48px;border-radius:50%;object-fit:cover;background:#e0e7ff;color:#4f46e5;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:18px}.ngx-record-editor__title{font-size:20px;font-weight:700;color:#111827;margin:0}.ngx-record-editor__subtitle{font-size:13px;color:#6b7280}.ngx-record-editor__header-toggle{display:flex;align-items:center;gap:8px;font-size:13px;color:#374151}.ngx-record-editor__header-toggle-text{font-weight:600}.ngx-record-editor__banner{padding:10px 14px;border-radius:8px;font-size:13px;font-weight:500}.ngx-record-editor__banner--info{background:#eff6ff;border:1px solid #dbeafe;color:#1d4ed8;display:flex;align-items:center;justify-content:space-between;gap:12px}.ngx-record-editor__banner-dismiss{background:none;border:none;cursor:pointer;color:inherit;font-size:14px;line-height:1;padding:2px 6px;border-radius:4px}.ngx-record-editor__banner-dismiss:hover{background:#0000000f}.ngx-record-editor__banner--error{background:#fef2f2;border:1px solid #fecaca;color:#b91c1c}.ngx-record-editor__section-bar{display:flex;align-items:center;justify-content:flex-end;gap:8px}.ngx-record-editor__section-hint{margin-right:auto;font-size:12px;color:#6b7280}.ngx-record-editor__banner--warning{background:#fffbeb;border:1px solid #fef3c7;color:#b45309}.ngx-record-editor__rows{border:1px solid #f3f4f6;border-radius:8px;padding:12px 14px;display:flex;flex-direction:column;gap:6px}.ngx-record-editor__rows-head{display:flex;align-items:center;justify-content:space-between;gap:8px}.ngx-record-editor__row{display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:6px;background:#f9fafb}.ngx-record-editor__row-text{flex:1;font-size:13px;overflow-wrap:anywhere}.ngx-record-editor__row-btn{background:none;border:1px solid #e5e7eb;border-radius:6px;cursor:pointer;font-size:12px;padding:2px 8px}.ngx-record-editor__row-empty{font-size:12px;color:#6b7280;margin:0}.ngx-record-editor__drawer{border:1px solid #dbeafe;background:#f8fafc;border-radius:8px;padding:14px;display:flex;flex-direction:column;gap:12px}.ngx-record-editor__summary-panel{background:#f9fafb;border:1px solid #f3f4f6;border-radius:8px;padding:12px 16px;display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px}.ngx-record-editor__summary-item{display:flex;flex-direction:column;cursor:pointer;padding:6px 10px;border-radius:6px;transition:background .15s}.ngx-record-editor__summary-item:hover{background:#eff6ff}.ngx-record-editor__summary-label{font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:#6b7280;font-weight:600}.ngx-record-editor__summary-val{font-size:14px;font-weight:600;color:#1f2937}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: DynamicFormComponent, selector: "ngx-dynamic-form", inputs: ["config", "rules", "initialData", "originalData", "userRoles", "language", "readonly", "readOnlyFields", "showInfoBanners", "loading", "error", "changeDebounceMs", "preview"], outputs: ["formSubmit", "formChange", "formReset", "activeTabChange"] }, { kind: "component", type: DynamicFieldComponent, selector: "ngx-dynamic-field", inputs: ["field", "control", "config", "currentTabId", "language", "readonly", "userRoles"] }] });
|
|
4932
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: DynamicRecordFormComponent, isStandalone: true, selector: "ngx-dynamic-record-form", inputs: { config: "config", rules: "rules", initialData: "initialData", userRoles: "userRoles", language: "language", readonly: "readonly", loading: "loading", error: "error", isReadOnly: "isReadOnly", readOnlyFields: "readOnlyFields", viewMode: "viewMode" }, outputs: { formSubmit: "formSubmit", formChange: "formChange", formReset: "formReset", sectionSave: "sectionSave" }, viewQueries: [{ propertyName: "dynamicFormComp", first: true, predicate: DynamicFormComponent, descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"ngx-record-editor\">\n <!-- Record Header -->\n <div class=\"ngx-record-editor__header\">\n <div class=\"ngx-record-editor__title-group\">\n @if (profileImageUrl) {\n <img\n class=\"ngx-record-editor__avatar\"\n data-testid=\"header-profile-image\"\n [src]=\"profileImageUrl\"\n [alt]=\"recordTitle\"\n />\n } @else {\n <div class=\"ngx-record-editor__avatar\" data-testid=\"header-avatar-initial\">\n {{ avatarLetter }}\n </div>\n }\n <div>\n <h2 class=\"ngx-record-editor__title\" data-testid=\"record-title\">\n {{ recordTitle }}\n </h2>\n <span class=\"ngx-record-editor__subtitle\">Entity: {{ config.entity }}</span>\n </div>\n </div>\n\n <!-- Record-level status switch, outside section editing -->\n @if (headerToggleField; as toggleField) {\n <label class=\"ngx-record-editor__header-toggle\">\n <span>{{ formatFieldLabel(toggleField) }}</span>\n <input\n type=\"checkbox\"\n role=\"switch\"\n data-testid=\"header-toggle\"\n [checked]=\"headerToggleValue\"\n [disabled]=\"recordReadOnly\"\n [attr.aria-checked]=\"headerToggleValue\"\n (change)=\"toggleHeaderStatus()\"\n />\n <span class=\"ngx-record-editor__header-toggle-text\">{{ headerToggleValue ? 'Yes' : 'No' }}</span>\n </label>\n }\n </div>\n\n <!-- Dismissible info banners (rules `info` action) -->\n @for (banner of infoBanners; track banner.fieldId) {\n <div\n class=\"ngx-record-editor__banner ngx-record-editor__banner--info\"\n role=\"status\"\n [attr.data-testid]=\"'info-banner-' + banner.fieldId\"\n >\n <span>\u2139\uFE0F {{ banner.message }}</span>\n <button\n type=\"button\"\n class=\"ngx-record-editor__banner-dismiss\"\n [attr.data-testid]=\"'dismiss-' + banner.fieldId\"\n [attr.aria-label]=\"'Dismiss: ' + banner.message\"\n (click)=\"dismissInfoBanner(banner.fieldId)\"\n >\u2715</button>\n </div>\n }\n\n <!-- Modification baseline banner -->\n @if (isModified()) {\n <div class=\"ngx-record-editor__banner ngx-record-editor__banner--warning\">\n \uD83D\uDD12 Record modified from session baseline \u2014 unsaved changes pending.\n </div>\n }\n\n <!-- Summary Panel (showOnMinimize fields with interactive jump) -->\n @if (summaryFields().length > 0) {\n <div class=\"ngx-record-editor__summary-panel\" data-testid=\"summary-panel\">\n @for (sf of summaryFields(); track sf.id) {\n <div\n class=\"ngx-record-editor__summary-item\"\n [attr.data-testid]=\"'summary-' + sf.id\"\n (click)=\"jumpToField(sf.id)\"\n [title]=\"'Click to jump to ' + formatFieldLabel(sf)\"\n >\n <span class=\"ngx-record-editor__summary-label\">{{ formatFieldLabel(sf) }}</span>\n <span class=\"ngx-record-editor__summary-val\">{{ formatFieldValue(sf) }}</span>\n </div>\n }\n </div>\n }\n\n <!-- Section edit bar: one tab is edited at a time, and validated on its own -->\n @if (viewMode && !recordReadOnly) {\n <div class=\"ngx-record-editor__section-bar\">\n @if (isEditingActiveTab) {\n <span class=\"ngx-record-editor__section-hint\">Editing this section</span>\n <button type=\"button\" class=\"ngx-form__reset\" data-testid=\"cancel-section\" (click)=\"cancelSection()\">\n Cancel\n </button>\n <button type=\"button\" class=\"ngx-form__submit\" data-testid=\"save-section\" (click)=\"saveSection()\">\n Save section\n </button>\n } @else {\n <button type=\"button\" class=\"ngx-form__reset\" data-testid=\"edit-section\" (click)=\"editSection()\">\n Edit section\n </button>\n }\n </div>\n }\n\n @if (Object.keys(sectionErrors()).length > 0) {\n <div class=\"ngx-record-editor__banner ngx-record-editor__banner--error\" role=\"alert\" data-testid=\"section-errors\">\n @for (key of Object.keys(sectionErrors()); track key) {\n <div>\u26A0\uFE0F {{ sectionErrors()[key] }}</div>\n }\n </div>\n }\n\n <!-- Array rows: listed here, edited in the drawer below -->\n @for (arrayField of arrayFieldsForActiveTab; track arrayField.id) {\n <div class=\"ngx-record-editor__rows\" [attr.data-testid]=\"'rows-' + arrayField.id\">\n <div class=\"ngx-record-editor__rows-head\">\n <span class=\"ngx-record-editor__summary-label\">{{ formatFieldLabel(arrayField) }}</span>\n @if (!sectionReadOnly) {\n <button\n type=\"button\"\n class=\"ngx-form__reset\"\n [attr.data-testid]=\"'add-row-' + arrayField.id\"\n (click)=\"openAddRow(arrayField)\"\n >+ Row</button>\n }\n </div>\n\n @for (row of rowsOf(arrayField); track $index) {\n <div class=\"ngx-record-editor__row\" [attr.data-testid]=\"'row-' + arrayField.id + '-' + $index\">\n <span class=\"ngx-record-editor__row-text\">{{ rowSummary(arrayField, row) }}</span>\n @if (!sectionReadOnly) {\n <button type=\"button\" class=\"ngx-record-editor__row-btn\"\n [attr.data-testid]=\"'edit-row-' + arrayField.id + '-' + $index\"\n (click)=\"openEditRow(arrayField, $index)\">Edit</button>\n <button type=\"button\" class=\"ngx-record-editor__row-btn\"\n [attr.data-testid]=\"'delete-row-' + arrayField.id + '-' + $index\"\n (click)=\"deleteRow(arrayField, $index)\">Delete</button>\n }\n </div>\n } @empty {\n <p class=\"ngx-record-editor__row-empty\">No rows yet.</p>\n }\n </div>\n }\n\n <!--\n Row drawer. Deliberately outside the tab panel: building a row's FormGroup inside the\n tab view races the tab's own initialisation (the reference hit this with FormArray).\n -->\n @if (inlineRowField(); as rowField) {\n <div class=\"ngx-record-editor__drawer\" data-testid=\"row-drawer\" [formGroup]=\"inlineRowForm!\">\n <div class=\"ngx-record-editor__rows-head\">\n <strong>{{ inlineRowIndex() === null ? 'Add' : 'Edit' }} {{ formatFieldLabel(rowField) }} row</strong>\n </div>\n\n <div class=\"ngx-form__panel\">\n @for (child of rowField.children ?? []; track child.id) {\n <ngx-dynamic-field\n [field]=\"child\"\n [control]=\"inlineRowForm!.get(child.id)!\"\n [config]=\"config\"\n [language]=\"language\"\n [readonly]=\"false\"\n [userRoles]=\"userRoles\"\n [style.gridColumn]=\"'span 12'\"\n />\n }\n </div>\n\n <div class=\"ngx-record-editor__section-bar\">\n <button type=\"button\" class=\"ngx-form__reset\" data-testid=\"cancel-row\" (click)=\"cancelRow()\">Cancel</button>\n <button type=\"button\" class=\"ngx-form__submit\" data-testid=\"save-row\" (click)=\"saveRow()\">Save row</button>\n </div>\n </div>\n }\n\n <!-- Main Dynamic Form -->\n <ngx-dynamic-form\n [config]=\"config\"\n [rules]=\"rules\"\n [initialData]=\"initialData\"\n [originalData]=\"originalBaseline()\"\n [userRoles]=\"userRoles\"\n [language]=\"language\"\n [readonly]=\"sectionReadOnly\"\n [readOnlyFields]=\"readOnlyFields\"\n [showInfoBanners]=\"false\"\n [loading]=\"loading\"\n [error]=\"error\"\n (formChange)=\"onFormChange($event)\"\n (formSubmit)=\"onFormSubmit($event)\"\n (formReset)=\"onFormReset()\"\n (activeTabChange)=\"onActiveTabChange($event)\"\n />\n</div>\n", styles: [".ngx-record-editor{display:flex;flex-direction:column;gap:16px;background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:20px;box-shadow:0 4px 6px -1px #0000000d}.ngx-record-editor__header{display:flex;align-items:center;justify-content:space-between;padding-bottom:16px;border-bottom:1px solid #f3f4f6}.ngx-record-editor__title-group{display:flex;align-items:center;gap:12px}.ngx-record-editor__avatar{width:48px;height:48px;border-radius:50%;object-fit:cover;background:#e0e7ff;color:#4f46e5;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:18px}.ngx-record-editor__title{font-size:20px;font-weight:700;color:#111827;margin:0}.ngx-record-editor__subtitle{font-size:13px;color:#6b7280}.ngx-record-editor__header-toggle{display:flex;align-items:center;gap:8px;font-size:13px;color:#374151}.ngx-record-editor__header-toggle-text{font-weight:600}.ngx-record-editor__banner{padding:10px 14px;border-radius:8px;font-size:13px;font-weight:500}.ngx-record-editor__banner--info{background:#eff6ff;border:1px solid #dbeafe;color:#1d4ed8;display:flex;align-items:center;justify-content:space-between;gap:12px}.ngx-record-editor__banner-dismiss{background:none;border:none;cursor:pointer;color:inherit;font-size:14px;line-height:1;padding:2px 6px;border-radius:4px}.ngx-record-editor__banner-dismiss:hover{background:#0000000f}.ngx-record-editor__banner--error{background:#fef2f2;border:1px solid #fecaca;color:#b91c1c}.ngx-record-editor__section-bar{display:flex;align-items:center;justify-content:flex-end;gap:8px}.ngx-record-editor__section-hint{margin-right:auto;font-size:12px;color:#6b7280}.ngx-record-editor__banner--warning{background:#fffbeb;border:1px solid #fef3c7;color:#b45309}.ngx-record-editor__rows{border:1px solid #f3f4f6;border-radius:8px;padding:12px 14px;display:flex;flex-direction:column;gap:6px}.ngx-record-editor__rows-head{display:flex;align-items:center;justify-content:space-between;gap:8px}.ngx-record-editor__row{display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:6px;background:#f9fafb}.ngx-record-editor__row-text{flex:1;font-size:13px;overflow-wrap:anywhere}.ngx-record-editor__row-btn{background:none;border:1px solid #e5e7eb;border-radius:6px;cursor:pointer;font-size:12px;padding:2px 8px}.ngx-record-editor__row-empty{font-size:12px;color:#6b7280;margin:0}.ngx-record-editor__drawer{border:1px solid #dbeafe;background:#f8fafc;border-radius:8px;padding:14px;display:flex;flex-direction:column;gap:12px}.ngx-record-editor__summary-panel{background:#f9fafb;border:1px solid #f3f4f6;border-radius:8px;padding:12px 16px;display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px}.ngx-record-editor__summary-item{display:flex;flex-direction:column;cursor:pointer;padding:6px 10px;border-radius:6px;transition:background .15s}.ngx-record-editor__summary-item:hover{background:#eff6ff}.ngx-record-editor__summary-label{font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:#6b7280;font-weight:600}.ngx-record-editor__summary-val{font-size:14px;font-weight:600;color:#1f2937}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: DynamicFormComponent, selector: "ngx-dynamic-form", inputs: ["config", "rules", "initialData", "originalData", "userRoles", "language", "readonly", "readOnlyFields", "showInfoBanners", "loading", "error", "changeDebounceMs", "preview"], outputs: ["formSubmit", "formChange", "formReset", "activeTabChange", "saveRejected"] }, { kind: "component", type: DynamicFieldComponent, selector: "ngx-dynamic-field", inputs: ["field", "control", "config", "currentTabId", "language", "readonly", "userRoles"] }] });
|
|
4415
4933
|
}
|
|
4416
4934
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: DynamicRecordFormComponent, decorators: [{
|
|
4417
4935
|
type: Component,
|
|
4418
|
-
args: [{ selector: 'ngx-dynamic-record-form', standalone: true, imports: [CommonModule, ReactiveFormsModule, DynamicFormComponent, DynamicFieldComponent], template: "<div class=\"ngx-record-editor\">\r\n <!-- Record Header -->\r\n <div class=\"ngx-record-editor__header\">\r\n <div class=\"ngx-record-editor__title-group\">\r\n @if (profileImageUrl) {\r\n <img\r\n class=\"ngx-record-editor__avatar\"\r\n data-testid=\"header-profile-image\"\r\n [src]=\"profileImageUrl\"\r\n [alt]=\"recordTitle\"\r\n />\r\n } @else {\r\n <div class=\"ngx-record-editor__avatar\" data-testid=\"header-avatar-initial\">\r\n {{ avatarLetter }}\r\n </div>\r\n }\r\n <div>\r\n <h2 class=\"ngx-record-editor__title\" data-testid=\"record-title\">\r\n {{ recordTitle }}\r\n </h2>\r\n <span class=\"ngx-record-editor__subtitle\">Entity: {{ config.entity }}</span>\r\n </div>\r\n </div>\r\n\r\n <!-- Record-level status switch, outside section editing -->\r\n @if (headerToggleField; as toggleField) {\r\n <label class=\"ngx-record-editor__header-toggle\">\r\n <span>{{ formatFieldLabel(toggleField) }}</span>\r\n <input\r\n type=\"checkbox\"\r\n role=\"switch\"\r\n data-testid=\"header-toggle\"\r\n [checked]=\"headerToggleValue\"\r\n [disabled]=\"recordReadOnly\"\r\n [attr.aria-checked]=\"headerToggleValue\"\r\n (change)=\"toggleHeaderStatus()\"\r\n />\r\n <span class=\"ngx-record-editor__header-toggle-text\">{{ headerToggleValue ? 'Yes' : 'No' }}</span>\r\n </label>\r\n }\r\n </div>\r\n\r\n <!-- Dismissible info banners (rules `info` action) -->\r\n @for (banner of infoBanners; track banner.fieldId) {\r\n <div\r\n class=\"ngx-record-editor__banner ngx-record-editor__banner--info\"\r\n role=\"status\"\r\n [attr.data-testid]=\"'info-banner-' + banner.fieldId\"\r\n >\r\n <span>\u2139\uFE0F {{ banner.message }}</span>\r\n <button\r\n type=\"button\"\r\n class=\"ngx-record-editor__banner-dismiss\"\r\n [attr.data-testid]=\"'dismiss-' + banner.fieldId\"\r\n [attr.aria-label]=\"'Dismiss: ' + banner.message\"\r\n (click)=\"dismissInfoBanner(banner.fieldId)\"\r\n >\u2715</button>\r\n </div>\r\n }\r\n\r\n <!-- Modification baseline banner -->\r\n @if (isModified()) {\r\n <div class=\"ngx-record-editor__banner ngx-record-editor__banner--warning\">\r\n \uD83D\uDD12 Record modified from session baseline \u2014 unsaved changes pending.\r\n </div>\r\n }\r\n\r\n <!-- Summary Panel (showOnMinimize fields with interactive jump) -->\r\n @if (summaryFields().length > 0) {\r\n <div class=\"ngx-record-editor__summary-panel\" data-testid=\"summary-panel\">\r\n @for (sf of summaryFields(); track sf.id) {\r\n <div\r\n class=\"ngx-record-editor__summary-item\"\r\n [attr.data-testid]=\"'summary-' + sf.id\"\r\n (click)=\"jumpToField(sf.id)\"\r\n [title]=\"'Click to jump to ' + formatFieldLabel(sf)\"\r\n >\r\n <span class=\"ngx-record-editor__summary-label\">{{ formatFieldLabel(sf) }}</span>\r\n <span class=\"ngx-record-editor__summary-val\">{{ formatFieldValue(sf) }}</span>\r\n </div>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Section edit bar: one tab is edited at a time, and validated on its own -->\r\n @if (viewMode && !recordReadOnly) {\r\n <div class=\"ngx-record-editor__section-bar\">\r\n @if (isEditingActiveTab) {\r\n <span class=\"ngx-record-editor__section-hint\">Editing this section</span>\r\n <button type=\"button\" class=\"ngx-form__reset\" data-testid=\"cancel-section\" (click)=\"cancelSection()\">\r\n Cancel\r\n </button>\r\n <button type=\"button\" class=\"ngx-form__submit\" data-testid=\"save-section\" (click)=\"saveSection()\">\r\n Save section\r\n </button>\r\n } @else {\r\n <button type=\"button\" class=\"ngx-form__reset\" data-testid=\"edit-section\" (click)=\"editSection()\">\r\n Edit section\r\n </button>\r\n }\r\n </div>\r\n }\r\n\r\n @if (Object.keys(sectionErrors()).length > 0) {\r\n <div class=\"ngx-record-editor__banner ngx-record-editor__banner--error\" role=\"alert\" data-testid=\"section-errors\">\r\n @for (key of Object.keys(sectionErrors()); track key) {\r\n <div>\u26A0\uFE0F {{ sectionErrors()[key] }}</div>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Array rows: listed here, edited in the drawer below -->\r\n @for (arrayField of arrayFieldsForActiveTab; track arrayField.id) {\r\n <div class=\"ngx-record-editor__rows\" [attr.data-testid]=\"'rows-' + arrayField.id\">\r\n <div class=\"ngx-record-editor__rows-head\">\r\n <span class=\"ngx-record-editor__summary-label\">{{ formatFieldLabel(arrayField) }}</span>\r\n @if (!sectionReadOnly) {\r\n <button\r\n type=\"button\"\r\n class=\"ngx-form__reset\"\r\n [attr.data-testid]=\"'add-row-' + arrayField.id\"\r\n (click)=\"openAddRow(arrayField)\"\r\n >+ Row</button>\r\n }\r\n </div>\r\n\r\n @for (row of rowsOf(arrayField); track $index) {\r\n <div class=\"ngx-record-editor__row\" [attr.data-testid]=\"'row-' + arrayField.id + '-' + $index\">\r\n <span class=\"ngx-record-editor__row-text\">{{ rowSummary(arrayField, row) }}</span>\r\n @if (!sectionReadOnly) {\r\n <button type=\"button\" class=\"ngx-record-editor__row-btn\"\r\n [attr.data-testid]=\"'edit-row-' + arrayField.id + '-' + $index\"\r\n (click)=\"openEditRow(arrayField, $index)\">Edit</button>\r\n <button type=\"button\" class=\"ngx-record-editor__row-btn\"\r\n [attr.data-testid]=\"'delete-row-' + arrayField.id + '-' + $index\"\r\n (click)=\"deleteRow(arrayField, $index)\">Delete</button>\r\n }\r\n </div>\r\n } @empty {\r\n <p class=\"ngx-record-editor__row-empty\">No rows yet.</p>\r\n }\r\n </div>\r\n }\r\n\r\n <!--\r\n Row drawer. Deliberately outside the tab panel: building a row's FormGroup inside the\r\n tab view races the tab's own initialisation (the reference hit this with FormArray).\r\n -->\r\n @if (inlineRowField(); as rowField) {\r\n <div class=\"ngx-record-editor__drawer\" data-testid=\"row-drawer\" [formGroup]=\"inlineRowForm!\">\r\n <div class=\"ngx-record-editor__rows-head\">\r\n <strong>{{ inlineRowIndex() === null ? 'Add' : 'Edit' }} {{ formatFieldLabel(rowField) }} row</strong>\r\n </div>\r\n\r\n <div class=\"ngx-form__panel\">\r\n @for (child of rowField.children ?? []; track child.id) {\r\n <ngx-dynamic-field\r\n [field]=\"child\"\r\n [control]=\"inlineRowForm!.get(child.id)!\"\r\n [config]=\"config\"\r\n [language]=\"language\"\r\n [readonly]=\"false\"\r\n [userRoles]=\"userRoles\"\r\n [style.gridColumn]=\"'span 12'\"\r\n />\r\n }\r\n </div>\r\n\r\n <div class=\"ngx-record-editor__section-bar\">\r\n <button type=\"button\" class=\"ngx-form__reset\" data-testid=\"cancel-row\" (click)=\"cancelRow()\">Cancel</button>\r\n <button type=\"button\" class=\"ngx-form__submit\" data-testid=\"save-row\" (click)=\"saveRow()\">Save row</button>\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- Main Dynamic Form -->\r\n <ngx-dynamic-form\r\n [config]=\"config\"\r\n [rules]=\"rules\"\r\n [initialData]=\"initialData\"\r\n [originalData]=\"originalBaseline()\"\r\n [userRoles]=\"userRoles\"\r\n [language]=\"language\"\r\n [readonly]=\"sectionReadOnly\"\r\n [readOnlyFields]=\"readOnlyFields\"\r\n [showInfoBanners]=\"false\"\r\n [loading]=\"loading\"\r\n [error]=\"error\"\r\n (formChange)=\"onFormChange($event)\"\r\n (formSubmit)=\"onFormSubmit($event)\"\r\n (formReset)=\"onFormReset()\"\r\n (activeTabChange)=\"onActiveTabChange($event)\"\r\n />\r\n</div>\r\n", styles: [".ngx-record-editor{display:flex;flex-direction:column;gap:16px;background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:20px;box-shadow:0 4px 6px -1px #0000000d}.ngx-record-editor__header{display:flex;align-items:center;justify-content:space-between;padding-bottom:16px;border-bottom:1px solid #f3f4f6}.ngx-record-editor__title-group{display:flex;align-items:center;gap:12px}.ngx-record-editor__avatar{width:48px;height:48px;border-radius:50%;object-fit:cover;background:#e0e7ff;color:#4f46e5;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:18px}.ngx-record-editor__title{font-size:20px;font-weight:700;color:#111827;margin:0}.ngx-record-editor__subtitle{font-size:13px;color:#6b7280}.ngx-record-editor__header-toggle{display:flex;align-items:center;gap:8px;font-size:13px;color:#374151}.ngx-record-editor__header-toggle-text{font-weight:600}.ngx-record-editor__banner{padding:10px 14px;border-radius:8px;font-size:13px;font-weight:500}.ngx-record-editor__banner--info{background:#eff6ff;border:1px solid #dbeafe;color:#1d4ed8;display:flex;align-items:center;justify-content:space-between;gap:12px}.ngx-record-editor__banner-dismiss{background:none;border:none;cursor:pointer;color:inherit;font-size:14px;line-height:1;padding:2px 6px;border-radius:4px}.ngx-record-editor__banner-dismiss:hover{background:#0000000f}.ngx-record-editor__banner--error{background:#fef2f2;border:1px solid #fecaca;color:#b91c1c}.ngx-record-editor__section-bar{display:flex;align-items:center;justify-content:flex-end;gap:8px}.ngx-record-editor__section-hint{margin-right:auto;font-size:12px;color:#6b7280}.ngx-record-editor__banner--warning{background:#fffbeb;border:1px solid #fef3c7;color:#b45309}.ngx-record-editor__rows{border:1px solid #f3f4f6;border-radius:8px;padding:12px 14px;display:flex;flex-direction:column;gap:6px}.ngx-record-editor__rows-head{display:flex;align-items:center;justify-content:space-between;gap:8px}.ngx-record-editor__row{display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:6px;background:#f9fafb}.ngx-record-editor__row-text{flex:1;font-size:13px;overflow-wrap:anywhere}.ngx-record-editor__row-btn{background:none;border:1px solid #e5e7eb;border-radius:6px;cursor:pointer;font-size:12px;padding:2px 8px}.ngx-record-editor__row-empty{font-size:12px;color:#6b7280;margin:0}.ngx-record-editor__drawer{border:1px solid #dbeafe;background:#f8fafc;border-radius:8px;padding:14px;display:flex;flex-direction:column;gap:12px}.ngx-record-editor__summary-panel{background:#f9fafb;border:1px solid #f3f4f6;border-radius:8px;padding:12px 16px;display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px}.ngx-record-editor__summary-item{display:flex;flex-direction:column;cursor:pointer;padding:6px 10px;border-radius:6px;transition:background .15s}.ngx-record-editor__summary-item:hover{background:#eff6ff}.ngx-record-editor__summary-label{font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:#6b7280;font-weight:600}.ngx-record-editor__summary-val{font-size:14px;font-weight:600;color:#1f2937}\n"] }]
|
|
4936
|
+
args: [{ selector: 'ngx-dynamic-record-form', standalone: true, imports: [CommonModule, ReactiveFormsModule, DynamicFormComponent, DynamicFieldComponent], template: "<div class=\"ngx-record-editor\">\n <!-- Record Header -->\n <div class=\"ngx-record-editor__header\">\n <div class=\"ngx-record-editor__title-group\">\n @if (profileImageUrl) {\n <img\n class=\"ngx-record-editor__avatar\"\n data-testid=\"header-profile-image\"\n [src]=\"profileImageUrl\"\n [alt]=\"recordTitle\"\n />\n } @else {\n <div class=\"ngx-record-editor__avatar\" data-testid=\"header-avatar-initial\">\n {{ avatarLetter }}\n </div>\n }\n <div>\n <h2 class=\"ngx-record-editor__title\" data-testid=\"record-title\">\n {{ recordTitle }}\n </h2>\n <span class=\"ngx-record-editor__subtitle\">Entity: {{ config.entity }}</span>\n </div>\n </div>\n\n <!-- Record-level status switch, outside section editing -->\n @if (headerToggleField; as toggleField) {\n <label class=\"ngx-record-editor__header-toggle\">\n <span>{{ formatFieldLabel(toggleField) }}</span>\n <input\n type=\"checkbox\"\n role=\"switch\"\n data-testid=\"header-toggle\"\n [checked]=\"headerToggleValue\"\n [disabled]=\"recordReadOnly\"\n [attr.aria-checked]=\"headerToggleValue\"\n (change)=\"toggleHeaderStatus()\"\n />\n <span class=\"ngx-record-editor__header-toggle-text\">{{ headerToggleValue ? 'Yes' : 'No' }}</span>\n </label>\n }\n </div>\n\n <!-- Dismissible info banners (rules `info` action) -->\n @for (banner of infoBanners; track banner.fieldId) {\n <div\n class=\"ngx-record-editor__banner ngx-record-editor__banner--info\"\n role=\"status\"\n [attr.data-testid]=\"'info-banner-' + banner.fieldId\"\n >\n <span>\u2139\uFE0F {{ banner.message }}</span>\n <button\n type=\"button\"\n class=\"ngx-record-editor__banner-dismiss\"\n [attr.data-testid]=\"'dismiss-' + banner.fieldId\"\n [attr.aria-label]=\"'Dismiss: ' + banner.message\"\n (click)=\"dismissInfoBanner(banner.fieldId)\"\n >\u2715</button>\n </div>\n }\n\n <!-- Modification baseline banner -->\n @if (isModified()) {\n <div class=\"ngx-record-editor__banner ngx-record-editor__banner--warning\">\n \uD83D\uDD12 Record modified from session baseline \u2014 unsaved changes pending.\n </div>\n }\n\n <!-- Summary Panel (showOnMinimize fields with interactive jump) -->\n @if (summaryFields().length > 0) {\n <div class=\"ngx-record-editor__summary-panel\" data-testid=\"summary-panel\">\n @for (sf of summaryFields(); track sf.id) {\n <div\n class=\"ngx-record-editor__summary-item\"\n [attr.data-testid]=\"'summary-' + sf.id\"\n (click)=\"jumpToField(sf.id)\"\n [title]=\"'Click to jump to ' + formatFieldLabel(sf)\"\n >\n <span class=\"ngx-record-editor__summary-label\">{{ formatFieldLabel(sf) }}</span>\n <span class=\"ngx-record-editor__summary-val\">{{ formatFieldValue(sf) }}</span>\n </div>\n }\n </div>\n }\n\n <!-- Section edit bar: one tab is edited at a time, and validated on its own -->\n @if (viewMode && !recordReadOnly) {\n <div class=\"ngx-record-editor__section-bar\">\n @if (isEditingActiveTab) {\n <span class=\"ngx-record-editor__section-hint\">Editing this section</span>\n <button type=\"button\" class=\"ngx-form__reset\" data-testid=\"cancel-section\" (click)=\"cancelSection()\">\n Cancel\n </button>\n <button type=\"button\" class=\"ngx-form__submit\" data-testid=\"save-section\" (click)=\"saveSection()\">\n Save section\n </button>\n } @else {\n <button type=\"button\" class=\"ngx-form__reset\" data-testid=\"edit-section\" (click)=\"editSection()\">\n Edit section\n </button>\n }\n </div>\n }\n\n @if (Object.keys(sectionErrors()).length > 0) {\n <div class=\"ngx-record-editor__banner ngx-record-editor__banner--error\" role=\"alert\" data-testid=\"section-errors\">\n @for (key of Object.keys(sectionErrors()); track key) {\n <div>\u26A0\uFE0F {{ sectionErrors()[key] }}</div>\n }\n </div>\n }\n\n <!-- Array rows: listed here, edited in the drawer below -->\n @for (arrayField of arrayFieldsForActiveTab; track arrayField.id) {\n <div class=\"ngx-record-editor__rows\" [attr.data-testid]=\"'rows-' + arrayField.id\">\n <div class=\"ngx-record-editor__rows-head\">\n <span class=\"ngx-record-editor__summary-label\">{{ formatFieldLabel(arrayField) }}</span>\n @if (!sectionReadOnly) {\n <button\n type=\"button\"\n class=\"ngx-form__reset\"\n [attr.data-testid]=\"'add-row-' + arrayField.id\"\n (click)=\"openAddRow(arrayField)\"\n >+ Row</button>\n }\n </div>\n\n @for (row of rowsOf(arrayField); track $index) {\n <div class=\"ngx-record-editor__row\" [attr.data-testid]=\"'row-' + arrayField.id + '-' + $index\">\n <span class=\"ngx-record-editor__row-text\">{{ rowSummary(arrayField, row) }}</span>\n @if (!sectionReadOnly) {\n <button type=\"button\" class=\"ngx-record-editor__row-btn\"\n [attr.data-testid]=\"'edit-row-' + arrayField.id + '-' + $index\"\n (click)=\"openEditRow(arrayField, $index)\">Edit</button>\n <button type=\"button\" class=\"ngx-record-editor__row-btn\"\n [attr.data-testid]=\"'delete-row-' + arrayField.id + '-' + $index\"\n (click)=\"deleteRow(arrayField, $index)\">Delete</button>\n }\n </div>\n } @empty {\n <p class=\"ngx-record-editor__row-empty\">No rows yet.</p>\n }\n </div>\n }\n\n <!--\n Row drawer. Deliberately outside the tab panel: building a row's FormGroup inside the\n tab view races the tab's own initialisation (the reference hit this with FormArray).\n -->\n @if (inlineRowField(); as rowField) {\n <div class=\"ngx-record-editor__drawer\" data-testid=\"row-drawer\" [formGroup]=\"inlineRowForm!\">\n <div class=\"ngx-record-editor__rows-head\">\n <strong>{{ inlineRowIndex() === null ? 'Add' : 'Edit' }} {{ formatFieldLabel(rowField) }} row</strong>\n </div>\n\n <div class=\"ngx-form__panel\">\n @for (child of rowField.children ?? []; track child.id) {\n <ngx-dynamic-field\n [field]=\"child\"\n [control]=\"inlineRowForm!.get(child.id)!\"\n [config]=\"config\"\n [language]=\"language\"\n [readonly]=\"false\"\n [userRoles]=\"userRoles\"\n [style.gridColumn]=\"'span 12'\"\n />\n }\n </div>\n\n <div class=\"ngx-record-editor__section-bar\">\n <button type=\"button\" class=\"ngx-form__reset\" data-testid=\"cancel-row\" (click)=\"cancelRow()\">Cancel</button>\n <button type=\"button\" class=\"ngx-form__submit\" data-testid=\"save-row\" (click)=\"saveRow()\">Save row</button>\n </div>\n </div>\n }\n\n <!-- Main Dynamic Form -->\n <ngx-dynamic-form\n [config]=\"config\"\n [rules]=\"rules\"\n [initialData]=\"initialData\"\n [originalData]=\"originalBaseline()\"\n [userRoles]=\"userRoles\"\n [language]=\"language\"\n [readonly]=\"sectionReadOnly\"\n [readOnlyFields]=\"readOnlyFields\"\n [showInfoBanners]=\"false\"\n [loading]=\"loading\"\n [error]=\"error\"\n (formChange)=\"onFormChange($event)\"\n (formSubmit)=\"onFormSubmit($event)\"\n (formReset)=\"onFormReset()\"\n (activeTabChange)=\"onActiveTabChange($event)\"\n />\n</div>\n", styles: [".ngx-record-editor{display:flex;flex-direction:column;gap:16px;background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:20px;box-shadow:0 4px 6px -1px #0000000d}.ngx-record-editor__header{display:flex;align-items:center;justify-content:space-between;padding-bottom:16px;border-bottom:1px solid #f3f4f6}.ngx-record-editor__title-group{display:flex;align-items:center;gap:12px}.ngx-record-editor__avatar{width:48px;height:48px;border-radius:50%;object-fit:cover;background:#e0e7ff;color:#4f46e5;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:18px}.ngx-record-editor__title{font-size:20px;font-weight:700;color:#111827;margin:0}.ngx-record-editor__subtitle{font-size:13px;color:#6b7280}.ngx-record-editor__header-toggle{display:flex;align-items:center;gap:8px;font-size:13px;color:#374151}.ngx-record-editor__header-toggle-text{font-weight:600}.ngx-record-editor__banner{padding:10px 14px;border-radius:8px;font-size:13px;font-weight:500}.ngx-record-editor__banner--info{background:#eff6ff;border:1px solid #dbeafe;color:#1d4ed8;display:flex;align-items:center;justify-content:space-between;gap:12px}.ngx-record-editor__banner-dismiss{background:none;border:none;cursor:pointer;color:inherit;font-size:14px;line-height:1;padding:2px 6px;border-radius:4px}.ngx-record-editor__banner-dismiss:hover{background:#0000000f}.ngx-record-editor__banner--error{background:#fef2f2;border:1px solid #fecaca;color:#b91c1c}.ngx-record-editor__section-bar{display:flex;align-items:center;justify-content:flex-end;gap:8px}.ngx-record-editor__section-hint{margin-right:auto;font-size:12px;color:#6b7280}.ngx-record-editor__banner--warning{background:#fffbeb;border:1px solid #fef3c7;color:#b45309}.ngx-record-editor__rows{border:1px solid #f3f4f6;border-radius:8px;padding:12px 14px;display:flex;flex-direction:column;gap:6px}.ngx-record-editor__rows-head{display:flex;align-items:center;justify-content:space-between;gap:8px}.ngx-record-editor__row{display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:6px;background:#f9fafb}.ngx-record-editor__row-text{flex:1;font-size:13px;overflow-wrap:anywhere}.ngx-record-editor__row-btn{background:none;border:1px solid #e5e7eb;border-radius:6px;cursor:pointer;font-size:12px;padding:2px 8px}.ngx-record-editor__row-empty{font-size:12px;color:#6b7280;margin:0}.ngx-record-editor__drawer{border:1px solid #dbeafe;background:#f8fafc;border-radius:8px;padding:14px;display:flex;flex-direction:column;gap:12px}.ngx-record-editor__summary-panel{background:#f9fafb;border:1px solid #f3f4f6;border-radius:8px;padding:12px 16px;display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px}.ngx-record-editor__summary-item{display:flex;flex-direction:column;cursor:pointer;padding:6px 10px;border-radius:6px;transition:background .15s}.ngx-record-editor__summary-item:hover{background:#eff6ff}.ngx-record-editor__summary-label{font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:#6b7280;font-weight:600}.ngx-record-editor__summary-val{font-size:14px;font-weight:600;color:#1f2937}\n"] }]
|
|
4419
4937
|
}], propDecorators: { config: [{
|
|
4420
4938
|
type: Input
|
|
4421
4939
|
}], rules: [{
|
|
@@ -4457,5 +4975,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
4457
4975
|
* Generated bundle index. Do not edit.
|
|
4458
4976
|
*/
|
|
4459
4977
|
|
|
4460
|
-
export { COMMON_MODULES_REGISTRY, CONFIG_SOURCE, CascadeDataService, CheckboxFieldComponent, ConfigSourceService, DateFieldComponent, DropdownFieldComponent, DynamicFieldComponent, DynamicFormComponent, DynamicRecordFormComponent, ENTITY_REF_CACHE_STORE, ENTITY_REF_REGISTRY, EntityRefFieldComponent, EntityRefRegistryService, EntityRefSelectionService, EntityReferenceService, FIELD_TYPE_REGISTRY, FIELD_TYPE_SETS, FieldRegistryService, HOOK_REGISTRY, HookRegistryService, InMemoryEntityRefCacheStore, LOOKUP_REGISTRY, LookupRegistryService, MASKED_ROLES, MultiSelectFieldComponent, NumberFieldComponent, RbacService, RulesEvaluationService, SYSTEM_DEFAULT_CAN_EDIT, TextFieldComponent, TextareaFieldComponent, UPLOAD_HANDLER, VALIDATOR_REGISTRY, ValidatorRegistryService, builtInFieldTypes, provideBuiltInFieldTypes, provideFieldTypes, provideNgxDynamicEntity, refreshChoiceOptions };
|
|
4978
|
+
export { ASYNC_VALIDATOR_REGISTRY, ArrayFieldComponent, BooleanFieldComponent, COMMON_MODULES_REGISTRY, CONFIG_SOURCE, CascadeDataService, CheckboxFieldComponent, ConfigSourceService, CurrencyFieldComponent, DEFAULT_VALIDATION_MESSAGES, DateFieldComponent, DropdownFieldComponent, DynamicFieldComponent, DynamicFormComponent, DynamicRecordFormComponent, ENTITY_REF_CACHE_STORE, ENTITY_REF_REGISTRY, EmailFieldComponent, EntityRefFieldComponent, EntityRefRegistryService, EntityRefSelectionService, EntityReferenceService, FIELD_TYPE_REGISTRY, FIELD_TYPE_SETS, FieldRegistryService, FileFieldComponent, GroupFieldComponent, HOOK_REGISTRY, HookRegistryService, ImageFieldComponent, InMemoryEntityRefCacheStore, LOOKUP_REGISTRY, LookupRegistryService, MASKED_ROLES, MonthYearFieldComponent, MultiSelectFieldComponent, NumberFieldComponent, PasswordFieldComponent, RECORD_MIGRATIONS, RadioFieldComponent, RbacService, RulesEvaluationService, SYSTEM_DEFAULT_CAN_EDIT, TextFieldComponent, TextareaFieldComponent, UPLOAD_HANDLER, VALIDATION_MESSAGES, VALIDATOR_REGISTRY, ValidationMessagesService, ValidatorRegistryService, builtInFieldTypes, provideBuiltInFieldTypes, provideFieldTypes, provideNgxDynamicEntity, refreshChoiceOptions };
|
|
4461
4979
|
//# sourceMappingURL=ngx-dynamic-entity.mjs.map
|