ngx-dynamic-entity 2.1.0 → 2.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 +1 -1
- package/fesm2022/ngx-dynamic-entity.mjs +2003 -572
- package/fesm2022/ngx-dynamic-entity.mjs.map +1 -1
- package/package.json +2 -2
- package/styles.css +210 -0
- package/types/ngx-dynamic-entity.d.ts +312 -184
|
@@ -2,7 +2,7 @@ import * as i0 from '@angular/core';
|
|
|
2
2
|
import { InjectionToken, makeEnvironmentProviders, inject, Injectable, signal, Input, ChangeDetectionStrategy, Component, ChangeDetectorRef, DestroyRef, Injector, isDevMode, ViewContainerRef, ViewChild, forwardRef, Optional, Inject, EventEmitter, ElementRef, computed, afterNextRender, HostListener, ViewChildren, Output } from '@angular/core';
|
|
3
3
|
import * as i1 from '@angular/forms';
|
|
4
4
|
import { ReactiveFormsModule, Validators, FormBuilder, FormGroup, FormArray, FormsModule } from '@angular/forms';
|
|
5
|
-
import { resolveLabel, formatDisplayValue, normalizeLookupValues, lookupValuesToOptions, valuesMatch, resolveOptionLabel, resolveOptionValue, buildReferenceCacheKey, normalizeReferenceOptions, entityKeyFromCacheKey, applyCascadeFilter, resolveEffectiveMask, shouldMaskField, findTab, fileRefName, evaluateFormRules, filterRulesForTab, evaluateFieldVisibility, getTabPath, parseFieldRef, setTabData, setValueByPath, normalizeArrayStructures, getTabData, getValueByPath, normalizeConfigOptions, migrateRecord, ambiguousFieldIds,
|
|
5
|
+
import { resolveLabel, formatDisplayValue, normalizeLookupValues, lookupValuesToOptions, valuesMatch, resolveOptionLabel, resolveOptionValue, buildReferenceCacheKey, normalizeReferenceOptions, entityKeyFromCacheKey, applyCascadeFilter, resolveEffectiveMask, shouldMaskField, findTab, isValidUrl, isValidPhone, fileRefName, sliderBounds, ratingScale, normalizeHexColor, normalizeTags, evaluateFormRules, filterRulesForTab, evaluateFieldVisibility, getTabPath, fieldRefFor, toRefToken, refOf, parseFieldRef, setTabData, setValueByPath, normalizeArrayStructures, getTabData, getValueByPath, normalizeConfigOptions, migrateRecord, ambiguousFieldIds, collectFieldScopes, applyAutoPatch, isUnsafePath, fieldsUnderTab, valuesEqual, parseCsv, deriveImportColumns, suggestMapping, applyMapping, toCsv, stripIndices, CORE_VERSION, coerceCell, buildTemplateSpec, collectLeafTargets, formatConfigProblems } 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';
|
|
@@ -196,7 +196,7 @@ const ENTITY_REF_CACHE_STORE = new InjectionToken('ENTITY_REF_CACHE_STORE', {
|
|
|
196
196
|
*/
|
|
197
197
|
const SYSTEM_DEFAULT_CAN_EDIT = new InjectionToken('SYSTEM_DEFAULT_CAN_EDIT');
|
|
198
198
|
/**
|
|
199
|
-
* Registry/Resolver for resolving an EntityFormConfig by entity key across entities
|
|
199
|
+
* Registry/Resolver for resolving an EntityFormConfig by entity key across entities.
|
|
200
200
|
* Enables cross-entity field referencing (`isReferenced`, `referencedEntityKey`, `referencedFieldId`, `hasDrift`).
|
|
201
201
|
*/
|
|
202
202
|
const CONFIG_SOURCE = new InjectionToken('CONFIG_SOURCE');
|
|
@@ -224,8 +224,7 @@ const MARKDOWN_RENDERER = new InjectionToken('MARKDOWN_RENDERER');
|
|
|
224
224
|
*
|
|
225
225
|
* Masking is presentational — see SECURITY.md — and what it prints is a product decision:
|
|
226
226
|
* `••••••••` reads as a redaction, "Hidden" reads as a permission, and a localized string
|
|
227
|
-
* reads as neither in English
|
|
228
|
-
* there was no way to choose.
|
|
227
|
+
* reads as neither in English, so it is the host's to choose.
|
|
229
228
|
*
|
|
230
229
|
* @example
|
|
231
230
|
* { provide: MASKED_PLACEHOLDER, useValue: '••••••••' }
|
|
@@ -303,6 +302,10 @@ const DEFAULT_VALIDATION_MESSAGES = {
|
|
|
303
302
|
required: 'This field is required.',
|
|
304
303
|
requiredSelection: 'Please select an option.',
|
|
305
304
|
email: 'Please enter a valid email address.',
|
|
305
|
+
url: 'Please enter a full web address, starting with https://.',
|
|
306
|
+
phone: 'Please enter a valid phone number.',
|
|
307
|
+
minItems: (_lang, err) => `At least ${err?.requiredLength} required.`,
|
|
308
|
+
maxItems: (_lang, err) => `No more than ${err?.requiredLength} allowed.`,
|
|
306
309
|
pattern: 'Invalid format.',
|
|
307
310
|
minlength: (_lang, err) => `Minimum ${err?.requiredLength} characters required.`,
|
|
308
311
|
maxlength: (_lang, err) => `Maximum ${err?.requiredLength} characters allowed.`,
|
|
@@ -319,18 +322,35 @@ const DEFAULT_VALIDATION_MESSAGES = {
|
|
|
319
322
|
* first error actually on the control — so the shared list costs nothing and spares every
|
|
320
323
|
* field component a near-identical copy of it.
|
|
321
324
|
*/
|
|
322
|
-
const COMMON_ORDER = [
|
|
325
|
+
const COMMON_ORDER = [
|
|
326
|
+
'required',
|
|
327
|
+
'email',
|
|
328
|
+
'url',
|
|
329
|
+
'phone',
|
|
330
|
+
'min',
|
|
331
|
+
'max',
|
|
332
|
+
'minlength',
|
|
333
|
+
'maxlength',
|
|
334
|
+
'pattern',
|
|
335
|
+
];
|
|
323
336
|
/**
|
|
324
337
|
* Where a field type disagrees with the common list.
|
|
325
338
|
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
*
|
|
339
|
+
* Each for a reason about how the control reads rather than what it can raise: a select says
|
|
340
|
+
* "Please select an option" where an input says "This field is required"; a number that is
|
|
341
|
+
* neither required nor out of range is not "invalid", it is not a number; a control with no
|
|
342
|
+
* text in it cannot fail a length or format rule; and a `tags` list is counted in items, not
|
|
343
|
+
* characters, although Angular reports both as `minlength`.
|
|
330
344
|
*/
|
|
331
345
|
const ORDER_BY_FIELD_TYPE = {
|
|
332
346
|
dropdown: { order: [['required', 'requiredSelection']], fallback: 'invalidSelection' },
|
|
333
347
|
number: { order: ['required', 'min', 'max'], fallback: 'invalidNumber' },
|
|
348
|
+
slider: { order: ['required', 'min', 'max'], fallback: 'invalid' },
|
|
349
|
+
rating: { order: ['required', 'min', 'max'], fallback: 'invalid' },
|
|
350
|
+
tags: {
|
|
351
|
+
order: ['required', ['minlength', 'minItems'], ['maxlength', 'maxItems'], 'pattern'],
|
|
352
|
+
fallback: 'invalid',
|
|
353
|
+
},
|
|
334
354
|
boolean: { order: ['required', 'pattern'], fallback: 'invalid' },
|
|
335
355
|
checkbox: { order: ['required', 'pattern'], fallback: 'invalid' },
|
|
336
356
|
file: { order: ['required', 'pattern'], fallback: 'invalid' },
|
|
@@ -363,12 +383,10 @@ class ValidationMessagesService {
|
|
|
363
383
|
/**
|
|
364
384
|
* The message for a field, chosen by its **type** rather than by a list the caller supplies.
|
|
365
385
|
*
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
* rendered
|
|
369
|
-
*
|
|
370
|
-
* (the offending field is usually on a tab that is not rendered), so it would have needed a
|
|
371
|
-
* second copy of the same table, and a dropdown would have said "This field is required" in
|
|
386
|
+
* Both the field itself and the error summary above the form render this message, and the
|
|
387
|
+
* summary has no field component to ask (the offending field is usually on a tab that is
|
|
388
|
+
* not rendered). Per-component key orders would drift, and a dropdown would say "This
|
|
389
|
+
* field is required" in
|
|
372
390
|
* the summary and "Please select an option" under the control.
|
|
373
391
|
*
|
|
374
392
|
* One table, read from both places, is what keeps those two sentences the same sentence.
|
|
@@ -388,10 +406,10 @@ class ValidationMessagesService {
|
|
|
388
406
|
return '';
|
|
389
407
|
return typeof message === 'function' ? message(language, error) : message;
|
|
390
408
|
}
|
|
391
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
392
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.
|
|
409
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: ValidationMessagesService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
410
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: ValidationMessagesService, providedIn: 'root' });
|
|
393
411
|
}
|
|
394
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
412
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: ValidationMessagesService, decorators: [{
|
|
395
413
|
type: Injectable,
|
|
396
414
|
args: [{ providedIn: 'root' }]
|
|
397
415
|
}] });
|
|
@@ -399,14 +417,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
399
417
|
/**
|
|
400
418
|
* DOM ids that survive being rendered more than once.
|
|
401
419
|
*
|
|
402
|
-
*
|
|
403
|
-
*
|
|
404
|
-
*
|
|
405
|
-
*
|
|
406
|
-
* the first row's input, and a screen reader read the same association twice. Duplicate ids on
|
|
407
|
-
* focusable elements are also a WCAG failure in their own right.
|
|
420
|
+
* `field.id` is unique in a config and *not* unique in a document: an `array` field renders
|
|
421
|
+
* the same child fields once per row. `<label for>` resolves to the first match, so a second
|
|
422
|
+
* row's label would focus the first row's input — and duplicate ids on focusable elements are
|
|
423
|
+
* a WCAG failure in their own right.
|
|
408
424
|
*
|
|
409
|
-
* The
|
|
425
|
+
* The answer is a counter, which is what Angular Material does for the same reason: the id stays
|
|
410
426
|
* readable — `email-de7` — and is unique for the lifetime of the page. It is deliberately not
|
|
411
427
|
* derived from the row index, because a field component is given exactly five inputs and none
|
|
412
428
|
* of them says where it is; adding a sixth would change the contract every custom field type
|
|
@@ -506,8 +522,8 @@ class TextFieldComponent {
|
|
|
506
522
|
return '';
|
|
507
523
|
return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
|
|
508
524
|
}
|
|
509
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
510
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.
|
|
525
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: TextFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
526
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: TextFieldComponent, isStandalone: true, selector: "ngx-text-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
511
527
|
<div
|
|
512
528
|
class="ngx-field ngx-field--text"
|
|
513
529
|
[attr.data-testid]="'field-' + field.id"
|
|
@@ -567,7 +583,7 @@ class TextFieldComponent {
|
|
|
567
583
|
</div>
|
|
568
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.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
569
585
|
}
|
|
570
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
586
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: TextFieldComponent, decorators: [{
|
|
571
587
|
type: Component,
|
|
572
588
|
args: [{
|
|
573
589
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
@@ -669,12 +685,6 @@ class TextareaFieldComponent {
|
|
|
669
685
|
get placeholder() {
|
|
670
686
|
return resolveLabel(this.field?.placeholder, this.language);
|
|
671
687
|
}
|
|
672
|
-
/**
|
|
673
|
-
* Resolved through `ValidationMessagesService`, so `provideNgxDynamicEntity({
|
|
674
|
-
* validationMessages })` reaches this field. It used to render a fixed
|
|
675
|
-
* "This field has an error", which made a documented, configurable feature work on three
|
|
676
|
-
* of fifteen field types.
|
|
677
|
-
*/
|
|
678
688
|
/** Author help text, shown under the control and named by `aria-describedby`. */
|
|
679
689
|
get hint() {
|
|
680
690
|
return resolveLabel(this.field?.hint, this.language);
|
|
@@ -688,8 +698,8 @@ class TextareaFieldComponent {
|
|
|
688
698
|
return '';
|
|
689
699
|
return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
|
|
690
700
|
}
|
|
691
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
692
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.
|
|
701
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: TextareaFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
702
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: TextareaFieldComponent, isStandalone: true, selector: "ngx-textarea-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
693
703
|
<div
|
|
694
704
|
class="ngx-field ngx-field--textarea"
|
|
695
705
|
[attr.data-testid]="'field-' + field.id"
|
|
@@ -738,7 +748,7 @@ class TextareaFieldComponent {
|
|
|
738
748
|
</div>
|
|
739
749
|
`, 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 });
|
|
740
750
|
}
|
|
741
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
751
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: TextareaFieldComponent, decorators: [{
|
|
742
752
|
type: Component,
|
|
743
753
|
args: [{
|
|
744
754
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
@@ -865,6 +875,11 @@ const DEFAULT_UI_TEXT = {
|
|
|
865
875
|
noImage: 'No image',
|
|
866
876
|
uploadImage: 'Upload',
|
|
867
877
|
changeImage: 'Change',
|
|
878
|
+
// Rating, colour and tags
|
|
879
|
+
ratingValue: '{value} of {max}',
|
|
880
|
+
clearValue: 'Clear {field}',
|
|
881
|
+
addTagPlaceholder: 'Type and press Enter',
|
|
882
|
+
removeTag: 'Remove {tag}',
|
|
868
883
|
// Month / year pickers
|
|
869
884
|
month: 'Month',
|
|
870
885
|
year: 'Year',
|
|
@@ -1007,10 +1022,10 @@ class UiTextService {
|
|
|
1007
1022
|
text(key, language = 'en', params) {
|
|
1008
1023
|
return resolveUiText(this.overrides, DEFAULT_UI_TEXT, key, language, params);
|
|
1009
1024
|
}
|
|
1010
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
1011
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.
|
|
1025
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: UiTextService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1026
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: UiTextService, providedIn: 'root' });
|
|
1012
1027
|
}
|
|
1013
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
1028
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: UiTextService, decorators: [{
|
|
1014
1029
|
type: Injectable,
|
|
1015
1030
|
args: [{ providedIn: 'root' }]
|
|
1016
1031
|
}] });
|
|
@@ -1124,12 +1139,6 @@ class MarkdownFieldComponent {
|
|
|
1124
1139
|
this.cached = { source, html };
|
|
1125
1140
|
return html;
|
|
1126
1141
|
}
|
|
1127
|
-
/**
|
|
1128
|
-
* Resolved through `ValidationMessagesService`, so `provideNgxDynamicEntity({
|
|
1129
|
-
* validationMessages })` reaches this field. It used to render a fixed
|
|
1130
|
-
* "This field has an error", which made a documented, configurable feature work on three
|
|
1131
|
-
* of fifteen field types.
|
|
1132
|
-
*/
|
|
1133
1142
|
/** Author help text, shown under the control and named by `aria-describedby`. */
|
|
1134
1143
|
get hint() {
|
|
1135
1144
|
return resolveLabel(this.field?.hint, this.language);
|
|
@@ -1143,8 +1152,8 @@ class MarkdownFieldComponent {
|
|
|
1143
1152
|
return '';
|
|
1144
1153
|
return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
|
|
1145
1154
|
}
|
|
1146
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
1147
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.
|
|
1155
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: MarkdownFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1156
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: MarkdownFieldComponent, isStandalone: true, selector: "ngx-markdown-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, usesOnChanges: true, ngImport: i0, template: `
|
|
1148
1157
|
<div
|
|
1149
1158
|
class="ngx-field ngx-field--markdown"
|
|
1150
1159
|
[attr.data-testid]="'field-' + field.id"
|
|
@@ -1244,7 +1253,7 @@ class MarkdownFieldComponent {
|
|
|
1244
1253
|
</div>
|
|
1245
1254
|
`, 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 });
|
|
1246
1255
|
}
|
|
1247
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
1256
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: MarkdownFieldComponent, decorators: [{
|
|
1248
1257
|
type: Component,
|
|
1249
1258
|
args: [{
|
|
1250
1259
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
@@ -1399,8 +1408,8 @@ class NumberFieldComponent {
|
|
|
1399
1408
|
return '';
|
|
1400
1409
|
return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
|
|
1401
1410
|
}
|
|
1402
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
1403
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.
|
|
1411
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: NumberFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1412
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: NumberFieldComponent, isStandalone: true, selector: "ngx-number-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
1404
1413
|
<div
|
|
1405
1414
|
class="ngx-field ngx-field--number"
|
|
1406
1415
|
[attr.data-testid]="'field-' + field.id"
|
|
@@ -1460,7 +1469,7 @@ class NumberFieldComponent {
|
|
|
1460
1469
|
</div>
|
|
1461
1470
|
`, 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 });
|
|
1462
1471
|
}
|
|
1463
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
1472
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: NumberFieldComponent, decorators: [{
|
|
1464
1473
|
type: Component,
|
|
1465
1474
|
args: [{
|
|
1466
1475
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
@@ -1575,12 +1584,6 @@ class CurrencyFieldComponent {
|
|
|
1575
1584
|
return '$';
|
|
1576
1585
|
}
|
|
1577
1586
|
}
|
|
1578
|
-
/**
|
|
1579
|
-
* Resolved through `ValidationMessagesService`, so `provideNgxDynamicEntity({
|
|
1580
|
-
* validationMessages })` reaches this field. It used to render a fixed
|
|
1581
|
-
* "This field has an error", which made a documented, configurable feature work on three
|
|
1582
|
-
* of fifteen field types.
|
|
1583
|
-
*/
|
|
1584
1587
|
/** Author help text, shown under the control and named by `aria-describedby`. */
|
|
1585
1588
|
get hint() {
|
|
1586
1589
|
return resolveLabel(this.field?.hint, this.language);
|
|
@@ -1594,8 +1597,8 @@ class CurrencyFieldComponent {
|
|
|
1594
1597
|
return '';
|
|
1595
1598
|
return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
|
|
1596
1599
|
}
|
|
1597
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
1598
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.
|
|
1600
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: CurrencyFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1601
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: CurrencyFieldComponent, isStandalone: true, selector: "ngx-currency-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
1599
1602
|
<div
|
|
1600
1603
|
class="ngx-field ngx-field--currency"
|
|
1601
1604
|
[attr.data-testid]="'field-' + field.id"
|
|
@@ -1650,7 +1653,7 @@ class CurrencyFieldComponent {
|
|
|
1650
1653
|
</div>
|
|
1651
1654
|
`, 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 });
|
|
1652
1655
|
}
|
|
1653
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
1656
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: CurrencyFieldComponent, decorators: [{
|
|
1654
1657
|
type: Component,
|
|
1655
1658
|
args: [{
|
|
1656
1659
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
@@ -1748,12 +1751,6 @@ class EmailFieldComponent {
|
|
|
1748
1751
|
get placeholder() {
|
|
1749
1752
|
return resolveLabel(this.field?.placeholder, this.language);
|
|
1750
1753
|
}
|
|
1751
|
-
/**
|
|
1752
|
-
* Resolved through `ValidationMessagesService`, so `provideNgxDynamicEntity({
|
|
1753
|
-
* validationMessages })` reaches this field. It used to render a fixed
|
|
1754
|
-
* "This field has an error", which made a documented, configurable feature work on three
|
|
1755
|
-
* of fifteen field types.
|
|
1756
|
-
*/
|
|
1757
1754
|
/** Author help text, shown under the control and named by `aria-describedby`. */
|
|
1758
1755
|
get hint() {
|
|
1759
1756
|
return resolveLabel(this.field?.hint, this.language);
|
|
@@ -1767,8 +1764,8 @@ class EmailFieldComponent {
|
|
|
1767
1764
|
return '';
|
|
1768
1765
|
return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
|
|
1769
1766
|
}
|
|
1770
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
1771
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.
|
|
1767
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: EmailFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1768
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: EmailFieldComponent, isStandalone: true, selector: "ngx-email-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
1772
1769
|
<div
|
|
1773
1770
|
class="ngx-field ngx-field--email"
|
|
1774
1771
|
[attr.data-testid]="'field-' + field.id"
|
|
@@ -1816,10 +1813,7 @@ class EmailFieldComponent {
|
|
|
1816
1813
|
[attr.disabled]="field.disabled ? true : null"
|
|
1817
1814
|
/>
|
|
1818
1815
|
@if (errorMessage) {
|
|
1819
|
-
<!-- One branch, resolved by the service
|
|
1820
|
-
required" and "Enter a valid email address" itself, so a host configuring
|
|
1821
|
-
validationMessages was overridden by markup it could not reach. The service
|
|
1822
|
-
defaults say the same thing. -->
|
|
1816
|
+
<!-- One branch, resolved by the service, so a host's validationMessages apply. -->
|
|
1823
1817
|
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'" [id]="domId('-error')">{{
|
|
1824
1818
|
errorMessage
|
|
1825
1819
|
}}</span>
|
|
@@ -1828,7 +1822,7 @@ class EmailFieldComponent {
|
|
|
1828
1822
|
</div>
|
|
1829
1823
|
`, 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 });
|
|
1830
1824
|
}
|
|
1831
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
1825
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: EmailFieldComponent, decorators: [{
|
|
1832
1826
|
type: Component,
|
|
1833
1827
|
args: [{
|
|
1834
1828
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
@@ -1883,10 +1877,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
1883
1877
|
[attr.disabled]="field.disabled ? true : null"
|
|
1884
1878
|
/>
|
|
1885
1879
|
@if (errorMessage) {
|
|
1886
|
-
<!-- One branch, resolved by the service
|
|
1887
|
-
required" and "Enter a valid email address" itself, so a host configuring
|
|
1888
|
-
validationMessages was overridden by markup it could not reach. The service
|
|
1889
|
-
defaults say the same thing. -->
|
|
1880
|
+
<!-- One branch, resolved by the service, so a host's validationMessages apply. -->
|
|
1890
1881
|
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'" [id]="domId('-error')">{{
|
|
1891
1882
|
errorMessage
|
|
1892
1883
|
}}</span>
|
|
@@ -1937,12 +1928,6 @@ class PasswordFieldComponent {
|
|
|
1937
1928
|
toggleVisibility() {
|
|
1938
1929
|
this.visible.update(v => !v);
|
|
1939
1930
|
}
|
|
1940
|
-
/**
|
|
1941
|
-
* Resolved through `ValidationMessagesService`, so `provideNgxDynamicEntity({
|
|
1942
|
-
* validationMessages })` reaches this field. It used to render a fixed
|
|
1943
|
-
* "This field has an error", which made a documented, configurable feature work on three
|
|
1944
|
-
* of fifteen field types.
|
|
1945
|
-
*/
|
|
1946
1931
|
/** Author help text, shown under the control and named by `aria-describedby`. */
|
|
1947
1932
|
get hint() {
|
|
1948
1933
|
return resolveLabel(this.field?.hint, this.language);
|
|
@@ -1956,8 +1941,8 @@ class PasswordFieldComponent {
|
|
|
1956
1941
|
return '';
|
|
1957
1942
|
return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
|
|
1958
1943
|
}
|
|
1959
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
1960
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.
|
|
1944
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: PasswordFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1945
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: PasswordFieldComponent, isStandalone: true, selector: "ngx-password-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
1961
1946
|
<div
|
|
1962
1947
|
class="ngx-field ngx-field--password"
|
|
1963
1948
|
[attr.data-testid]="'field-' + field.id"
|
|
@@ -2017,7 +2002,7 @@ class PasswordFieldComponent {
|
|
|
2017
2002
|
</div>
|
|
2018
2003
|
`, 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 });
|
|
2019
2004
|
}
|
|
2020
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
2005
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: PasswordFieldComponent, decorators: [{
|
|
2021
2006
|
type: Component,
|
|
2022
2007
|
args: [{
|
|
2023
2008
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
@@ -2118,12 +2103,6 @@ class CheckboxFieldComponent {
|
|
|
2118
2103
|
get label() {
|
|
2119
2104
|
return resolveLabel(this.field?.label, this.language);
|
|
2120
2105
|
}
|
|
2121
|
-
/**
|
|
2122
|
-
* Configured messages reach this field too.
|
|
2123
|
-
*
|
|
2124
|
-
* It rendered no error element at all, so a required checkbox left unticked told the user
|
|
2125
|
-
* nothing — and `validationMessages` could not reach a field that never displayed one.
|
|
2126
|
-
*/
|
|
2127
2106
|
/** Author help text, shown under the control and named by `aria-describedby`. */
|
|
2128
2107
|
get hint() {
|
|
2129
2108
|
return resolveLabel(this.field?.hint, this.language);
|
|
@@ -2137,8 +2116,8 @@ class CheckboxFieldComponent {
|
|
|
2137
2116
|
return '';
|
|
2138
2117
|
return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
|
|
2139
2118
|
}
|
|
2140
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
2141
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.
|
|
2119
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: CheckboxFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2120
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: CheckboxFieldComponent, isStandalone: true, selector: "ngx-checkbox-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
2142
2121
|
<div
|
|
2143
2122
|
class="ngx-field ngx-field--checkbox"
|
|
2144
2123
|
[attr.data-testid]="'field-' + field.id"
|
|
@@ -2194,7 +2173,7 @@ class CheckboxFieldComponent {
|
|
|
2194
2173
|
</div>
|
|
2195
2174
|
`, 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 });
|
|
2196
2175
|
}
|
|
2197
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
2176
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: CheckboxFieldComponent, decorators: [{
|
|
2198
2177
|
type: Component,
|
|
2199
2178
|
args: [{
|
|
2200
2179
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
@@ -2296,12 +2275,6 @@ class BooleanFieldComponent {
|
|
|
2296
2275
|
get label() {
|
|
2297
2276
|
return resolveLabel(this.field?.label, this.language);
|
|
2298
2277
|
}
|
|
2299
|
-
/**
|
|
2300
|
-
* Configured messages reach this field too.
|
|
2301
|
-
*
|
|
2302
|
-
* It rendered no error element at all, so a required checkbox left unticked told the user
|
|
2303
|
-
* nothing — and `validationMessages` could not reach a field that never displayed one.
|
|
2304
|
-
*/
|
|
2305
2278
|
/** Author help text, shown under the control and named by `aria-describedby`. */
|
|
2306
2279
|
get hint() {
|
|
2307
2280
|
return resolveLabel(this.field?.hint, this.language);
|
|
@@ -2315,8 +2288,8 @@ class BooleanFieldComponent {
|
|
|
2315
2288
|
return '';
|
|
2316
2289
|
return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
|
|
2317
2290
|
}
|
|
2318
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
2319
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.
|
|
2291
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: BooleanFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2292
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: BooleanFieldComponent, isStandalone: true, selector: "ngx-boolean-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
2320
2293
|
<div
|
|
2321
2294
|
class="ngx-field ngx-field--boolean"
|
|
2322
2295
|
[attr.data-testid]="'field-' + field.id"
|
|
@@ -2380,7 +2353,7 @@ class BooleanFieldComponent {
|
|
|
2380
2353
|
</div>
|
|
2381
2354
|
`, 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 });
|
|
2382
2355
|
}
|
|
2383
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
2356
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: BooleanFieldComponent, decorators: [{
|
|
2384
2357
|
type: Component,
|
|
2385
2358
|
args: [{
|
|
2386
2359
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
@@ -2486,14 +2459,10 @@ class DateFieldComponent {
|
|
|
2486
2459
|
/**
|
|
2487
2460
|
* ISO 8601 date → display string, through the formatter the host configured.
|
|
2488
2461
|
*
|
|
2489
|
-
*
|
|
2490
|
-
*
|
|
2491
|
-
* field but not this one. A host that configured formatters got them almost everywhere,
|
|
2492
|
-
* and silently did not get them on the field type most likely to be the reason they
|
|
2493
|
-
* configured formatters at all.
|
|
2462
|
+
* Never call `toLocaleDateString()` directly here: `setDateFormatters` is the one seam for
|
|
2463
|
+
* choosing how a date renders, and every date display has to go through it.
|
|
2494
2464
|
*
|
|
2495
|
-
* The default formatter is `toLocaleDateString()` with no locale
|
|
2496
|
-
* has not called `setDateFormatters` renders exactly what it rendered before.
|
|
2465
|
+
* The default formatter is `toLocaleDateString()` with no locale.
|
|
2497
2466
|
*/
|
|
2498
2467
|
formatDate(value) {
|
|
2499
2468
|
if (!value)
|
|
@@ -2508,12 +2477,6 @@ class DateFieldComponent {
|
|
|
2508
2477
|
return value;
|
|
2509
2478
|
return formatDisplayValue('date', undefined, value, this.language);
|
|
2510
2479
|
}
|
|
2511
|
-
/**
|
|
2512
|
-
* Resolved through `ValidationMessagesService`, so `provideNgxDynamicEntity({
|
|
2513
|
-
* validationMessages })` reaches this field. It used to render a fixed
|
|
2514
|
-
* "This field has an error", which made a documented, configurable feature work on three
|
|
2515
|
-
* of fifteen field types.
|
|
2516
|
-
*/
|
|
2517
2480
|
/** Author help text, shown under the control and named by `aria-describedby`. */
|
|
2518
2481
|
get hint() {
|
|
2519
2482
|
return resolveLabel(this.field?.hint, this.language);
|
|
@@ -2527,8 +2490,8 @@ class DateFieldComponent {
|
|
|
2527
2490
|
return '';
|
|
2528
2491
|
return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
|
|
2529
2492
|
}
|
|
2530
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
2531
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.
|
|
2493
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: DateFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2494
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: DateFieldComponent, isStandalone: true, selector: "ngx-date-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
2532
2495
|
<div
|
|
2533
2496
|
class="ngx-field ngx-field--date"
|
|
2534
2497
|
[attr.data-testid]="'field-' + field.id"
|
|
@@ -2578,7 +2541,7 @@ class DateFieldComponent {
|
|
|
2578
2541
|
</div>
|
|
2579
2542
|
`, 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 });
|
|
2580
2543
|
}
|
|
2581
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
2544
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: DateFieldComponent, decorators: [{
|
|
2582
2545
|
type: Component,
|
|
2583
2546
|
args: [{
|
|
2584
2547
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
@@ -2652,8 +2615,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
2652
2615
|
*
|
|
2653
2616
|
* A bare calendar date is pinned to *local* midnight. `new Date('2020-01-01')` reads a
|
|
2654
2617
|
* date-only string as UTC midnight, which renders as the previous day anywhere west of
|
|
2655
|
-
* Greenwich — and
|
|
2656
|
-
*
|
|
2618
|
+
* Greenwich — and legacy `datetime` values written by a `type="date"` input have exactly
|
|
2619
|
+
* that shape.
|
|
2657
2620
|
*/
|
|
2658
2621
|
function parseStored(value) {
|
|
2659
2622
|
if (value === null || value === undefined || value === '')
|
|
@@ -2734,12 +2697,6 @@ class DateTimeFieldComponent {
|
|
|
2734
2697
|
return '—';
|
|
2735
2698
|
return formatDisplayValue('datetime', undefined, d.toISOString(), this.language);
|
|
2736
2699
|
}
|
|
2737
|
-
/**
|
|
2738
|
-
* Resolved through `ValidationMessagesService`, so `provideNgxDynamicEntity({
|
|
2739
|
-
* validationMessages })` reaches this field. It used to render a fixed
|
|
2740
|
-
* "This field has an error", which made a documented, configurable feature work on three
|
|
2741
|
-
* of fifteen field types.
|
|
2742
|
-
*/
|
|
2743
2700
|
/** Author help text, shown under the control and named by `aria-describedby`. */
|
|
2744
2701
|
get hint() {
|
|
2745
2702
|
return resolveLabel(this.field?.hint, this.language);
|
|
@@ -2753,8 +2710,8 @@ class DateTimeFieldComponent {
|
|
|
2753
2710
|
return '';
|
|
2754
2711
|
return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
|
|
2755
2712
|
}
|
|
2756
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
2757
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.
|
|
2713
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: DateTimeFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2714
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: DateTimeFieldComponent, isStandalone: true, selector: "ngx-date-time-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
2758
2715
|
<div
|
|
2759
2716
|
class="ngx-field ngx-field--datetime"
|
|
2760
2717
|
[attr.data-testid]="'field-' + field.id"
|
|
@@ -2805,7 +2762,7 @@ class DateTimeFieldComponent {
|
|
|
2805
2762
|
</div>
|
|
2806
2763
|
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2807
2764
|
}
|
|
2808
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
2765
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: DateTimeFieldComponent, decorators: [{
|
|
2809
2766
|
type: Component,
|
|
2810
2767
|
args: [{
|
|
2811
2768
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
@@ -2911,12 +2868,6 @@ class TimeFieldComponent {
|
|
|
2911
2868
|
formatTime(value) {
|
|
2912
2869
|
return formatDisplayValue('time', undefined, value, this.language);
|
|
2913
2870
|
}
|
|
2914
|
-
/**
|
|
2915
|
-
* Resolved through `ValidationMessagesService`, so `provideNgxDynamicEntity({
|
|
2916
|
-
* validationMessages })` reaches this field. It used to render a fixed
|
|
2917
|
-
* "This field has an error", which made a documented, configurable feature work on three
|
|
2918
|
-
* of fifteen field types.
|
|
2919
|
-
*/
|
|
2920
2871
|
/** Author help text, shown under the control and named by `aria-describedby`. */
|
|
2921
2872
|
get hint() {
|
|
2922
2873
|
return resolveLabel(this.field?.hint, this.language);
|
|
@@ -2930,8 +2881,8 @@ class TimeFieldComponent {
|
|
|
2930
2881
|
return '';
|
|
2931
2882
|
return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
|
|
2932
2883
|
}
|
|
2933
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
2934
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.
|
|
2884
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: TimeFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2885
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: TimeFieldComponent, isStandalone: true, selector: "ngx-time-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
2935
2886
|
<div
|
|
2936
2887
|
class="ngx-field ngx-field--time"
|
|
2937
2888
|
[attr.data-testid]="'field-' + field.id"
|
|
@@ -2981,7 +2932,7 @@ class TimeFieldComponent {
|
|
|
2981
2932
|
</div>
|
|
2982
2933
|
`, 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 });
|
|
2983
2934
|
}
|
|
2984
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
2935
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: TimeFieldComponent, decorators: [{
|
|
2985
2936
|
type: Component,
|
|
2986
2937
|
args: [{
|
|
2987
2938
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
@@ -3120,12 +3071,6 @@ class MonthYearFieldComponent {
|
|
|
3120
3071
|
const monthName = MONTH_NAMES[parseInt(monthNum, 10) - 1];
|
|
3121
3072
|
return monthName && year ? `${monthName} ${year}` : String(value);
|
|
3122
3073
|
}
|
|
3123
|
-
/**
|
|
3124
|
-
* Resolved through `ValidationMessagesService`, so `provideNgxDynamicEntity({
|
|
3125
|
-
* validationMessages })` reaches this field. It used to render a fixed
|
|
3126
|
-
* "This field has an error", which made a documented, configurable feature work on three
|
|
3127
|
-
* of fifteen field types.
|
|
3128
|
-
*/
|
|
3129
3074
|
/** Author help text, shown under the control and named by `aria-describedby`. */
|
|
3130
3075
|
get hint() {
|
|
3131
3076
|
return resolveLabel(this.field?.hint, this.language);
|
|
@@ -3139,8 +3084,8 @@ class MonthYearFieldComponent {
|
|
|
3139
3084
|
return '';
|
|
3140
3085
|
return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
|
|
3141
3086
|
}
|
|
3142
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
3143
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.
|
|
3087
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: MonthYearFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3088
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: MonthYearFieldComponent, isStandalone: true, selector: "ngx-month-year-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
3144
3089
|
<div
|
|
3145
3090
|
class="ngx-field ngx-field--month-year"
|
|
3146
3091
|
[attr.data-testid]="'field-' + field.id"
|
|
@@ -3219,7 +3164,7 @@ class MonthYearFieldComponent {
|
|
|
3219
3164
|
</div>
|
|
3220
3165
|
`, 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 });
|
|
3221
3166
|
}
|
|
3222
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
3167
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: MonthYearFieldComponent, decorators: [{
|
|
3223
3168
|
type: Component,
|
|
3224
3169
|
args: [{
|
|
3225
3170
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
@@ -3520,10 +3465,10 @@ class LookupRegistryService {
|
|
|
3520
3465
|
value !== null &&
|
|
3521
3466
|
typeof value.subscribe === 'function');
|
|
3522
3467
|
}
|
|
3523
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
3524
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.
|
|
3468
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: LookupRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
3469
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: LookupRegistryService, providedIn: 'root' });
|
|
3525
3470
|
}
|
|
3526
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
3471
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: LookupRegistryService, decorators: [{
|
|
3527
3472
|
type: Injectable,
|
|
3528
3473
|
args: [{ providedIn: 'root' }]
|
|
3529
3474
|
}] });
|
|
@@ -3610,10 +3555,7 @@ class DropdownFieldComponent {
|
|
|
3610
3555
|
* The value this option stores — the option object itself.
|
|
3611
3556
|
*
|
|
3612
3557
|
* Kept as a method rather than binding `option` straight into the template, because it is
|
|
3613
|
-
* the one place that says *why* the two are the same thing.
|
|
3614
|
-
* `getOptionStoredValue`, an exported identity function whose two branches returned their
|
|
3615
|
-
* argument unchanged; naming a transformation that does not happen is worse than naming
|
|
3616
|
-
* nothing, because a reader has to go and check.
|
|
3558
|
+
* the one place that says *why* the two are the same thing.
|
|
3617
3559
|
*/
|
|
3618
3560
|
getOptStoredVal(option) {
|
|
3619
3561
|
return option;
|
|
@@ -3652,8 +3594,8 @@ class DropdownFieldComponent {
|
|
|
3652
3594
|
// dropdown than "This field is required", and both stay independently overridable.
|
|
3653
3595
|
return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
|
|
3654
3596
|
}
|
|
3655
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
3656
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.
|
|
3597
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: DropdownFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3598
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: DropdownFieldComponent, isStandalone: true, selector: "ngx-dropdown-field", inputs: { field: "field", language: "language", control: "control", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
3657
3599
|
<div
|
|
3658
3600
|
class="ngx-field ngx-field--dropdown"
|
|
3659
3601
|
[attr.data-testid]="'field-' + field.id"
|
|
@@ -3723,7 +3665,7 @@ class DropdownFieldComponent {
|
|
|
3723
3665
|
</div>
|
|
3724
3666
|
`, 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 });
|
|
3725
3667
|
}
|
|
3726
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
3668
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: DropdownFieldComponent, decorators: [{
|
|
3727
3669
|
type: Component,
|
|
3728
3670
|
args: [{
|
|
3729
3671
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
@@ -3875,10 +3817,7 @@ class RadioFieldComponent {
|
|
|
3875
3817
|
* The value this option stores — the option object itself.
|
|
3876
3818
|
*
|
|
3877
3819
|
* Kept as a method rather than binding `option` straight into the template, because it is
|
|
3878
|
-
* the one place that says *why* the two are the same thing.
|
|
3879
|
-
* `getOptionStoredValue`, an exported identity function whose two branches returned their
|
|
3880
|
-
* argument unchanged; naming a transformation that does not happen is worse than naming
|
|
3881
|
-
* nothing, because a reader has to go and check.
|
|
3820
|
+
* the one place that says *why* the two are the same thing.
|
|
3882
3821
|
*/
|
|
3883
3822
|
getOptStoredVal(option) {
|
|
3884
3823
|
return option;
|
|
@@ -3928,12 +3867,6 @@ class RadioFieldComponent {
|
|
|
3928
3867
|
return cached;
|
|
3929
3868
|
return typeof value === 'object' ? resolveLabel(value, this.language) : String(value ?? '—');
|
|
3930
3869
|
}
|
|
3931
|
-
/**
|
|
3932
|
-
* Resolved through `ValidationMessagesService`, so `provideNgxDynamicEntity({
|
|
3933
|
-
* validationMessages })` reaches this field. It used to render a fixed
|
|
3934
|
-
* "This field has an error", which made a documented, configurable feature work on three
|
|
3935
|
-
* of fifteen field types.
|
|
3936
|
-
*/
|
|
3937
3870
|
/** Author help text, shown under the control and named by `aria-describedby`. */
|
|
3938
3871
|
get hint() {
|
|
3939
3872
|
return resolveLabel(this.field?.hint, this.language);
|
|
@@ -3947,8 +3880,8 @@ class RadioFieldComponent {
|
|
|
3947
3880
|
return '';
|
|
3948
3881
|
return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
|
|
3949
3882
|
}
|
|
3950
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
3951
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.
|
|
3883
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: RadioFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3884
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: RadioFieldComponent, isStandalone: true, selector: "ngx-radio-field", inputs: { field: "field", language: "language", control: "control", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
3952
3885
|
<div
|
|
3953
3886
|
class="ngx-field ngx-field--radio"
|
|
3954
3887
|
[attr.data-testid]="'field-' + field.id"
|
|
@@ -4012,7 +3945,7 @@ class RadioFieldComponent {
|
|
|
4012
3945
|
</div>
|
|
4013
3946
|
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
4014
3947
|
}
|
|
4015
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
3948
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: RadioFieldComponent, decorators: [{
|
|
4016
3949
|
type: Component,
|
|
4017
3950
|
args: [{
|
|
4018
3951
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
@@ -4141,10 +4074,7 @@ class MultiSelectFieldComponent {
|
|
|
4141
4074
|
* The value this option stores — the option object itself.
|
|
4142
4075
|
*
|
|
4143
4076
|
* Kept as a method rather than binding `option` straight into the template, because it is
|
|
4144
|
-
* the one place that says *why* the two are the same thing.
|
|
4145
|
-
* `getOptionStoredValue`, an exported identity function whose two branches returned their
|
|
4146
|
-
* argument unchanged; naming a transformation that does not happen is worse than naming
|
|
4147
|
-
* nothing, because a reader has to go and check.
|
|
4077
|
+
* the one place that says *why* the two are the same thing.
|
|
4148
4078
|
*/
|
|
4149
4079
|
getOptStoredVal(option) {
|
|
4150
4080
|
return option;
|
|
@@ -4168,12 +4098,6 @@ class MultiSelectFieldComponent {
|
|
|
4168
4098
|
})
|
|
4169
4099
|
.join(', ');
|
|
4170
4100
|
}
|
|
4171
|
-
/**
|
|
4172
|
-
* Resolved through `ValidationMessagesService`, so `provideNgxDynamicEntity({
|
|
4173
|
-
* validationMessages })` reaches this field. It used to render a fixed
|
|
4174
|
-
* "This field has an error", which made a documented, configurable feature work on three
|
|
4175
|
-
* of fifteen field types.
|
|
4176
|
-
*/
|
|
4177
4101
|
/** Author help text, shown under the control and named by `aria-describedby`. */
|
|
4178
4102
|
get hint() {
|
|
4179
4103
|
return resolveLabel(this.field?.hint, this.language);
|
|
@@ -4187,8 +4111,8 @@ class MultiSelectFieldComponent {
|
|
|
4187
4111
|
return '';
|
|
4188
4112
|
return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
|
|
4189
4113
|
}
|
|
4190
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
4191
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.
|
|
4114
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: MultiSelectFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
4115
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: MultiSelectFieldComponent, isStandalone: true, selector: "ngx-multi-select-field", inputs: { field: "field", language: "language", control: "control", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
4192
4116
|
<div
|
|
4193
4117
|
class="ngx-field ngx-field--multiSelect"
|
|
4194
4118
|
[attr.data-testid]="'field-' + field.id"
|
|
@@ -4248,7 +4172,7 @@ class MultiSelectFieldComponent {
|
|
|
4248
4172
|
</div>
|
|
4249
4173
|
`, 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 });
|
|
4250
4174
|
}
|
|
4251
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
4175
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: MultiSelectFieldComponent, decorators: [{
|
|
4252
4176
|
type: Component,
|
|
4253
4177
|
args: [{
|
|
4254
4178
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
@@ -4347,10 +4271,10 @@ class EntityRefRegistryService {
|
|
|
4347
4271
|
has(entityKey) {
|
|
4348
4272
|
return this.registry.has(entityKey);
|
|
4349
4273
|
}
|
|
4350
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
4351
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.
|
|
4274
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: EntityRefRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
4275
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: EntityRefRegistryService, providedIn: 'root' });
|
|
4352
4276
|
}
|
|
4353
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
4277
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: EntityRefRegistryService, decorators: [{
|
|
4354
4278
|
type: Injectable,
|
|
4355
4279
|
args: [{ providedIn: 'root' }]
|
|
4356
4280
|
}] });
|
|
@@ -4510,10 +4434,10 @@ class EntityReferenceService {
|
|
|
4510
4434
|
matches(a, b, lang = 'en') {
|
|
4511
4435
|
return valuesMatch(a, b, lang);
|
|
4512
4436
|
}
|
|
4513
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
4514
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.
|
|
4437
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: EntityReferenceService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
4438
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: EntityReferenceService, providedIn: 'root' });
|
|
4515
4439
|
}
|
|
4516
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
4440
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: EntityReferenceService, decorators: [{
|
|
4517
4441
|
type: Injectable,
|
|
4518
4442
|
args: [{ providedIn: 'root' }]
|
|
4519
4443
|
}] });
|
|
@@ -4634,10 +4558,10 @@ class CascadeDataService {
|
|
|
4634
4558
|
walkTabs(config?.tabs);
|
|
4635
4559
|
return found;
|
|
4636
4560
|
}
|
|
4637
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
4638
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.
|
|
4561
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: CascadeDataService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
4562
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: CascadeDataService, providedIn: 'root' });
|
|
4639
4563
|
}
|
|
4640
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
4564
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: CascadeDataService, decorators: [{
|
|
4641
4565
|
type: Injectable,
|
|
4642
4566
|
args: [{ providedIn: 'root' }]
|
|
4643
4567
|
}] });
|
|
@@ -4658,10 +4582,10 @@ class EntityRefSelectionService {
|
|
|
4658
4582
|
emit(fieldId, option) {
|
|
4659
4583
|
this.subject.next({ fieldId, option });
|
|
4660
4584
|
}
|
|
4661
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
4662
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.
|
|
4585
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: EntityRefSelectionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
4586
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: EntityRefSelectionService, providedIn: 'root' });
|
|
4663
4587
|
}
|
|
4664
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
4588
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: EntityRefSelectionService, decorators: [{
|
|
4665
4589
|
type: Injectable,
|
|
4666
4590
|
args: [{ providedIn: 'root' }]
|
|
4667
4591
|
}] });
|
|
@@ -4776,12 +4700,6 @@ class EntityRefFieldComponent {
|
|
|
4776
4700
|
void this.reload();
|
|
4777
4701
|
});
|
|
4778
4702
|
}
|
|
4779
|
-
/**
|
|
4780
|
-
* Resolved through `ValidationMessagesService`, so `provideNgxDynamicEntity({
|
|
4781
|
-
* validationMessages })` reaches this field. It used to render a fixed
|
|
4782
|
-
* "This field has an error", which made a documented, configurable feature work on three
|
|
4783
|
-
* of fifteen field types.
|
|
4784
|
-
*/
|
|
4785
4703
|
/** Author help text, shown under the control and named by `aria-describedby`. */
|
|
4786
4704
|
get hint() {
|
|
4787
4705
|
return resolveLabel(this.field?.hint, this.language);
|
|
@@ -4795,8 +4713,8 @@ class EntityRefFieldComponent {
|
|
|
4795
4713
|
return '';
|
|
4796
4714
|
return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
|
|
4797
4715
|
}
|
|
4798
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
4799
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.
|
|
4716
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: EntityRefFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
4717
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: EntityRefFieldComponent, isStandalone: true, selector: "ngx-entity-ref-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
4800
4718
|
<div
|
|
4801
4719
|
class="ngx-field ngx-field--entity-ref"
|
|
4802
4720
|
[attr.data-testid]="'field-' + field.id"
|
|
@@ -4861,7 +4779,7 @@ class EntityRefFieldComponent {
|
|
|
4861
4779
|
</div>
|
|
4862
4780
|
`, 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 });
|
|
4863
4781
|
}
|
|
4864
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
4782
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: EntityRefFieldComponent, decorators: [{
|
|
4865
4783
|
type: Component,
|
|
4866
4784
|
args: [{
|
|
4867
4785
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
@@ -4989,10 +4907,10 @@ class FieldRegistryService {
|
|
|
4989
4907
|
registeredTypes() {
|
|
4990
4908
|
return [...new Set([...this.dynamicRegistry.keys(), ...this.consumerRegistry.keys()])].sort();
|
|
4991
4909
|
}
|
|
4992
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
4993
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.
|
|
4910
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: FieldRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
4911
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: FieldRegistryService, providedIn: 'root' });
|
|
4994
4912
|
}
|
|
4995
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
4913
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: FieldRegistryService, decorators: [{
|
|
4996
4914
|
type: Injectable,
|
|
4997
4915
|
args: [{ providedIn: 'root' }]
|
|
4998
4916
|
}], ctorParameters: () => [] });
|
|
@@ -5031,10 +4949,10 @@ class RbacService {
|
|
|
5031
4949
|
canDelete: this.hasPermission(userRoles, config.permissions?.delete),
|
|
5032
4950
|
};
|
|
5033
4951
|
}
|
|
5034
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
5035
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.
|
|
4952
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: RbacService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
4953
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: RbacService, providedIn: 'root' });
|
|
5036
4954
|
}
|
|
5037
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
4955
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: RbacService, decorators: [{
|
|
5038
4956
|
type: Injectable,
|
|
5039
4957
|
args: [{ providedIn: 'root' }]
|
|
5040
4958
|
}] });
|
|
@@ -5127,7 +5045,7 @@ class DynamicFieldComponent {
|
|
|
5127
5045
|
*
|
|
5128
5046
|
* `detectChanges()` rather than `markForCheck()`: a selection that arrives from a native
|
|
5129
5047
|
* `change` event (or Playwright) may not be followed by another Angular tick, so a dirty
|
|
5130
|
-
* flag would never be flushed. Running CD on the hosted component
|
|
5048
|
+
* flag would never be flushed. Running CD on the hosted component immediately is what shows the copied value.
|
|
5131
5049
|
*
|
|
5132
5050
|
* Done here rather than in each of the eighteen field components: the host owns the
|
|
5133
5051
|
* component reference, so one subscription covers every type including custom ones.
|
|
@@ -5156,10 +5074,10 @@ class DynamicFieldComponent {
|
|
|
5156
5074
|
ref.setInput('readonly', this.readonly || !!this.field.readonly);
|
|
5157
5075
|
ref.setInput('masked', masked);
|
|
5158
5076
|
}
|
|
5159
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
5160
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.
|
|
5077
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: DynamicFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
5078
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.24", type: DynamicFieldComponent, isStandalone: true, selector: "ngx-dynamic-field", inputs: { field: "field", control: "control", config: "config", currentTabId: "currentTabId", language: "language", readonly: "readonly", userRoles: "userRoles" }, viewQueries: [{ propertyName: "fieldHost", first: true, predicate: ["fieldHost"], descendants: true, read: ViewContainerRef, static: true }], usesOnChanges: true, ngImport: i0, template: `<ng-container #fieldHost></ng-container>`, isInline: true, styles: [":host{display:block;width:100%;min-width:0}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
5161
5079
|
}
|
|
5162
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
5080
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: DynamicFieldComponent, decorators: [{
|
|
5163
5081
|
type: Component,
|
|
5164
5082
|
args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'ngx-dynamic-field', standalone: true, template: `<ng-container #fieldHost></ng-container>`, styles: [":host{display:block;width:100%;min-width:0}\n"] }]
|
|
5165
5083
|
}], propDecorators: { field: [{
|
|
@@ -5203,8 +5121,8 @@ class GroupFieldComponent {
|
|
|
5203
5121
|
const group = this.control;
|
|
5204
5122
|
return group?.get(childId);
|
|
5205
5123
|
}
|
|
5206
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
5207
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.
|
|
5124
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: GroupFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
5125
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", 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: `
|
|
5208
5126
|
<fieldset
|
|
5209
5127
|
class="ngx-field ngx-field--group"
|
|
5210
5128
|
[attr.data-testid]="'field-' + field.id"
|
|
@@ -5245,7 +5163,7 @@ class GroupFieldComponent {
|
|
|
5245
5163
|
</fieldset>
|
|
5246
5164
|
`, 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 });
|
|
5247
5165
|
}
|
|
5248
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
5166
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: GroupFieldComponent, decorators: [{
|
|
5249
5167
|
type: Component,
|
|
5250
5168
|
args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'ngx-group-field', standalone: true, imports: [ReactiveFormsModule, forwardRef(() => DynamicFieldComponent)], template: `
|
|
5251
5169
|
<fieldset
|
|
@@ -5303,6 +5221,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
5303
5221
|
type: Input
|
|
5304
5222
|
}] } });
|
|
5305
5223
|
|
|
5224
|
+
/**
|
|
5225
|
+
* A format check that, like `Validators.email`, passes an empty value.
|
|
5226
|
+
*
|
|
5227
|
+
* Emptiness is `required`'s question. Answering it here too would give an optional field an
|
|
5228
|
+
* error the moment it was cleared.
|
|
5229
|
+
*/
|
|
5230
|
+
function formatValidator(key, test) {
|
|
5231
|
+
return (control) => {
|
|
5232
|
+
const value = control.value;
|
|
5233
|
+
if (value === null || value === undefined || value === '')
|
|
5234
|
+
return null;
|
|
5235
|
+
return test(String(value)) ? null : { [key]: true };
|
|
5236
|
+
};
|
|
5237
|
+
}
|
|
5238
|
+
/** The built-in web address check. The rule itself is in core, so an import applies the same one. */
|
|
5239
|
+
const urlValidator = formatValidator('url', isValidUrl);
|
|
5240
|
+
/** The built-in phone number check. The rule itself is in core, so an import applies the same one. */
|
|
5241
|
+
const phoneValidator = formatValidator('phone', isValidPhone);
|
|
5306
5242
|
/**
|
|
5307
5243
|
* ValidatorRegistryService — resolves validator configs / key strings to Angular ValidatorFns.
|
|
5308
5244
|
*/
|
|
@@ -5321,6 +5257,10 @@ class ValidatorRegistryService {
|
|
|
5321
5257
|
return Validators.required;
|
|
5322
5258
|
if (validatorKey === 'email')
|
|
5323
5259
|
return Validators.email;
|
|
5260
|
+
if (validatorKey === 'url')
|
|
5261
|
+
return urlValidator;
|
|
5262
|
+
if (validatorKey === 'phone')
|
|
5263
|
+
return phoneValidator;
|
|
5324
5264
|
const [name, param] = validatorKey.split(':');
|
|
5325
5265
|
const value = parseFloat(param);
|
|
5326
5266
|
if (name === 'min' && !isNaN(value))
|
|
@@ -5370,10 +5310,12 @@ class ValidatorRegistryService {
|
|
|
5370
5310
|
}
|
|
5371
5311
|
if (config.email)
|
|
5372
5312
|
fnList.push(Validators.email);
|
|
5373
|
-
|
|
5374
|
-
|
|
5375
|
-
|
|
5376
|
-
|
|
5313
|
+
if (config.url)
|
|
5314
|
+
fnList.push(urlValidator);
|
|
5315
|
+
if (config.phone)
|
|
5316
|
+
fnList.push(phoneValidator);
|
|
5317
|
+
// Named validators from the consumer registry, so a registered validator can be named
|
|
5318
|
+
// from a typed schema.
|
|
5377
5319
|
for (const key of config.custom ?? []) {
|
|
5378
5320
|
const fn = this.resolve(key);
|
|
5379
5321
|
if (fn)
|
|
@@ -5409,10 +5351,9 @@ class ValidatorRegistryService {
|
|
|
5409
5351
|
/**
|
|
5410
5352
|
* Say when a named validator resolved to nothing.
|
|
5411
5353
|
*
|
|
5412
|
-
*
|
|
5413
|
-
*
|
|
5414
|
-
*
|
|
5415
|
-
* lands in the database.
|
|
5354
|
+
* Silence would be the worst failure mode this library has: the schema says the field is
|
|
5355
|
+
* checked, the field is not checked, the form saves, and nothing says so. For
|
|
5356
|
+
* `customAsync` that is typically a uniqueness check, so the duplicate lands in the database.
|
|
5416
5357
|
*
|
|
5417
5358
|
* Dev only, once per name — this runs while controls are built, which happens on every
|
|
5418
5359
|
* config change. `validateConfig` with `knownValidators` is the build-time version of the
|
|
@@ -5430,10 +5371,10 @@ class ValidatorRegistryService {
|
|
|
5430
5371
|
this.warned.add(id);
|
|
5431
5372
|
console.warn(`[ngx-dynamic-entity] ${message}`);
|
|
5432
5373
|
}
|
|
5433
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
5434
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.
|
|
5374
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: ValidatorRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
5375
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: ValidatorRegistryService, providedIn: 'root' });
|
|
5435
5376
|
}
|
|
5436
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
5377
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: ValidatorRegistryService, decorators: [{
|
|
5437
5378
|
type: Injectable,
|
|
5438
5379
|
args: [{ providedIn: 'root' }]
|
|
5439
5380
|
}] });
|
|
@@ -5486,8 +5427,8 @@ class ArrayFieldComponent {
|
|
|
5486
5427
|
this.formArray.removeAt(index);
|
|
5487
5428
|
this.cdr.markForCheck();
|
|
5488
5429
|
}
|
|
5489
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
5490
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.
|
|
5430
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: ArrayFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
5431
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", 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: `
|
|
5491
5432
|
<div
|
|
5492
5433
|
class="ngx-field ngx-field--array"
|
|
5493
5434
|
[attr.data-testid]="'field-' + field.id"
|
|
@@ -5566,7 +5507,7 @@ class ArrayFieldComponent {
|
|
|
5566
5507
|
</div>
|
|
5567
5508
|
`, 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 });
|
|
5568
5509
|
}
|
|
5569
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
5510
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: ArrayFieldComponent, decorators: [{
|
|
5570
5511
|
type: Component,
|
|
5571
5512
|
args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'ngx-array-field', standalone: true, imports: [ReactiveFormsModule, forwardRef(() => DynamicFieldComponent)], template: `
|
|
5572
5513
|
<div
|
|
@@ -5740,10 +5681,10 @@ class FileUploadService {
|
|
|
5740
5681
|
value !== null &&
|
|
5741
5682
|
typeof value.subscribe === 'function');
|
|
5742
5683
|
}
|
|
5743
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
5744
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.
|
|
5684
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: FileUploadService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
5685
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: FileUploadService, providedIn: 'root' });
|
|
5745
5686
|
}
|
|
5746
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
5687
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: FileUploadService, decorators: [{
|
|
5747
5688
|
type: Injectable,
|
|
5748
5689
|
args: [{ providedIn: 'root' }]
|
|
5749
5690
|
}] });
|
|
@@ -5828,8 +5769,8 @@ class ImageFieldComponent {
|
|
|
5828
5769
|
return '';
|
|
5829
5770
|
return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
|
|
5830
5771
|
}
|
|
5831
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
5832
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.
|
|
5772
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: ImageFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
5773
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: ImageFieldComponent, isStandalone: true, selector: "ngx-image-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
5833
5774
|
<div
|
|
5834
5775
|
class="ngx-field ngx-field--image"
|
|
5835
5776
|
[attr.data-testid]="'field-' + field.id"
|
|
@@ -5908,7 +5849,7 @@ class ImageFieldComponent {
|
|
|
5908
5849
|
</div>
|
|
5909
5850
|
`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
5910
5851
|
}
|
|
5911
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
5852
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: ImageFieldComponent, decorators: [{
|
|
5912
5853
|
type: Component,
|
|
5913
5854
|
args: [{
|
|
5914
5855
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
@@ -6078,8 +6019,8 @@ class FileFieldComponent {
|
|
|
6078
6019
|
return '';
|
|
6079
6020
|
return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
|
|
6080
6021
|
}
|
|
6081
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
6082
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.
|
|
6022
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: FileFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
6023
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: FileFieldComponent, isStandalone: true, selector: "ngx-file-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
6083
6024
|
<div
|
|
6084
6025
|
class="ngx-field ngx-field--file"
|
|
6085
6026
|
[attr.data-testid]="'field-' + field.id"
|
|
@@ -6161,7 +6102,7 @@ class FileFieldComponent {
|
|
|
6161
6102
|
</div>
|
|
6162
6103
|
`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
6163
6104
|
}
|
|
6164
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
6105
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: FileFieldComponent, decorators: [{
|
|
6165
6106
|
type: Component,
|
|
6166
6107
|
args: [{
|
|
6167
6108
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
@@ -6263,220 +6204,1701 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
6263
6204
|
}] } });
|
|
6264
6205
|
|
|
6265
6206
|
/**
|
|
6266
|
-
*
|
|
6267
|
-
*
|
|
6268
|
-
* `FieldRegistryService` imports no components. Whatever you register here is what gets
|
|
6269
|
-
* bundled, so an app that only uses text/number/dropdown pays for three components and
|
|
6270
|
-
* their imports, not twenty-one.
|
|
6271
|
-
*
|
|
6272
|
-
* ```ts
|
|
6273
|
-
* // Everything (convenient; bundles all built-ins):
|
|
6274
|
-
* provideNgxDynamicEntity(), provideBuiltInFieldTypes()
|
|
6275
|
-
*
|
|
6276
|
-
* // Only what this app renders:
|
|
6277
|
-
* provideFieldTypes({ text: TextFieldComponent, number: NumberFieldComponent })
|
|
6278
|
-
* ```
|
|
6279
|
-
*
|
|
6280
|
-
* Both merge into `FIELD_TYPE_REGISTRY`; later registrations win on key collision.
|
|
6281
|
-
*/
|
|
6282
|
-
/** Map of every built-in field type key → component. Referencing this pulls in all of them. */
|
|
6283
|
-
function builtInFieldTypes() {
|
|
6284
|
-
return {
|
|
6285
|
-
text: TextFieldComponent,
|
|
6286
|
-
textarea: TextareaFieldComponent,
|
|
6287
|
-
markdown: MarkdownFieldComponent,
|
|
6288
|
-
number: NumberFieldComponent,
|
|
6289
|
-
currency: CurrencyFieldComponent,
|
|
6290
|
-
email: EmailFieldComponent,
|
|
6291
|
-
password: PasswordFieldComponent,
|
|
6292
|
-
checkbox: CheckboxFieldComponent,
|
|
6293
|
-
boolean: BooleanFieldComponent,
|
|
6294
|
-
date: DateFieldComponent,
|
|
6295
|
-
datetime: DateTimeFieldComponent,
|
|
6296
|
-
time: TimeFieldComponent,
|
|
6297
|
-
monthYear: MonthYearFieldComponent,
|
|
6298
|
-
dropdown: DropdownFieldComponent,
|
|
6299
|
-
radio: RadioFieldComponent,
|
|
6300
|
-
multiSelect: MultiSelectFieldComponent,
|
|
6301
|
-
'entity-ref': EntityRefFieldComponent,
|
|
6302
|
-
group: GroupFieldComponent,
|
|
6303
|
-
array: ArrayFieldComponent,
|
|
6304
|
-
image: ImageFieldComponent,
|
|
6305
|
-
file: FileFieldComponent,
|
|
6306
|
-
};
|
|
6307
|
-
}
|
|
6308
|
-
/** Register a specific set of field types. Only the components you name are bundled. */
|
|
6309
|
-
function provideFieldTypes(types) {
|
|
6310
|
-
return makeEnvironmentProviders([
|
|
6311
|
-
{ provide: FIELD_TYPE_SETS, useValue: types, multi: true },
|
|
6312
|
-
]);
|
|
6313
|
-
}
|
|
6314
|
-
/** Register all 21 built-in field type keys. Convenience for apps that use most of them. */
|
|
6315
|
-
function provideBuiltInFieldTypes() {
|
|
6316
|
-
return provideFieldTypes(builtInFieldTypes());
|
|
6317
|
-
}
|
|
6318
|
-
|
|
6319
|
-
/**
|
|
6320
|
-
* HookRegistryService — stores and runs Angular-side pre/post hooks.
|
|
6321
|
-
* Consumers register hooks via provideNgxDynamicEntity({ hooks: { 'clients:beforeSave': fn } }).
|
|
6322
|
-
*/
|
|
6323
|
-
class HookRegistryService {
|
|
6324
|
-
registry = inject(HOOK_REGISTRY, { optional: true }) ?? new Map();
|
|
6325
|
-
has(key) {
|
|
6326
|
-
return this.registry.has(key);
|
|
6327
|
-
}
|
|
6328
|
-
async run(key, data, context = {}) {
|
|
6329
|
-
const hook = this.registry.get(key);
|
|
6330
|
-
if (!hook)
|
|
6331
|
-
return data;
|
|
6332
|
-
return hook(data, context);
|
|
6333
|
-
}
|
|
6334
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: HookRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
6335
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: HookRegistryService, providedIn: 'root' });
|
|
6336
|
-
}
|
|
6337
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: HookRegistryService, decorators: [{
|
|
6338
|
-
type: Injectable,
|
|
6339
|
-
args: [{ providedIn: 'root' }]
|
|
6340
|
-
}] });
|
|
6341
|
-
|
|
6342
|
-
/**
|
|
6343
|
-
* RulesEvaluationService — evaluates rules, and owns what a result *means*.
|
|
6207
|
+
* URL field: a `type="url"` input, and a link when read-only.
|
|
6344
6208
|
*
|
|
6345
|
-
*
|
|
6346
|
-
*
|
|
6347
|
-
*
|
|
6348
|
-
*
|
|
6349
|
-
* `form.invalid` true forever with nothing on screen to explain it.
|
|
6350
|
-
*
|
|
6351
|
-
* Precedence lives here now, in one method that both callers go through. Still stateless —
|
|
6352
|
-
* every method takes the result it is interpreting, so there is nothing to go stale against a
|
|
6353
|
-
* rebuilt form and nothing to reset between configs.
|
|
6209
|
+
* The read-only link is rendered only for a value that passes `isValidUrl`, meaning an http or
|
|
6210
|
+
* https address. A record can hold anything, including `javascript:`, and a field whose job is
|
|
6211
|
+
* to show an address must not turn a stored script into something to click. Anything else is
|
|
6212
|
+
* shown as plain text.
|
|
6354
6213
|
*/
|
|
6355
|
-
class
|
|
6214
|
+
class UrlFieldComponent {
|
|
6356
6215
|
/**
|
|
6357
|
-
*
|
|
6216
|
+
* Unique to this component instance: an `array` renders the same field once per row, and a
|
|
6217
|
+
* DOM id may not repeat. See `field-dom-id.ts`.
|
|
6358
6218
|
*/
|
|
6359
|
-
|
|
6360
|
-
|
|
6219
|
+
instanceId = nextFieldInstanceId();
|
|
6220
|
+
domId(suffix = '') {
|
|
6221
|
+
return fieldDomId(this.field, this.instanceId, suffix);
|
|
6361
6222
|
}
|
|
6362
|
-
/**
|
|
6363
|
-
|
|
6364
|
-
|
|
6365
|
-
|
|
6366
|
-
|
|
6223
|
+
/** Overridable via MASKED_PLACEHOLDER; the default is the historic literal. */
|
|
6224
|
+
maskedText = inject(MASKED_PLACEHOLDER, { optional: true }) ?? 'XXXXXXXXX';
|
|
6225
|
+
messages = inject(ValidationMessagesService);
|
|
6226
|
+
field;
|
|
6227
|
+
control;
|
|
6228
|
+
language = 'en';
|
|
6229
|
+
readonly = false;
|
|
6230
|
+
masked = false;
|
|
6231
|
+
get label() {
|
|
6232
|
+
return resolveLabel(this.field?.label, this.language);
|
|
6367
6233
|
}
|
|
6368
|
-
|
|
6369
|
-
|
|
6370
|
-
*
|
|
6371
|
-
* An explicit hide beats everything. A show beats static hiding — `visibility: false`, or a
|
|
6372
|
-
* `showWhen` that does not currently hold — because a rule that could only hide what the
|
|
6373
|
-
* config already hides would have nothing to say, and `{ visibility: true }` was a
|
|
6374
|
-
* documented no-op until it did. Otherwise the config's own visibility decides.
|
|
6375
|
-
*/
|
|
6376
|
-
isFieldVisible(result, field, names, values) {
|
|
6377
|
-
const sets = this.nameSets(result);
|
|
6378
|
-
if (names.some(name => sets.hidden.has(name)))
|
|
6379
|
-
return false;
|
|
6380
|
-
if (names.some(name => sets.shown.has(name)))
|
|
6381
|
-
return true;
|
|
6382
|
-
return evaluateFieldVisibility(field, values);
|
|
6234
|
+
get placeholder() {
|
|
6235
|
+
return resolveLabel(this.field?.placeholder, this.language);
|
|
6383
6236
|
}
|
|
6384
|
-
/**
|
|
6385
|
-
|
|
6386
|
-
|
|
6387
|
-
return false;
|
|
6388
|
-
return tab.visibility !== false || result.shownTabs.includes(tab.id);
|
|
6237
|
+
/** Author help text, shown under the control and named by `aria-describedby`. */
|
|
6238
|
+
get hint() {
|
|
6239
|
+
return resolveLabel(this.field?.hint, this.language);
|
|
6389
6240
|
}
|
|
6390
|
-
/**
|
|
6391
|
-
|
|
6392
|
-
|
|
6393
|
-
|
|
6394
|
-
* validators stay attached — so a hidden *required* field holds `form.invalid` true
|
|
6395
|
-
* forever, permanently disabling Save with nothing on screen to explain why.
|
|
6396
|
-
*
|
|
6397
|
-
* Disabling is the fix rather than stripping validators: Angular excludes disabled controls
|
|
6398
|
-
* from validity, and the field's own validators survive intact for when it comes back.
|
|
6399
|
-
* Values are untouched — a disabled control still carries its value, so rule evaluation and
|
|
6400
|
-
* the emitted record see exactly what they saw before. That also keeps this from feeding
|
|
6401
|
-
* back on itself: hiding a field cannot change the values the rules are evaluated over.
|
|
6402
|
-
*
|
|
6403
|
-
* The predicate is `isFieldVisible`, the same one the render filter uses, so what is on
|
|
6404
|
-
* screen and what counts toward validity cannot drift apart.
|
|
6405
|
-
*
|
|
6406
|
-
* Entries must arrive parent-first, which is what `collectFieldScopes` yields: a container's
|
|
6407
|
-
* children are skipped once the container itself is hidden, because Angular's `disable()`
|
|
6408
|
-
* cascades to descendants and a child's `enable()` walks back up recalculating ancestors.
|
|
6409
|
-
* Toggling a child inside a hidden group would fight its parent.
|
|
6410
|
-
*/
|
|
6411
|
-
syncHiddenFieldState(entries, ctx) {
|
|
6412
|
-
const hiddenContainers = [];
|
|
6413
|
-
for (const entry of entries) {
|
|
6414
|
-
const field = entry.field;
|
|
6415
|
-
if (!field?.id)
|
|
6416
|
-
continue;
|
|
6417
|
-
const address = ctx.addressOf(field);
|
|
6418
|
-
if (hiddenContainers.some(prefix => address.startsWith(`${prefix}.`)))
|
|
6419
|
-
continue;
|
|
6420
|
-
const hidden = !this.isFieldVisible(ctx.result, field, ctx.namesOf(field), ctx.values);
|
|
6421
|
-
if (hidden && (field.type === 'group' || field.type === 'array'))
|
|
6422
|
-
hiddenContainers.push(address);
|
|
6423
|
-
const ctrl = ctx.controlFor(entry);
|
|
6424
|
-
if (!ctrl)
|
|
6425
|
-
continue;
|
|
6426
|
-
if (hidden) {
|
|
6427
|
-
if (ctrl.enabled)
|
|
6428
|
-
ctrl.disable({ emitEvent: false });
|
|
6429
|
-
}
|
|
6430
|
-
else if (ctrl.disabled && !field.disabled) {
|
|
6431
|
-
ctrl.enable({ emitEvent: false });
|
|
6432
|
-
}
|
|
6433
|
-
}
|
|
6241
|
+
/** Whether the stored value is an http(s) address, and so safe to render as a link. */
|
|
6242
|
+
get linkable() {
|
|
6243
|
+
const value = this.control?.value;
|
|
6244
|
+
return typeof value === 'string' && isValidUrl(value);
|
|
6434
6245
|
}
|
|
6435
|
-
/**
|
|
6436
|
-
|
|
6437
|
-
|
|
6438
|
-
* `isFieldVisible` runs once per field per change-detection pass; building two sets from
|
|
6439
|
-
* two arrays each time turns a rule result into allocation proportional to the form. A
|
|
6440
|
-
* `WeakMap` keyed by the result means it is built once per evaluation and collected with it.
|
|
6441
|
-
*/
|
|
6442
|
-
nameSets(result) {
|
|
6443
|
-
let sets = this.nameSetCache.get(result);
|
|
6444
|
-
if (!sets) {
|
|
6445
|
-
sets = { hidden: new Set(result.hiddenFields), shown: new Set(result.shownFields) };
|
|
6446
|
-
this.nameSetCache.set(result, sets);
|
|
6447
|
-
}
|
|
6448
|
-
return sets;
|
|
6246
|
+
/** The ids of whatever is describing this control right now. */
|
|
6247
|
+
describedBy() {
|
|
6248
|
+
return fieldDescribedBy(s => this.domId(s), { hint: !!this.hint, error: !!this.errorMessage });
|
|
6449
6249
|
}
|
|
6450
|
-
|
|
6451
|
-
|
|
6452
|
-
|
|
6453
|
-
|
|
6454
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: RulesEvaluationService, decorators: [{
|
|
6455
|
-
type: Injectable,
|
|
6456
|
-
args: [{ providedIn: 'root' }]
|
|
6457
|
-
}] });
|
|
6458
|
-
|
|
6459
|
-
class ConfigSourceService {
|
|
6460
|
-
cache = new Map();
|
|
6461
|
-
inFlight = new Map();
|
|
6462
|
-
handler;
|
|
6463
|
-
constructor(handler) {
|
|
6464
|
-
this.handler = handler;
|
|
6250
|
+
get errorMessage() {
|
|
6251
|
+
if (!this.control?.errors || !this.control.touched)
|
|
6252
|
+
return '';
|
|
6253
|
+
return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
|
|
6465
6254
|
}
|
|
6466
|
-
|
|
6467
|
-
|
|
6468
|
-
|
|
6469
|
-
|
|
6470
|
-
|
|
6471
|
-
|
|
6472
|
-
|
|
6473
|
-
|
|
6474
|
-
|
|
6475
|
-
|
|
6476
|
-
|
|
6477
|
-
|
|
6478
|
-
|
|
6479
|
-
|
|
6255
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: UrlFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
6256
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: UrlFieldComponent, isStandalone: true, selector: "ngx-url-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
6257
|
+
<div
|
|
6258
|
+
class="ngx-field ngx-field--url"
|
|
6259
|
+
[attr.data-testid]="'field-' + field.id"
|
|
6260
|
+
[attr.data-field-type]="field.type"
|
|
6261
|
+
[class.ngx-field--readonly]="readonly"
|
|
6262
|
+
[class.ngx-field--masked]="masked"
|
|
6263
|
+
[class.ngx-field--invalid]="control && control.invalid && control.touched"
|
|
6264
|
+
>
|
|
6265
|
+
<div class="ngx-field__label-row">
|
|
6266
|
+
<label class="ngx-field__label" [attr.for]="domId()">
|
|
6267
|
+
{{ label }}
|
|
6268
|
+
@if (field.validators?.required) {
|
|
6269
|
+
<span class="ngx-field__req">*</span>
|
|
6270
|
+
}
|
|
6271
|
+
</label>
|
|
6272
|
+
@if (hint) {
|
|
6273
|
+
<span class="ngx-field__hint-wrap">
|
|
6274
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
6275
|
+
<span
|
|
6276
|
+
class="ngx-field__hint"
|
|
6277
|
+
role="tooltip"
|
|
6278
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
6279
|
+
[id]="domId('-hint')"
|
|
6280
|
+
>{{ hint }}</span
|
|
6281
|
+
>
|
|
6282
|
+
</span>
|
|
6283
|
+
}
|
|
6284
|
+
</div>
|
|
6285
|
+
@if (masked) {
|
|
6286
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
6287
|
+
maskedText
|
|
6288
|
+
}}</span>
|
|
6289
|
+
} @else if (readonly) {
|
|
6290
|
+
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">
|
|
6291
|
+
@if (linkable) {
|
|
6292
|
+
<a [href]="control.value" class="ngx-field__link" target="_blank" rel="noopener noreferrer">{{
|
|
6293
|
+
control.value
|
|
6294
|
+
}}</a>
|
|
6295
|
+
} @else {
|
|
6296
|
+
{{ control.value || '—' }}
|
|
6297
|
+
}
|
|
6298
|
+
</span>
|
|
6299
|
+
} @else {
|
|
6300
|
+
<input
|
|
6301
|
+
[id]="domId()"
|
|
6302
|
+
class="ngx-field__input"
|
|
6303
|
+
[attr.data-testid]="'field-' + field.id + '-input'"
|
|
6304
|
+
type="url"
|
|
6305
|
+
inputmode="url"
|
|
6306
|
+
autocomplete="url"
|
|
6307
|
+
[formControl]="$any(control)"
|
|
6308
|
+
[attr.aria-invalid]="control.invalid && control.touched"
|
|
6309
|
+
[attr.aria-describedby]="describedBy()"
|
|
6310
|
+
[placeholder]="placeholder || 'https://example.com'"
|
|
6311
|
+
[attr.disabled]="field.disabled ? true : null"
|
|
6312
|
+
/>
|
|
6313
|
+
@if (errorMessage) {
|
|
6314
|
+
<span
|
|
6315
|
+
class="ngx-field__error"
|
|
6316
|
+
[attr.data-testid]="'field-' + field.id + '-error'"
|
|
6317
|
+
[id]="domId('-error')"
|
|
6318
|
+
role="alert"
|
|
6319
|
+
>{{ errorMessage }}</span
|
|
6320
|
+
>
|
|
6321
|
+
}
|
|
6322
|
+
}
|
|
6323
|
+
</div>
|
|
6324
|
+
`, 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 });
|
|
6325
|
+
}
|
|
6326
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: UrlFieldComponent, decorators: [{
|
|
6327
|
+
type: Component,
|
|
6328
|
+
args: [{
|
|
6329
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
6330
|
+
selector: 'ngx-url-field',
|
|
6331
|
+
standalone: true,
|
|
6332
|
+
imports: [ReactiveFormsModule],
|
|
6333
|
+
template: `
|
|
6334
|
+
<div
|
|
6335
|
+
class="ngx-field ngx-field--url"
|
|
6336
|
+
[attr.data-testid]="'field-' + field.id"
|
|
6337
|
+
[attr.data-field-type]="field.type"
|
|
6338
|
+
[class.ngx-field--readonly]="readonly"
|
|
6339
|
+
[class.ngx-field--masked]="masked"
|
|
6340
|
+
[class.ngx-field--invalid]="control && control.invalid && control.touched"
|
|
6341
|
+
>
|
|
6342
|
+
<div class="ngx-field__label-row">
|
|
6343
|
+
<label class="ngx-field__label" [attr.for]="domId()">
|
|
6344
|
+
{{ label }}
|
|
6345
|
+
@if (field.validators?.required) {
|
|
6346
|
+
<span class="ngx-field__req">*</span>
|
|
6347
|
+
}
|
|
6348
|
+
</label>
|
|
6349
|
+
@if (hint) {
|
|
6350
|
+
<span class="ngx-field__hint-wrap">
|
|
6351
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
6352
|
+
<span
|
|
6353
|
+
class="ngx-field__hint"
|
|
6354
|
+
role="tooltip"
|
|
6355
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
6356
|
+
[id]="domId('-hint')"
|
|
6357
|
+
>{{ hint }}</span
|
|
6358
|
+
>
|
|
6359
|
+
</span>
|
|
6360
|
+
}
|
|
6361
|
+
</div>
|
|
6362
|
+
@if (masked) {
|
|
6363
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
6364
|
+
maskedText
|
|
6365
|
+
}}</span>
|
|
6366
|
+
} @else if (readonly) {
|
|
6367
|
+
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">
|
|
6368
|
+
@if (linkable) {
|
|
6369
|
+
<a [href]="control.value" class="ngx-field__link" target="_blank" rel="noopener noreferrer">{{
|
|
6370
|
+
control.value
|
|
6371
|
+
}}</a>
|
|
6372
|
+
} @else {
|
|
6373
|
+
{{ control.value || '—' }}
|
|
6374
|
+
}
|
|
6375
|
+
</span>
|
|
6376
|
+
} @else {
|
|
6377
|
+
<input
|
|
6378
|
+
[id]="domId()"
|
|
6379
|
+
class="ngx-field__input"
|
|
6380
|
+
[attr.data-testid]="'field-' + field.id + '-input'"
|
|
6381
|
+
type="url"
|
|
6382
|
+
inputmode="url"
|
|
6383
|
+
autocomplete="url"
|
|
6384
|
+
[formControl]="$any(control)"
|
|
6385
|
+
[attr.aria-invalid]="control.invalid && control.touched"
|
|
6386
|
+
[attr.aria-describedby]="describedBy()"
|
|
6387
|
+
[placeholder]="placeholder || 'https://example.com'"
|
|
6388
|
+
[attr.disabled]="field.disabled ? true : null"
|
|
6389
|
+
/>
|
|
6390
|
+
@if (errorMessage) {
|
|
6391
|
+
<span
|
|
6392
|
+
class="ngx-field__error"
|
|
6393
|
+
[attr.data-testid]="'field-' + field.id + '-error'"
|
|
6394
|
+
[id]="domId('-error')"
|
|
6395
|
+
role="alert"
|
|
6396
|
+
>{{ errorMessage }}</span
|
|
6397
|
+
>
|
|
6398
|
+
}
|
|
6399
|
+
}
|
|
6400
|
+
</div>
|
|
6401
|
+
`,
|
|
6402
|
+
}]
|
|
6403
|
+
}], propDecorators: { field: [{
|
|
6404
|
+
type: Input
|
|
6405
|
+
}], control: [{
|
|
6406
|
+
type: Input
|
|
6407
|
+
}], language: [{
|
|
6408
|
+
type: Input
|
|
6409
|
+
}], readonly: [{
|
|
6410
|
+
type: Input
|
|
6411
|
+
}], masked: [{
|
|
6412
|
+
type: Input
|
|
6413
|
+
}] } });
|
|
6414
|
+
|
|
6415
|
+
/**
|
|
6416
|
+
* Phone field: a `type="tel"` input, and a `tel:` link when read-only.
|
|
6417
|
+
*
|
|
6418
|
+
* The value is stored exactly as typed, spaces and brackets included. The number is shown back
|
|
6419
|
+
* to people far more often than it is dialled, and `+44 20 7946 0958` is easier to read than
|
|
6420
|
+
* its digits run together. Only the link target is stripped down to what a dialler needs.
|
|
6421
|
+
*/
|
|
6422
|
+
class PhoneFieldComponent {
|
|
6423
|
+
/**
|
|
6424
|
+
* Unique to this component instance: an `array` renders the same field once per row, and a
|
|
6425
|
+
* DOM id may not repeat. See `field-dom-id.ts`.
|
|
6426
|
+
*/
|
|
6427
|
+
instanceId = nextFieldInstanceId();
|
|
6428
|
+
domId(suffix = '') {
|
|
6429
|
+
return fieldDomId(this.field, this.instanceId, suffix);
|
|
6430
|
+
}
|
|
6431
|
+
/** Overridable via MASKED_PLACEHOLDER; the default is the historic literal. */
|
|
6432
|
+
maskedText = inject(MASKED_PLACEHOLDER, { optional: true }) ?? 'XXXXXXXXX';
|
|
6433
|
+
messages = inject(ValidationMessagesService);
|
|
6434
|
+
field;
|
|
6435
|
+
control;
|
|
6436
|
+
language = 'en';
|
|
6437
|
+
readonly = false;
|
|
6438
|
+
masked = false;
|
|
6439
|
+
get label() {
|
|
6440
|
+
return resolveLabel(this.field?.label, this.language);
|
|
6441
|
+
}
|
|
6442
|
+
get placeholder() {
|
|
6443
|
+
return resolveLabel(this.field?.placeholder, this.language);
|
|
6444
|
+
}
|
|
6445
|
+
/** Author help text, shown under the control and named by `aria-describedby`. */
|
|
6446
|
+
get hint() {
|
|
6447
|
+
return resolveLabel(this.field?.hint, this.language);
|
|
6448
|
+
}
|
|
6449
|
+
/** `tel:` plus the digits and a leading `+`, or null for a value that is not a phone number. */
|
|
6450
|
+
get dialHref() {
|
|
6451
|
+
const value = this.control?.value;
|
|
6452
|
+
if (typeof value !== 'string' || !isValidPhone(value))
|
|
6453
|
+
return null;
|
|
6454
|
+
return `tel:${value.trim().startsWith('+') ? '+' : ''}${value.replace(/\D/g, '')}`;
|
|
6455
|
+
}
|
|
6456
|
+
/** The ids of whatever is describing this control right now. */
|
|
6457
|
+
describedBy() {
|
|
6458
|
+
return fieldDescribedBy(s => this.domId(s), { hint: !!this.hint, error: !!this.errorMessage });
|
|
6459
|
+
}
|
|
6460
|
+
get errorMessage() {
|
|
6461
|
+
if (!this.control?.errors || !this.control.touched)
|
|
6462
|
+
return '';
|
|
6463
|
+
return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
|
|
6464
|
+
}
|
|
6465
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: PhoneFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
6466
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: PhoneFieldComponent, isStandalone: true, selector: "ngx-phone-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
6467
|
+
<div
|
|
6468
|
+
class="ngx-field ngx-field--phone"
|
|
6469
|
+
[attr.data-testid]="'field-' + field.id"
|
|
6470
|
+
[attr.data-field-type]="field.type"
|
|
6471
|
+
[class.ngx-field--readonly]="readonly"
|
|
6472
|
+
[class.ngx-field--masked]="masked"
|
|
6473
|
+
[class.ngx-field--invalid]="control && control.invalid && control.touched"
|
|
6474
|
+
>
|
|
6475
|
+
<div class="ngx-field__label-row">
|
|
6476
|
+
<label class="ngx-field__label" [attr.for]="domId()">
|
|
6477
|
+
{{ label }}
|
|
6478
|
+
@if (field.validators?.required) {
|
|
6479
|
+
<span class="ngx-field__req">*</span>
|
|
6480
|
+
}
|
|
6481
|
+
</label>
|
|
6482
|
+
@if (hint) {
|
|
6483
|
+
<span class="ngx-field__hint-wrap">
|
|
6484
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
6485
|
+
<span
|
|
6486
|
+
class="ngx-field__hint"
|
|
6487
|
+
role="tooltip"
|
|
6488
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
6489
|
+
[id]="domId('-hint')"
|
|
6490
|
+
>{{ hint }}</span
|
|
6491
|
+
>
|
|
6492
|
+
</span>
|
|
6493
|
+
}
|
|
6494
|
+
</div>
|
|
6495
|
+
@if (masked) {
|
|
6496
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
6497
|
+
maskedText
|
|
6498
|
+
}}</span>
|
|
6499
|
+
} @else if (readonly) {
|
|
6500
|
+
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">
|
|
6501
|
+
@if (dialHref) {
|
|
6502
|
+
<a [href]="dialHref" class="ngx-field__link">{{ control.value }}</a>
|
|
6503
|
+
} @else {
|
|
6504
|
+
{{ control.value || '—' }}
|
|
6505
|
+
}
|
|
6506
|
+
</span>
|
|
6507
|
+
} @else {
|
|
6508
|
+
<input
|
|
6509
|
+
[id]="domId()"
|
|
6510
|
+
class="ngx-field__input"
|
|
6511
|
+
[attr.data-testid]="'field-' + field.id + '-input'"
|
|
6512
|
+
type="tel"
|
|
6513
|
+
inputmode="tel"
|
|
6514
|
+
autocomplete="tel"
|
|
6515
|
+
[formControl]="$any(control)"
|
|
6516
|
+
[attr.aria-invalid]="control.invalid && control.touched"
|
|
6517
|
+
[attr.aria-describedby]="describedBy()"
|
|
6518
|
+
[placeholder]="placeholder"
|
|
6519
|
+
[attr.disabled]="field.disabled ? true : null"
|
|
6520
|
+
/>
|
|
6521
|
+
@if (errorMessage) {
|
|
6522
|
+
<span
|
|
6523
|
+
class="ngx-field__error"
|
|
6524
|
+
[attr.data-testid]="'field-' + field.id + '-error'"
|
|
6525
|
+
[id]="domId('-error')"
|
|
6526
|
+
role="alert"
|
|
6527
|
+
>{{ errorMessage }}</span
|
|
6528
|
+
>
|
|
6529
|
+
}
|
|
6530
|
+
}
|
|
6531
|
+
</div>
|
|
6532
|
+
`, 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 });
|
|
6533
|
+
}
|
|
6534
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: PhoneFieldComponent, decorators: [{
|
|
6535
|
+
type: Component,
|
|
6536
|
+
args: [{
|
|
6537
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
6538
|
+
selector: 'ngx-phone-field',
|
|
6539
|
+
standalone: true,
|
|
6540
|
+
imports: [ReactiveFormsModule],
|
|
6541
|
+
template: `
|
|
6542
|
+
<div
|
|
6543
|
+
class="ngx-field ngx-field--phone"
|
|
6544
|
+
[attr.data-testid]="'field-' + field.id"
|
|
6545
|
+
[attr.data-field-type]="field.type"
|
|
6546
|
+
[class.ngx-field--readonly]="readonly"
|
|
6547
|
+
[class.ngx-field--masked]="masked"
|
|
6548
|
+
[class.ngx-field--invalid]="control && control.invalid && control.touched"
|
|
6549
|
+
>
|
|
6550
|
+
<div class="ngx-field__label-row">
|
|
6551
|
+
<label class="ngx-field__label" [attr.for]="domId()">
|
|
6552
|
+
{{ label }}
|
|
6553
|
+
@if (field.validators?.required) {
|
|
6554
|
+
<span class="ngx-field__req">*</span>
|
|
6555
|
+
}
|
|
6556
|
+
</label>
|
|
6557
|
+
@if (hint) {
|
|
6558
|
+
<span class="ngx-field__hint-wrap">
|
|
6559
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
6560
|
+
<span
|
|
6561
|
+
class="ngx-field__hint"
|
|
6562
|
+
role="tooltip"
|
|
6563
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
6564
|
+
[id]="domId('-hint')"
|
|
6565
|
+
>{{ hint }}</span
|
|
6566
|
+
>
|
|
6567
|
+
</span>
|
|
6568
|
+
}
|
|
6569
|
+
</div>
|
|
6570
|
+
@if (masked) {
|
|
6571
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
6572
|
+
maskedText
|
|
6573
|
+
}}</span>
|
|
6574
|
+
} @else if (readonly) {
|
|
6575
|
+
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">
|
|
6576
|
+
@if (dialHref) {
|
|
6577
|
+
<a [href]="dialHref" class="ngx-field__link">{{ control.value }}</a>
|
|
6578
|
+
} @else {
|
|
6579
|
+
{{ control.value || '—' }}
|
|
6580
|
+
}
|
|
6581
|
+
</span>
|
|
6582
|
+
} @else {
|
|
6583
|
+
<input
|
|
6584
|
+
[id]="domId()"
|
|
6585
|
+
class="ngx-field__input"
|
|
6586
|
+
[attr.data-testid]="'field-' + field.id + '-input'"
|
|
6587
|
+
type="tel"
|
|
6588
|
+
inputmode="tel"
|
|
6589
|
+
autocomplete="tel"
|
|
6590
|
+
[formControl]="$any(control)"
|
|
6591
|
+
[attr.aria-invalid]="control.invalid && control.touched"
|
|
6592
|
+
[attr.aria-describedby]="describedBy()"
|
|
6593
|
+
[placeholder]="placeholder"
|
|
6594
|
+
[attr.disabled]="field.disabled ? true : null"
|
|
6595
|
+
/>
|
|
6596
|
+
@if (errorMessage) {
|
|
6597
|
+
<span
|
|
6598
|
+
class="ngx-field__error"
|
|
6599
|
+
[attr.data-testid]="'field-' + field.id + '-error'"
|
|
6600
|
+
[id]="domId('-error')"
|
|
6601
|
+
role="alert"
|
|
6602
|
+
>{{ errorMessage }}</span
|
|
6603
|
+
>
|
|
6604
|
+
}
|
|
6605
|
+
}
|
|
6606
|
+
</div>
|
|
6607
|
+
`,
|
|
6608
|
+
}]
|
|
6609
|
+
}], propDecorators: { field: [{
|
|
6610
|
+
type: Input
|
|
6611
|
+
}], control: [{
|
|
6612
|
+
type: Input
|
|
6613
|
+
}], language: [{
|
|
6614
|
+
type: Input
|
|
6615
|
+
}], readonly: [{
|
|
6616
|
+
type: Input
|
|
6617
|
+
}], masked: [{
|
|
6618
|
+
type: Input
|
|
6619
|
+
}] } });
|
|
6620
|
+
|
|
6621
|
+
/**
|
|
6622
|
+
* Slider field: a range input over `validators.min`–`validators.max` in steps of `field.step`.
|
|
6623
|
+
*
|
|
6624
|
+
* A range input cannot show that it is empty. With no value it puts the thumb at the midpoint,
|
|
6625
|
+
* which looks exactly like a chosen value. The readout beside it is therefore the value itself,
|
|
6626
|
+
* and shows a dash until someone moves the thumb. Without it, a required slider nobody touched
|
|
6627
|
+
* would look answered and still fail at save.
|
|
6628
|
+
*/
|
|
6629
|
+
class SliderFieldComponent {
|
|
6630
|
+
/**
|
|
6631
|
+
* Unique to this component instance: an `array` renders the same field once per row, and a
|
|
6632
|
+
* DOM id may not repeat. See `field-dom-id.ts`.
|
|
6633
|
+
*/
|
|
6634
|
+
instanceId = nextFieldInstanceId();
|
|
6635
|
+
domId(suffix = '') {
|
|
6636
|
+
return fieldDomId(this.field, this.instanceId, suffix);
|
|
6637
|
+
}
|
|
6638
|
+
/** Overridable via MASKED_PLACEHOLDER; the default is the historic literal. */
|
|
6639
|
+
maskedText = inject(MASKED_PLACEHOLDER, { optional: true }) ?? 'XXXXXXXXX';
|
|
6640
|
+
messages = inject(ValidationMessagesService);
|
|
6641
|
+
field;
|
|
6642
|
+
control;
|
|
6643
|
+
language = 'en';
|
|
6644
|
+
readonly = false;
|
|
6645
|
+
masked = false;
|
|
6646
|
+
get label() {
|
|
6647
|
+
return resolveLabel(this.field?.label, this.language);
|
|
6648
|
+
}
|
|
6649
|
+
/** Author help text, shown under the control and named by `aria-describedby`. */
|
|
6650
|
+
get hint() {
|
|
6651
|
+
return resolveLabel(this.field?.hint, this.language);
|
|
6652
|
+
}
|
|
6653
|
+
get bounds() {
|
|
6654
|
+
return sliderBounds(this.field);
|
|
6655
|
+
}
|
|
6656
|
+
/** Whether the control holds a number. The thumb's position cannot say. */
|
|
6657
|
+
get hasValue() {
|
|
6658
|
+
const value = this.control?.value;
|
|
6659
|
+
return value !== null && value !== undefined && value !== '' && Number.isFinite(Number(value));
|
|
6660
|
+
}
|
|
6661
|
+
get display() {
|
|
6662
|
+
return this.hasValue ? String(this.control.value) : '—';
|
|
6663
|
+
}
|
|
6664
|
+
/** The ids of whatever is describing this control right now. */
|
|
6665
|
+
describedBy() {
|
|
6666
|
+
return fieldDescribedBy(s => this.domId(s), { hint: !!this.hint, error: !!this.errorMessage });
|
|
6667
|
+
}
|
|
6668
|
+
get errorMessage() {
|
|
6669
|
+
if (!this.control?.errors || !this.control.touched)
|
|
6670
|
+
return '';
|
|
6671
|
+
return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
|
|
6672
|
+
}
|
|
6673
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SliderFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
6674
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: SliderFieldComponent, isStandalone: true, selector: "ngx-slider-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
6675
|
+
<div
|
|
6676
|
+
class="ngx-field ngx-field--slider"
|
|
6677
|
+
[attr.data-testid]="'field-' + field.id"
|
|
6678
|
+
[attr.data-field-type]="field.type"
|
|
6679
|
+
[class.ngx-field--readonly]="readonly"
|
|
6680
|
+
[class.ngx-field--masked]="masked"
|
|
6681
|
+
[class.ngx-field--invalid]="control && control.invalid && control.touched"
|
|
6682
|
+
>
|
|
6683
|
+
<div class="ngx-field__label-row">
|
|
6684
|
+
<label class="ngx-field__label" [attr.for]="domId()">
|
|
6685
|
+
{{ label }}
|
|
6686
|
+
@if (field.validators?.required) {
|
|
6687
|
+
<span class="ngx-field__req">*</span>
|
|
6688
|
+
}
|
|
6689
|
+
</label>
|
|
6690
|
+
@if (hint) {
|
|
6691
|
+
<span class="ngx-field__hint-wrap">
|
|
6692
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
6693
|
+
<span
|
|
6694
|
+
class="ngx-field__hint"
|
|
6695
|
+
role="tooltip"
|
|
6696
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
6697
|
+
[id]="domId('-hint')"
|
|
6698
|
+
>{{ hint }}</span
|
|
6699
|
+
>
|
|
6700
|
+
</span>
|
|
6701
|
+
}
|
|
6702
|
+
</div>
|
|
6703
|
+
@if (masked) {
|
|
6704
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
6705
|
+
maskedText
|
|
6706
|
+
}}</span>
|
|
6707
|
+
} @else if (readonly) {
|
|
6708
|
+
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ display }}</span>
|
|
6709
|
+
} @else {
|
|
6710
|
+
<div class="ngx-field__slider-wrap">
|
|
6711
|
+
<input
|
|
6712
|
+
[id]="domId()"
|
|
6713
|
+
type="range"
|
|
6714
|
+
class="ngx-field__slider"
|
|
6715
|
+
[class.ngx-field__slider--unset]="!hasValue"
|
|
6716
|
+
[attr.data-testid]="'field-' + field.id + '-input'"
|
|
6717
|
+
[min]="bounds.min"
|
|
6718
|
+
[max]="bounds.max"
|
|
6719
|
+
[step]="bounds.step"
|
|
6720
|
+
[formControl]="$any(control)"
|
|
6721
|
+
[attr.aria-valuetext]="hasValue ? null : display"
|
|
6722
|
+
[attr.aria-invalid]="control.invalid && control.touched"
|
|
6723
|
+
[attr.aria-describedby]="describedBy()"
|
|
6724
|
+
[attr.disabled]="field.disabled ? true : null"
|
|
6725
|
+
/>
|
|
6726
|
+
<output
|
|
6727
|
+
class="ngx-field__slider-value"
|
|
6728
|
+
[attr.for]="domId()"
|
|
6729
|
+
[attr.data-testid]="'field-' + field.id + '-readout'"
|
|
6730
|
+
aria-hidden="true"
|
|
6731
|
+
>{{ display }}</output
|
|
6732
|
+
>
|
|
6733
|
+
</div>
|
|
6734
|
+
@if (errorMessage) {
|
|
6735
|
+
<span
|
|
6736
|
+
class="ngx-field__error"
|
|
6737
|
+
[attr.data-testid]="'field-' + field.id + '-error'"
|
|
6738
|
+
[id]="domId('-error')"
|
|
6739
|
+
role="alert"
|
|
6740
|
+
>{{ errorMessage }}</span
|
|
6741
|
+
>
|
|
6742
|
+
}
|
|
6743
|
+
}
|
|
6744
|
+
</div>
|
|
6745
|
+
`, 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.RangeValueAccessor, selector: "input[type=range][formControlName],input[type=range][formControl],input[type=range][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 });
|
|
6746
|
+
}
|
|
6747
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SliderFieldComponent, decorators: [{
|
|
6748
|
+
type: Component,
|
|
6749
|
+
args: [{
|
|
6750
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
6751
|
+
selector: 'ngx-slider-field',
|
|
6752
|
+
standalone: true,
|
|
6753
|
+
imports: [ReactiveFormsModule],
|
|
6754
|
+
template: `
|
|
6755
|
+
<div
|
|
6756
|
+
class="ngx-field ngx-field--slider"
|
|
6757
|
+
[attr.data-testid]="'field-' + field.id"
|
|
6758
|
+
[attr.data-field-type]="field.type"
|
|
6759
|
+
[class.ngx-field--readonly]="readonly"
|
|
6760
|
+
[class.ngx-field--masked]="masked"
|
|
6761
|
+
[class.ngx-field--invalid]="control && control.invalid && control.touched"
|
|
6762
|
+
>
|
|
6763
|
+
<div class="ngx-field__label-row">
|
|
6764
|
+
<label class="ngx-field__label" [attr.for]="domId()">
|
|
6765
|
+
{{ label }}
|
|
6766
|
+
@if (field.validators?.required) {
|
|
6767
|
+
<span class="ngx-field__req">*</span>
|
|
6768
|
+
}
|
|
6769
|
+
</label>
|
|
6770
|
+
@if (hint) {
|
|
6771
|
+
<span class="ngx-field__hint-wrap">
|
|
6772
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
6773
|
+
<span
|
|
6774
|
+
class="ngx-field__hint"
|
|
6775
|
+
role="tooltip"
|
|
6776
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
6777
|
+
[id]="domId('-hint')"
|
|
6778
|
+
>{{ hint }}</span
|
|
6779
|
+
>
|
|
6780
|
+
</span>
|
|
6781
|
+
}
|
|
6782
|
+
</div>
|
|
6783
|
+
@if (masked) {
|
|
6784
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
6785
|
+
maskedText
|
|
6786
|
+
}}</span>
|
|
6787
|
+
} @else if (readonly) {
|
|
6788
|
+
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ display }}</span>
|
|
6789
|
+
} @else {
|
|
6790
|
+
<div class="ngx-field__slider-wrap">
|
|
6791
|
+
<input
|
|
6792
|
+
[id]="domId()"
|
|
6793
|
+
type="range"
|
|
6794
|
+
class="ngx-field__slider"
|
|
6795
|
+
[class.ngx-field__slider--unset]="!hasValue"
|
|
6796
|
+
[attr.data-testid]="'field-' + field.id + '-input'"
|
|
6797
|
+
[min]="bounds.min"
|
|
6798
|
+
[max]="bounds.max"
|
|
6799
|
+
[step]="bounds.step"
|
|
6800
|
+
[formControl]="$any(control)"
|
|
6801
|
+
[attr.aria-valuetext]="hasValue ? null : display"
|
|
6802
|
+
[attr.aria-invalid]="control.invalid && control.touched"
|
|
6803
|
+
[attr.aria-describedby]="describedBy()"
|
|
6804
|
+
[attr.disabled]="field.disabled ? true : null"
|
|
6805
|
+
/>
|
|
6806
|
+
<output
|
|
6807
|
+
class="ngx-field__slider-value"
|
|
6808
|
+
[attr.for]="domId()"
|
|
6809
|
+
[attr.data-testid]="'field-' + field.id + '-readout'"
|
|
6810
|
+
aria-hidden="true"
|
|
6811
|
+
>{{ display }}</output
|
|
6812
|
+
>
|
|
6813
|
+
</div>
|
|
6814
|
+
@if (errorMessage) {
|
|
6815
|
+
<span
|
|
6816
|
+
class="ngx-field__error"
|
|
6817
|
+
[attr.data-testid]="'field-' + field.id + '-error'"
|
|
6818
|
+
[id]="domId('-error')"
|
|
6819
|
+
role="alert"
|
|
6820
|
+
>{{ errorMessage }}</span
|
|
6821
|
+
>
|
|
6822
|
+
}
|
|
6823
|
+
}
|
|
6824
|
+
</div>
|
|
6825
|
+
`,
|
|
6826
|
+
}]
|
|
6827
|
+
}], propDecorators: { field: [{
|
|
6828
|
+
type: Input
|
|
6829
|
+
}], control: [{
|
|
6830
|
+
type: Input
|
|
6831
|
+
}], language: [{
|
|
6832
|
+
type: Input
|
|
6833
|
+
}], readonly: [{
|
|
6834
|
+
type: Input
|
|
6835
|
+
}], masked: [{
|
|
6836
|
+
type: Input
|
|
6837
|
+
}] } });
|
|
6838
|
+
|
|
6839
|
+
/**
|
|
6840
|
+
* Rating field: 1 to `validators.max` stars (default 5), stored as a number.
|
|
6841
|
+
*
|
|
6842
|
+
* Built from native radio inputs in a fieldset, not clickable spans. That gives it the
|
|
6843
|
+
* behaviour a radio group already has: arrow keys move the choice, Tab enters and leaves the
|
|
6844
|
+
* group once, and a screen reader announces "3 of 5, radio button, 3 of 5". Each star's
|
|
6845
|
+
* label is text a host can translate (`ratingValue`), because a star glyph read aloud is
|
|
6846
|
+
* "black star".
|
|
6847
|
+
*/
|
|
6848
|
+
class RatingFieldComponent {
|
|
6849
|
+
/**
|
|
6850
|
+
* Unique to this component instance: an `array` renders the same field once per row, and a
|
|
6851
|
+
* DOM id may not repeat. See `field-dom-id.ts`.
|
|
6852
|
+
*/
|
|
6853
|
+
instanceId = nextFieldInstanceId();
|
|
6854
|
+
domId(suffix = '') {
|
|
6855
|
+
return fieldDomId(this.field, this.instanceId, suffix);
|
|
6856
|
+
}
|
|
6857
|
+
/** Overridable via MASKED_PLACEHOLDER; the default is the historic literal. */
|
|
6858
|
+
maskedText = inject(MASKED_PLACEHOLDER, { optional: true }) ?? 'XXXXXXXXX';
|
|
6859
|
+
messages = inject(ValidationMessagesService);
|
|
6860
|
+
cdr = inject(ChangeDetectorRef);
|
|
6861
|
+
ui = inject(UiTextService);
|
|
6862
|
+
field;
|
|
6863
|
+
language = 'en';
|
|
6864
|
+
readonly = false;
|
|
6865
|
+
masked = false;
|
|
6866
|
+
_control;
|
|
6867
|
+
_controlSub;
|
|
6868
|
+
/**
|
|
6869
|
+
* Subscribed because the radios are not bound through a form directive. A value patched in
|
|
6870
|
+
* from outside (a reset, an autoPatch) would otherwise leave an OnPush view showing the old
|
|
6871
|
+
* stars.
|
|
6872
|
+
*/
|
|
6873
|
+
set control(value) {
|
|
6874
|
+
this._control = value;
|
|
6875
|
+
this._controlSub?.unsubscribe();
|
|
6876
|
+
this._controlSub = this._control?.valueChanges?.subscribe(() => this.cdr.markForCheck());
|
|
6877
|
+
}
|
|
6878
|
+
get control() {
|
|
6879
|
+
return this._control;
|
|
6880
|
+
}
|
|
6881
|
+
ngOnDestroy() {
|
|
6882
|
+
this._controlSub?.unsubscribe();
|
|
6883
|
+
}
|
|
6884
|
+
get label() {
|
|
6885
|
+
return resolveLabel(this.field?.label, this.language);
|
|
6886
|
+
}
|
|
6887
|
+
/** Author help text, shown under the control and named by `aria-describedby`. */
|
|
6888
|
+
get hint() {
|
|
6889
|
+
return resolveLabel(this.field?.hint, this.language);
|
|
6890
|
+
}
|
|
6891
|
+
get scale() {
|
|
6892
|
+
return ratingScale(this.field);
|
|
6893
|
+
}
|
|
6894
|
+
get stars() {
|
|
6895
|
+
return Array.from({ length: this.scale }, (_, i) => i + 1);
|
|
6896
|
+
}
|
|
6897
|
+
/** The stored rating, or null when there is none or it is not a star on this scale. */
|
|
6898
|
+
get current() {
|
|
6899
|
+
const value = Number(this.control?.value);
|
|
6900
|
+
const raw = this.control?.value;
|
|
6901
|
+
if (raw === null || raw === undefined || raw === '' || !Number.isInteger(value))
|
|
6902
|
+
return null;
|
|
6903
|
+
return value >= 1 && value <= this.scale ? value : null;
|
|
6904
|
+
}
|
|
6905
|
+
select(star) {
|
|
6906
|
+
this.control?.setValue(star);
|
|
6907
|
+
this.control?.markAsTouched();
|
|
6908
|
+
this.control?.markAsDirty();
|
|
6909
|
+
}
|
|
6910
|
+
clear() {
|
|
6911
|
+
this.control?.setValue(null);
|
|
6912
|
+
this.control?.markAsTouched();
|
|
6913
|
+
this.control?.markAsDirty();
|
|
6914
|
+
}
|
|
6915
|
+
/** The ids of whatever is describing this control right now. */
|
|
6916
|
+
describedBy() {
|
|
6917
|
+
return fieldDescribedBy(s => this.domId(s), { hint: !!this.hint, error: !!this.errorMessage });
|
|
6918
|
+
}
|
|
6919
|
+
get errorMessage() {
|
|
6920
|
+
if (!this.control?.errors || !this.control.touched)
|
|
6921
|
+
return '';
|
|
6922
|
+
return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
|
|
6923
|
+
}
|
|
6924
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: RatingFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
6925
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: RatingFieldComponent, isStandalone: true, selector: "ngx-rating-field", inputs: { field: "field", language: "language", readonly: "readonly", masked: "masked", control: "control" }, ngImport: i0, template: `
|
|
6926
|
+
<div
|
|
6927
|
+
class="ngx-field ngx-field--rating"
|
|
6928
|
+
[attr.data-testid]="'field-' + field.id"
|
|
6929
|
+
[attr.data-field-type]="field.type"
|
|
6930
|
+
[class.ngx-field--readonly]="readonly"
|
|
6931
|
+
[class.ngx-field--masked]="masked"
|
|
6932
|
+
[class.ngx-field--invalid]="control && control.invalid && control.touched"
|
|
6933
|
+
>
|
|
6934
|
+
<fieldset class="ngx-field__fieldset">
|
|
6935
|
+
<legend class="ngx-field__label">
|
|
6936
|
+
{{ label }}
|
|
6937
|
+
@if (field.validators?.required) {
|
|
6938
|
+
<span class="ngx-field__req">*</span>
|
|
6939
|
+
}
|
|
6940
|
+
@if (hint) {
|
|
6941
|
+
<!-- Inside the legend: a fieldset allows one legend, and it must come first. -->
|
|
6942
|
+
<span class="ngx-field__hint-wrap">
|
|
6943
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
6944
|
+
<span
|
|
6945
|
+
class="ngx-field__hint"
|
|
6946
|
+
role="tooltip"
|
|
6947
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
6948
|
+
[id]="domId('-hint')"
|
|
6949
|
+
>{{ hint }}</span
|
|
6950
|
+
>
|
|
6951
|
+
</span>
|
|
6952
|
+
}
|
|
6953
|
+
</legend>
|
|
6954
|
+
@if (masked) {
|
|
6955
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
6956
|
+
maskedText
|
|
6957
|
+
}}</span>
|
|
6958
|
+
} @else if (readonly) {
|
|
6959
|
+
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">
|
|
6960
|
+
@if (current) {
|
|
6961
|
+
<span class="ngx-field__stars" aria-hidden="true">
|
|
6962
|
+
@for (star of stars; track star) {
|
|
6963
|
+
<span class="ngx-field__star-glyph" [class.ngx-field__star-glyph--on]="star <= current">{{
|
|
6964
|
+
star <= current ? '★' : '☆'
|
|
6965
|
+
}}</span>
|
|
6966
|
+
}
|
|
6967
|
+
</span>
|
|
6968
|
+
{{ ui.text('ratingValue', language, { value: current, max: scale }) }}
|
|
6969
|
+
} @else {
|
|
6970
|
+
—
|
|
6971
|
+
}
|
|
6972
|
+
</span>
|
|
6973
|
+
} @else {
|
|
6974
|
+
<div class="ngx-field__rating">
|
|
6975
|
+
@for (star of stars; track star) {
|
|
6976
|
+
<label class="ngx-field__star" [class.ngx-field__star--on]="current !== null && star <= current">
|
|
6977
|
+
<input
|
|
6978
|
+
type="radio"
|
|
6979
|
+
class="ngx-field__star-input"
|
|
6980
|
+
[id]="domId('-' + star)"
|
|
6981
|
+
[name]="domId()"
|
|
6982
|
+
[value]="star"
|
|
6983
|
+
[checked]="current === star"
|
|
6984
|
+
[attr.data-testid]="'field-' + field.id + '-star-' + star"
|
|
6985
|
+
[attr.aria-describedby]="describedBy()"
|
|
6986
|
+
[attr.disabled]="field.disabled ? true : null"
|
|
6987
|
+
(change)="select(star)"
|
|
6988
|
+
/>
|
|
6989
|
+
<!-- Filled and hollow, so the rating is readable without telling colours apart. -->
|
|
6990
|
+
<span class="ngx-field__star-glyph" aria-hidden="true">{{
|
|
6991
|
+
current !== null && star <= current ? '★' : '☆'
|
|
6992
|
+
}}</span>
|
|
6993
|
+
<span class="ngx-field__sr-only">{{ ui.text('ratingValue', language, { value: star, max: scale }) }}</span>
|
|
6994
|
+
</label>
|
|
6995
|
+
}
|
|
6996
|
+
@if (current !== null && !field.disabled) {
|
|
6997
|
+
<button
|
|
6998
|
+
type="button"
|
|
6999
|
+
class="ngx-field__remove-btn"
|
|
7000
|
+
[attr.data-testid]="'field-' + field.id + '-clear'"
|
|
7001
|
+
[attr.aria-label]="ui.text('clearValue', language, { field: label })"
|
|
7002
|
+
(click)="clear()"
|
|
7003
|
+
>
|
|
7004
|
+
×
|
|
7005
|
+
</button>
|
|
7006
|
+
}
|
|
7007
|
+
</div>
|
|
7008
|
+
@if (errorMessage) {
|
|
7009
|
+
<span
|
|
7010
|
+
class="ngx-field__error"
|
|
7011
|
+
[attr.data-testid]="'field-' + field.id + '-error'"
|
|
7012
|
+
[id]="domId('-error')"
|
|
7013
|
+
role="alert"
|
|
7014
|
+
>{{ errorMessage }}</span
|
|
7015
|
+
>
|
|
7016
|
+
}
|
|
7017
|
+
}
|
|
7018
|
+
</fieldset>
|
|
7019
|
+
</div>
|
|
7020
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
7021
|
+
}
|
|
7022
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: RatingFieldComponent, decorators: [{
|
|
7023
|
+
type: Component,
|
|
7024
|
+
args: [{
|
|
7025
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
7026
|
+
selector: 'ngx-rating-field',
|
|
7027
|
+
standalone: true,
|
|
7028
|
+
imports: [ReactiveFormsModule],
|
|
7029
|
+
template: `
|
|
7030
|
+
<div
|
|
7031
|
+
class="ngx-field ngx-field--rating"
|
|
7032
|
+
[attr.data-testid]="'field-' + field.id"
|
|
7033
|
+
[attr.data-field-type]="field.type"
|
|
7034
|
+
[class.ngx-field--readonly]="readonly"
|
|
7035
|
+
[class.ngx-field--masked]="masked"
|
|
7036
|
+
[class.ngx-field--invalid]="control && control.invalid && control.touched"
|
|
7037
|
+
>
|
|
7038
|
+
<fieldset class="ngx-field__fieldset">
|
|
7039
|
+
<legend class="ngx-field__label">
|
|
7040
|
+
{{ label }}
|
|
7041
|
+
@if (field.validators?.required) {
|
|
7042
|
+
<span class="ngx-field__req">*</span>
|
|
7043
|
+
}
|
|
7044
|
+
@if (hint) {
|
|
7045
|
+
<!-- Inside the legend: a fieldset allows one legend, and it must come first. -->
|
|
7046
|
+
<span class="ngx-field__hint-wrap">
|
|
7047
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
7048
|
+
<span
|
|
7049
|
+
class="ngx-field__hint"
|
|
7050
|
+
role="tooltip"
|
|
7051
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
7052
|
+
[id]="domId('-hint')"
|
|
7053
|
+
>{{ hint }}</span
|
|
7054
|
+
>
|
|
7055
|
+
</span>
|
|
7056
|
+
}
|
|
7057
|
+
</legend>
|
|
7058
|
+
@if (masked) {
|
|
7059
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
7060
|
+
maskedText
|
|
7061
|
+
}}</span>
|
|
7062
|
+
} @else if (readonly) {
|
|
7063
|
+
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">
|
|
7064
|
+
@if (current) {
|
|
7065
|
+
<span class="ngx-field__stars" aria-hidden="true">
|
|
7066
|
+
@for (star of stars; track star) {
|
|
7067
|
+
<span class="ngx-field__star-glyph" [class.ngx-field__star-glyph--on]="star <= current">{{
|
|
7068
|
+
star <= current ? '★' : '☆'
|
|
7069
|
+
}}</span>
|
|
7070
|
+
}
|
|
7071
|
+
</span>
|
|
7072
|
+
{{ ui.text('ratingValue', language, { value: current, max: scale }) }}
|
|
7073
|
+
} @else {
|
|
7074
|
+
—
|
|
7075
|
+
}
|
|
7076
|
+
</span>
|
|
7077
|
+
} @else {
|
|
7078
|
+
<div class="ngx-field__rating">
|
|
7079
|
+
@for (star of stars; track star) {
|
|
7080
|
+
<label class="ngx-field__star" [class.ngx-field__star--on]="current !== null && star <= current">
|
|
7081
|
+
<input
|
|
7082
|
+
type="radio"
|
|
7083
|
+
class="ngx-field__star-input"
|
|
7084
|
+
[id]="domId('-' + star)"
|
|
7085
|
+
[name]="domId()"
|
|
7086
|
+
[value]="star"
|
|
7087
|
+
[checked]="current === star"
|
|
7088
|
+
[attr.data-testid]="'field-' + field.id + '-star-' + star"
|
|
7089
|
+
[attr.aria-describedby]="describedBy()"
|
|
7090
|
+
[attr.disabled]="field.disabled ? true : null"
|
|
7091
|
+
(change)="select(star)"
|
|
7092
|
+
/>
|
|
7093
|
+
<!-- Filled and hollow, so the rating is readable without telling colours apart. -->
|
|
7094
|
+
<span class="ngx-field__star-glyph" aria-hidden="true">{{
|
|
7095
|
+
current !== null && star <= current ? '★' : '☆'
|
|
7096
|
+
}}</span>
|
|
7097
|
+
<span class="ngx-field__sr-only">{{ ui.text('ratingValue', language, { value: star, max: scale }) }}</span>
|
|
7098
|
+
</label>
|
|
7099
|
+
}
|
|
7100
|
+
@if (current !== null && !field.disabled) {
|
|
7101
|
+
<button
|
|
7102
|
+
type="button"
|
|
7103
|
+
class="ngx-field__remove-btn"
|
|
7104
|
+
[attr.data-testid]="'field-' + field.id + '-clear'"
|
|
7105
|
+
[attr.aria-label]="ui.text('clearValue', language, { field: label })"
|
|
7106
|
+
(click)="clear()"
|
|
7107
|
+
>
|
|
7108
|
+
×
|
|
7109
|
+
</button>
|
|
7110
|
+
}
|
|
7111
|
+
</div>
|
|
7112
|
+
@if (errorMessage) {
|
|
7113
|
+
<span
|
|
7114
|
+
class="ngx-field__error"
|
|
7115
|
+
[attr.data-testid]="'field-' + field.id + '-error'"
|
|
7116
|
+
[id]="domId('-error')"
|
|
7117
|
+
role="alert"
|
|
7118
|
+
>{{ errorMessage }}</span
|
|
7119
|
+
>
|
|
7120
|
+
}
|
|
7121
|
+
}
|
|
7122
|
+
</fieldset>
|
|
7123
|
+
</div>
|
|
7124
|
+
`,
|
|
7125
|
+
}]
|
|
7126
|
+
}], propDecorators: { field: [{
|
|
7127
|
+
type: Input
|
|
7128
|
+
}], language: [{
|
|
7129
|
+
type: Input
|
|
7130
|
+
}], readonly: [{
|
|
7131
|
+
type: Input
|
|
7132
|
+
}], masked: [{
|
|
7133
|
+
type: Input
|
|
7134
|
+
}], control: [{
|
|
7135
|
+
type: Input
|
|
7136
|
+
}] } });
|
|
7137
|
+
|
|
7138
|
+
/**
|
|
7139
|
+
* Colour field: a native colour picker, storing `#rrggbb`.
|
|
7140
|
+
*
|
|
7141
|
+
* Like a range input, a colour input has no empty state and shows black. The code beside it is
|
|
7142
|
+
* therefore the real value, a dash until a colour is chosen, and a clear button sets the value
|
|
7143
|
+
* back to nothing. Without them, "no colour" and "black" would look the same.
|
|
7144
|
+
*
|
|
7145
|
+
* Only a value that passes `normalizeHexColor` reaches a `style` binding. A record can hold
|
|
7146
|
+
* any string, and a colour swatch is no place to interpret one.
|
|
7147
|
+
*/
|
|
7148
|
+
class ColorFieldComponent {
|
|
7149
|
+
/**
|
|
7150
|
+
* Unique to this component instance: an `array` renders the same field once per row, and a
|
|
7151
|
+
* DOM id may not repeat. See `field-dom-id.ts`.
|
|
7152
|
+
*/
|
|
7153
|
+
instanceId = nextFieldInstanceId();
|
|
7154
|
+
domId(suffix = '') {
|
|
7155
|
+
return fieldDomId(this.field, this.instanceId, suffix);
|
|
7156
|
+
}
|
|
7157
|
+
/** Overridable via MASKED_PLACEHOLDER; the default is the historic literal. */
|
|
7158
|
+
maskedText = inject(MASKED_PLACEHOLDER, { optional: true }) ?? 'XXXXXXXXX';
|
|
7159
|
+
messages = inject(ValidationMessagesService);
|
|
7160
|
+
ui = inject(UiTextService);
|
|
7161
|
+
field;
|
|
7162
|
+
control;
|
|
7163
|
+
language = 'en';
|
|
7164
|
+
readonly = false;
|
|
7165
|
+
masked = false;
|
|
7166
|
+
get label() {
|
|
7167
|
+
return resolveLabel(this.field?.label, this.language);
|
|
7168
|
+
}
|
|
7169
|
+
/** Author help text, shown under the control and named by `aria-describedby`. */
|
|
7170
|
+
get hint() {
|
|
7171
|
+
return resolveLabel(this.field?.hint, this.language);
|
|
7172
|
+
}
|
|
7173
|
+
/** The stored colour as `#rrggbb`, or null when there is none or it is not a colour. */
|
|
7174
|
+
get hex() {
|
|
7175
|
+
return normalizeHexColor(this.control?.value);
|
|
7176
|
+
}
|
|
7177
|
+
clear() {
|
|
7178
|
+
this.control?.setValue(null);
|
|
7179
|
+
this.control?.markAsTouched();
|
|
7180
|
+
this.control?.markAsDirty();
|
|
7181
|
+
}
|
|
7182
|
+
/** The ids of whatever is describing this control right now. */
|
|
7183
|
+
describedBy() {
|
|
7184
|
+
return fieldDescribedBy(s => this.domId(s), { hint: !!this.hint, error: !!this.errorMessage });
|
|
7185
|
+
}
|
|
7186
|
+
get errorMessage() {
|
|
7187
|
+
if (!this.control?.errors || !this.control.touched)
|
|
7188
|
+
return '';
|
|
7189
|
+
return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
|
|
7190
|
+
}
|
|
7191
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: ColorFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
7192
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: ColorFieldComponent, isStandalone: true, selector: "ngx-color-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
7193
|
+
<div
|
|
7194
|
+
class="ngx-field ngx-field--color"
|
|
7195
|
+
[attr.data-testid]="'field-' + field.id"
|
|
7196
|
+
[attr.data-field-type]="field.type"
|
|
7197
|
+
[class.ngx-field--readonly]="readonly"
|
|
7198
|
+
[class.ngx-field--masked]="masked"
|
|
7199
|
+
[class.ngx-field--invalid]="control && control.invalid && control.touched"
|
|
7200
|
+
>
|
|
7201
|
+
<div class="ngx-field__label-row">
|
|
7202
|
+
<label class="ngx-field__label" [attr.for]="domId()">
|
|
7203
|
+
{{ label }}
|
|
7204
|
+
@if (field.validators?.required) {
|
|
7205
|
+
<span class="ngx-field__req">*</span>
|
|
7206
|
+
}
|
|
7207
|
+
</label>
|
|
7208
|
+
@if (hint) {
|
|
7209
|
+
<span class="ngx-field__hint-wrap">
|
|
7210
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
7211
|
+
<span
|
|
7212
|
+
class="ngx-field__hint"
|
|
7213
|
+
role="tooltip"
|
|
7214
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
7215
|
+
[id]="domId('-hint')"
|
|
7216
|
+
>{{ hint }}</span
|
|
7217
|
+
>
|
|
7218
|
+
</span>
|
|
7219
|
+
}
|
|
7220
|
+
</div>
|
|
7221
|
+
@if (masked) {
|
|
7222
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
7223
|
+
maskedText
|
|
7224
|
+
}}</span>
|
|
7225
|
+
} @else if (readonly) {
|
|
7226
|
+
<span class="ngx-field__value ngx-field__color-value" [attr.data-testid]="'field-' + field.id + '-value'">
|
|
7227
|
+
@if (hex) {
|
|
7228
|
+
<span class="ngx-field__swatch" [style.background-color]="hex" aria-hidden="true"></span>
|
|
7229
|
+
{{ hex }}
|
|
7230
|
+
} @else {
|
|
7231
|
+
{{ control.value || '—' }}
|
|
7232
|
+
}
|
|
7233
|
+
</span>
|
|
7234
|
+
} @else {
|
|
7235
|
+
<div class="ngx-field__color-wrap">
|
|
7236
|
+
<input
|
|
7237
|
+
[id]="domId()"
|
|
7238
|
+
type="color"
|
|
7239
|
+
class="ngx-field__color"
|
|
7240
|
+
[attr.data-testid]="'field-' + field.id + '-input'"
|
|
7241
|
+
[formControl]="$any(control)"
|
|
7242
|
+
[attr.aria-invalid]="control.invalid && control.touched"
|
|
7243
|
+
[attr.aria-describedby]="describedBy()"
|
|
7244
|
+
[attr.disabled]="field.disabled ? true : null"
|
|
7245
|
+
/>
|
|
7246
|
+
<span class="ngx-field__color-code" [attr.data-testid]="'field-' + field.id + '-code'">{{ hex || '—' }}</span>
|
|
7247
|
+
@if (hex && !field.disabled) {
|
|
7248
|
+
<button
|
|
7249
|
+
type="button"
|
|
7250
|
+
class="ngx-field__remove-btn"
|
|
7251
|
+
[attr.data-testid]="'field-' + field.id + '-clear'"
|
|
7252
|
+
[attr.aria-label]="ui.text('clearValue', language, { field: label })"
|
|
7253
|
+
(click)="clear()"
|
|
7254
|
+
>
|
|
7255
|
+
×
|
|
7256
|
+
</button>
|
|
7257
|
+
}
|
|
7258
|
+
</div>
|
|
7259
|
+
@if (errorMessage) {
|
|
7260
|
+
<span
|
|
7261
|
+
class="ngx-field__error"
|
|
7262
|
+
[attr.data-testid]="'field-' + field.id + '-error'"
|
|
7263
|
+
[id]="domId('-error')"
|
|
7264
|
+
role="alert"
|
|
7265
|
+
>{{ errorMessage }}</span
|
|
7266
|
+
>
|
|
7267
|
+
}
|
|
7268
|
+
}
|
|
7269
|
+
</div>
|
|
7270
|
+
`, 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 });
|
|
7271
|
+
}
|
|
7272
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: ColorFieldComponent, decorators: [{
|
|
7273
|
+
type: Component,
|
|
7274
|
+
args: [{
|
|
7275
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
7276
|
+
selector: 'ngx-color-field',
|
|
7277
|
+
standalone: true,
|
|
7278
|
+
imports: [ReactiveFormsModule],
|
|
7279
|
+
template: `
|
|
7280
|
+
<div
|
|
7281
|
+
class="ngx-field ngx-field--color"
|
|
7282
|
+
[attr.data-testid]="'field-' + field.id"
|
|
7283
|
+
[attr.data-field-type]="field.type"
|
|
7284
|
+
[class.ngx-field--readonly]="readonly"
|
|
7285
|
+
[class.ngx-field--masked]="masked"
|
|
7286
|
+
[class.ngx-field--invalid]="control && control.invalid && control.touched"
|
|
7287
|
+
>
|
|
7288
|
+
<div class="ngx-field__label-row">
|
|
7289
|
+
<label class="ngx-field__label" [attr.for]="domId()">
|
|
7290
|
+
{{ label }}
|
|
7291
|
+
@if (field.validators?.required) {
|
|
7292
|
+
<span class="ngx-field__req">*</span>
|
|
7293
|
+
}
|
|
7294
|
+
</label>
|
|
7295
|
+
@if (hint) {
|
|
7296
|
+
<span class="ngx-field__hint-wrap">
|
|
7297
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
7298
|
+
<span
|
|
7299
|
+
class="ngx-field__hint"
|
|
7300
|
+
role="tooltip"
|
|
7301
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
7302
|
+
[id]="domId('-hint')"
|
|
7303
|
+
>{{ hint }}</span
|
|
7304
|
+
>
|
|
7305
|
+
</span>
|
|
7306
|
+
}
|
|
7307
|
+
</div>
|
|
7308
|
+
@if (masked) {
|
|
7309
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
7310
|
+
maskedText
|
|
7311
|
+
}}</span>
|
|
7312
|
+
} @else if (readonly) {
|
|
7313
|
+
<span class="ngx-field__value ngx-field__color-value" [attr.data-testid]="'field-' + field.id + '-value'">
|
|
7314
|
+
@if (hex) {
|
|
7315
|
+
<span class="ngx-field__swatch" [style.background-color]="hex" aria-hidden="true"></span>
|
|
7316
|
+
{{ hex }}
|
|
7317
|
+
} @else {
|
|
7318
|
+
{{ control.value || '—' }}
|
|
7319
|
+
}
|
|
7320
|
+
</span>
|
|
7321
|
+
} @else {
|
|
7322
|
+
<div class="ngx-field__color-wrap">
|
|
7323
|
+
<input
|
|
7324
|
+
[id]="domId()"
|
|
7325
|
+
type="color"
|
|
7326
|
+
class="ngx-field__color"
|
|
7327
|
+
[attr.data-testid]="'field-' + field.id + '-input'"
|
|
7328
|
+
[formControl]="$any(control)"
|
|
7329
|
+
[attr.aria-invalid]="control.invalid && control.touched"
|
|
7330
|
+
[attr.aria-describedby]="describedBy()"
|
|
7331
|
+
[attr.disabled]="field.disabled ? true : null"
|
|
7332
|
+
/>
|
|
7333
|
+
<span class="ngx-field__color-code" [attr.data-testid]="'field-' + field.id + '-code'">{{ hex || '—' }}</span>
|
|
7334
|
+
@if (hex && !field.disabled) {
|
|
7335
|
+
<button
|
|
7336
|
+
type="button"
|
|
7337
|
+
class="ngx-field__remove-btn"
|
|
7338
|
+
[attr.data-testid]="'field-' + field.id + '-clear'"
|
|
7339
|
+
[attr.aria-label]="ui.text('clearValue', language, { field: label })"
|
|
7340
|
+
(click)="clear()"
|
|
7341
|
+
>
|
|
7342
|
+
×
|
|
7343
|
+
</button>
|
|
7344
|
+
}
|
|
7345
|
+
</div>
|
|
7346
|
+
@if (errorMessage) {
|
|
7347
|
+
<span
|
|
7348
|
+
class="ngx-field__error"
|
|
7349
|
+
[attr.data-testid]="'field-' + field.id + '-error'"
|
|
7350
|
+
[id]="domId('-error')"
|
|
7351
|
+
role="alert"
|
|
7352
|
+
>{{ errorMessage }}</span
|
|
7353
|
+
>
|
|
7354
|
+
}
|
|
7355
|
+
}
|
|
7356
|
+
</div>
|
|
7357
|
+
`,
|
|
7358
|
+
}]
|
|
7359
|
+
}], propDecorators: { field: [{
|
|
7360
|
+
type: Input
|
|
7361
|
+
}], control: [{
|
|
7362
|
+
type: Input
|
|
7363
|
+
}], language: [{
|
|
7364
|
+
type: Input
|
|
7365
|
+
}], readonly: [{
|
|
7366
|
+
type: Input
|
|
7367
|
+
}], masked: [{
|
|
7368
|
+
type: Input
|
|
7369
|
+
}] } });
|
|
7370
|
+
|
|
7371
|
+
/** What ends a tag while typing, or separates several in pasted text. */
|
|
7372
|
+
const TAG_SEPARATOR = /[,;\n]/;
|
|
7373
|
+
/**
|
|
7374
|
+
* Tags field: free text entered as a list of chips, stored as `string[]`.
|
|
7375
|
+
*
|
|
7376
|
+
* Unlike `multiSelect`, the values are whatever the user types, so there is no option list to
|
|
7377
|
+
* keep in step. Enter or a comma ends a tag, Backspace in an empty box removes the last one,
|
|
7378
|
+
* and pasted text is split on commas, semicolons and line breaks. Leaving the box commits
|
|
7379
|
+
* what is in it, so a tag someone typed and then tabbed away from is not silently lost.
|
|
7380
|
+
*
|
|
7381
|
+
* The control holds the array. The text box is only a draft, which is why it is not bound
|
|
7382
|
+
* with a form directive.
|
|
7383
|
+
*/
|
|
7384
|
+
class TagsFieldComponent {
|
|
7385
|
+
/**
|
|
7386
|
+
* Unique to this component instance: an `array` renders the same field once per row, and a
|
|
7387
|
+
* DOM id may not repeat. See `field-dom-id.ts`.
|
|
7388
|
+
*/
|
|
7389
|
+
instanceId = nextFieldInstanceId();
|
|
7390
|
+
domId(suffix = '') {
|
|
7391
|
+
return fieldDomId(this.field, this.instanceId, suffix);
|
|
7392
|
+
}
|
|
7393
|
+
/** Overridable via MASKED_PLACEHOLDER; the default is the historic literal. */
|
|
7394
|
+
maskedText = inject(MASKED_PLACEHOLDER, { optional: true }) ?? 'XXXXXXXXX';
|
|
7395
|
+
messages = inject(ValidationMessagesService);
|
|
7396
|
+
cdr = inject(ChangeDetectorRef);
|
|
7397
|
+
ui = inject(UiTextService);
|
|
7398
|
+
field;
|
|
7399
|
+
language = 'en';
|
|
7400
|
+
readonly = false;
|
|
7401
|
+
masked = false;
|
|
7402
|
+
_control;
|
|
7403
|
+
_controlSub;
|
|
7404
|
+
/** Subscribed for the same reason as the rating field: nothing here is a form directive. */
|
|
7405
|
+
set control(value) {
|
|
7406
|
+
this._control = value;
|
|
7407
|
+
this._controlSub?.unsubscribe();
|
|
7408
|
+
this._controlSub = this._control?.valueChanges?.subscribe(() => this.cdr.markForCheck());
|
|
7409
|
+
}
|
|
7410
|
+
get control() {
|
|
7411
|
+
return this._control;
|
|
7412
|
+
}
|
|
7413
|
+
ngOnDestroy() {
|
|
7414
|
+
this._controlSub?.unsubscribe();
|
|
7415
|
+
}
|
|
7416
|
+
get label() {
|
|
7417
|
+
return resolveLabel(this.field?.label, this.language);
|
|
7418
|
+
}
|
|
7419
|
+
get placeholder() {
|
|
7420
|
+
return resolveLabel(this.field?.placeholder, this.language);
|
|
7421
|
+
}
|
|
7422
|
+
/** Author help text, shown under the control and named by `aria-describedby`. */
|
|
7423
|
+
get hint() {
|
|
7424
|
+
return resolveLabel(this.field?.hint, this.language);
|
|
7425
|
+
}
|
|
7426
|
+
/** The stored tags, read leniently. A record may hold a single string or anything else. */
|
|
7427
|
+
get tags() {
|
|
7428
|
+
return normalizeTags(this.control?.value);
|
|
7429
|
+
}
|
|
7430
|
+
onKeydown(event, entry) {
|
|
7431
|
+
if (event.key === 'Enter' || event.key === ',' || event.key === ';') {
|
|
7432
|
+
// Enter inside a form submits it. Adding a tag is what the user meant.
|
|
7433
|
+
event.preventDefault();
|
|
7434
|
+
this.commit(entry);
|
|
7435
|
+
}
|
|
7436
|
+
else if (event.key === 'Backspace' && entry.value === '' && this.tags.length) {
|
|
7437
|
+
this.remove(this.tags[this.tags.length - 1]);
|
|
7438
|
+
}
|
|
7439
|
+
}
|
|
7440
|
+
onPaste(event, entry) {
|
|
7441
|
+
const text = event.clipboardData?.getData('text') ?? '';
|
|
7442
|
+
if (!TAG_SEPARATOR.test(text))
|
|
7443
|
+
return;
|
|
7444
|
+
event.preventDefault();
|
|
7445
|
+
// Split here, not by writing the text into the box first: a single-line input strips line
|
|
7446
|
+
// breaks on assignment, which would join "z\nw" into one tag.
|
|
7447
|
+
const draft = entry.value;
|
|
7448
|
+
entry.value = '';
|
|
7449
|
+
this.add([draft, ...text.split(TAG_SEPARATOR)]);
|
|
7450
|
+
}
|
|
7451
|
+
onBlur(entry) {
|
|
7452
|
+
this.commit(entry);
|
|
7453
|
+
this.control?.markAsTouched();
|
|
7454
|
+
}
|
|
7455
|
+
/** Add whatever is in the draft box, split on separators, and empty it. */
|
|
7456
|
+
commit(entry) {
|
|
7457
|
+
const added = entry.value.split(TAG_SEPARATOR);
|
|
7458
|
+
entry.value = '';
|
|
7459
|
+
this.add(added);
|
|
7460
|
+
}
|
|
7461
|
+
add(candidates) {
|
|
7462
|
+
const next = normalizeTags([...this.tags, ...candidates]);
|
|
7463
|
+
if (next.length !== this.tags.length)
|
|
7464
|
+
this.write(next);
|
|
7465
|
+
}
|
|
7466
|
+
remove(tag) {
|
|
7467
|
+
this.write(this.tags.filter(t => t !== tag));
|
|
7468
|
+
}
|
|
7469
|
+
/**
|
|
7470
|
+
* An empty list is stored as `[]`, not `null`. `required` treats both as empty, and `[]`
|
|
7471
|
+
* stays the same shape as every other value this field holds.
|
|
7472
|
+
*/
|
|
7473
|
+
write(tags) {
|
|
7474
|
+
this.control?.setValue(tags);
|
|
7475
|
+
this.control?.markAsTouched();
|
|
7476
|
+
this.control?.markAsDirty();
|
|
7477
|
+
}
|
|
7478
|
+
/** The ids of whatever is describing this control right now. */
|
|
7479
|
+
describedBy() {
|
|
7480
|
+
return fieldDescribedBy(s => this.domId(s), { hint: !!this.hint, error: !!this.errorMessage });
|
|
7481
|
+
}
|
|
7482
|
+
get errorMessage() {
|
|
7483
|
+
if (!this.control?.errors || !this.control.touched)
|
|
7484
|
+
return '';
|
|
7485
|
+
return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
|
|
7486
|
+
}
|
|
7487
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: TagsFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
7488
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: TagsFieldComponent, isStandalone: true, selector: "ngx-tags-field", inputs: { field: "field", language: "language", readonly: "readonly", masked: "masked", control: "control" }, ngImport: i0, template: `
|
|
7489
|
+
<div
|
|
7490
|
+
class="ngx-field ngx-field--tags"
|
|
7491
|
+
[attr.data-testid]="'field-' + field.id"
|
|
7492
|
+
[attr.data-field-type]="field.type"
|
|
7493
|
+
[class.ngx-field--readonly]="readonly"
|
|
7494
|
+
[class.ngx-field--masked]="masked"
|
|
7495
|
+
[class.ngx-field--invalid]="control && control.invalid && control.touched"
|
|
7496
|
+
>
|
|
7497
|
+
<div class="ngx-field__label-row">
|
|
7498
|
+
<label class="ngx-field__label" [attr.for]="domId()">
|
|
7499
|
+
{{ label }}
|
|
7500
|
+
@if (field.validators?.required) {
|
|
7501
|
+
<span class="ngx-field__req">*</span>
|
|
7502
|
+
}
|
|
7503
|
+
</label>
|
|
7504
|
+
@if (hint) {
|
|
7505
|
+
<span class="ngx-field__hint-wrap">
|
|
7506
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
7507
|
+
<span
|
|
7508
|
+
class="ngx-field__hint"
|
|
7509
|
+
role="tooltip"
|
|
7510
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
7511
|
+
[id]="domId('-hint')"
|
|
7512
|
+
>{{ hint }}</span
|
|
7513
|
+
>
|
|
7514
|
+
</span>
|
|
7515
|
+
}
|
|
7516
|
+
</div>
|
|
7517
|
+
@if (masked) {
|
|
7518
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
7519
|
+
maskedText
|
|
7520
|
+
}}</span>
|
|
7521
|
+
} @else if (readonly) {
|
|
7522
|
+
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">
|
|
7523
|
+
@if (tags.length) {
|
|
7524
|
+
<span class="ngx-field__tags ngx-field__tags--readonly">
|
|
7525
|
+
@for (tag of tags; track tag) {
|
|
7526
|
+
<span class="ngx-field__tag">{{ tag }}</span>
|
|
7527
|
+
}
|
|
7528
|
+
</span>
|
|
7529
|
+
} @else {
|
|
7530
|
+
—
|
|
7531
|
+
}
|
|
7532
|
+
</span>
|
|
7533
|
+
} @else {
|
|
7534
|
+
<div class="ngx-field__tags" [class.ngx-field__tags--disabled]="field.disabled">
|
|
7535
|
+
@for (tag of tags; track tag) {
|
|
7536
|
+
<span class="ngx-field__tag" [attr.data-testid]="'field-' + field.id + '-tag'">
|
|
7537
|
+
{{ tag }}
|
|
7538
|
+
@if (!field.disabled) {
|
|
7539
|
+
<button
|
|
7540
|
+
type="button"
|
|
7541
|
+
class="ngx-field__tag-remove"
|
|
7542
|
+
[attr.aria-label]="ui.text('removeTag', language, { tag: tag })"
|
|
7543
|
+
(click)="remove(tag)"
|
|
7544
|
+
>
|
|
7545
|
+
×
|
|
7546
|
+
</button>
|
|
7547
|
+
}
|
|
7548
|
+
</span>
|
|
7549
|
+
}
|
|
7550
|
+
<input
|
|
7551
|
+
#entry
|
|
7552
|
+
[id]="domId()"
|
|
7553
|
+
type="text"
|
|
7554
|
+
class="ngx-field__tags-input"
|
|
7555
|
+
[attr.data-testid]="'field-' + field.id + '-input'"
|
|
7556
|
+
[attr.aria-invalid]="control.invalid && control.touched"
|
|
7557
|
+
[attr.aria-describedby]="describedBy()"
|
|
7558
|
+
[placeholder]="placeholder || ui.text('addTagPlaceholder', language)"
|
|
7559
|
+
[attr.disabled]="field.disabled ? true : null"
|
|
7560
|
+
(keydown)="onKeydown($event, entry)"
|
|
7561
|
+
(paste)="onPaste($event, entry)"
|
|
7562
|
+
(blur)="onBlur(entry)"
|
|
7563
|
+
/>
|
|
7564
|
+
</div>
|
|
7565
|
+
@if (errorMessage) {
|
|
7566
|
+
<span
|
|
7567
|
+
class="ngx-field__error"
|
|
7568
|
+
[attr.data-testid]="'field-' + field.id + '-error'"
|
|
7569
|
+
[id]="domId('-error')"
|
|
7570
|
+
role="alert"
|
|
7571
|
+
>{{ errorMessage }}</span
|
|
7572
|
+
>
|
|
7573
|
+
}
|
|
7574
|
+
}
|
|
7575
|
+
</div>
|
|
7576
|
+
`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
7577
|
+
}
|
|
7578
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: TagsFieldComponent, decorators: [{
|
|
7579
|
+
type: Component,
|
|
7580
|
+
args: [{
|
|
7581
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
7582
|
+
selector: 'ngx-tags-field',
|
|
7583
|
+
standalone: true,
|
|
7584
|
+
template: `
|
|
7585
|
+
<div
|
|
7586
|
+
class="ngx-field ngx-field--tags"
|
|
7587
|
+
[attr.data-testid]="'field-' + field.id"
|
|
7588
|
+
[attr.data-field-type]="field.type"
|
|
7589
|
+
[class.ngx-field--readonly]="readonly"
|
|
7590
|
+
[class.ngx-field--masked]="masked"
|
|
7591
|
+
[class.ngx-field--invalid]="control && control.invalid && control.touched"
|
|
7592
|
+
>
|
|
7593
|
+
<div class="ngx-field__label-row">
|
|
7594
|
+
<label class="ngx-field__label" [attr.for]="domId()">
|
|
7595
|
+
{{ label }}
|
|
7596
|
+
@if (field.validators?.required) {
|
|
7597
|
+
<span class="ngx-field__req">*</span>
|
|
7598
|
+
}
|
|
7599
|
+
</label>
|
|
7600
|
+
@if (hint) {
|
|
7601
|
+
<span class="ngx-field__hint-wrap">
|
|
7602
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
7603
|
+
<span
|
|
7604
|
+
class="ngx-field__hint"
|
|
7605
|
+
role="tooltip"
|
|
7606
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
7607
|
+
[id]="domId('-hint')"
|
|
7608
|
+
>{{ hint }}</span
|
|
7609
|
+
>
|
|
7610
|
+
</span>
|
|
7611
|
+
}
|
|
7612
|
+
</div>
|
|
7613
|
+
@if (masked) {
|
|
7614
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
7615
|
+
maskedText
|
|
7616
|
+
}}</span>
|
|
7617
|
+
} @else if (readonly) {
|
|
7618
|
+
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">
|
|
7619
|
+
@if (tags.length) {
|
|
7620
|
+
<span class="ngx-field__tags ngx-field__tags--readonly">
|
|
7621
|
+
@for (tag of tags; track tag) {
|
|
7622
|
+
<span class="ngx-field__tag">{{ tag }}</span>
|
|
7623
|
+
}
|
|
7624
|
+
</span>
|
|
7625
|
+
} @else {
|
|
7626
|
+
—
|
|
7627
|
+
}
|
|
7628
|
+
</span>
|
|
7629
|
+
} @else {
|
|
7630
|
+
<div class="ngx-field__tags" [class.ngx-field__tags--disabled]="field.disabled">
|
|
7631
|
+
@for (tag of tags; track tag) {
|
|
7632
|
+
<span class="ngx-field__tag" [attr.data-testid]="'field-' + field.id + '-tag'">
|
|
7633
|
+
{{ tag }}
|
|
7634
|
+
@if (!field.disabled) {
|
|
7635
|
+
<button
|
|
7636
|
+
type="button"
|
|
7637
|
+
class="ngx-field__tag-remove"
|
|
7638
|
+
[attr.aria-label]="ui.text('removeTag', language, { tag: tag })"
|
|
7639
|
+
(click)="remove(tag)"
|
|
7640
|
+
>
|
|
7641
|
+
×
|
|
7642
|
+
</button>
|
|
7643
|
+
}
|
|
7644
|
+
</span>
|
|
7645
|
+
}
|
|
7646
|
+
<input
|
|
7647
|
+
#entry
|
|
7648
|
+
[id]="domId()"
|
|
7649
|
+
type="text"
|
|
7650
|
+
class="ngx-field__tags-input"
|
|
7651
|
+
[attr.data-testid]="'field-' + field.id + '-input'"
|
|
7652
|
+
[attr.aria-invalid]="control.invalid && control.touched"
|
|
7653
|
+
[attr.aria-describedby]="describedBy()"
|
|
7654
|
+
[placeholder]="placeholder || ui.text('addTagPlaceholder', language)"
|
|
7655
|
+
[attr.disabled]="field.disabled ? true : null"
|
|
7656
|
+
(keydown)="onKeydown($event, entry)"
|
|
7657
|
+
(paste)="onPaste($event, entry)"
|
|
7658
|
+
(blur)="onBlur(entry)"
|
|
7659
|
+
/>
|
|
7660
|
+
</div>
|
|
7661
|
+
@if (errorMessage) {
|
|
7662
|
+
<span
|
|
7663
|
+
class="ngx-field__error"
|
|
7664
|
+
[attr.data-testid]="'field-' + field.id + '-error'"
|
|
7665
|
+
[id]="domId('-error')"
|
|
7666
|
+
role="alert"
|
|
7667
|
+
>{{ errorMessage }}</span
|
|
7668
|
+
>
|
|
7669
|
+
}
|
|
7670
|
+
}
|
|
7671
|
+
</div>
|
|
7672
|
+
`,
|
|
7673
|
+
}]
|
|
7674
|
+
}], propDecorators: { field: [{
|
|
7675
|
+
type: Input
|
|
7676
|
+
}], language: [{
|
|
7677
|
+
type: Input
|
|
7678
|
+
}], readonly: [{
|
|
7679
|
+
type: Input
|
|
7680
|
+
}], masked: [{
|
|
7681
|
+
type: Input
|
|
7682
|
+
}], control: [{
|
|
7683
|
+
type: Input
|
|
7684
|
+
}] } });
|
|
7685
|
+
|
|
7686
|
+
/**
|
|
7687
|
+
* Field-type registration — the tree-shaking seam.
|
|
7688
|
+
*
|
|
7689
|
+
* `FieldRegistryService` imports no components. Whatever you register here is what gets
|
|
7690
|
+
* bundled, so an app that only uses text/number/dropdown pays for three components and
|
|
7691
|
+
* their imports, not twenty-seven.
|
|
7692
|
+
*
|
|
7693
|
+
* ```ts
|
|
7694
|
+
* // Everything (convenient; bundles all built-ins):
|
|
7695
|
+
* provideNgxDynamicEntity(), provideBuiltInFieldTypes()
|
|
7696
|
+
*
|
|
7697
|
+
* // Only what this app renders:
|
|
7698
|
+
* provideFieldTypes({ text: TextFieldComponent, number: NumberFieldComponent })
|
|
7699
|
+
* ```
|
|
7700
|
+
*
|
|
7701
|
+
* Both merge into `FIELD_TYPE_REGISTRY`; later registrations win on key collision.
|
|
7702
|
+
*/
|
|
7703
|
+
/** Map of every built-in field type key → component. Referencing this pulls in all of them. */
|
|
7704
|
+
function builtInFieldTypes() {
|
|
7705
|
+
return {
|
|
7706
|
+
text: TextFieldComponent,
|
|
7707
|
+
textarea: TextareaFieldComponent,
|
|
7708
|
+
markdown: MarkdownFieldComponent,
|
|
7709
|
+
number: NumberFieldComponent,
|
|
7710
|
+
currency: CurrencyFieldComponent,
|
|
7711
|
+
email: EmailFieldComponent,
|
|
7712
|
+
password: PasswordFieldComponent,
|
|
7713
|
+
checkbox: CheckboxFieldComponent,
|
|
7714
|
+
boolean: BooleanFieldComponent,
|
|
7715
|
+
date: DateFieldComponent,
|
|
7716
|
+
datetime: DateTimeFieldComponent,
|
|
7717
|
+
time: TimeFieldComponent,
|
|
7718
|
+
monthYear: MonthYearFieldComponent,
|
|
7719
|
+
dropdown: DropdownFieldComponent,
|
|
7720
|
+
radio: RadioFieldComponent,
|
|
7721
|
+
multiSelect: MultiSelectFieldComponent,
|
|
7722
|
+
'entity-ref': EntityRefFieldComponent,
|
|
7723
|
+
group: GroupFieldComponent,
|
|
7724
|
+
array: ArrayFieldComponent,
|
|
7725
|
+
image: ImageFieldComponent,
|
|
7726
|
+
file: FileFieldComponent,
|
|
7727
|
+
url: UrlFieldComponent,
|
|
7728
|
+
phone: PhoneFieldComponent,
|
|
7729
|
+
slider: SliderFieldComponent,
|
|
7730
|
+
rating: RatingFieldComponent,
|
|
7731
|
+
color: ColorFieldComponent,
|
|
7732
|
+
tags: TagsFieldComponent,
|
|
7733
|
+
};
|
|
7734
|
+
}
|
|
7735
|
+
/** Register a specific set of field types. Only the components you name are bundled. */
|
|
7736
|
+
function provideFieldTypes(types) {
|
|
7737
|
+
return makeEnvironmentProviders([
|
|
7738
|
+
{ provide: FIELD_TYPE_SETS, useValue: types, multi: true },
|
|
7739
|
+
]);
|
|
7740
|
+
}
|
|
7741
|
+
/** Register all 27 built-in field type keys. Convenience for apps that use most of them. */
|
|
7742
|
+
function provideBuiltInFieldTypes() {
|
|
7743
|
+
return provideFieldTypes(builtInFieldTypes());
|
|
7744
|
+
}
|
|
7745
|
+
|
|
7746
|
+
/**
|
|
7747
|
+
* HookRegistryService — stores and runs Angular-side pre/post hooks.
|
|
7748
|
+
* Consumers register hooks via provideNgxDynamicEntity({ hooks: { 'clients:beforeSave': fn } }).
|
|
7749
|
+
*/
|
|
7750
|
+
class HookRegistryService {
|
|
7751
|
+
registry = inject(HOOK_REGISTRY, { optional: true }) ?? new Map();
|
|
7752
|
+
has(key) {
|
|
7753
|
+
return this.registry.has(key);
|
|
7754
|
+
}
|
|
7755
|
+
async run(key, data, context = {}) {
|
|
7756
|
+
const hook = this.registry.get(key);
|
|
7757
|
+
if (!hook)
|
|
7758
|
+
return data;
|
|
7759
|
+
return hook(data, context);
|
|
7760
|
+
}
|
|
7761
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: HookRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
7762
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: HookRegistryService, providedIn: 'root' });
|
|
7763
|
+
}
|
|
7764
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: HookRegistryService, decorators: [{
|
|
7765
|
+
type: Injectable,
|
|
7766
|
+
args: [{ providedIn: 'root' }]
|
|
7767
|
+
}] });
|
|
7768
|
+
|
|
7769
|
+
/**
|
|
7770
|
+
* RulesEvaluationService — evaluates rules, and owns what a result *means*.
|
|
7771
|
+
*
|
|
7772
|
+
* Precedence lives in one method that both the render filter and the validity sync go
|
|
7773
|
+
* through, so what is on screen and what counts toward validity cannot disagree. Stateless —
|
|
7774
|
+
* every method takes the result it is interpreting, so there is nothing to go stale against a
|
|
7775
|
+
* rebuilt form and nothing to reset between configs.
|
|
7776
|
+
*/
|
|
7777
|
+
class RulesEvaluationService {
|
|
7778
|
+
/**
|
|
7779
|
+
* Evaluate rules against form values and optional original baseline values.
|
|
7780
|
+
*/
|
|
7781
|
+
evaluate(rules, values, originalValues, options) {
|
|
7782
|
+
return evaluateFormRules(rules, values, originalValues, options);
|
|
7783
|
+
}
|
|
7784
|
+
/**
|
|
7785
|
+
* Filter rules pertinent to a single tab.
|
|
7786
|
+
*/
|
|
7787
|
+
filterForTab(rules, tabId, config) {
|
|
7788
|
+
return filterRulesForTab(rules, tabId, config);
|
|
7789
|
+
}
|
|
7790
|
+
/**
|
|
7791
|
+
* Whether a field renders, under the precedence documented on `RuleEvaluationResult`.
|
|
7792
|
+
*
|
|
7793
|
+
* An explicit hide beats everything. A show beats static hiding — `visibility: false`, or a
|
|
7794
|
+
* `showWhen` that does not currently hold — because a rule that could only hide what the
|
|
7795
|
+
* config already hides would have nothing to say, and `{ visibility: true }` was a
|
|
7796
|
+
* documented no-op until it did. Otherwise the config's own visibility decides.
|
|
7797
|
+
*/
|
|
7798
|
+
isFieldVisible(result, field, names, values) {
|
|
7799
|
+
const sets = this.nameSets(result);
|
|
7800
|
+
if (names.some(name => sets.hidden.has(name)))
|
|
7801
|
+
return false;
|
|
7802
|
+
if (names.some(name => sets.shown.has(name)))
|
|
7803
|
+
return true;
|
|
7804
|
+
return evaluateFieldVisibility(field, values);
|
|
7805
|
+
}
|
|
7806
|
+
/** The same precedence for a tab. */
|
|
7807
|
+
isTabVisible(result, tab) {
|
|
7808
|
+
if (result.hiddenTabs.includes(tab.id))
|
|
7809
|
+
return false;
|
|
7810
|
+
return tab.visibility !== false || result.shownTabs.includes(tab.id);
|
|
7811
|
+
}
|
|
7812
|
+
/**
|
|
7813
|
+
* Keep a hidden field's control out of form validity.
|
|
7814
|
+
*
|
|
7815
|
+
* A field hidden by a rule or a `showWhen` is filtered out of the render, but its
|
|
7816
|
+
* validators stay attached — so a hidden *required* field holds `form.invalid` true
|
|
7817
|
+
* forever, permanently disabling Save with nothing on screen to explain why.
|
|
7818
|
+
*
|
|
7819
|
+
* Disabling is the fix rather than stripping validators: Angular excludes disabled controls
|
|
7820
|
+
* from validity, and the field's own validators survive intact for when it comes back.
|
|
7821
|
+
* Values are untouched — a disabled control still carries its value, so rule evaluation and
|
|
7822
|
+
* the emitted record see exactly what they saw before. That also keeps this from feeding
|
|
7823
|
+
* back on itself: hiding a field cannot change the values the rules are evaluated over.
|
|
7824
|
+
*
|
|
7825
|
+
* The predicate is `isFieldVisible`, the same one the render filter uses, so what is on
|
|
7826
|
+
* screen and what counts toward validity cannot drift apart.
|
|
7827
|
+
*
|
|
7828
|
+
* Entries must arrive parent-first, which is what `collectFieldScopes` yields: a container's
|
|
7829
|
+
* children are skipped once the container itself is hidden, because Angular's `disable()`
|
|
7830
|
+
* cascades to descendants and a child's `enable()` walks back up recalculating ancestors.
|
|
7831
|
+
* Toggling a child inside a hidden group would fight its parent.
|
|
7832
|
+
*/
|
|
7833
|
+
syncHiddenFieldState(entries, ctx) {
|
|
7834
|
+
const hiddenContainers = [];
|
|
7835
|
+
for (const entry of entries) {
|
|
7836
|
+
const field = entry.field;
|
|
7837
|
+
if (!field?.id)
|
|
7838
|
+
continue;
|
|
7839
|
+
const address = ctx.addressOf(field);
|
|
7840
|
+
if (hiddenContainers.some(prefix => address.startsWith(`${prefix}.`)))
|
|
7841
|
+
continue;
|
|
7842
|
+
const hidden = !this.isFieldVisible(ctx.result, field, ctx.namesOf(field), ctx.values);
|
|
7843
|
+
if (hidden && (field.type === 'group' || field.type === 'array'))
|
|
7844
|
+
hiddenContainers.push(address);
|
|
7845
|
+
const ctrl = ctx.controlFor(entry);
|
|
7846
|
+
if (!ctrl)
|
|
7847
|
+
continue;
|
|
7848
|
+
if (hidden) {
|
|
7849
|
+
if (ctrl.enabled)
|
|
7850
|
+
ctrl.disable({ emitEvent: false });
|
|
7851
|
+
}
|
|
7852
|
+
else if (ctrl.disabled && !field.disabled) {
|
|
7853
|
+
ctrl.enable({ emitEvent: false });
|
|
7854
|
+
}
|
|
7855
|
+
}
|
|
7856
|
+
}
|
|
7857
|
+
/**
|
|
7858
|
+
* The hidden and shown names as sets, memoised against the result they came from.
|
|
7859
|
+
*
|
|
7860
|
+
* `isFieldVisible` runs once per field per change-detection pass; building two sets from
|
|
7861
|
+
* two arrays each time turns a rule result into allocation proportional to the form. A
|
|
7862
|
+
* `WeakMap` keyed by the result means it is built once per evaluation and collected with it.
|
|
7863
|
+
*/
|
|
7864
|
+
nameSets(result) {
|
|
7865
|
+
let sets = this.nameSetCache.get(result);
|
|
7866
|
+
if (!sets) {
|
|
7867
|
+
sets = { hidden: new Set(result.hiddenFields), shown: new Set(result.shownFields) };
|
|
7868
|
+
this.nameSetCache.set(result, sets);
|
|
7869
|
+
}
|
|
7870
|
+
return sets;
|
|
7871
|
+
}
|
|
7872
|
+
nameSetCache = new WeakMap();
|
|
7873
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: RulesEvaluationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
7874
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: RulesEvaluationService, providedIn: 'root' });
|
|
7875
|
+
}
|
|
7876
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: RulesEvaluationService, decorators: [{
|
|
7877
|
+
type: Injectable,
|
|
7878
|
+
args: [{ providedIn: 'root' }]
|
|
7879
|
+
}] });
|
|
7880
|
+
|
|
7881
|
+
class ConfigSourceService {
|
|
7882
|
+
cache = new Map();
|
|
7883
|
+
inFlight = new Map();
|
|
7884
|
+
handler;
|
|
7885
|
+
constructor(handler) {
|
|
7886
|
+
this.handler = handler;
|
|
7887
|
+
}
|
|
7888
|
+
/** Retrieve an entity config by key, with caching and in-flight deduplication. */
|
|
7889
|
+
async getConfig(entityKey) {
|
|
7890
|
+
if (!entityKey || !this.handler)
|
|
7891
|
+
return undefined;
|
|
7892
|
+
if (this.cache.has(entityKey))
|
|
7893
|
+
return this.cache.get(entityKey);
|
|
7894
|
+
if (this.inFlight.has(entityKey))
|
|
7895
|
+
return this.inFlight.get(entityKey);
|
|
7896
|
+
const promise = (async () => {
|
|
7897
|
+
try {
|
|
7898
|
+
const raw = this.handler(entityKey);
|
|
7899
|
+
let resolved;
|
|
7900
|
+
if (isObservable(raw)) {
|
|
7901
|
+
resolved = await firstValueFrom(raw);
|
|
6480
7902
|
}
|
|
6481
7903
|
else {
|
|
6482
7904
|
resolved = await Promise.resolve(raw);
|
|
@@ -6506,10 +7928,10 @@ class ConfigSourceService {
|
|
|
6506
7928
|
this.cache.clear();
|
|
6507
7929
|
}
|
|
6508
7930
|
}
|
|
6509
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
6510
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.
|
|
7931
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: ConfigSourceService, deps: [{ token: CONFIG_SOURCE, optional: true }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
7932
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: ConfigSourceService, providedIn: 'root' });
|
|
6511
7933
|
}
|
|
6512
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
7934
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: ConfigSourceService, decorators: [{
|
|
6513
7935
|
type: Injectable,
|
|
6514
7936
|
args: [{ providedIn: 'root' }]
|
|
6515
7937
|
}], ctorParameters: () => [{ type: undefined, decorators: [{
|
|
@@ -6524,9 +7946,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
6524
7946
|
*
|
|
6525
7947
|
* The one question this answers is *where a value lives*: which control a field owns, how
|
|
6526
7948
|
* the tab tree nests into groups, how a record is read out of the tree and patched back in.
|
|
6527
|
-
*
|
|
6528
|
-
* evaluation and RBAC — and the cost of that was not length but ownership: the bug Phase 2
|
|
6529
|
-
* fixed was two methods forty lines apart disagreeing about what "every field" meant.
|
|
7949
|
+
* Anything that has to find a field's control goes through here, so there is one answer.
|
|
6530
7950
|
*
|
|
6531
7951
|
* Deliberately stateless. Every method takes the form and the config it is operating on, so
|
|
6532
7952
|
* there is no cached `this.form` to go stale against a rebuilt one, and every method is
|
|
@@ -6618,6 +8038,44 @@ class FormStructureService {
|
|
|
6618
8038
|
}
|
|
6619
8039
|
return curr instanceof FormGroup ? curr : null;
|
|
6620
8040
|
}
|
|
8041
|
+
/**
|
|
8042
|
+
* The control a scope entry addresses, found by its path rather than its bare id.
|
|
8043
|
+
*
|
|
8044
|
+
* The control tree mirrors the scope path exactly — a tab nests by its id and a `group`
|
|
8045
|
+
* under its own — so `form.get(path)` names exactly one control. A bare-id lookup falls
|
|
8046
|
+
* back to a first-match search and, with `address` on two tabs, finds the wrong one.
|
|
8047
|
+
*
|
|
8048
|
+
* A field inside an `array` has no static path: its controls live in `FormArray` rows
|
|
8049
|
+
* built per row. `form.get()` returns null for those, and callers leave them alone.
|
|
8050
|
+
*/
|
|
8051
|
+
controlAt(form, entry) {
|
|
8052
|
+
return form?.get(fieldRefFor(entry.scope, entry.field.id)) ?? null;
|
|
8053
|
+
}
|
|
8054
|
+
/**
|
|
8055
|
+
* The value map rules and `showWhen` are evaluated against.
|
|
8056
|
+
*
|
|
8057
|
+
* Every field appears under two keys: its bare id, and its ref wrapped in brackets. The
|
|
8058
|
+
* bare id is what most configs use, but ids are unique only per scope, so when two scopes
|
|
8059
|
+
* define one the last field walked wins. `[personal.address]` names one field and cannot
|
|
8060
|
+
* be ambiguous, which is why a rule that has to tell them apart uses the ref.
|
|
8061
|
+
*
|
|
8062
|
+
* Both live in one flat map on purpose: `evaluateFormRules` takes a `Record<string,
|
|
8063
|
+
* unknown>` and needs no knowledge of refs at all — the extra keys simply resolve.
|
|
8064
|
+
*/
|
|
8065
|
+
flattenValues(form, entries) {
|
|
8066
|
+
const out = {};
|
|
8067
|
+
for (const entry of entries) {
|
|
8068
|
+
const field = entry.field;
|
|
8069
|
+
if (!field?.id)
|
|
8070
|
+
continue;
|
|
8071
|
+
const ctrl = this.controlAt(form, entry);
|
|
8072
|
+
if (!ctrl)
|
|
8073
|
+
continue;
|
|
8074
|
+
out[field.id] = ctrl.value;
|
|
8075
|
+
out[toRefToken(refOf(field, entry.scope))] = ctrl.value;
|
|
8076
|
+
}
|
|
8077
|
+
return out;
|
|
8078
|
+
}
|
|
6621
8079
|
/**
|
|
6622
8080
|
* Find a control by reference: a bracketed path, or a bare field id.
|
|
6623
8081
|
*
|
|
@@ -6761,10 +8219,10 @@ class FormStructureService {
|
|
|
6761
8219
|
fieldsById.has(key) &&
|
|
6762
8220
|
!patchedFieldIds.has(key));
|
|
6763
8221
|
}
|
|
6764
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
6765
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.
|
|
8222
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: FormStructureService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
8223
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: FormStructureService });
|
|
6766
8224
|
}
|
|
6767
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
8225
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: FormStructureService, decorators: [{
|
|
6768
8226
|
type: Injectable
|
|
6769
8227
|
}] });
|
|
6770
8228
|
|
|
@@ -6796,6 +8254,12 @@ const AUTO_COL_SPAN = {
|
|
|
6796
8254
|
boolean: 4,
|
|
6797
8255
|
checkbox: 4,
|
|
6798
8256
|
'entity-ref': 6,
|
|
8257
|
+
url: 6,
|
|
8258
|
+
phone: 6,
|
|
8259
|
+
slider: 6,
|
|
8260
|
+
rating: 4,
|
|
8261
|
+
color: 4,
|
|
8262
|
+
tags: 6,
|
|
6799
8263
|
// Left at the full width deliberately — textarea, markdown, image, file, group and array
|
|
6800
8264
|
// each hold something a half-row would crop.
|
|
6801
8265
|
};
|
|
@@ -6937,9 +8401,8 @@ class DynamicFormComponent {
|
|
|
6937
8401
|
// ─── Computed ─────────────────────────────────────────────────────────────
|
|
6938
8402
|
ruleResult = computed(() => this.rulesEvaluation.evaluate(this.rules, this.formValues(), this.baseline(), {
|
|
6939
8403
|
// The engine reports rather than logs, because it also runs on a server. This is the
|
|
6940
|
-
// client's answer: say it once per distinct problem, in dev only
|
|
6941
|
-
//
|
|
6942
|
-
// silently skipped rule is exactly as hard to diagnose as a thrown one.
|
|
8404
|
+
// client's answer: say it once per distinct problem, in dev only — a silently skipped
|
|
8405
|
+
// rule is as hard to diagnose as a thrown one.
|
|
6943
8406
|
onProblem: message => this.warnOnce(message),
|
|
6944
8407
|
}), ...(ngDevMode ? [{ debugName: "ruleResult" }] : /* istanbul ignore next */ []));
|
|
6945
8408
|
/** Warned-about rule problems, per component instance — see `warnedAmbiguousIds`. */
|
|
@@ -7056,8 +8519,7 @@ class DynamicFormComponent {
|
|
|
7056
8519
|
/**
|
|
7057
8520
|
* Whether the current roles may see this record at all.
|
|
7058
8521
|
*
|
|
7059
|
-
* `permissions.view`
|
|
7060
|
-
* still received the complete form with every value in the DOM. It is honoured now.
|
|
8522
|
+
* A user whose roles fail `permissions.view` gets no form, so no value reaches the DOM.
|
|
7061
8523
|
*
|
|
7062
8524
|
* As with masking, this is presentational: it stops the browser rendering data, it does
|
|
7063
8525
|
* not stop the data reaching the browser. Authorize on the server.
|
|
@@ -7115,13 +8577,11 @@ class DynamicFormComponent {
|
|
|
7115
8577
|
/**
|
|
7116
8578
|
* Whether the Save button is unavailable.
|
|
7117
8579
|
*
|
|
7118
|
-
*
|
|
7119
|
-
*
|
|
7120
|
-
*
|
|
7121
|
-
* user is left comparing a disabled button against a form that looks, to them, filled in.
|
|
7122
|
-
* On a tabbed form the offending field is usually not even on screen.
|
|
8580
|
+
* Deliberately not `submitBlocked`: a greyed button is the worst answer to "why can't I
|
|
8581
|
+
* save?". It cannot be clicked, so the form never gets to say which field is at fault or
|
|
8582
|
+
* which tab it is on — and on a tabbed form that field is usually not on screen.
|
|
7123
8583
|
*
|
|
7124
|
-
* Save
|
|
8584
|
+
* Save stays available while the form is merely invalid. Clicking it still saves
|
|
7125
8585
|
* nothing — `submit()` checks `submitBlocked` and refuses — but the refusal is where the
|
|
7126
8586
|
* error summary, the tab badges and the jump to the first bad field come from, so pressing
|
|
7127
8587
|
* Save produces an explanation instead of silence.
|
|
@@ -7156,10 +8616,9 @@ class DynamicFormComponent {
|
|
|
7156
8616
|
this.buildForm();
|
|
7157
8617
|
// Clear *before* the guard below, not after it.
|
|
7158
8618
|
//
|
|
7159
|
-
//
|
|
7160
|
-
//
|
|
7161
|
-
//
|
|
7162
|
-
// survived into the next one. Clearing is unconditional and cheap, and safe on its own
|
|
8619
|
+
// A swap can arrive while no tab is visible — a config still being applied, or every
|
|
8620
|
+
// tab hidden by a rule at that instant — and the previous record's tab must not survive
|
|
8621
|
+
// into the next one. Clearing is unconditional and cheap, and safe on its own
|
|
7163
8622
|
// because `activeTabConfig` already falls back to the first visible tab when nothing
|
|
7164
8623
|
// is selected. Selecting the tab is what needs tabs to exist; forgetting the old one
|
|
7165
8624
|
// does not.
|
|
@@ -7169,14 +8628,9 @@ class DynamicFormComponent {
|
|
|
7169
8628
|
}
|
|
7170
8629
|
if (this.visibleTabs.length === 0)
|
|
7171
8630
|
return;
|
|
7172
|
-
//
|
|
7173
|
-
//
|
|
7174
|
-
//
|
|
7175
|
-
// by destroying the form between records; a host that keeps it mounted did not.
|
|
7176
|
-
//
|
|
7177
|
-
// Resetting here is consistent with what already happens: `buildForm` assigns a brand
|
|
7178
|
-
// new FormGroup, so a changed `initialData` has already discarded every control and
|
|
7179
|
-
// all validation state. The tab was the one thing pretending nothing had changed.
|
|
8631
|
+
// A form that stays mounted while `initialData` is swapped must open the next record
|
|
8632
|
+
// on its first tab, not the tab the previous one was left on. `buildForm` has already
|
|
8633
|
+
// discarded every control and all validation state; the active tab resets with them.
|
|
7180
8634
|
if (!this.activeTab()) {
|
|
7181
8635
|
// No focus steal: the panel gains focus when a *user* picks a tab, not when a host
|
|
7182
8636
|
// swaps the record underneath them.
|
|
@@ -7610,25 +9064,10 @@ class DynamicFormComponent {
|
|
|
7610
9064
|
/**
|
|
7611
9065
|
* Ids already warned about, per component instance.
|
|
7612
9066
|
*
|
|
7613
|
-
*
|
|
7614
|
-
*
|
|
7615
|
-
* that lived as long as the server did, and the first request to render a config
|
|
7616
|
-
* suppressed the warning for every request after it. Per instance, the warning is once per
|
|
7617
|
-
* form — which is what "warn once" was meant to mean.
|
|
9067
|
+
* Per instance, never `static`: under SSR a module-global set would outlive every request,
|
|
9068
|
+
* and the first render of a config would suppress the warning for every render after it.
|
|
7618
9069
|
*/
|
|
7619
9070
|
warnedAmbiguousIds = new Set();
|
|
7620
|
-
/**
|
|
7621
|
-
* The value map rules and `showWhen` are evaluated against.
|
|
7622
|
-
*
|
|
7623
|
-
* Every field appears under two keys: its bare id, and its `refererField` path wrapped in brackets.
|
|
7624
|
-
* The bare id is what every config written so far uses and is kept exactly as it was — but
|
|
7625
|
-
* ids are unique per scope, so when two scopes define one the last field walked wins and
|
|
7626
|
-
* the rule reads whichever that is. `[personal.address]` names one field and cannot be
|
|
7627
|
-
* ambiguous, which is why a rule that has to distinguish them uses the ref.
|
|
7628
|
-
*
|
|
7629
|
-
* Both live in one flat map on purpose: `evaluateFormRules` takes a `Record<string,
|
|
7630
|
-
* unknown>` and needs no knowledge of refs at all — the extra keys simply resolve.
|
|
7631
|
-
*/
|
|
7632
9071
|
/**
|
|
7633
9072
|
* Every name a rule may address this field by.
|
|
7634
9073
|
*
|
|
@@ -7662,9 +9101,8 @@ class DynamicFormComponent {
|
|
|
7662
9101
|
/**
|
|
7663
9102
|
* Every field in the config with the scope its value is stored under, cached per config.
|
|
7664
9103
|
*
|
|
7665
|
-
* `collectFieldScopes` walks the whole tree
|
|
7666
|
-
*
|
|
7667
|
-
* tab — a quadratic walk on every change-detection pass. Keyed by config identity, so a
|
|
9104
|
+
* `collectFieldScopes` walks the whole tree, and `pathFor` runs once per rendered field on
|
|
9105
|
+
* every change-detection pass — uncached, that is quadratic. Keyed by config identity, so a
|
|
7668
9106
|
* new config invalidates it without an explicit lifecycle hook to forget.
|
|
7669
9107
|
*/
|
|
7670
9108
|
fieldScopes() {
|
|
@@ -7683,35 +9121,13 @@ class DynamicFormComponent {
|
|
|
7683
9121
|
return this.scopeCache?.byField.get(field);
|
|
7684
9122
|
}
|
|
7685
9123
|
scopeCache;
|
|
7686
|
-
/**
|
|
7687
|
-
* The control a scope entry addresses, named by path rather than by bare id.
|
|
7688
|
-
*
|
|
7689
|
-
* `getControl(field.id)` with no tab falls through to a first-match recursive search of the
|
|
7690
|
-
* whole form, so with `address` on two tabs, hiding one disabled the other. The control
|
|
7691
|
-
* tree mirrors the scope path exactly — `buildTabControls` nests by tab id and
|
|
7692
|
-
* `buildFieldControl` nests a `group` under its own id — so a `[path]` ref resolves through
|
|
7693
|
-
* `form.get()` on `getControl`'s first branch and names exactly one control.
|
|
7694
|
-
*
|
|
7695
|
-
* A field inside an `array` has no static path: its controls live in `FormArray` rows built
|
|
7696
|
-
* per row. `form.get()` returns null for those and they are left alone, which is correct —
|
|
7697
|
-
* a row's controls are created and destroyed with the row.
|
|
7698
|
-
*/
|
|
9124
|
+
/** The control a scope entry addresses — see `FormStructureService.controlAt`. */
|
|
7699
9125
|
controlForEntry(entry) {
|
|
7700
|
-
return this.
|
|
9126
|
+
return this.structure.controlAt(this.form, entry);
|
|
7701
9127
|
}
|
|
9128
|
+
/** The value map rules and `showWhen` are evaluated against — see `FormStructureService.flattenValues`. */
|
|
7702
9129
|
flattenFormValues() {
|
|
7703
|
-
|
|
7704
|
-
for (const entry of collectFieldScopes(this.config)) {
|
|
7705
|
-
const field = entry.field;
|
|
7706
|
-
if (!field?.id)
|
|
7707
|
-
continue;
|
|
7708
|
-
const ctrl = this.getControl(field.id, entry.scope.split('.').pop());
|
|
7709
|
-
if (!ctrl)
|
|
7710
|
-
continue;
|
|
7711
|
-
out[field.id] = ctrl.value;
|
|
7712
|
-
out[toRefToken(refOf(field, entry.scope))] = ctrl.value;
|
|
7713
|
-
}
|
|
7714
|
-
return out;
|
|
9130
|
+
return this.structure.flattenValues(this.form, this.fieldScopes());
|
|
7715
9131
|
}
|
|
7716
9132
|
// ─── autoPatch / patchOnTrue ──────────────────────────────────────────────
|
|
7717
9133
|
/**
|
|
@@ -7738,22 +9154,46 @@ class DynamicFormComponent {
|
|
|
7738
9154
|
/**
|
|
7739
9155
|
* `patchOnTrue` — when a boolean/checkbox field transitions to `true`,
|
|
7740
9156
|
* copy `from` → `to` within the current record.
|
|
9157
|
+
*
|
|
9158
|
+
* The trigger is read by its ref, and a bare `from`/`to` resolves to a sibling in the
|
|
9159
|
+
* trigger's own scope before falling back to a form-wide lookup — so two groups that each
|
|
9160
|
+
* carry `home`, `billing` and a "same as" flag patch within themselves, not across.
|
|
7741
9161
|
*/
|
|
7742
9162
|
runPatchOnTrue(values) {
|
|
7743
|
-
for (const
|
|
7744
|
-
const
|
|
7745
|
-
|
|
9163
|
+
for (const entry of this.fieldScopes()) {
|
|
9164
|
+
const field = entry.field;
|
|
9165
|
+
const mappings = field?.patchOnTrue;
|
|
9166
|
+
if (!field?.id || !mappings?.length)
|
|
7746
9167
|
continue;
|
|
7747
|
-
const
|
|
7748
|
-
|
|
7749
|
-
if (!isTrue || wasTrue)
|
|
9168
|
+
const trigger = toRefToken(refOf(field, entry.scope));
|
|
9169
|
+
if (values[trigger] !== true || this.previousValues[trigger] === true)
|
|
7750
9170
|
continue;
|
|
7751
|
-
const
|
|
7752
|
-
|
|
7753
|
-
|
|
9171
|
+
for (const mapping of mappings) {
|
|
9172
|
+
if (!mapping || isUnsafePath(mapping.to))
|
|
9173
|
+
continue;
|
|
9174
|
+
const from = this.resolveInScope(mapping.from, entry.scope);
|
|
9175
|
+
if (!from || !(from.key in values))
|
|
9176
|
+
continue;
|
|
9177
|
+
this.resolveInScope(mapping.to, entry.scope)?.control?.patchValue(values[from.key], { emitEvent: false });
|
|
7754
9178
|
}
|
|
7755
9179
|
}
|
|
7756
9180
|
}
|
|
9181
|
+
/**
|
|
9182
|
+
* A field named by a mapping, preferring a sibling in `scope` when the name is a bare id.
|
|
9183
|
+
*
|
|
9184
|
+
* `key` is the field's entry in the flattened value map; `control` is the control it owns.
|
|
9185
|
+
*/
|
|
9186
|
+
resolveInScope(name, scope) {
|
|
9187
|
+
if (typeof name !== 'string' || !name)
|
|
9188
|
+
return null;
|
|
9189
|
+
const parsed = parseFieldRef(name);
|
|
9190
|
+
if (parsed.kind !== 'ref') {
|
|
9191
|
+
const sibling = this.fieldScopes().find(e => e.scope === scope && e.field?.id === parsed.value);
|
|
9192
|
+
if (sibling)
|
|
9193
|
+
return { key: toRefToken(refOf(sibling.field, sibling.scope)), control: this.controlForEntry(sibling) };
|
|
9194
|
+
}
|
|
9195
|
+
return { key: name, control: this.getControl(name) };
|
|
9196
|
+
}
|
|
7757
9197
|
/**
|
|
7758
9198
|
* Prefer a control declared on the configured target tab; fall back to a top-level control.
|
|
7759
9199
|
*
|
|
@@ -7889,11 +9329,9 @@ class DynamicFormComponent {
|
|
|
7889
9329
|
async submit() {
|
|
7890
9330
|
if (!this.canSubmit || this.submitBlocked) {
|
|
7891
9331
|
/*
|
|
7892
|
-
*
|
|
7893
|
-
*
|
|
7894
|
-
*
|
|
7895
|
-
* whichever tabs hold them, the user is looking at a different one, and nothing on
|
|
7896
|
-
* screen changes. Three things fix it, and all three are needed — the summary says how
|
|
9332
|
+
* Marking every control touched is not enough on its own: on a tabbed form the errors
|
|
9333
|
+
* appear on whichever tabs hold them, the user is looking at a different one, and
|
|
9334
|
+
* nothing on screen changes. Three things are needed — the summary says how
|
|
7897
9335
|
* many and which, the tab badges say where, and this jump takes the user to the first.
|
|
7898
9336
|
*/
|
|
7899
9337
|
this.markAllTouched();
|
|
@@ -7946,10 +9384,10 @@ class DynamicFormComponent {
|
|
|
7946
9384
|
this.submitAttempted.set(false);
|
|
7947
9385
|
this.formReset.emit();
|
|
7948
9386
|
}
|
|
7949
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
7950
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", 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", layout: "layout" }, outputs: { formSubmit: "formSubmit", formChange: "formChange", formReset: "formReset", activeTabChange: "activeTabChange", saveRejected: "saveRejected" }, host: { listeners: { "window:keydown": "handleKeyboardEvent($event)" } }, providers: [EntityRefSelectionService, FormStructureService], 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\" [class.ngx-form--readonly]=\"readonly\" novalidate>\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 <!--\n Why the last Save did nothing.\n Only after a refused submit \u2014 see `submitAttempted`. Each entry jumps to its field,\n switching tabs on the way, because the field is usually not the one on screen.\n -->\n @if (submitAttempted() && invalidFields().length > 0) {\n <div class=\"ngx-form__error-summary\" role=\"alert\" data-testid=\"error-summary\" tabindex=\"-1\">\n <!--\n `error-summary-heading`, not `-title`: the links below are `error-summary-<fieldId>`,\n and a field whose id happens to be `title` would then share a test id with this line.\n The `extensions` entity has exactly such a field.\n -->\n <p class=\"ngx-form__error-summary-title\" data-testid=\"error-summary-heading\">\n {{ ui.text('errorSummaryTitle', language, { count: invalidFields().length }) }}\n </p>\n <ul class=\"ngx-form__error-summary-list\">\n @for (entry of invalidFields(); track entry.field.id) {\n <li>\n <!--\n The field, and what is wrong with it \u2014 the same sentence the field renders\n under itself, so the summary is not a second opinion. Naming only the field\n left the user to visit each one to find out what it wanted.\n -->\n <button\n type=\"button\"\n class=\"ngx-form__error-summary-link\"\n [attr.data-testid]=\"'error-summary-' + entry.field.id\"\n (click)=\"jumpToField(entry.field.id)\"\n >\n <span class=\"ngx-form__error-summary-field\">{{ resolveFieldLabel(entry.field) }}</span>\n @if (entry.message) {\n <span class=\"ngx-form__error-summary-reason\">{{ entry.message }}</span>\n }\n </button>\n </li>\n }\n </ul>\n </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 {{ ui.text('criticalFieldChanged', language, { fields: changedCriticalFieldLabels() }) }}\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 <!--\n How many fields on this tab are stopping the save.\n The count is in the accessible name rather than the badge, because \"3\" read out\n after a tab name says nothing about what it counts.\n -->\n @if (tabErrorCount(tab); as errors) {\n <span\n class=\"ngx-form__tab-badge\"\n [attr.data-testid]=\"'tab-errors-' + tab.id\"\n [attr.aria-label]=\"ui.text('tabErrorCount', language, { count: errors })\"\n >{{ errors }}</span\n >\n }\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\n class=\"ngx-form__tabs ngx-form__tabs--sub\"\n role=\"tablist\"\n data-testid=\"subtab-strip\"\n style=\"margin-top: -12px; margin-bottom: 12px; font-size: 13px\"\n >\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 @if (subTabErrorCount(subTab); as errors) {\n <span\n class=\"ngx-form__tab-badge\"\n [attr.data-testid]=\"'subtab-errors-' + subTab.id\"\n [attr.aria-label]=\"ui.text('tabErrorCount', language, { count: errors })\"\n >{{ errors }}</span\n >\n }\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 role=\"tabpanel\" tabindex=\"-1\" #formPanel class=\"ngx-form__tabpanel\" [attr.aria-label]=\"activeTabLabel\">\n @if (activeTabModuleComponent) {\n <div class=\"ngx-form__module-panel\" data-testid=\"module-panel\">\n <ng-container\n *ngComponentOutlet=\"activeTabModuleComponent; inputs: (activeSubTabConfig || activeTabConfig)?.moduleInputs\"\n ></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 tabindex=\"-1\"\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 }\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 ui.text(isFieldLocked(field) ? 'unlockField' : 'lockField', language, {\n field: resolveFieldLabel(field),\n })\n \"\n [title]=\"ui.text(isFieldLocked(field) ? 'criticalFieldUnlockHint' : 'criticalFieldLockHint', language)\"\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 type=\"button\" class=\"ngx-form__reset\" data-testid=\"form-reset\" (click)=\"reset()\" [disabled]=\"isSaving()\">\n {{ ui.text('reset', language) }}\n </button>\n <!--\n Two different \"unavailable\" states, and they are not the same thing.\n\n `disabled` is for what a retry cannot fix \u2014 a save in flight, an async check still\n outstanding. `data-blocked` is for validation: the button stays fully operable,\n because pressing it is how the user gets the error summary, but it should not look\n ready either. The stylesheet mutes it without taking the click away.\n\n Deliberately not `aria-disabled`. That announces a control as present-but-inoperable,\n and this one is operable and useful \u2014 pressing it raises a `role=\"alert\"` naming\n every field at fault, which is more than a disabled button could ever say. Tooling\n agrees: Playwright refuses to click through `aria-disabled`, and it would be right\n to, because the attribute would be claiming something untrue.\n -->\n <button\n type=\"submit\"\n class=\"ngx-form__submit\"\n data-testid=\"form-submit\"\n [disabled]=\"submitDisabled\"\n [attr.data-blocked]=\"submitBlocked ? '' : null\"\n [title]=\"ui.text('saveShortcutHint', language)\"\n >\n @if (isSaving() || loading) {\n {{ ui.text('saving', language) }}\n } @else {\n {{ ui.text('save', language) }}\n }\n </button>\n </div>\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 {{ ui.text('accessDenied', language) }}\n </div>\n}\n", styles: [".ngx-form__panel{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:var(--ngx-gap, 18px);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:var(--ngx-radius-sm, 6px);cursor:pointer;font-size:14px;line-height:1;padding:4px 6px}.ngx-form__lock:hover{border-color:var(--ngx-color-border, #d1d5db);background:var(--ngx-color-surface-alt, #f9fafb)}@media(max-width:640px){.ngx-form__panel{grid-template-columns:minmax(0,1fr)}.ngx-form__field-slot{grid-column:1 / -1!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],[formArray],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", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
9387
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: DynamicFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
9388
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", 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", layout: "layout" }, outputs: { formSubmit: "formSubmit", formChange: "formChange", formReset: "formReset", activeTabChange: "activeTabChange", saveRejected: "saveRejected" }, host: { listeners: { "window:keydown": "handleKeyboardEvent($event)" } }, providers: [EntityRefSelectionService, FormStructureService], 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\" [class.ngx-form--readonly]=\"readonly\" novalidate>\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 <!--\n Why the last Save did nothing.\n Only after a refused submit \u2014 see `submitAttempted`. Each entry jumps to its field,\n switching tabs on the way, because the field is usually not the one on screen.\n -->\n @if (submitAttempted() && invalidFields().length > 0) {\n <div class=\"ngx-form__error-summary\" role=\"alert\" data-testid=\"error-summary\" tabindex=\"-1\">\n <!--\n `error-summary-heading`, not `-title`: the links below are `error-summary-<fieldId>`,\n and a field whose id happens to be `title` would then share a test id with this line.\n The `extensions` entity has exactly such a field.\n -->\n <p class=\"ngx-form__error-summary-title\" data-testid=\"error-summary-heading\">\n {{ ui.text('errorSummaryTitle', language, { count: invalidFields().length }) }}\n </p>\n <ul class=\"ngx-form__error-summary-list\">\n @for (entry of invalidFields(); track entry.field.id) {\n <li>\n <!--\n The field, and what is wrong with it \u2014 the same sentence the field renders\n under itself, so the summary is not a second opinion. Naming only the field\n left the user to visit each one to find out what it wanted.\n -->\n <button\n type=\"button\"\n class=\"ngx-form__error-summary-link\"\n [attr.data-testid]=\"'error-summary-' + entry.field.id\"\n (click)=\"jumpToField(entry.field.id)\"\n >\n <span class=\"ngx-form__error-summary-field\">{{ resolveFieldLabel(entry.field) }}</span>\n @if (entry.message) {\n <span class=\"ngx-form__error-summary-reason\">{{ entry.message }}</span>\n }\n </button>\n </li>\n }\n </ul>\n </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 {{ ui.text('criticalFieldChanged', language, { fields: changedCriticalFieldLabels() }) }}\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 <!--\n How many fields on this tab are stopping the save.\n The count is in the accessible name rather than the badge, because \"3\" read out\n after a tab name says nothing about what it counts.\n -->\n @if (tabErrorCount(tab); as errors) {\n <span\n class=\"ngx-form__tab-badge\"\n [attr.data-testid]=\"'tab-errors-' + tab.id\"\n [attr.aria-label]=\"ui.text('tabErrorCount', language, { count: errors })\"\n >{{ errors }}</span\n >\n }\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\n class=\"ngx-form__tabs ngx-form__tabs--sub\"\n role=\"tablist\"\n data-testid=\"subtab-strip\"\n style=\"margin-top: -12px; margin-bottom: 12px; font-size: 13px\"\n >\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 @if (subTabErrorCount(subTab); as errors) {\n <span\n class=\"ngx-form__tab-badge\"\n [attr.data-testid]=\"'subtab-errors-' + subTab.id\"\n [attr.aria-label]=\"ui.text('tabErrorCount', language, { count: errors })\"\n >{{ errors }}</span\n >\n }\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 role=\"tabpanel\" tabindex=\"-1\" #formPanel class=\"ngx-form__tabpanel\" [attr.aria-label]=\"activeTabLabel\">\n @if (activeTabModuleComponent) {\n <div class=\"ngx-form__module-panel\" data-testid=\"module-panel\">\n <ng-container\n *ngComponentOutlet=\"activeTabModuleComponent; inputs: (activeSubTabConfig || activeTabConfig)?.moduleInputs\"\n ></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 tabindex=\"-1\"\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 }\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 ui.text(isFieldLocked(field) ? 'unlockField' : 'lockField', language, {\n field: resolveFieldLabel(field),\n })\n \"\n [title]=\"ui.text(isFieldLocked(field) ? 'criticalFieldUnlockHint' : 'criticalFieldLockHint', language)\"\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 type=\"button\" class=\"ngx-form__reset\" data-testid=\"form-reset\" (click)=\"reset()\" [disabled]=\"isSaving()\">\n {{ ui.text('reset', language) }}\n </button>\n <!--\n Two different \"unavailable\" states, and they are not the same thing.\n\n `disabled` is for what a retry cannot fix \u2014 a save in flight, an async check still\n outstanding. `data-blocked` is for validation: the button stays fully operable,\n because pressing it is how the user gets the error summary, but it should not look\n ready either. The stylesheet mutes it without taking the click away.\n\n Deliberately not `aria-disabled`. That announces a control as present-but-inoperable,\n and this one is operable and useful \u2014 pressing it raises a `role=\"alert\"` naming\n every field at fault, which is more than a disabled button could ever say. Tooling\n agrees: Playwright refuses to click through `aria-disabled`, and it would be right\n to, because the attribute would be claiming something untrue.\n -->\n <button\n type=\"submit\"\n class=\"ngx-form__submit\"\n data-testid=\"form-submit\"\n [disabled]=\"submitDisabled\"\n [attr.data-blocked]=\"submitBlocked ? '' : null\"\n [title]=\"ui.text('saveShortcutHint', language)\"\n >\n @if (isSaving() || loading) {\n {{ ui.text('saving', language) }}\n } @else {\n {{ ui.text('save', language) }}\n }\n </button>\n </div>\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 {{ ui.text('accessDenied', language) }}\n </div>\n}\n", styles: [".ngx-form__panel{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:var(--ngx-gap, 18px);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:var(--ngx-radius-sm, 6px);cursor:pointer;font-size:14px;line-height:1;padding:4px 6px}.ngx-form__lock:hover{border-color:var(--ngx-color-border, #d1d5db);background:var(--ngx-color-surface-alt, #f9fafb)}@media(max-width:640px){.ngx-form__panel{grid-template-columns:minmax(0,1fr)}.ngx-form__field-slot{grid-column:1 / -1!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],[formArray],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", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
7951
9389
|
}
|
|
7952
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
9390
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: DynamicFormComponent, decorators: [{
|
|
7953
9391
|
type: Component,
|
|
7954
9392
|
args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'ngx-dynamic-form', standalone: true, imports: [ReactiveFormsModule, DynamicFieldComponent, NgComponentOutlet], providers: [EntityRefSelectionService, FormStructureService], template: "@if (canView) {\n <form [formGroup]=\"form\" (ngSubmit)=\"submit()\" class=\"ngx-form\" [class.ngx-form--readonly]=\"readonly\" novalidate>\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 <!--\n Why the last Save did nothing.\n Only after a refused submit \u2014 see `submitAttempted`. Each entry jumps to its field,\n switching tabs on the way, because the field is usually not the one on screen.\n -->\n @if (submitAttempted() && invalidFields().length > 0) {\n <div class=\"ngx-form__error-summary\" role=\"alert\" data-testid=\"error-summary\" tabindex=\"-1\">\n <!--\n `error-summary-heading`, not `-title`: the links below are `error-summary-<fieldId>`,\n and a field whose id happens to be `title` would then share a test id with this line.\n The `extensions` entity has exactly such a field.\n -->\n <p class=\"ngx-form__error-summary-title\" data-testid=\"error-summary-heading\">\n {{ ui.text('errorSummaryTitle', language, { count: invalidFields().length }) }}\n </p>\n <ul class=\"ngx-form__error-summary-list\">\n @for (entry of invalidFields(); track entry.field.id) {\n <li>\n <!--\n The field, and what is wrong with it \u2014 the same sentence the field renders\n under itself, so the summary is not a second opinion. Naming only the field\n left the user to visit each one to find out what it wanted.\n -->\n <button\n type=\"button\"\n class=\"ngx-form__error-summary-link\"\n [attr.data-testid]=\"'error-summary-' + entry.field.id\"\n (click)=\"jumpToField(entry.field.id)\"\n >\n <span class=\"ngx-form__error-summary-field\">{{ resolveFieldLabel(entry.field) }}</span>\n @if (entry.message) {\n <span class=\"ngx-form__error-summary-reason\">{{ entry.message }}</span>\n }\n </button>\n </li>\n }\n </ul>\n </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 {{ ui.text('criticalFieldChanged', language, { fields: changedCriticalFieldLabels() }) }}\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 <!--\n How many fields on this tab are stopping the save.\n The count is in the accessible name rather than the badge, because \"3\" read out\n after a tab name says nothing about what it counts.\n -->\n @if (tabErrorCount(tab); as errors) {\n <span\n class=\"ngx-form__tab-badge\"\n [attr.data-testid]=\"'tab-errors-' + tab.id\"\n [attr.aria-label]=\"ui.text('tabErrorCount', language, { count: errors })\"\n >{{ errors }}</span\n >\n }\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\n class=\"ngx-form__tabs ngx-form__tabs--sub\"\n role=\"tablist\"\n data-testid=\"subtab-strip\"\n style=\"margin-top: -12px; margin-bottom: 12px; font-size: 13px\"\n >\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 @if (subTabErrorCount(subTab); as errors) {\n <span\n class=\"ngx-form__tab-badge\"\n [attr.data-testid]=\"'subtab-errors-' + subTab.id\"\n [attr.aria-label]=\"ui.text('tabErrorCount', language, { count: errors })\"\n >{{ errors }}</span\n >\n }\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 role=\"tabpanel\" tabindex=\"-1\" #formPanel class=\"ngx-form__tabpanel\" [attr.aria-label]=\"activeTabLabel\">\n @if (activeTabModuleComponent) {\n <div class=\"ngx-form__module-panel\" data-testid=\"module-panel\">\n <ng-container\n *ngComponentOutlet=\"activeTabModuleComponent; inputs: (activeSubTabConfig || activeTabConfig)?.moduleInputs\"\n ></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 tabindex=\"-1\"\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 }\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 ui.text(isFieldLocked(field) ? 'unlockField' : 'lockField', language, {\n field: resolveFieldLabel(field),\n })\n \"\n [title]=\"ui.text(isFieldLocked(field) ? 'criticalFieldUnlockHint' : 'criticalFieldLockHint', language)\"\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 type=\"button\" class=\"ngx-form__reset\" data-testid=\"form-reset\" (click)=\"reset()\" [disabled]=\"isSaving()\">\n {{ ui.text('reset', language) }}\n </button>\n <!--\n Two different \"unavailable\" states, and they are not the same thing.\n\n `disabled` is for what a retry cannot fix \u2014 a save in flight, an async check still\n outstanding. `data-blocked` is for validation: the button stays fully operable,\n because pressing it is how the user gets the error summary, but it should not look\n ready either. The stylesheet mutes it without taking the click away.\n\n Deliberately not `aria-disabled`. That announces a control as present-but-inoperable,\n and this one is operable and useful \u2014 pressing it raises a `role=\"alert\"` naming\n every field at fault, which is more than a disabled button could ever say. Tooling\n agrees: Playwright refuses to click through `aria-disabled`, and it would be right\n to, because the attribute would be claiming something untrue.\n -->\n <button\n type=\"submit\"\n class=\"ngx-form__submit\"\n data-testid=\"form-submit\"\n [disabled]=\"submitDisabled\"\n [attr.data-blocked]=\"submitBlocked ? '' : null\"\n [title]=\"ui.text('saveShortcutHint', language)\"\n >\n @if (isSaving() || loading) {\n {{ ui.text('saving', language) }}\n } @else {\n {{ ui.text('save', language) }}\n }\n </button>\n </div>\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 {{ ui.text('accessDenied', language) }}\n </div>\n}\n", styles: [".ngx-form__panel{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:var(--ngx-gap, 18px);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:var(--ngx-radius-sm, 6px);cursor:pointer;font-size:14px;line-height:1;padding:4px 6px}.ngx-form__lock:hover{border-color:var(--ngx-color-border, #d1d5db);background:var(--ngx-color-surface-alt, #f9fafb)}@media(max-width:640px){.ngx-form__panel{grid-template-columns:minmax(0,1fr)}.ngx-form__field-slot{grid-column:1 / -1!important}}\n"] }]
|
|
7955
9393
|
}], propDecorators: { config: [{
|
|
@@ -8367,10 +9805,8 @@ class DynamicRecordFormComponent {
|
|
|
8367
9805
|
/**
|
|
8368
9806
|
* What this record is called, as opposed to what kind of thing it is.
|
|
8369
9807
|
*
|
|
8370
|
-
*
|
|
8371
|
-
*
|
|
8372
|
-
* looking at, was the one thing it did not say. The entity keeps the subtitle; this takes
|
|
8373
|
-
* the heading.
|
|
9808
|
+
* A record header exists to answer *which* record am I looking at, so this takes the
|
|
9809
|
+
* heading and the entity name keeps the subtitle.
|
|
8374
9810
|
*
|
|
8375
9811
|
* The order is deliberate. `showOnMinimize` is the author saying "this is what identifies a
|
|
8376
9812
|
* record" — it is what the summary panel and the collapsed view already show — so it is
|
|
@@ -8552,11 +9988,6 @@ class DynamicRecordFormComponent {
|
|
|
8552
9988
|
formatFieldValue(field) {
|
|
8553
9989
|
return formatDisplayValue(field.type, field.options, this.fieldValue(field.id), this.language);
|
|
8554
9990
|
}
|
|
8555
|
-
/**
|
|
8556
|
-
* Finds which tab — and, where the field lives one level down, which sub-tab — owns a
|
|
8557
|
-
* field. The walk used to check top-level `fields` only, so every field in a sub-tab was
|
|
8558
|
-
* simply not found and the jump did nothing at all.
|
|
8559
|
-
*/
|
|
8560
9991
|
/**
|
|
8561
9992
|
* Switches to the tab holding `fieldId`, then scrolls it into view and moves focus to it.
|
|
8562
9993
|
*
|
|
@@ -8569,10 +10000,10 @@ class DynamicRecordFormComponent {
|
|
|
8569
10000
|
jumpToField(fieldId) {
|
|
8570
10001
|
this.dynamicFormComp?.jumpToField(fieldId);
|
|
8571
10002
|
}
|
|
8572
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
8573
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", 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", layout: "layout", viewMode: "viewMode" }, outputs: { formSubmit: "formSubmit", formChange: "formChange", formReset: "formReset", sectionSave: "sectionSave", saveRejected: "saveRejected" }, 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]=\"recordHeading\"\n />\n } @else {\n <div class=\"ngx-record-editor__avatar\" data-testid=\"header-avatar-initial\">\n {{ avatarLetter }}\n </div>\n }\n <div>\n <!--\n The record's own name, with the entity underneath. These were the other way round \u2014\n both showing the entity \u2014 so the header never said which record was open.\n -->\n <h2 class=\"ngx-record-editor__title\" data-testid=\"record-title\">\n {{ recordHeading }}\n </h2>\n <span class=\"ngx-record-editor__subtitle\">{{ ui.text('entityLabel', language, { entity: recordTitle }) }}</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\">{{ ui.text(headerToggleValue ? 'yes' : 'no', language) }}</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]=\"ui.text('dismissBanner', language, { message: banner.message })\"\n (click)=\"dismissInfoBanner(banner.fieldId)\"\n >\n \u2715\n </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 {{ ui.text('recordModified', language) }}\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]=\"ui.text('jumpToField', language, { field: 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\">{{ ui.text('editingSection', language) }}</span>\n <button type=\"button\" class=\"ngx-form__reset\" data-testid=\"cancel-section\" (click)=\"cancelSection()\">\n {{ ui.text('cancel', language) }}\n </button>\n <button type=\"button\" class=\"ngx-form__submit\" data-testid=\"save-section\" (click)=\"saveSection()\">\n {{ ui.text('saveSection', language) }}\n </button>\n } @else {\n <button type=\"button\" class=\"ngx-form__reset\" data-testid=\"edit-section\" (click)=\"editSection()\">\n {{ ui.text('editSection', language) }}\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 >\n {{ ui.text('addRow', language) }}\n </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\n type=\"button\"\n class=\"ngx-record-editor__row-btn\"\n [attr.data-testid]=\"'edit-row-' + arrayField.id + '-' + $index\"\n (click)=\"openEditRow(arrayField, $index)\"\n >\n {{ ui.text('edit', language) }}\n </button>\n <button\n type=\"button\"\n class=\"ngx-record-editor__row-btn\"\n [attr.data-testid]=\"'delete-row-' + arrayField.id + '-' + $index\"\n (click)=\"deleteRow(arrayField, $index)\"\n >\n {{ ui.text('delete', language) }}\n </button>\n }\n </div>\n } @empty {\n <p class=\"ngx-record-editor__row-empty\">{{ ui.text('noRows', language) }}</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>{{ rowDrawerTitle(rowField) }}</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()\">\n {{ ui.text('cancel', language) }}\n </button>\n <button type=\"button\" class=\"ngx-form__submit\" data-testid=\"save-row\" (click)=\"saveRow()\">\n {{ ui.text('saveRow', language) }}\n </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 [layout]=\"layout\"\n [showInfoBanners]=\"false\"\n [loading]=\"loading\"\n [error]=\"error\"\n (formChange)=\"onFormChange($event)\"\n (formSubmit)=\"onFormSubmit($event)\"\n (saveRejected)=\"saveRejected.emit($event)\"\n (formReset)=\"onFormReset()\"\n (activeTabChange)=\"onActiveTabChange($event)\"\n />\n</div>\n", styles: [".ngx-record-editor{display:flex;flex-direction:column;gap:var(--ngx-gap, 16px);background:var(--ngx-color-surface, #ffffff);border:1px solid var(--ngx-color-border, #e5e7eb);border-radius:var(--ngx-radius, 12px);padding:20px}.ngx-record-editor__header{display:flex;align-items:center;justify-content:space-between;gap:14px;padding-bottom:var(--ngx-gap-sm, 16px);border-bottom:1px solid var(--ngx-color-border, #f3f4f6)}.ngx-record-editor__title-group{display:flex;align-items:center;gap:12px;min-width:0}.ngx-record-editor__avatar{width:44px;height:44px;border-radius:50%;object-fit:cover;background:var(--ngx-color-accent-soft, #e0e7ff);color:var(--ngx-color-accent, #4f46e5);border:1px solid var(--ngx-color-border, transparent);display:flex;align-items:center;justify-content:center;font-weight:700;font-size:17px;flex:none}.ngx-record-editor__title{font-size:19px;font-weight:700;color:var(--ngx-color-text, #111827);margin:0;letter-spacing:-.01em}.ngx-record-editor__subtitle{font-size:var(--ngx-hint-size, 13px);color:var(--ngx-color-muted, #6b7280)}.ngx-record-editor__header-toggle{display:flex;align-items:center;gap:8px;font-size:var(--ngx-label-size, 13px);font-weight:var(--ngx-label-weight, 600);color:var(--ngx-color-text, #374151)}.ngx-record-editor__header-toggle-text{font-weight:600}.ngx-record-editor__banner{padding:11px 14px;border-radius:var(--ngx-radius-sm, 8px);font-size:var(--ngx-font-size, 13px);border:1px solid transparent;border-left-width:3px}.ngx-record-editor__banner--info{background:var(--ngx-color-info-soft, #eff6ff);border-color:var(--ngx-color-border, #dbeafe);border-left-color:var(--ngx-color-info, #1d4ed8);color:var(--ngx-color-info, #1d4ed8);display:flex;align-items:center;justify-content:space-between;gap:12px}.ngx-record-editor__banner--error{background:var(--ngx-color-error-soft, #fef2f2);border-color:var(--ngx-color-border, #fecaca);border-left-color:var(--ngx-color-error, #b91c1c);color:var(--ngx-color-error, #b91c1c)}.ngx-record-editor__banner--warning{background:var(--ngx-color-warning-soft, #fffbeb);border-color:var(--ngx-color-border, #fef3c7);border-left-color:var(--ngx-color-warning, #b45309);color:var(--ngx-color-warning, #b45309)}.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__section-bar{display:flex;align-items:center;justify-content:flex-end;flex-wrap:wrap;gap:8px}.ngx-record-editor__section-hint{margin-right:auto;font-size:var(--ngx-hint-size, 12px);color:var(--ngx-color-muted, #6b7280)}.ngx-record-editor__rows{border:1px solid var(--ngx-color-border, #f3f4f6);border-radius:var(--ngx-radius-sm, 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:8px 10px;border-radius:var(--ngx-radius-sm, 6px);background:var(--ngx-color-surface-alt, #f9fafb)}.ngx-record-editor__row-text{flex:1;font-size:var(--ngx-font-size, 13px);overflow-wrap:anywhere}.ngx-record-editor__row-btn{background:var(--ngx-color-surface, none);border:1px solid var(--ngx-color-border, #e5e7eb);border-radius:var(--ngx-radius-sm, 6px);cursor:pointer;font:inherit;font-size:var(--ngx-hint-size, 12px);font-weight:var(--ngx-label-weight, 600);color:inherit;padding:4px 10px}.ngx-record-editor__row-btn:hover{border-color:var(--ngx-color-accent, #e5e7eb);color:var(--ngx-color-accent, inherit)}.ngx-record-editor__row-empty{font-size:var(--ngx-hint-size, 12px);color:var(--ngx-color-muted, #6b7280);margin:0}.ngx-record-editor__drawer{border:1px solid var(--ngx-color-border, #dbeafe);background:var(--ngx-color-surface-alt, #f8fafc);border-radius:var(--ngx-radius, 8px);padding:16px;display:flex;flex-direction:column;gap:12px}.ngx-record-editor__summary-panel{background:var(--ngx-color-surface-alt, #f9fafb);border:1px solid var(--ngx-color-border, #f3f4f6);border-radius:var(--ngx-radius, 8px);padding:12px 16px;display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:4px var(--ngx-gap, 12px)}.ngx-record-editor__summary-item{display:flex;flex-direction:column;cursor:pointer;padding:6px 10px;border-radius:var(--ngx-radius-sm, 6px);min-width:0;transition:background .15s}.ngx-record-editor__summary-item:hover{background:var(--ngx-color-accent-soft, #eff6ff)}.ngx-record-editor__summary-label{font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:var(--ngx-color-muted, #6b7280);font-weight:600}.ngx-record-editor__summary-val{font-size:var(--ngx-font-size, 14px);font-weight:600;color:var(--ngx-color-text, #1f2937);overflow-wrap:anywhere}@media(prefers-reduced-motion:reduce){.ngx-record-editor__summary-item{transition:none}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],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", "layout"], outputs: ["formSubmit", "formChange", "formReset", "activeTabChange", "saveRejected"] }, { kind: "component", type: DynamicFieldComponent, selector: "ngx-dynamic-field", inputs: ["field", "control", "config", "currentTabId", "language", "readonly", "userRoles"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
10003
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: DynamicRecordFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
10004
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", 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", layout: "layout", viewMode: "viewMode" }, outputs: { formSubmit: "formSubmit", formChange: "formChange", formReset: "formReset", sectionSave: "sectionSave", saveRejected: "saveRejected" }, 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]=\"recordHeading\"\n />\n } @else {\n <div class=\"ngx-record-editor__avatar\" data-testid=\"header-avatar-initial\">\n {{ avatarLetter }}\n </div>\n }\n <div>\n <!--\n The record's own name, with the entity underneath. These were the other way round \u2014\n both showing the entity \u2014 so the header never said which record was open.\n -->\n <h2 class=\"ngx-record-editor__title\" data-testid=\"record-title\">\n {{ recordHeading }}\n </h2>\n <span class=\"ngx-record-editor__subtitle\">{{ ui.text('entityLabel', language, { entity: recordTitle }) }}</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\">{{ ui.text(headerToggleValue ? 'yes' : 'no', language) }}</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]=\"ui.text('dismissBanner', language, { message: banner.message })\"\n (click)=\"dismissInfoBanner(banner.fieldId)\"\n >\n \u2715\n </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 {{ ui.text('recordModified', language) }}\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]=\"ui.text('jumpToField', language, { field: 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\">{{ ui.text('editingSection', language) }}</span>\n <button type=\"button\" class=\"ngx-form__reset\" data-testid=\"cancel-section\" (click)=\"cancelSection()\">\n {{ ui.text('cancel', language) }}\n </button>\n <button type=\"button\" class=\"ngx-form__submit\" data-testid=\"save-section\" (click)=\"saveSection()\">\n {{ ui.text('saveSection', language) }}\n </button>\n } @else {\n <button type=\"button\" class=\"ngx-form__reset\" data-testid=\"edit-section\" (click)=\"editSection()\">\n {{ ui.text('editSection', language) }}\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 >\n {{ ui.text('addRow', language) }}\n </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\n type=\"button\"\n class=\"ngx-record-editor__row-btn\"\n [attr.data-testid]=\"'edit-row-' + arrayField.id + '-' + $index\"\n (click)=\"openEditRow(arrayField, $index)\"\n >\n {{ ui.text('edit', language) }}\n </button>\n <button\n type=\"button\"\n class=\"ngx-record-editor__row-btn\"\n [attr.data-testid]=\"'delete-row-' + arrayField.id + '-' + $index\"\n (click)=\"deleteRow(arrayField, $index)\"\n >\n {{ ui.text('delete', language) }}\n </button>\n }\n </div>\n } @empty {\n <p class=\"ngx-record-editor__row-empty\">{{ ui.text('noRows', language) }}</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>{{ rowDrawerTitle(rowField) }}</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()\">\n {{ ui.text('cancel', language) }}\n </button>\n <button type=\"button\" class=\"ngx-form__submit\" data-testid=\"save-row\" (click)=\"saveRow()\">\n {{ ui.text('saveRow', language) }}\n </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 [layout]=\"layout\"\n [showInfoBanners]=\"false\"\n [loading]=\"loading\"\n [error]=\"error\"\n (formChange)=\"onFormChange($event)\"\n (formSubmit)=\"onFormSubmit($event)\"\n (saveRejected)=\"saveRejected.emit($event)\"\n (formReset)=\"onFormReset()\"\n (activeTabChange)=\"onActiveTabChange($event)\"\n />\n</div>\n", styles: [".ngx-record-editor{display:flex;flex-direction:column;gap:var(--ngx-gap, 16px);background:var(--ngx-color-surface, #ffffff);border:1px solid var(--ngx-color-border, #e5e7eb);border-radius:var(--ngx-radius, 12px);padding:20px}.ngx-record-editor__header{display:flex;align-items:center;justify-content:space-between;gap:14px;padding-bottom:var(--ngx-gap-sm, 16px);border-bottom:1px solid var(--ngx-color-border, #f3f4f6)}.ngx-record-editor__title-group{display:flex;align-items:center;gap:12px;min-width:0}.ngx-record-editor__avatar{width:44px;height:44px;border-radius:50%;object-fit:cover;background:var(--ngx-color-accent-soft, #e0e7ff);color:var(--ngx-color-accent, #4f46e5);border:1px solid var(--ngx-color-border, transparent);display:flex;align-items:center;justify-content:center;font-weight:700;font-size:17px;flex:none}.ngx-record-editor__title{font-size:19px;font-weight:700;color:var(--ngx-color-text, #111827);margin:0;letter-spacing:-.01em}.ngx-record-editor__subtitle{font-size:var(--ngx-hint-size, 13px);color:var(--ngx-color-muted, #6b7280)}.ngx-record-editor__header-toggle{display:flex;align-items:center;gap:8px;font-size:var(--ngx-label-size, 13px);font-weight:var(--ngx-label-weight, 600);color:var(--ngx-color-text, #374151)}.ngx-record-editor__header-toggle-text{font-weight:600}.ngx-record-editor__banner{padding:11px 14px;border-radius:var(--ngx-radius-sm, 8px);font-size:var(--ngx-font-size, 13px);border:1px solid transparent;border-left-width:3px}.ngx-record-editor__banner--info{background:var(--ngx-color-info-soft, #eff6ff);border-color:var(--ngx-color-border, #dbeafe);border-left-color:var(--ngx-color-info, #1d4ed8);color:var(--ngx-color-info, #1d4ed8);display:flex;align-items:center;justify-content:space-between;gap:12px}.ngx-record-editor__banner--error{background:var(--ngx-color-error-soft, #fef2f2);border-color:var(--ngx-color-border, #fecaca);border-left-color:var(--ngx-color-error, #b91c1c);color:var(--ngx-color-error, #b91c1c)}.ngx-record-editor__banner--warning{background:var(--ngx-color-warning-soft, #fffbeb);border-color:var(--ngx-color-border, #fef3c7);border-left-color:var(--ngx-color-warning, #b45309);color:var(--ngx-color-warning, #b45309)}.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__section-bar{display:flex;align-items:center;justify-content:flex-end;flex-wrap:wrap;gap:8px}.ngx-record-editor__section-hint{margin-right:auto;font-size:var(--ngx-hint-size, 12px);color:var(--ngx-color-muted, #6b7280)}.ngx-record-editor__rows{border:1px solid var(--ngx-color-border, #f3f4f6);border-radius:var(--ngx-radius-sm, 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:8px 10px;border-radius:var(--ngx-radius-sm, 6px);background:var(--ngx-color-surface-alt, #f9fafb)}.ngx-record-editor__row-text{flex:1;font-size:var(--ngx-font-size, 13px);overflow-wrap:anywhere}.ngx-record-editor__row-btn{background:var(--ngx-color-surface, none);border:1px solid var(--ngx-color-border, #e5e7eb);border-radius:var(--ngx-radius-sm, 6px);cursor:pointer;font:inherit;font-size:var(--ngx-hint-size, 12px);font-weight:var(--ngx-label-weight, 600);color:inherit;padding:4px 10px}.ngx-record-editor__row-btn:hover{border-color:var(--ngx-color-accent, #e5e7eb);color:var(--ngx-color-accent, inherit)}.ngx-record-editor__row-empty{font-size:var(--ngx-hint-size, 12px);color:var(--ngx-color-muted, #6b7280);margin:0}.ngx-record-editor__drawer{border:1px solid var(--ngx-color-border, #dbeafe);background:var(--ngx-color-surface-alt, #f8fafc);border-radius:var(--ngx-radius, 8px);padding:16px;display:flex;flex-direction:column;gap:12px}.ngx-record-editor__summary-panel{background:var(--ngx-color-surface-alt, #f9fafb);border:1px solid var(--ngx-color-border, #f3f4f6);border-radius:var(--ngx-radius, 8px);padding:12px 16px;display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:4px var(--ngx-gap, 12px)}.ngx-record-editor__summary-item{display:flex;flex-direction:column;cursor:pointer;padding:6px 10px;border-radius:var(--ngx-radius-sm, 6px);min-width:0;transition:background .15s}.ngx-record-editor__summary-item:hover{background:var(--ngx-color-accent-soft, #eff6ff)}.ngx-record-editor__summary-label{font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:var(--ngx-color-muted, #6b7280);font-weight:600}.ngx-record-editor__summary-val{font-size:var(--ngx-font-size, 14px);font-weight:600;color:var(--ngx-color-text, #1f2937);overflow-wrap:anywhere}@media(prefers-reduced-motion:reduce){.ngx-record-editor__summary-item{transition:none}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],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", "layout"], outputs: ["formSubmit", "formChange", "formReset", "activeTabChange", "saveRejected"] }, { kind: "component", type: DynamicFieldComponent, selector: "ngx-dynamic-field", inputs: ["field", "control", "config", "currentTabId", "language", "readonly", "userRoles"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
8574
10005
|
}
|
|
8575
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
10006
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: DynamicRecordFormComponent, decorators: [{
|
|
8576
10007
|
type: Component,
|
|
8577
10008
|
args: [{ changeDetection: ChangeDetectionStrategy.OnPush, 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]=\"recordHeading\"\n />\n } @else {\n <div class=\"ngx-record-editor__avatar\" data-testid=\"header-avatar-initial\">\n {{ avatarLetter }}\n </div>\n }\n <div>\n <!--\n The record's own name, with the entity underneath. These were the other way round \u2014\n both showing the entity \u2014 so the header never said which record was open.\n -->\n <h2 class=\"ngx-record-editor__title\" data-testid=\"record-title\">\n {{ recordHeading }}\n </h2>\n <span class=\"ngx-record-editor__subtitle\">{{ ui.text('entityLabel', language, { entity: recordTitle }) }}</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\">{{ ui.text(headerToggleValue ? 'yes' : 'no', language) }}</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]=\"ui.text('dismissBanner', language, { message: banner.message })\"\n (click)=\"dismissInfoBanner(banner.fieldId)\"\n >\n \u2715\n </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 {{ ui.text('recordModified', language) }}\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]=\"ui.text('jumpToField', language, { field: 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\">{{ ui.text('editingSection', language) }}</span>\n <button type=\"button\" class=\"ngx-form__reset\" data-testid=\"cancel-section\" (click)=\"cancelSection()\">\n {{ ui.text('cancel', language) }}\n </button>\n <button type=\"button\" class=\"ngx-form__submit\" data-testid=\"save-section\" (click)=\"saveSection()\">\n {{ ui.text('saveSection', language) }}\n </button>\n } @else {\n <button type=\"button\" class=\"ngx-form__reset\" data-testid=\"edit-section\" (click)=\"editSection()\">\n {{ ui.text('editSection', language) }}\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 >\n {{ ui.text('addRow', language) }}\n </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\n type=\"button\"\n class=\"ngx-record-editor__row-btn\"\n [attr.data-testid]=\"'edit-row-' + arrayField.id + '-' + $index\"\n (click)=\"openEditRow(arrayField, $index)\"\n >\n {{ ui.text('edit', language) }}\n </button>\n <button\n type=\"button\"\n class=\"ngx-record-editor__row-btn\"\n [attr.data-testid]=\"'delete-row-' + arrayField.id + '-' + $index\"\n (click)=\"deleteRow(arrayField, $index)\"\n >\n {{ ui.text('delete', language) }}\n </button>\n }\n </div>\n } @empty {\n <p class=\"ngx-record-editor__row-empty\">{{ ui.text('noRows', language) }}</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>{{ rowDrawerTitle(rowField) }}</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()\">\n {{ ui.text('cancel', language) }}\n </button>\n <button type=\"button\" class=\"ngx-form__submit\" data-testid=\"save-row\" (click)=\"saveRow()\">\n {{ ui.text('saveRow', language) }}\n </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 [layout]=\"layout\"\n [showInfoBanners]=\"false\"\n [loading]=\"loading\"\n [error]=\"error\"\n (formChange)=\"onFormChange($event)\"\n (formSubmit)=\"onFormSubmit($event)\"\n (saveRejected)=\"saveRejected.emit($event)\"\n (formReset)=\"onFormReset()\"\n (activeTabChange)=\"onActiveTabChange($event)\"\n />\n</div>\n", styles: [".ngx-record-editor{display:flex;flex-direction:column;gap:var(--ngx-gap, 16px);background:var(--ngx-color-surface, #ffffff);border:1px solid var(--ngx-color-border, #e5e7eb);border-radius:var(--ngx-radius, 12px);padding:20px}.ngx-record-editor__header{display:flex;align-items:center;justify-content:space-between;gap:14px;padding-bottom:var(--ngx-gap-sm, 16px);border-bottom:1px solid var(--ngx-color-border, #f3f4f6)}.ngx-record-editor__title-group{display:flex;align-items:center;gap:12px;min-width:0}.ngx-record-editor__avatar{width:44px;height:44px;border-radius:50%;object-fit:cover;background:var(--ngx-color-accent-soft, #e0e7ff);color:var(--ngx-color-accent, #4f46e5);border:1px solid var(--ngx-color-border, transparent);display:flex;align-items:center;justify-content:center;font-weight:700;font-size:17px;flex:none}.ngx-record-editor__title{font-size:19px;font-weight:700;color:var(--ngx-color-text, #111827);margin:0;letter-spacing:-.01em}.ngx-record-editor__subtitle{font-size:var(--ngx-hint-size, 13px);color:var(--ngx-color-muted, #6b7280)}.ngx-record-editor__header-toggle{display:flex;align-items:center;gap:8px;font-size:var(--ngx-label-size, 13px);font-weight:var(--ngx-label-weight, 600);color:var(--ngx-color-text, #374151)}.ngx-record-editor__header-toggle-text{font-weight:600}.ngx-record-editor__banner{padding:11px 14px;border-radius:var(--ngx-radius-sm, 8px);font-size:var(--ngx-font-size, 13px);border:1px solid transparent;border-left-width:3px}.ngx-record-editor__banner--info{background:var(--ngx-color-info-soft, #eff6ff);border-color:var(--ngx-color-border, #dbeafe);border-left-color:var(--ngx-color-info, #1d4ed8);color:var(--ngx-color-info, #1d4ed8);display:flex;align-items:center;justify-content:space-between;gap:12px}.ngx-record-editor__banner--error{background:var(--ngx-color-error-soft, #fef2f2);border-color:var(--ngx-color-border, #fecaca);border-left-color:var(--ngx-color-error, #b91c1c);color:var(--ngx-color-error, #b91c1c)}.ngx-record-editor__banner--warning{background:var(--ngx-color-warning-soft, #fffbeb);border-color:var(--ngx-color-border, #fef3c7);border-left-color:var(--ngx-color-warning, #b45309);color:var(--ngx-color-warning, #b45309)}.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__section-bar{display:flex;align-items:center;justify-content:flex-end;flex-wrap:wrap;gap:8px}.ngx-record-editor__section-hint{margin-right:auto;font-size:var(--ngx-hint-size, 12px);color:var(--ngx-color-muted, #6b7280)}.ngx-record-editor__rows{border:1px solid var(--ngx-color-border, #f3f4f6);border-radius:var(--ngx-radius-sm, 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:8px 10px;border-radius:var(--ngx-radius-sm, 6px);background:var(--ngx-color-surface-alt, #f9fafb)}.ngx-record-editor__row-text{flex:1;font-size:var(--ngx-font-size, 13px);overflow-wrap:anywhere}.ngx-record-editor__row-btn{background:var(--ngx-color-surface, none);border:1px solid var(--ngx-color-border, #e5e7eb);border-radius:var(--ngx-radius-sm, 6px);cursor:pointer;font:inherit;font-size:var(--ngx-hint-size, 12px);font-weight:var(--ngx-label-weight, 600);color:inherit;padding:4px 10px}.ngx-record-editor__row-btn:hover{border-color:var(--ngx-color-accent, #e5e7eb);color:var(--ngx-color-accent, inherit)}.ngx-record-editor__row-empty{font-size:var(--ngx-hint-size, 12px);color:var(--ngx-color-muted, #6b7280);margin:0}.ngx-record-editor__drawer{border:1px solid var(--ngx-color-border, #dbeafe);background:var(--ngx-color-surface-alt, #f8fafc);border-radius:var(--ngx-radius, 8px);padding:16px;display:flex;flex-direction:column;gap:12px}.ngx-record-editor__summary-panel{background:var(--ngx-color-surface-alt, #f9fafb);border:1px solid var(--ngx-color-border, #f3f4f6);border-radius:var(--ngx-radius, 8px);padding:12px 16px;display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:4px var(--ngx-gap, 12px)}.ngx-record-editor__summary-item{display:flex;flex-direction:column;cursor:pointer;padding:6px 10px;border-radius:var(--ngx-radius-sm, 6px);min-width:0;transition:background .15s}.ngx-record-editor__summary-item:hover{background:var(--ngx-color-accent-soft, #eff6ff)}.ngx-record-editor__summary-label{font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:var(--ngx-color-muted, #6b7280);font-weight:600}.ngx-record-editor__summary-val{font-size:var(--ngx-font-size, 14px);font-weight:600;color:var(--ngx-color-text, #1f2937);overflow-wrap:anywhere}@media(prefers-reduced-motion:reduce){.ngx-record-editor__summary-item{transition:none}}\n"] }]
|
|
8578
10009
|
}], propDecorators: { config: [{
|
|
@@ -8738,10 +10169,10 @@ class LocalImportTransport {
|
|
|
8738
10169
|
const csv = toCsv(spec.columns.map(column => column.header));
|
|
8739
10170
|
return new Blob([csv], { type: 'text/csv;charset=utf-8' });
|
|
8740
10171
|
}
|
|
8741
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
8742
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.
|
|
10172
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: LocalImportTransport, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
10173
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: LocalImportTransport, providedIn: 'root' });
|
|
8743
10174
|
}
|
|
8744
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
10175
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: LocalImportTransport, decorators: [{
|
|
8745
10176
|
type: Injectable,
|
|
8746
10177
|
args: [{ providedIn: 'root' }]
|
|
8747
10178
|
}] });
|
|
@@ -8965,8 +10396,8 @@ class ImportErrorsComponent {
|
|
|
8965
10396
|
.sort((a, b) => a[0] - b[0])
|
|
8966
10397
|
.map(([row, problems]) => ({ row, problems }));
|
|
8967
10398
|
}
|
|
8968
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
8969
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.
|
|
10399
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: ImportErrorsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
10400
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: ImportErrorsComponent, isStandalone: true, selector: "ngx-import-errors", inputs: { errors: "errors", language: "language" }, ngImport: i0, template: `
|
|
8970
10401
|
@if (errors.length) {
|
|
8971
10402
|
<section class="ngx-import-errors" data-testid="import-errors">
|
|
8972
10403
|
<h4 class="ngx-import-errors__heading">{{ ui.text('importErrorsHeading', language) }}</h4>
|
|
@@ -8998,7 +10429,7 @@ class ImportErrorsComponent {
|
|
|
8998
10429
|
}
|
|
8999
10430
|
`, isInline: true, styles: [".ngx-import-errors__heading{margin:0 0 8px;font-size:var(--ngx-font-size, 14px);color:var(--ngx-color-error, #b91c1c)}.ngx-import-errors__table{width:100%;border-collapse:collapse}.ngx-import-errors__table th,.ngx-import-errors__table td{text-align:left;padding:6px 10px;border-bottom:1px solid var(--ngx-color-border, #e5e7eb);vertical-align:top}.ngx-import-errors__table thead th{color:var(--ngx-color-muted, #6b7280)}.ngx-import-errors__table tbody th{white-space:nowrap;color:var(--ngx-color-error, #b91c1c)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
9000
10431
|
}
|
|
9001
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
10432
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: ImportErrorsComponent, decorators: [{
|
|
9002
10433
|
type: Component,
|
|
9003
10434
|
args: [{ selector: 'ngx-import-errors', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [], template: `
|
|
9004
10435
|
@if (errors.length) {
|
|
@@ -9109,8 +10540,8 @@ class ImportMapperComponent {
|
|
|
9109
10540
|
entries,
|
|
9110
10541
|
};
|
|
9111
10542
|
}
|
|
9112
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
9113
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.
|
|
10543
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: ImportMapperComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
10544
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: ImportMapperComponent, isStandalone: true, selector: "ngx-import-mapper", inputs: { config: "config", headers: "headers", plan: "plan", language: "language" }, outputs: { planChange: "planChange" }, usesOnChanges: true, ngImport: i0, template: `
|
|
9114
10545
|
<section class="ngx-import-mapper" data-testid="import-mapper">
|
|
9115
10546
|
<p class="ngx-import-mapper__summary" data-testid="import-mapped-count">
|
|
9116
10547
|
{{ ui.text('importMappedCount', language, { mapped: mappedCount(), total: rows().length }) }}
|
|
@@ -9168,7 +10599,7 @@ class ImportMapperComponent {
|
|
|
9168
10599
|
</section>
|
|
9169
10600
|
`, isInline: true, styles: [".ngx-import-mapper__summary,.ngx-import-mapper__warning,.ngx-import-mapper__note{margin:0 0 10px}.ngx-import-mapper__warning{color:var(--ngx-color-warning, #b45309);background:var(--ngx-color-warning-soft, #fffbeb);border:1px solid var(--ngx-color-border, #fef3c7);border-radius:var(--ngx-radius-sm, 6px);padding:8px 12px}.ngx-import-mapper__note{color:var(--ngx-color-muted, #6b7280)}.ngx-import-mapper__table{width:100%;border-collapse:collapse}.ngx-import-mapper__table th,.ngx-import-mapper__table td{text-align:left;padding:8px 10px;border-bottom:1px solid var(--ngx-color-border, #e5e7eb);vertical-align:middle}.ngx-import-mapper__table thead th{color:var(--ngx-color-muted, #6b7280);font-size:var(--ngx-font-size, 13px);text-transform:uppercase;letter-spacing:.04em}.ngx-import-mapper__required{color:var(--ngx-color-error, #b91c1c);margin-left:2px}.ngx-import-mapper__select{min-width:220px;max-width:100%;padding:6px 8px;border:1px solid var(--ngx-color-border, #d1d5db);border-radius:var(--ngx-radius-sm, 6px);background:var(--ngx-color-surface, #ffffff);color:inherit;font:inherit}.ngx-import-mapper__guess{margin-left:8px;padding:2px 8px;border-radius:999px;font-size:12px;color:var(--ngx-color-info, #1d4ed8);background:var(--ngx-color-info-soft, #eff6ff)}@media(max-width:640px){.ngx-import-mapper__select{min-width:0;width:100%}}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { 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.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
9170
10601
|
}
|
|
9171
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
10602
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: ImportMapperComponent, decorators: [{
|
|
9172
10603
|
type: Component,
|
|
9173
10604
|
args: [{ selector: 'ngx-import-mapper', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [FormsModule], template: `
|
|
9174
10605
|
<section class="ngx-import-mapper" data-testid="import-mapper">
|
|
@@ -9310,8 +10741,8 @@ class ImportPreviewComponent {
|
|
|
9310
10741
|
}),
|
|
9311
10742
|
})));
|
|
9312
10743
|
}
|
|
9313
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
9314
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.
|
|
10744
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: ImportPreviewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
10745
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: ImportPreviewComponent, isStandalone: true, selector: "ngx-import-preview", inputs: { config: "config", plan: "plan", rows: "rows", lookups: "lookups", rules: "rules", language: "language" }, usesOnChanges: true, ngImport: i0, template: `
|
|
9315
10746
|
<section class="ngx-import-preview" data-testid="import-preview">
|
|
9316
10747
|
<h4 class="ngx-import-preview__heading">
|
|
9317
10748
|
{{ ui.text('importPreviewHeading', language, { count: rows.length }) }}
|
|
@@ -9349,7 +10780,7 @@ class ImportPreviewComponent {
|
|
|
9349
10780
|
</section>
|
|
9350
10781
|
`, isInline: true, styles: [".ngx-import-preview__heading{margin:0 0 8px;font-size:var(--ngx-font-size, 14px)}.ngx-import-preview__scroll{overflow-x:auto;border:1px solid var(--ngx-color-border, #e5e7eb);border-radius:var(--ngx-radius-sm, 6px)}.ngx-import-preview__table{width:100%;border-collapse:collapse;white-space:nowrap}.ngx-import-preview__table th,.ngx-import-preview__table td{text-align:left;padding:6px 10px;border-bottom:1px solid var(--ngx-color-border, #f3f4f6)}.ngx-import-preview__table thead th{background:var(--ngx-color-surface-alt, #f9fafb);color:var(--ngx-color-muted, #6b7280)}.ngx-import-preview__cell--error{color:var(--ngx-color-error, #b91c1c);background:var(--ngx-color-error-soft, #fef2f2)}.ngx-import-preview__row--rejected td{opacity:.65}.ngx-import-preview__reason td{white-space:normal;color:var(--ngx-color-error, #b91c1c);background:var(--ngx-color-error-soft, #fef2f2)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
9351
10782
|
}
|
|
9352
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
10783
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: ImportPreviewComponent, decorators: [{
|
|
9353
10784
|
type: Component,
|
|
9354
10785
|
args: [{ selector: 'ngx-import-preview', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [], template: `
|
|
9355
10786
|
<section class="ngx-import-preview" data-testid="import-preview">
|
|
@@ -9458,8 +10889,8 @@ class ImportTemplateComponent {
|
|
|
9458
10889
|
selectAll(selected) {
|
|
9459
10890
|
this.choices.update(choices => choices.map(choice => ({ ...choice, selected })));
|
|
9460
10891
|
}
|
|
9461
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
9462
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.
|
|
10892
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: ImportTemplateComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
10893
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: ImportTemplateComponent, isStandalone: true, selector: "ngx-import-template", inputs: { config: "config", language: "language" }, outputs: { download: "download" }, usesOnChanges: true, ngImport: i0, template: `
|
|
9463
10894
|
<section class="ngx-import-template" data-testid="import-template">
|
|
9464
10895
|
<h4 class="ngx-import-template__heading">{{ ui.text('importTemplateHeading', language) }}</h4>
|
|
9465
10896
|
<p class="ngx-import-template__explain">{{ ui.text('importTemplateExplain', language) }}</p>
|
|
@@ -9510,7 +10941,7 @@ class ImportTemplateComponent {
|
|
|
9510
10941
|
</section>
|
|
9511
10942
|
`, isInline: true, styles: [".ngx-import-template{padding:14px;border:1px solid var(--ngx-color-border, #e5e7eb);border-radius:var(--ngx-radius-sm, 6px);background:var(--ngx-color-surface-alt, #f9fafb)}.ngx-import-template__heading{margin:0 0 4px;font-size:var(--ngx-font-size, 14px)}.ngx-import-template__explain{margin:0 0 10px;color:var(--ngx-color-muted, #6b7280)}.ngx-import-template__bulk{display:flex;gap:8px;margin-bottom:10px}.ngx-import-template__list{list-style:none;margin:0 0 10px;padding:0;max-height:240px;overflow-y:auto;display:grid;gap:4px}.ngx-import-template__item{display:flex;flex-wrap:wrap;align-items:baseline;gap:8px}.ngx-import-template__item label{display:flex;align-items:center;gap:6px;cursor:pointer}.ngx-import-template__note{color:var(--ngx-color-muted, #6b7280);font-size:12px}.ngx-import-template__unsupported{margin:0 0 10px;color:var(--ngx-color-muted, #6b7280)}.ngx-import-template button{padding:6px 12px;border:1px solid var(--ngx-color-border, #d1d5db);border-radius:var(--ngx-radius-sm, 6px);background:var(--ngx-color-surface, #ffffff);color:inherit;font:inherit;cursor:pointer}.ngx-import-template button:disabled{opacity:.55;cursor:not-allowed}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
9512
10943
|
}
|
|
9513
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
10944
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: ImportTemplateComponent, decorators: [{
|
|
9514
10945
|
type: Component,
|
|
9515
10946
|
args: [{ selector: 'ngx-import-template', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [], template: `
|
|
9516
10947
|
<section class="ngx-import-template" data-testid="import-template">
|
|
@@ -9796,8 +11227,8 @@ class EntityImportComponent {
|
|
|
9796
11227
|
this.problem.set(null);
|
|
9797
11228
|
this.step.set('upload');
|
|
9798
11229
|
}
|
|
9799
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
9800
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.
|
|
11230
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: EntityImportComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
11231
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: EntityImportComponent, isStandalone: true, selector: "ngx-entity-import", inputs: { config: "config", rules: "rules", lookups: "lookups", language: "language", templateFormat: "templateFormat" }, outputs: { importComplete: "importComplete", templateReady: "templateReady" }, ngImport: i0, template: `
|
|
9801
11232
|
<div class="ngx-import" data-testid="entity-import">
|
|
9802
11233
|
<ol class="ngx-import__steps" data-testid="import-steps">
|
|
9803
11234
|
@for (name of steps; track name) {
|
|
@@ -9915,7 +11346,7 @@ class EntityImportComponent {
|
|
|
9915
11346
|
</div>
|
|
9916
11347
|
`, isInline: true, styles: [".ngx-import{display:flex;flex-direction:column;gap:var(--ngx-gap, 16px);color:var(--ngx-color-text, #1f2937);font-size:var(--ngx-font-size, 14px)}.ngx-import__steps{display:flex;flex-wrap:wrap;gap:8px;list-style:none;margin:0;padding:0}.ngx-import__step{padding:4px 12px;border:1px solid var(--ngx-color-border, #e5e7eb);border-radius:var(--ngx-radius-sm, 6px);color:var(--ngx-color-muted, #6b7280);background:var(--ngx-color-surface-alt, #f9fafb)}.ngx-import__step--current{color:var(--ngx-color-accent, #4f46e5);border-color:var(--ngx-color-accent, #4f46e5);background:var(--ngx-color-accent-soft, #e0e7ff);font-weight:600}.ngx-import__problem{margin:0;padding:10px 12px;border:1px solid var(--ngx-color-border, #fecaca);border-radius:var(--ngx-radius-sm, 6px);background:var(--ngx-color-error-soft, #fef2f2);color:var(--ngx-color-error, #b91c1c);overflow-wrap:anywhere}.ngx-import__file{display:flex;flex-direction:column;gap:6px;padding:14px;border:1px dashed var(--ngx-color-border, #d1d5db);border-radius:var(--ngx-radius-sm, 6px);background:var(--ngx-color-surface, #ffffff)}.ngx-import button{align-self:flex-start;padding:8px 16px;border:1px solid var(--ngx-color-border, #d1d5db);border-radius:var(--ngx-radius-sm, 6px);background:var(--ngx-color-surface, #ffffff);color:inherit;font:inherit;cursor:pointer}.ngx-import button:hover:not(:disabled){border-color:var(--ngx-color-accent, #4f46e5);background:var(--ngx-color-accent-soft, #e0e7ff)}.ngx-import button:disabled{opacity:.55;cursor:not-allowed}\n"], dependencies: [{ kind: "component", type: ImportTemplateComponent, selector: "ngx-import-template", inputs: ["config", "language"], outputs: ["download"] }, { kind: "component", type: ImportMapperComponent, selector: "ngx-import-mapper", inputs: ["config", "headers", "plan", "language"], outputs: ["planChange"] }, { kind: "component", type: ImportPreviewComponent, selector: "ngx-import-preview", inputs: ["config", "plan", "rows", "lookups", "rules", "language"] }, { kind: "component", type: ImportErrorsComponent, selector: "ngx-import-errors", inputs: ["errors", "language"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
9917
11348
|
}
|
|
9918
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
11349
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: EntityImportComponent, decorators: [{
|
|
9919
11350
|
type: Component,
|
|
9920
11351
|
args: [{ selector: 'ngx-entity-import', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [
|
|
9921
11352
|
ImportTemplateComponent,
|
|
@@ -10085,5 +11516,5 @@ function saveBlob(blob, filename) {
|
|
|
10085
11516
|
* Generated bundle index. Do not edit.
|
|
10086
11517
|
*/
|
|
10087
11518
|
|
|
10088
|
-
export { ASYNC_VALIDATOR_REGISTRY, ArrayFieldComponent, BooleanFieldComponent, COMMON_MODULES_REGISTRY, CONFIG_SOURCE, CascadeDataService, CheckboxFieldComponent, ConfigSourceService, CurrencyFieldComponent, DEFAULT_UI_TEXT, DEFAULT_VALIDATION_MESSAGES, DateFieldComponent, DateTimeFieldComponent, DropdownFieldComponent, DynamicFieldComponent, DynamicFormComponent, DynamicRecordFormComponent, ENTITY_REF_CACHE_STORE, ENTITY_REF_REGISTRY, EmailFieldComponent, EntityImportComponent, EntityRefFieldComponent, EntityRefRegistryService, EntityRefSelectionService, EntityReferenceService, FIELD_TYPE_REGISTRY, FIELD_TYPE_SETS, FieldRegistryService, FileFieldComponent, GroupFieldComponent, HOOK_REGISTRY, HookRegistryService, HttpImportTransport, IMPORT_TRANSPORT, ImageFieldComponent, ImportErrorsComponent, ImportMapperComponent, ImportPreviewComponent, ImportTemplateComponent, InMemoryEntityRefCacheStore, LOOKUP_REGISTRY, LocalImportTransport, LookupRegistryService, MARKDOWN_RENDERER, MASKED_PLACEHOLDER, MASKED_ROLES, MarkdownFieldComponent, MonthYearFieldComponent, MultiSelectFieldComponent, NumberFieldComponent, PasswordFieldComponent, RECORD_MIGRATIONS, RadioFieldComponent, RbacService, RulesEvaluationService, SHEET_PARSER, SYSTEM_DEFAULT_CAN_EDIT, TextFieldComponent, TextareaFieldComponent, TimeFieldComponent, UI_TEXT, UPLOAD_HANDLER, UiTextService, VALIDATION_MESSAGES, VALIDATOR_REGISTRY, ValidationMessagesService, ValidatorRegistryService, builtInFieldTypes, defaultSheetParser, fieldDescribedBy, fieldDomId, nextFieldInstanceId, provideBuiltInFieldTypes, provideFieldTypes, provideHttpImportTransport, provideNgxDynamicEntity, refreshChoiceOptions, resolveUiText };
|
|
11519
|
+
export { ASYNC_VALIDATOR_REGISTRY, ArrayFieldComponent, BooleanFieldComponent, COMMON_MODULES_REGISTRY, CONFIG_SOURCE, CascadeDataService, CheckboxFieldComponent, ColorFieldComponent, ConfigSourceService, CurrencyFieldComponent, DEFAULT_UI_TEXT, DEFAULT_VALIDATION_MESSAGES, DateFieldComponent, DateTimeFieldComponent, DropdownFieldComponent, DynamicFieldComponent, DynamicFormComponent, DynamicRecordFormComponent, ENTITY_REF_CACHE_STORE, ENTITY_REF_REGISTRY, EmailFieldComponent, EntityImportComponent, EntityRefFieldComponent, EntityRefRegistryService, EntityRefSelectionService, EntityReferenceService, FIELD_TYPE_REGISTRY, FIELD_TYPE_SETS, FieldRegistryService, FileFieldComponent, GroupFieldComponent, HOOK_REGISTRY, HookRegistryService, HttpImportTransport, IMPORT_TRANSPORT, ImageFieldComponent, ImportErrorsComponent, ImportMapperComponent, ImportPreviewComponent, ImportTemplateComponent, InMemoryEntityRefCacheStore, LOOKUP_REGISTRY, LocalImportTransport, LookupRegistryService, MARKDOWN_RENDERER, MASKED_PLACEHOLDER, MASKED_ROLES, MarkdownFieldComponent, MonthYearFieldComponent, MultiSelectFieldComponent, NumberFieldComponent, PasswordFieldComponent, PhoneFieldComponent, RECORD_MIGRATIONS, RadioFieldComponent, RatingFieldComponent, RbacService, RulesEvaluationService, SHEET_PARSER, SYSTEM_DEFAULT_CAN_EDIT, SliderFieldComponent, TagsFieldComponent, TextFieldComponent, TextareaFieldComponent, TimeFieldComponent, UI_TEXT, UPLOAD_HANDLER, UiTextService, UrlFieldComponent, VALIDATION_MESSAGES, VALIDATOR_REGISTRY, ValidationMessagesService, ValidatorRegistryService, builtInFieldTypes, defaultSheetParser, fieldDescribedBy, fieldDomId, nextFieldInstanceId, phoneValidator, provideBuiltInFieldTypes, provideFieldTypes, provideHttpImportTransport, provideNgxDynamicEntity, refreshChoiceOptions, resolveUiText, urlValidator };
|
|
10089
11520
|
//# sourceMappingURL=ngx-dynamic-entity.mjs.map
|