ngx-dynamic-entity 1.9.1 → 1.11.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 +9 -2
- package/fesm2022/ngx-dynamic-entity.mjs +1451 -314
- package/fesm2022/ngx-dynamic-entity.mjs.map +1 -1
- package/package.json +2 -2
- package/styles.css +723 -195
- package/types/ngx-dynamic-entity.d.ts +310 -20
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken, makeEnvironmentProviders, inject, Injectable, signal, Input, ChangeDetectionStrategy, Component, ChangeDetectorRef, DestroyRef, Injector, isDevMode, ViewContainerRef, ViewChild, forwardRef, Optional, Inject, EventEmitter, computed, HostListener, ViewChildren, Output
|
|
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, FormArray, FormGroup } from '@angular/forms';
|
|
5
5
|
import { resolveLabel, formatDisplayValue, normalizeLookupValues, lookupValuesToOptions, valuesMatch, getOptionStoredValue, resolveOptionLabel, resolveOptionValue, buildReferenceCacheKey, normalizeReferenceOptions, entityKeyFromCacheKey, applyCascadeFilter, resolveEffectiveMask, shouldMaskField, findTab, fileRefName, evaluateFormRules, filterRulesForTab, evaluateFieldVisibility, normalizeConfigOptions, migrateRecord, getTabData, getValueByPath, setTabData, setValueByPath, normalizeArrayStructures, ambiguousFieldIds, parseFieldRef, toRefToken, collectFieldScopes, fieldRefFor, refOf, getTabPath, applyAutoPatch, applyPatchOnTrue } from '@dynamic-entity/core';
|
|
@@ -268,6 +268,30 @@ const DEFAULT_VALIDATION_MESSAGES = {
|
|
|
268
268
|
invalidNumber: 'Invalid number.',
|
|
269
269
|
invalidSelection: 'Invalid selection.',
|
|
270
270
|
};
|
|
271
|
+
/**
|
|
272
|
+
* The keys a plain input can raise, most specific first.
|
|
273
|
+
*
|
|
274
|
+
* A type that cannot raise one of these is unaffected by its presence — `resolve` returns the
|
|
275
|
+
* first error actually on the control — so the shared list costs nothing and spares every
|
|
276
|
+
* field component a near-identical copy of it.
|
|
277
|
+
*/
|
|
278
|
+
const COMMON_ORDER = ['required', 'email', 'min', 'max', 'minlength', 'maxlength', 'pattern'];
|
|
279
|
+
/**
|
|
280
|
+
* Where a field type disagrees with the common list.
|
|
281
|
+
*
|
|
282
|
+
* Only three do, and each for a reason about how the control reads rather than what it can
|
|
283
|
+
* raise: a select says "Please select an option" where an input says "This field is
|
|
284
|
+
* required"; a number that is neither required nor out of range is not "invalid", it is not a
|
|
285
|
+
* number; and a control with no text in it cannot fail a length or format rule.
|
|
286
|
+
*/
|
|
287
|
+
const ORDER_BY_FIELD_TYPE = {
|
|
288
|
+
dropdown: { order: [['required', 'requiredSelection']], fallback: 'invalidSelection' },
|
|
289
|
+
number: { order: ['required', 'min', 'max'], fallback: 'invalidNumber' },
|
|
290
|
+
boolean: { order: ['required', 'pattern'], fallback: 'invalid' },
|
|
291
|
+
checkbox: { order: ['required', 'pattern'], fallback: 'invalid' },
|
|
292
|
+
file: { order: ['required', 'pattern'], fallback: 'invalid' },
|
|
293
|
+
image: { order: ['required', 'pattern'], fallback: 'invalid' },
|
|
294
|
+
};
|
|
271
295
|
class ValidationMessagesService {
|
|
272
296
|
overrides = inject(VALIDATION_MESSAGES, { optional: true }) ?? {};
|
|
273
297
|
/**
|
|
@@ -292,6 +316,27 @@ class ValidationMessagesService {
|
|
|
292
316
|
}
|
|
293
317
|
return this.messageFor(fallbackKey, language, null);
|
|
294
318
|
}
|
|
319
|
+
/**
|
|
320
|
+
* The message for a field, chosen by its **type** rather than by a list the caller supplies.
|
|
321
|
+
*
|
|
322
|
+
* Every field component used to pass its own copy of the key order, and the copies were
|
|
323
|
+
* near-identical — which was tolerable while the field itself was the only thing that
|
|
324
|
+
* rendered a message. It stopped being tolerable when the error summary above the form
|
|
325
|
+
* started naming what is wrong with each field: the summary has no field component to ask
|
|
326
|
+
* (the offending field is usually on a tab that is not rendered), so it would have needed a
|
|
327
|
+
* second copy of the same table, and a dropdown would have said "This field is required" in
|
|
328
|
+
* the summary and "Please select an option" under the control.
|
|
329
|
+
*
|
|
330
|
+
* One table, read from both places, is what keeps those two sentences the same sentence.
|
|
331
|
+
*/
|
|
332
|
+
resolveForField(errors, language, fieldType) {
|
|
333
|
+
const { order, fallback } = this.orderFor(fieldType);
|
|
334
|
+
return this.resolve(errors, language, order, fallback);
|
|
335
|
+
}
|
|
336
|
+
/** A field type's key order. An unrecognised type — a consumer's own — gets the common list. */
|
|
337
|
+
orderFor(fieldType) {
|
|
338
|
+
return ORDER_BY_FIELD_TYPE[fieldType] ?? { order: COMMON_ORDER, fallback: 'invalid' };
|
|
339
|
+
}
|
|
295
340
|
/** One message by key, honouring an override before the default. */
|
|
296
341
|
messageFor(key, language, error) {
|
|
297
342
|
const message = this.overrides[key] ?? DEFAULT_VALIDATION_MESSAGES[key];
|
|
@@ -342,6 +387,29 @@ function nextFieldInstanceId() {
|
|
|
342
387
|
function fieldDomId(field, instance, suffix = '') {
|
|
343
388
|
return `${field?.id ?? 'field'}-${instance}${suffix}`;
|
|
344
389
|
}
|
|
390
|
+
/**
|
|
391
|
+
* The `aria-describedby` a control needs, given what is rendered beside it right now.
|
|
392
|
+
*
|
|
393
|
+
* A field can show two things under its control — the author's help text and, once touched, a
|
|
394
|
+
* validation message — and a screen reader announces only what this attribute names. Three
|
|
395
|
+
* field components named the error; the other sixteen named nothing at all, so their error
|
|
396
|
+
* messages were on screen and silent. Help text would have inherited the same gap.
|
|
397
|
+
*
|
|
398
|
+
* Order matters: the hint describes the field and the error describes the attempt, so the hint
|
|
399
|
+
* is read first. `null` rather than an empty string when there is nothing to describe — an
|
|
400
|
+
* empty `aria-describedby` is a dangling reference, not an absent one.
|
|
401
|
+
*
|
|
402
|
+
* Shared rather than written into each of the nineteen field components, because the rule is
|
|
403
|
+
* the same everywhere and getting it wrong is invisible until somebody uses a screen reader.
|
|
404
|
+
*/
|
|
405
|
+
function fieldDescribedBy(domId, parts) {
|
|
406
|
+
const ids = [];
|
|
407
|
+
if (parts.hint)
|
|
408
|
+
ids.push(domId('-hint'));
|
|
409
|
+
if (parts.error)
|
|
410
|
+
ids.push(domId('-error'));
|
|
411
|
+
return ids.length ? ids.join(' ') : null;
|
|
412
|
+
}
|
|
345
413
|
|
|
346
414
|
class TextFieldComponent {
|
|
347
415
|
/**
|
|
@@ -381,16 +449,18 @@ class TextFieldComponent {
|
|
|
381
449
|
get placeholder() {
|
|
382
450
|
return resolveLabel(this.field?.placeholder, this.language);
|
|
383
451
|
}
|
|
452
|
+
/** Author help text, shown under the control and named by `aria-describedby`. */
|
|
453
|
+
get hint() {
|
|
454
|
+
return resolveLabel(this.field?.hint, this.language);
|
|
455
|
+
}
|
|
456
|
+
/** The ids of whatever is describing this control right now. */
|
|
457
|
+
describedBy() {
|
|
458
|
+
return fieldDescribedBy(s => this.domId(s), { hint: !!this.hint, error: !!this.errorMessage });
|
|
459
|
+
}
|
|
384
460
|
get errorMessage() {
|
|
385
461
|
if (!this.control || !this.control.errors || !this.control.touched)
|
|
386
462
|
return '';
|
|
387
|
-
return this.messages.
|
|
388
|
-
'required',
|
|
389
|
-
'email',
|
|
390
|
-
'minlength',
|
|
391
|
-
'maxlength',
|
|
392
|
-
'pattern',
|
|
393
|
-
]);
|
|
463
|
+
return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
|
|
394
464
|
}
|
|
395
465
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: TextFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
396
466
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: TextFieldComponent, isStandalone: true, selector: "ngx-text-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
@@ -402,12 +472,26 @@ class TextFieldComponent {
|
|
|
402
472
|
[class.ngx-field--masked]="masked"
|
|
403
473
|
[class.ngx-field--invalid]="control && control.invalid && control.touched"
|
|
404
474
|
>
|
|
405
|
-
<
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
475
|
+
<div class="ngx-field__label-row">
|
|
476
|
+
<label class="ngx-field__label" [attr.for]="domId()"
|
|
477
|
+
>{{ label }}
|
|
478
|
+
@if (field.validators?.required) {
|
|
479
|
+
<span class="ngx-field__req">*</span>
|
|
480
|
+
}
|
|
481
|
+
</label>
|
|
482
|
+
@if (hint) {
|
|
483
|
+
<span class="ngx-field__hint-wrap">
|
|
484
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
485
|
+
<span
|
|
486
|
+
class="ngx-field__hint"
|
|
487
|
+
role="tooltip"
|
|
488
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
489
|
+
[id]="domId('-hint')"
|
|
490
|
+
>{{ hint }}</span
|
|
491
|
+
>
|
|
492
|
+
</span>
|
|
409
493
|
}
|
|
410
|
-
</
|
|
494
|
+
</div>
|
|
411
495
|
@if (masked) {
|
|
412
496
|
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
413
497
|
maskedText
|
|
@@ -424,7 +508,7 @@ class TextFieldComponent {
|
|
|
424
508
|
[placeholder]="placeholder"
|
|
425
509
|
[attr.disabled]="field.disabled ? true : null"
|
|
426
510
|
[attr.aria-invalid]="control.invalid && control.touched"
|
|
427
|
-
[attr.aria-describedby]="
|
|
511
|
+
[attr.aria-describedby]="describedBy()"
|
|
428
512
|
/>
|
|
429
513
|
@if (errorMessage) {
|
|
430
514
|
<span
|
|
@@ -455,12 +539,26 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
455
539
|
[class.ngx-field--masked]="masked"
|
|
456
540
|
[class.ngx-field--invalid]="control && control.invalid && control.touched"
|
|
457
541
|
>
|
|
458
|
-
<
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
542
|
+
<div class="ngx-field__label-row">
|
|
543
|
+
<label class="ngx-field__label" [attr.for]="domId()"
|
|
544
|
+
>{{ label }}
|
|
545
|
+
@if (field.validators?.required) {
|
|
546
|
+
<span class="ngx-field__req">*</span>
|
|
547
|
+
}
|
|
548
|
+
</label>
|
|
549
|
+
@if (hint) {
|
|
550
|
+
<span class="ngx-field__hint-wrap">
|
|
551
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
552
|
+
<span
|
|
553
|
+
class="ngx-field__hint"
|
|
554
|
+
role="tooltip"
|
|
555
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
556
|
+
[id]="domId('-hint')"
|
|
557
|
+
>{{ hint }}</span
|
|
558
|
+
>
|
|
559
|
+
</span>
|
|
462
560
|
}
|
|
463
|
-
</
|
|
561
|
+
</div>
|
|
464
562
|
@if (masked) {
|
|
465
563
|
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
466
564
|
maskedText
|
|
@@ -477,7 +575,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
477
575
|
[placeholder]="placeholder"
|
|
478
576
|
[attr.disabled]="field.disabled ? true : null"
|
|
479
577
|
[attr.aria-invalid]="control.invalid && control.touched"
|
|
480
|
-
[attr.aria-describedby]="
|
|
578
|
+
[attr.aria-describedby]="describedBy()"
|
|
481
579
|
/>
|
|
482
580
|
@if (errorMessage) {
|
|
483
581
|
<span
|
|
@@ -533,18 +631,18 @@ class TextareaFieldComponent {
|
|
|
533
631
|
* "This field has an error", which made a documented, configurable feature work on three
|
|
534
632
|
* of fifteen field types.
|
|
535
633
|
*/
|
|
634
|
+
/** Author help text, shown under the control and named by `aria-describedby`. */
|
|
635
|
+
get hint() {
|
|
636
|
+
return resolveLabel(this.field?.hint, this.language);
|
|
637
|
+
}
|
|
638
|
+
/** The ids of whatever is describing this control right now. */
|
|
639
|
+
describedBy() {
|
|
640
|
+
return fieldDescribedBy(s => this.domId(s), { hint: !!this.hint, error: !!this.errorMessage });
|
|
641
|
+
}
|
|
536
642
|
get errorMessage() {
|
|
537
643
|
if (!this.control?.errors || !this.control.touched)
|
|
538
644
|
return '';
|
|
539
|
-
return this.messages.
|
|
540
|
-
'required',
|
|
541
|
-
'email',
|
|
542
|
-
'min',
|
|
543
|
-
'max',
|
|
544
|
-
'minlength',
|
|
545
|
-
'maxlength',
|
|
546
|
-
'pattern',
|
|
547
|
-
]);
|
|
645
|
+
return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
|
|
548
646
|
}
|
|
549
647
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: TextareaFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
550
648
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: TextareaFieldComponent, isStandalone: true, selector: "ngx-textarea-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
@@ -555,7 +653,21 @@ class TextareaFieldComponent {
|
|
|
555
653
|
[class.ngx-field--readonly]="readonly"
|
|
556
654
|
[class.ngx-field--masked]="masked"
|
|
557
655
|
>
|
|
558
|
-
<
|
|
656
|
+
<div class="ngx-field__label-row">
|
|
657
|
+
<label class="ngx-field__label" [attr.for]="domId()">{{ label }}</label>
|
|
658
|
+
@if (hint) {
|
|
659
|
+
<span class="ngx-field__hint-wrap">
|
|
660
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
661
|
+
<span
|
|
662
|
+
class="ngx-field__hint"
|
|
663
|
+
role="tooltip"
|
|
664
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
665
|
+
[id]="domId('-hint')"
|
|
666
|
+
>{{ hint }}</span
|
|
667
|
+
>
|
|
668
|
+
</span>
|
|
669
|
+
}
|
|
670
|
+
</div>
|
|
559
671
|
@if (masked) {
|
|
560
672
|
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
561
673
|
maskedText
|
|
@@ -568,12 +680,15 @@ class TextareaFieldComponent {
|
|
|
568
680
|
class="ngx-field__input"
|
|
569
681
|
[attr.data-testid]="'field-' + field.id + '-input'"
|
|
570
682
|
[formControl]="$any(control)"
|
|
683
|
+
[attr.aria-describedby]="describedBy()"
|
|
571
684
|
[placeholder]="placeholder"
|
|
572
685
|
[attr.disabled]="field.disabled ? true : null"
|
|
573
686
|
rows="3"
|
|
574
687
|
></textarea>
|
|
575
688
|
@if (errorMessage) {
|
|
576
|
-
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">{{
|
|
689
|
+
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'" [id]="domId('-error')">{{
|
|
690
|
+
errorMessage
|
|
691
|
+
}}</span>
|
|
577
692
|
}
|
|
578
693
|
}
|
|
579
694
|
</div>
|
|
@@ -594,7 +709,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
594
709
|
[class.ngx-field--readonly]="readonly"
|
|
595
710
|
[class.ngx-field--masked]="masked"
|
|
596
711
|
>
|
|
597
|
-
<
|
|
712
|
+
<div class="ngx-field__label-row">
|
|
713
|
+
<label class="ngx-field__label" [attr.for]="domId()">{{ label }}</label>
|
|
714
|
+
@if (hint) {
|
|
715
|
+
<span class="ngx-field__hint-wrap">
|
|
716
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
717
|
+
<span
|
|
718
|
+
class="ngx-field__hint"
|
|
719
|
+
role="tooltip"
|
|
720
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
721
|
+
[id]="domId('-hint')"
|
|
722
|
+
>{{ hint }}</span
|
|
723
|
+
>
|
|
724
|
+
</span>
|
|
725
|
+
}
|
|
726
|
+
</div>
|
|
598
727
|
@if (masked) {
|
|
599
728
|
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
600
729
|
maskedText
|
|
@@ -607,12 +736,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
607
736
|
class="ngx-field__input"
|
|
608
737
|
[attr.data-testid]="'field-' + field.id + '-input'"
|
|
609
738
|
[formControl]="$any(control)"
|
|
739
|
+
[attr.aria-describedby]="describedBy()"
|
|
610
740
|
[placeholder]="placeholder"
|
|
611
741
|
[attr.disabled]="field.disabled ? true : null"
|
|
612
742
|
rows="3"
|
|
613
743
|
></textarea>
|
|
614
744
|
@if (errorMessage) {
|
|
615
|
-
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">{{
|
|
745
|
+
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'" [id]="domId('-error')">{{
|
|
746
|
+
errorMessage
|
|
747
|
+
}}</span>
|
|
616
748
|
}
|
|
617
749
|
}
|
|
618
750
|
</div>
|
|
@@ -654,6 +786,9 @@ const DEFAULT_UI_TEXT = {
|
|
|
654
786
|
reset: 'Reset',
|
|
655
787
|
saveShortcutHint: 'Shortcut: Ctrl+S',
|
|
656
788
|
loading: 'Loading…',
|
|
789
|
+
// Why a save was refused — the summary above the form, and the count on each tab
|
|
790
|
+
errorSummaryTitle: '{count} field(s) need attention before this can be saved.',
|
|
791
|
+
tabErrorCount: '{count} field(s) need attention on this tab',
|
|
657
792
|
// Record view — the per-tab edit flow
|
|
658
793
|
editSection: 'Edit section',
|
|
659
794
|
saveSection: 'Save section',
|
|
@@ -896,18 +1031,18 @@ class MarkdownFieldComponent {
|
|
|
896
1031
|
* "This field has an error", which made a documented, configurable feature work on three
|
|
897
1032
|
* of fifteen field types.
|
|
898
1033
|
*/
|
|
1034
|
+
/** Author help text, shown under the control and named by `aria-describedby`. */
|
|
1035
|
+
get hint() {
|
|
1036
|
+
return resolveLabel(this.field?.hint, this.language);
|
|
1037
|
+
}
|
|
1038
|
+
/** The ids of whatever is describing this control right now. */
|
|
1039
|
+
describedBy() {
|
|
1040
|
+
return fieldDescribedBy(s => this.domId(s), { hint: !!this.hint, error: !!this.errorMessage });
|
|
1041
|
+
}
|
|
899
1042
|
get errorMessage() {
|
|
900
1043
|
if (!this.control?.errors || !this.control.touched)
|
|
901
1044
|
return '';
|
|
902
|
-
return this.messages.
|
|
903
|
-
'required',
|
|
904
|
-
'email',
|
|
905
|
-
'min',
|
|
906
|
-
'max',
|
|
907
|
-
'minlength',
|
|
908
|
-
'maxlength',
|
|
909
|
-
'pattern',
|
|
910
|
-
]);
|
|
1045
|
+
return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
|
|
911
1046
|
}
|
|
912
1047
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MarkdownFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
913
1048
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: MarkdownFieldComponent, isStandalone: true, selector: "ngx-markdown-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, usesOnChanges: true, ngImport: i0, template: `
|
|
@@ -918,7 +1053,21 @@ class MarkdownFieldComponent {
|
|
|
918
1053
|
[class.ngx-field--readonly]="readonly"
|
|
919
1054
|
[class.ngx-field--masked]="masked"
|
|
920
1055
|
>
|
|
921
|
-
<
|
|
1056
|
+
<div class="ngx-field__label-row">
|
|
1057
|
+
<label class="ngx-field__label" [attr.for]="domId()">{{ label }}</label>
|
|
1058
|
+
@if (hint) {
|
|
1059
|
+
<span class="ngx-field__hint-wrap">
|
|
1060
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
1061
|
+
<span
|
|
1062
|
+
class="ngx-field__hint"
|
|
1063
|
+
role="tooltip"
|
|
1064
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
1065
|
+
[id]="domId('-hint')"
|
|
1066
|
+
>{{ hint }}</span
|
|
1067
|
+
>
|
|
1068
|
+
</span>
|
|
1069
|
+
}
|
|
1070
|
+
</div>
|
|
922
1071
|
|
|
923
1072
|
@if (masked) {
|
|
924
1073
|
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
@@ -979,6 +1128,7 @@ class MarkdownFieldComponent {
|
|
|
979
1128
|
class="ngx-field__input ngx-field__input--markdown"
|
|
980
1129
|
[attr.data-testid]="'field-' + field.id + '-input'"
|
|
981
1130
|
[formControl]="$any(control)"
|
|
1131
|
+
[attr.aria-describedby]="describedBy()"
|
|
982
1132
|
[placeholder]="placeholder"
|
|
983
1133
|
[attr.disabled]="field.disabled ? true : null"
|
|
984
1134
|
rows="6"
|
|
@@ -987,7 +1137,9 @@ class MarkdownFieldComponent {
|
|
|
987
1137
|
</div>
|
|
988
1138
|
|
|
989
1139
|
@if (errorMessage) {
|
|
990
|
-
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">{{
|
|
1140
|
+
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'" [id]="domId('-error')">{{
|
|
1141
|
+
errorMessage
|
|
1142
|
+
}}</span>
|
|
991
1143
|
}
|
|
992
1144
|
}
|
|
993
1145
|
</div>
|
|
@@ -1008,7 +1160,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
1008
1160
|
[class.ngx-field--readonly]="readonly"
|
|
1009
1161
|
[class.ngx-field--masked]="masked"
|
|
1010
1162
|
>
|
|
1011
|
-
<
|
|
1163
|
+
<div class="ngx-field__label-row">
|
|
1164
|
+
<label class="ngx-field__label" [attr.for]="domId()">{{ label }}</label>
|
|
1165
|
+
@if (hint) {
|
|
1166
|
+
<span class="ngx-field__hint-wrap">
|
|
1167
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
1168
|
+
<span
|
|
1169
|
+
class="ngx-field__hint"
|
|
1170
|
+
role="tooltip"
|
|
1171
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
1172
|
+
[id]="domId('-hint')"
|
|
1173
|
+
>{{ hint }}</span
|
|
1174
|
+
>
|
|
1175
|
+
</span>
|
|
1176
|
+
}
|
|
1177
|
+
</div>
|
|
1012
1178
|
|
|
1013
1179
|
@if (masked) {
|
|
1014
1180
|
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
@@ -1069,6 +1235,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
1069
1235
|
class="ngx-field__input ngx-field__input--markdown"
|
|
1070
1236
|
[attr.data-testid]="'field-' + field.id + '-input'"
|
|
1071
1237
|
[formControl]="$any(control)"
|
|
1238
|
+
[attr.aria-describedby]="describedBy()"
|
|
1072
1239
|
[placeholder]="placeholder"
|
|
1073
1240
|
[attr.disabled]="field.disabled ? true : null"
|
|
1074
1241
|
rows="6"
|
|
@@ -1077,7 +1244,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
1077
1244
|
</div>
|
|
1078
1245
|
|
|
1079
1246
|
@if (errorMessage) {
|
|
1080
|
-
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">{{
|
|
1247
|
+
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'" [id]="domId('-error')">{{
|
|
1248
|
+
errorMessage
|
|
1249
|
+
}}</span>
|
|
1081
1250
|
}
|
|
1082
1251
|
}
|
|
1083
1252
|
</div>
|
|
@@ -1118,10 +1287,18 @@ class NumberFieldComponent {
|
|
|
1118
1287
|
get placeholder() {
|
|
1119
1288
|
return resolveLabel(this.field?.placeholder, this.language);
|
|
1120
1289
|
}
|
|
1290
|
+
/** Author help text, shown under the control and named by `aria-describedby`. */
|
|
1291
|
+
get hint() {
|
|
1292
|
+
return resolveLabel(this.field?.hint, this.language);
|
|
1293
|
+
}
|
|
1294
|
+
/** The ids of whatever is describing this control right now. */
|
|
1295
|
+
describedBy() {
|
|
1296
|
+
return fieldDescribedBy(s => this.domId(s), { hint: !!this.hint, error: !!this.errorMessage });
|
|
1297
|
+
}
|
|
1121
1298
|
get errorMessage() {
|
|
1122
1299
|
if (!this.control || !this.control.errors || !this.control.touched)
|
|
1123
1300
|
return '';
|
|
1124
|
-
return this.messages.
|
|
1301
|
+
return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
|
|
1125
1302
|
}
|
|
1126
1303
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: NumberFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1127
1304
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: NumberFieldComponent, isStandalone: true, selector: "ngx-number-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
@@ -1133,12 +1310,26 @@ class NumberFieldComponent {
|
|
|
1133
1310
|
[class.ngx-field--masked]="masked"
|
|
1134
1311
|
[class.ngx-field--invalid]="control && control.invalid && control.touched"
|
|
1135
1312
|
>
|
|
1136
|
-
<
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1313
|
+
<div class="ngx-field__label-row">
|
|
1314
|
+
<label class="ngx-field__label" [attr.for]="domId()">
|
|
1315
|
+
{{ label }}
|
|
1316
|
+
@if (field.validators?.required) {
|
|
1317
|
+
<span class="ngx-field__req">*</span>
|
|
1318
|
+
}
|
|
1319
|
+
</label>
|
|
1320
|
+
@if (hint) {
|
|
1321
|
+
<span class="ngx-field__hint-wrap">
|
|
1322
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
1323
|
+
<span
|
|
1324
|
+
class="ngx-field__hint"
|
|
1325
|
+
role="tooltip"
|
|
1326
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
1327
|
+
[id]="domId('-hint')"
|
|
1328
|
+
>{{ hint }}</span
|
|
1329
|
+
>
|
|
1330
|
+
</span>
|
|
1140
1331
|
}
|
|
1141
|
-
</
|
|
1332
|
+
</div>
|
|
1142
1333
|
@if (masked) {
|
|
1143
1334
|
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
1144
1335
|
maskedText
|
|
@@ -1155,7 +1346,7 @@ class NumberFieldComponent {
|
|
|
1155
1346
|
[placeholder]="placeholder"
|
|
1156
1347
|
[attr.disabled]="field.disabled ? true : null"
|
|
1157
1348
|
[attr.aria-invalid]="control.invalid && control.touched"
|
|
1158
|
-
[attr.aria-describedby]="
|
|
1349
|
+
[attr.aria-describedby]="describedBy()"
|
|
1159
1350
|
/>
|
|
1160
1351
|
@if (errorMessage) {
|
|
1161
1352
|
<span
|
|
@@ -1186,12 +1377,26 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
1186
1377
|
[class.ngx-field--masked]="masked"
|
|
1187
1378
|
[class.ngx-field--invalid]="control && control.invalid && control.touched"
|
|
1188
1379
|
>
|
|
1189
|
-
<
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1380
|
+
<div class="ngx-field__label-row">
|
|
1381
|
+
<label class="ngx-field__label" [attr.for]="domId()">
|
|
1382
|
+
{{ label }}
|
|
1383
|
+
@if (field.validators?.required) {
|
|
1384
|
+
<span class="ngx-field__req">*</span>
|
|
1385
|
+
}
|
|
1386
|
+
</label>
|
|
1387
|
+
@if (hint) {
|
|
1388
|
+
<span class="ngx-field__hint-wrap">
|
|
1389
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
1390
|
+
<span
|
|
1391
|
+
class="ngx-field__hint"
|
|
1392
|
+
role="tooltip"
|
|
1393
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
1394
|
+
[id]="domId('-hint')"
|
|
1395
|
+
>{{ hint }}</span
|
|
1396
|
+
>
|
|
1397
|
+
</span>
|
|
1193
1398
|
}
|
|
1194
|
-
</
|
|
1399
|
+
</div>
|
|
1195
1400
|
@if (masked) {
|
|
1196
1401
|
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
1197
1402
|
maskedText
|
|
@@ -1208,7 +1413,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
1208
1413
|
[placeholder]="placeholder"
|
|
1209
1414
|
[attr.disabled]="field.disabled ? true : null"
|
|
1210
1415
|
[attr.aria-invalid]="control.invalid && control.touched"
|
|
1211
|
-
[attr.aria-describedby]="
|
|
1416
|
+
[attr.aria-describedby]="describedBy()"
|
|
1212
1417
|
/>
|
|
1213
1418
|
@if (errorMessage) {
|
|
1214
1419
|
<span
|
|
@@ -1277,18 +1482,18 @@ class CurrencyFieldComponent {
|
|
|
1277
1482
|
* "This field has an error", which made a documented, configurable feature work on three
|
|
1278
1483
|
* of fifteen field types.
|
|
1279
1484
|
*/
|
|
1485
|
+
/** Author help text, shown under the control and named by `aria-describedby`. */
|
|
1486
|
+
get hint() {
|
|
1487
|
+
return resolveLabel(this.field?.hint, this.language);
|
|
1488
|
+
}
|
|
1489
|
+
/** The ids of whatever is describing this control right now. */
|
|
1490
|
+
describedBy() {
|
|
1491
|
+
return fieldDescribedBy(s => this.domId(s), { hint: !!this.hint, error: !!this.errorMessage });
|
|
1492
|
+
}
|
|
1280
1493
|
get errorMessage() {
|
|
1281
1494
|
if (!this.control?.errors || !this.control.touched)
|
|
1282
1495
|
return '';
|
|
1283
|
-
return this.messages.
|
|
1284
|
-
'required',
|
|
1285
|
-
'email',
|
|
1286
|
-
'min',
|
|
1287
|
-
'max',
|
|
1288
|
-
'minlength',
|
|
1289
|
-
'maxlength',
|
|
1290
|
-
'pattern',
|
|
1291
|
-
]);
|
|
1496
|
+
return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
|
|
1292
1497
|
}
|
|
1293
1498
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: CurrencyFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1294
1499
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: CurrencyFieldComponent, isStandalone: true, selector: "ngx-currency-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
@@ -1299,7 +1504,21 @@ class CurrencyFieldComponent {
|
|
|
1299
1504
|
[class.ngx-field--readonly]="readonly"
|
|
1300
1505
|
[class.ngx-field--masked]="masked"
|
|
1301
1506
|
>
|
|
1302
|
-
<
|
|
1507
|
+
<div class="ngx-field__label-row">
|
|
1508
|
+
<label class="ngx-field__label" [attr.for]="domId()">{{ label }}</label>
|
|
1509
|
+
@if (hint) {
|
|
1510
|
+
<span class="ngx-field__hint-wrap">
|
|
1511
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
1512
|
+
<span
|
|
1513
|
+
class="ngx-field__hint"
|
|
1514
|
+
role="tooltip"
|
|
1515
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
1516
|
+
[id]="domId('-hint')"
|
|
1517
|
+
>{{ hint }}</span
|
|
1518
|
+
>
|
|
1519
|
+
</span>
|
|
1520
|
+
}
|
|
1521
|
+
</div>
|
|
1303
1522
|
@if (masked) {
|
|
1304
1523
|
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
1305
1524
|
maskedText
|
|
@@ -1318,12 +1537,15 @@ class CurrencyFieldComponent {
|
|
|
1318
1537
|
type="number"
|
|
1319
1538
|
step="0.01"
|
|
1320
1539
|
[formControl]="$any(control)"
|
|
1540
|
+
[attr.aria-describedby]="describedBy()"
|
|
1321
1541
|
[placeholder]="placeholder || '0.00'"
|
|
1322
1542
|
[attr.disabled]="field.disabled ? true : null"
|
|
1323
1543
|
/>
|
|
1324
1544
|
</div>
|
|
1325
1545
|
@if (errorMessage) {
|
|
1326
|
-
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">{{
|
|
1546
|
+
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'" [id]="domId('-error')">{{
|
|
1547
|
+
errorMessage
|
|
1548
|
+
}}</span>
|
|
1327
1549
|
}
|
|
1328
1550
|
}
|
|
1329
1551
|
</div>
|
|
@@ -1344,7 +1566,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
1344
1566
|
[class.ngx-field--readonly]="readonly"
|
|
1345
1567
|
[class.ngx-field--masked]="masked"
|
|
1346
1568
|
>
|
|
1347
|
-
<
|
|
1569
|
+
<div class="ngx-field__label-row">
|
|
1570
|
+
<label class="ngx-field__label" [attr.for]="domId()">{{ label }}</label>
|
|
1571
|
+
@if (hint) {
|
|
1572
|
+
<span class="ngx-field__hint-wrap">
|
|
1573
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
1574
|
+
<span
|
|
1575
|
+
class="ngx-field__hint"
|
|
1576
|
+
role="tooltip"
|
|
1577
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
1578
|
+
[id]="domId('-hint')"
|
|
1579
|
+
>{{ hint }}</span
|
|
1580
|
+
>
|
|
1581
|
+
</span>
|
|
1582
|
+
}
|
|
1583
|
+
</div>
|
|
1348
1584
|
@if (masked) {
|
|
1349
1585
|
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
1350
1586
|
maskedText
|
|
@@ -1363,12 +1599,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
1363
1599
|
type="number"
|
|
1364
1600
|
step="0.01"
|
|
1365
1601
|
[formControl]="$any(control)"
|
|
1602
|
+
[attr.aria-describedby]="describedBy()"
|
|
1366
1603
|
[placeholder]="placeholder || '0.00'"
|
|
1367
1604
|
[attr.disabled]="field.disabled ? true : null"
|
|
1368
1605
|
/>
|
|
1369
1606
|
</div>
|
|
1370
1607
|
@if (errorMessage) {
|
|
1371
|
-
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">{{
|
|
1608
|
+
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'" [id]="domId('-error')">{{
|
|
1609
|
+
errorMessage
|
|
1610
|
+
}}</span>
|
|
1372
1611
|
}
|
|
1373
1612
|
}
|
|
1374
1613
|
</div>
|
|
@@ -1416,18 +1655,18 @@ class EmailFieldComponent {
|
|
|
1416
1655
|
* "This field has an error", which made a documented, configurable feature work on three
|
|
1417
1656
|
* of fifteen field types.
|
|
1418
1657
|
*/
|
|
1658
|
+
/** Author help text, shown under the control and named by `aria-describedby`. */
|
|
1659
|
+
get hint() {
|
|
1660
|
+
return resolveLabel(this.field?.hint, this.language);
|
|
1661
|
+
}
|
|
1662
|
+
/** The ids of whatever is describing this control right now. */
|
|
1663
|
+
describedBy() {
|
|
1664
|
+
return fieldDescribedBy(s => this.domId(s), { hint: !!this.hint, error: !!this.errorMessage });
|
|
1665
|
+
}
|
|
1419
1666
|
get errorMessage() {
|
|
1420
1667
|
if (!this.control?.errors || !this.control.touched)
|
|
1421
1668
|
return '';
|
|
1422
|
-
return this.messages.
|
|
1423
|
-
'required',
|
|
1424
|
-
'email',
|
|
1425
|
-
'min',
|
|
1426
|
-
'max',
|
|
1427
|
-
'minlength',
|
|
1428
|
-
'maxlength',
|
|
1429
|
-
'pattern',
|
|
1430
|
-
]);
|
|
1669
|
+
return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
|
|
1431
1670
|
}
|
|
1432
1671
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: EmailFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1433
1672
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: EmailFieldComponent, isStandalone: true, selector: "ngx-email-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
@@ -1438,7 +1677,21 @@ class EmailFieldComponent {
|
|
|
1438
1677
|
[class.ngx-field--readonly]="readonly"
|
|
1439
1678
|
[class.ngx-field--masked]="masked"
|
|
1440
1679
|
>
|
|
1441
|
-
<
|
|
1680
|
+
<div class="ngx-field__label-row">
|
|
1681
|
+
<label class="ngx-field__label" [attr.for]="domId()">{{ label }}</label>
|
|
1682
|
+
@if (hint) {
|
|
1683
|
+
<span class="ngx-field__hint-wrap">
|
|
1684
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
1685
|
+
<span
|
|
1686
|
+
class="ngx-field__hint"
|
|
1687
|
+
role="tooltip"
|
|
1688
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
1689
|
+
[id]="domId('-hint')"
|
|
1690
|
+
>{{ hint }}</span
|
|
1691
|
+
>
|
|
1692
|
+
</span>
|
|
1693
|
+
}
|
|
1694
|
+
</div>
|
|
1442
1695
|
@if (masked) {
|
|
1443
1696
|
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
1444
1697
|
maskedText
|
|
@@ -1455,6 +1708,7 @@ class EmailFieldComponent {
|
|
|
1455
1708
|
type="email"
|
|
1456
1709
|
autocomplete="email"
|
|
1457
1710
|
[formControl]="$any(control)"
|
|
1711
|
+
[attr.aria-describedby]="describedBy()"
|
|
1458
1712
|
[placeholder]="placeholder || 'you@example.com'"
|
|
1459
1713
|
[attr.disabled]="field.disabled ? true : null"
|
|
1460
1714
|
/>
|
|
@@ -1463,7 +1717,9 @@ class EmailFieldComponent {
|
|
|
1463
1717
|
required" and "Enter a valid email address" itself, so a host configuring
|
|
1464
1718
|
validationMessages was overridden by markup it could not reach. The service
|
|
1465
1719
|
defaults say the same thing. -->
|
|
1466
|
-
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">{{
|
|
1720
|
+
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'" [id]="domId('-error')">{{
|
|
1721
|
+
errorMessage
|
|
1722
|
+
}}</span>
|
|
1467
1723
|
}
|
|
1468
1724
|
}
|
|
1469
1725
|
</div>
|
|
@@ -1484,7 +1740,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
1484
1740
|
[class.ngx-field--readonly]="readonly"
|
|
1485
1741
|
[class.ngx-field--masked]="masked"
|
|
1486
1742
|
>
|
|
1487
|
-
<
|
|
1743
|
+
<div class="ngx-field__label-row">
|
|
1744
|
+
<label class="ngx-field__label" [attr.for]="domId()">{{ label }}</label>
|
|
1745
|
+
@if (hint) {
|
|
1746
|
+
<span class="ngx-field__hint-wrap">
|
|
1747
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
1748
|
+
<span
|
|
1749
|
+
class="ngx-field__hint"
|
|
1750
|
+
role="tooltip"
|
|
1751
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
1752
|
+
[id]="domId('-hint')"
|
|
1753
|
+
>{{ hint }}</span
|
|
1754
|
+
>
|
|
1755
|
+
</span>
|
|
1756
|
+
}
|
|
1757
|
+
</div>
|
|
1488
1758
|
@if (masked) {
|
|
1489
1759
|
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
1490
1760
|
maskedText
|
|
@@ -1501,6 +1771,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
1501
1771
|
type="email"
|
|
1502
1772
|
autocomplete="email"
|
|
1503
1773
|
[formControl]="$any(control)"
|
|
1774
|
+
[attr.aria-describedby]="describedBy()"
|
|
1504
1775
|
[placeholder]="placeholder || 'you@example.com'"
|
|
1505
1776
|
[attr.disabled]="field.disabled ? true : null"
|
|
1506
1777
|
/>
|
|
@@ -1509,7 +1780,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
1509
1780
|
required" and "Enter a valid email address" itself, so a host configuring
|
|
1510
1781
|
validationMessages was overridden by markup it could not reach. The service
|
|
1511
1782
|
defaults say the same thing. -->
|
|
1512
|
-
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">{{
|
|
1783
|
+
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'" [id]="domId('-error')">{{
|
|
1784
|
+
errorMessage
|
|
1785
|
+
}}</span>
|
|
1513
1786
|
}
|
|
1514
1787
|
}
|
|
1515
1788
|
</div>
|
|
@@ -1563,18 +1836,18 @@ class PasswordFieldComponent {
|
|
|
1563
1836
|
* "This field has an error", which made a documented, configurable feature work on three
|
|
1564
1837
|
* of fifteen field types.
|
|
1565
1838
|
*/
|
|
1839
|
+
/** Author help text, shown under the control and named by `aria-describedby`. */
|
|
1840
|
+
get hint() {
|
|
1841
|
+
return resolveLabel(this.field?.hint, this.language);
|
|
1842
|
+
}
|
|
1843
|
+
/** The ids of whatever is describing this control right now. */
|
|
1844
|
+
describedBy() {
|
|
1845
|
+
return fieldDescribedBy(s => this.domId(s), { hint: !!this.hint, error: !!this.errorMessage });
|
|
1846
|
+
}
|
|
1566
1847
|
get errorMessage() {
|
|
1567
1848
|
if (!this.control?.errors || !this.control.touched)
|
|
1568
1849
|
return '';
|
|
1569
|
-
return this.messages.
|
|
1570
|
-
'required',
|
|
1571
|
-
'email',
|
|
1572
|
-
'min',
|
|
1573
|
-
'max',
|
|
1574
|
-
'minlength',
|
|
1575
|
-
'maxlength',
|
|
1576
|
-
'pattern',
|
|
1577
|
-
]);
|
|
1850
|
+
return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
|
|
1578
1851
|
}
|
|
1579
1852
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: PasswordFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1580
1853
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: PasswordFieldComponent, isStandalone: true, selector: "ngx-password-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
@@ -1585,7 +1858,21 @@ class PasswordFieldComponent {
|
|
|
1585
1858
|
[class.ngx-field--readonly]="readonly"
|
|
1586
1859
|
[class.ngx-field--masked]="masked"
|
|
1587
1860
|
>
|
|
1588
|
-
<
|
|
1861
|
+
<div class="ngx-field__label-row">
|
|
1862
|
+
<label class="ngx-field__label" [attr.for]="domId()">{{ label }}</label>
|
|
1863
|
+
@if (hint) {
|
|
1864
|
+
<span class="ngx-field__hint-wrap">
|
|
1865
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
1866
|
+
<span
|
|
1867
|
+
class="ngx-field__hint"
|
|
1868
|
+
role="tooltip"
|
|
1869
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
1870
|
+
[id]="domId('-hint')"
|
|
1871
|
+
>{{ hint }}</span
|
|
1872
|
+
>
|
|
1873
|
+
</span>
|
|
1874
|
+
}
|
|
1875
|
+
</div>
|
|
1589
1876
|
@if (masked) {
|
|
1590
1877
|
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
1591
1878
|
maskedText
|
|
@@ -1601,6 +1888,7 @@ class PasswordFieldComponent {
|
|
|
1601
1888
|
[type]="visible() ? 'text' : 'password'"
|
|
1602
1889
|
autocomplete="current-password"
|
|
1603
1890
|
[formControl]="$any(control)"
|
|
1891
|
+
[attr.aria-describedby]="describedBy()"
|
|
1604
1892
|
[placeholder]="placeholder || '••••••••'"
|
|
1605
1893
|
[attr.disabled]="field.disabled ? true : null"
|
|
1606
1894
|
/>
|
|
@@ -1614,7 +1902,9 @@ class PasswordFieldComponent {
|
|
|
1614
1902
|
</button>
|
|
1615
1903
|
</div>
|
|
1616
1904
|
@if (errorMessage) {
|
|
1617
|
-
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">{{
|
|
1905
|
+
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'" [id]="domId('-error')">{{
|
|
1906
|
+
errorMessage
|
|
1907
|
+
}}</span>
|
|
1618
1908
|
}
|
|
1619
1909
|
}
|
|
1620
1910
|
</div>
|
|
@@ -1635,7 +1925,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
1635
1925
|
[class.ngx-field--readonly]="readonly"
|
|
1636
1926
|
[class.ngx-field--masked]="masked"
|
|
1637
1927
|
>
|
|
1638
|
-
<
|
|
1928
|
+
<div class="ngx-field__label-row">
|
|
1929
|
+
<label class="ngx-field__label" [attr.for]="domId()">{{ label }}</label>
|
|
1930
|
+
@if (hint) {
|
|
1931
|
+
<span class="ngx-field__hint-wrap">
|
|
1932
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
1933
|
+
<span
|
|
1934
|
+
class="ngx-field__hint"
|
|
1935
|
+
role="tooltip"
|
|
1936
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
1937
|
+
[id]="domId('-hint')"
|
|
1938
|
+
>{{ hint }}</span
|
|
1939
|
+
>
|
|
1940
|
+
</span>
|
|
1941
|
+
}
|
|
1942
|
+
</div>
|
|
1639
1943
|
@if (masked) {
|
|
1640
1944
|
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
1641
1945
|
maskedText
|
|
@@ -1651,6 +1955,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
1651
1955
|
[type]="visible() ? 'text' : 'password'"
|
|
1652
1956
|
autocomplete="current-password"
|
|
1653
1957
|
[formControl]="$any(control)"
|
|
1958
|
+
[attr.aria-describedby]="describedBy()"
|
|
1654
1959
|
[placeholder]="placeholder || '••••••••'"
|
|
1655
1960
|
[attr.disabled]="field.disabled ? true : null"
|
|
1656
1961
|
/>
|
|
@@ -1664,7 +1969,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
1664
1969
|
</button>
|
|
1665
1970
|
</div>
|
|
1666
1971
|
@if (errorMessage) {
|
|
1667
|
-
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">{{
|
|
1972
|
+
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'" [id]="domId('-error')">{{
|
|
1973
|
+
errorMessage
|
|
1974
|
+
}}</span>
|
|
1668
1975
|
}
|
|
1669
1976
|
}
|
|
1670
1977
|
</div>
|
|
@@ -1710,10 +2017,18 @@ class CheckboxFieldComponent {
|
|
|
1710
2017
|
* It rendered no error element at all, so a required checkbox left unticked told the user
|
|
1711
2018
|
* nothing — and `validationMessages` could not reach a field that never displayed one.
|
|
1712
2019
|
*/
|
|
2020
|
+
/** Author help text, shown under the control and named by `aria-describedby`. */
|
|
2021
|
+
get hint() {
|
|
2022
|
+
return resolveLabel(this.field?.hint, this.language);
|
|
2023
|
+
}
|
|
2024
|
+
/** The ids of whatever is describing this control right now. */
|
|
2025
|
+
describedBy() {
|
|
2026
|
+
return fieldDescribedBy(s => this.domId(s), { hint: !!this.hint, error: !!this.errorMessage });
|
|
2027
|
+
}
|
|
1713
2028
|
get errorMessage() {
|
|
1714
2029
|
if (!this.control?.errors || !this.control.touched)
|
|
1715
2030
|
return '';
|
|
1716
|
-
return this.messages.
|
|
2031
|
+
return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
|
|
1717
2032
|
}
|
|
1718
2033
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: CheckboxFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1719
2034
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: CheckboxFieldComponent, isStandalone: true, selector: "ngx-checkbox-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
@@ -1742,14 +2057,31 @@ class CheckboxFieldComponent {
|
|
|
1742
2057
|
[attr.data-testid]="'field-' + field.id + '-input'"
|
|
1743
2058
|
type="checkbox"
|
|
1744
2059
|
[formControl]="$any(control)"
|
|
2060
|
+
[attr.aria-describedby]="describedBy()"
|
|
1745
2061
|
[attr.disabled]="field.disabled ? true : null"
|
|
1746
2062
|
/>
|
|
1747
2063
|
{{ label }}
|
|
2064
|
+
@if (hint) {
|
|
2065
|
+
<span class="ngx-field__hint-wrap">
|
|
2066
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
2067
|
+
<span
|
|
2068
|
+
class="ngx-field__hint"
|
|
2069
|
+
role="tooltip"
|
|
2070
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
2071
|
+
[id]="domId('-hint')"
|
|
2072
|
+
>{{ hint }}</span
|
|
2073
|
+
>
|
|
2074
|
+
</span>
|
|
2075
|
+
}
|
|
1748
2076
|
</label>
|
|
1749
2077
|
@if (errorMessage) {
|
|
1750
|
-
<span
|
|
1751
|
-
|
|
1752
|
-
|
|
2078
|
+
<span
|
|
2079
|
+
class="ngx-field__error"
|
|
2080
|
+
[attr.data-testid]="'field-' + field.id + '-error'"
|
|
2081
|
+
[id]="domId('-error')"
|
|
2082
|
+
role="alert"
|
|
2083
|
+
>{{ errorMessage }}</span
|
|
2084
|
+
>
|
|
1753
2085
|
}
|
|
1754
2086
|
}
|
|
1755
2087
|
</div>
|
|
@@ -1788,14 +2120,31 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
1788
2120
|
[attr.data-testid]="'field-' + field.id + '-input'"
|
|
1789
2121
|
type="checkbox"
|
|
1790
2122
|
[formControl]="$any(control)"
|
|
2123
|
+
[attr.aria-describedby]="describedBy()"
|
|
1791
2124
|
[attr.disabled]="field.disabled ? true : null"
|
|
1792
2125
|
/>
|
|
1793
2126
|
{{ label }}
|
|
2127
|
+
@if (hint) {
|
|
2128
|
+
<span class="ngx-field__hint-wrap">
|
|
2129
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
2130
|
+
<span
|
|
2131
|
+
class="ngx-field__hint"
|
|
2132
|
+
role="tooltip"
|
|
2133
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
2134
|
+
[id]="domId('-hint')"
|
|
2135
|
+
>{{ hint }}</span
|
|
2136
|
+
>
|
|
2137
|
+
</span>
|
|
2138
|
+
}
|
|
1794
2139
|
</label>
|
|
1795
2140
|
@if (errorMessage) {
|
|
1796
|
-
<span
|
|
1797
|
-
|
|
1798
|
-
|
|
2141
|
+
<span
|
|
2142
|
+
class="ngx-field__error"
|
|
2143
|
+
[attr.data-testid]="'field-' + field.id + '-error'"
|
|
2144
|
+
[id]="domId('-error')"
|
|
2145
|
+
role="alert"
|
|
2146
|
+
>{{ errorMessage }}</span
|
|
2147
|
+
>
|
|
1799
2148
|
}
|
|
1800
2149
|
}
|
|
1801
2150
|
</div>
|
|
@@ -1819,6 +2168,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
1819
2168
|
* toggle switch with Yes/No labels for clearer binary state communication.
|
|
1820
2169
|
*/
|
|
1821
2170
|
class BooleanFieldComponent {
|
|
2171
|
+
/**
|
|
2172
|
+
* Unique to this component instance: an `array` renders the same field once per row, and a
|
|
2173
|
+
* DOM id may not repeat. See `field-dom-id.ts`.
|
|
2174
|
+
*/
|
|
2175
|
+
instanceId = nextFieldInstanceId();
|
|
2176
|
+
domId(suffix = '') {
|
|
2177
|
+
return fieldDomId(this.field, this.instanceId, suffix);
|
|
2178
|
+
}
|
|
1822
2179
|
/** Library chrome, overridable via UI_TEXT. */
|
|
1823
2180
|
ui = inject(UiTextService);
|
|
1824
2181
|
/** Overridable via MASKED_PLACEHOLDER; the default is the historic literal. */
|
|
@@ -1838,10 +2195,18 @@ class BooleanFieldComponent {
|
|
|
1838
2195
|
* It rendered no error element at all, so a required checkbox left unticked told the user
|
|
1839
2196
|
* nothing — and `validationMessages` could not reach a field that never displayed one.
|
|
1840
2197
|
*/
|
|
2198
|
+
/** Author help text, shown under the control and named by `aria-describedby`. */
|
|
2199
|
+
get hint() {
|
|
2200
|
+
return resolveLabel(this.field?.hint, this.language);
|
|
2201
|
+
}
|
|
2202
|
+
/** The ids of whatever is describing this control right now. */
|
|
2203
|
+
describedBy() {
|
|
2204
|
+
return fieldDescribedBy(s => this.domId(s), { hint: !!this.hint, error: !!this.errorMessage });
|
|
2205
|
+
}
|
|
1841
2206
|
get errorMessage() {
|
|
1842
2207
|
if (!this.control?.errors || !this.control.touched)
|
|
1843
2208
|
return '';
|
|
1844
|
-
return this.messages.
|
|
2209
|
+
return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
|
|
1845
2210
|
}
|
|
1846
2211
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: BooleanFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1847
2212
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: BooleanFieldComponent, isStandalone: true, selector: "ngx-boolean-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
@@ -1865,12 +2230,25 @@ class BooleanFieldComponent {
|
|
|
1865
2230
|
} @else {
|
|
1866
2231
|
<div class="ngx-field__toggle-wrap">
|
|
1867
2232
|
<label class="ngx-field__toggle-label">{{ label }}</label>
|
|
2233
|
+
@if (hint) {
|
|
2234
|
+
<span class="ngx-field__hint-wrap">
|
|
2235
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
2236
|
+
<span
|
|
2237
|
+
class="ngx-field__hint"
|
|
2238
|
+
role="tooltip"
|
|
2239
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
2240
|
+
[id]="domId('-hint')"
|
|
2241
|
+
>{{ hint }}</span
|
|
2242
|
+
>
|
|
2243
|
+
</span>
|
|
2244
|
+
}
|
|
1868
2245
|
<label class="ngx-field__toggle-switch" [class.ngx-field__toggle-switch--checked]="control.value">
|
|
1869
2246
|
<input
|
|
1870
2247
|
class="ngx-field__toggle-input"
|
|
1871
2248
|
type="checkbox"
|
|
1872
2249
|
role="switch"
|
|
1873
2250
|
[formControl]="$any(control)"
|
|
2251
|
+
[attr.aria-describedby]="describedBy()"
|
|
1874
2252
|
[attr.disabled]="field.disabled ? true : null"
|
|
1875
2253
|
[attr.aria-checked]="control.value"
|
|
1876
2254
|
/>
|
|
@@ -1883,9 +2261,13 @@ class BooleanFieldComponent {
|
|
|
1883
2261
|
</label>
|
|
1884
2262
|
</div>
|
|
1885
2263
|
@if (errorMessage) {
|
|
1886
|
-
<span
|
|
1887
|
-
|
|
1888
|
-
|
|
2264
|
+
<span
|
|
2265
|
+
class="ngx-field__error"
|
|
2266
|
+
[attr.data-testid]="'field-' + field.id + '-error'"
|
|
2267
|
+
[id]="domId('-error')"
|
|
2268
|
+
role="alert"
|
|
2269
|
+
>{{ errorMessage }}</span
|
|
2270
|
+
>
|
|
1889
2271
|
}
|
|
1890
2272
|
}
|
|
1891
2273
|
</div>
|
|
@@ -1919,12 +2301,25 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
1919
2301
|
} @else {
|
|
1920
2302
|
<div class="ngx-field__toggle-wrap">
|
|
1921
2303
|
<label class="ngx-field__toggle-label">{{ label }}</label>
|
|
2304
|
+
@if (hint) {
|
|
2305
|
+
<span class="ngx-field__hint-wrap">
|
|
2306
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
2307
|
+
<span
|
|
2308
|
+
class="ngx-field__hint"
|
|
2309
|
+
role="tooltip"
|
|
2310
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
2311
|
+
[id]="domId('-hint')"
|
|
2312
|
+
>{{ hint }}</span
|
|
2313
|
+
>
|
|
2314
|
+
</span>
|
|
2315
|
+
}
|
|
1922
2316
|
<label class="ngx-field__toggle-switch" [class.ngx-field__toggle-switch--checked]="control.value">
|
|
1923
2317
|
<input
|
|
1924
2318
|
class="ngx-field__toggle-input"
|
|
1925
2319
|
type="checkbox"
|
|
1926
2320
|
role="switch"
|
|
1927
2321
|
[formControl]="$any(control)"
|
|
2322
|
+
[attr.aria-describedby]="describedBy()"
|
|
1928
2323
|
[attr.disabled]="field.disabled ? true : null"
|
|
1929
2324
|
[attr.aria-checked]="control.value"
|
|
1930
2325
|
/>
|
|
@@ -1937,9 +2332,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
1937
2332
|
</label>
|
|
1938
2333
|
</div>
|
|
1939
2334
|
@if (errorMessage) {
|
|
1940
|
-
<span
|
|
1941
|
-
|
|
1942
|
-
|
|
2335
|
+
<span
|
|
2336
|
+
class="ngx-field__error"
|
|
2337
|
+
[attr.data-testid]="'field-' + field.id + '-error'"
|
|
2338
|
+
[id]="domId('-error')"
|
|
2339
|
+
role="alert"
|
|
2340
|
+
>{{ errorMessage }}</span
|
|
2341
|
+
>
|
|
1943
2342
|
}
|
|
1944
2343
|
}
|
|
1945
2344
|
</div>
|
|
@@ -1977,16 +2376,30 @@ class DateFieldComponent {
|
|
|
1977
2376
|
get label() {
|
|
1978
2377
|
return resolveLabel(this.field?.label, this.language);
|
|
1979
2378
|
}
|
|
1980
|
-
/**
|
|
2379
|
+
/**
|
|
2380
|
+
* ISO 8601 date → display string, through the formatter the host configured.
|
|
2381
|
+
*
|
|
2382
|
+
* This used to call `toLocaleDateString()` directly, which meant `setDateFormatters` — the
|
|
2383
|
+
* one seam for choosing how a date is rendered — reached the record summary and the `time`
|
|
2384
|
+
* field but not this one. A host that configured formatters got them almost everywhere,
|
|
2385
|
+
* and silently did not get them on the field type most likely to be the reason they
|
|
2386
|
+
* configured formatters at all.
|
|
2387
|
+
*
|
|
2388
|
+
* The default formatter is `toLocaleDateString()` with no locale, so an application that
|
|
2389
|
+
* has not called `setDateFormatters` renders exactly what it rendered before.
|
|
2390
|
+
*/
|
|
1981
2391
|
formatDate(value) {
|
|
1982
2392
|
if (!value)
|
|
1983
2393
|
return '—';
|
|
1984
2394
|
// `new Date('nonsense')` does not throw and `toLocaleDateString()` returns the *string*
|
|
1985
2395
|
// "Invalid Date", so the try/catch this replaced never fired and readers saw that text
|
|
1986
2396
|
// instead of their data. Records outlive schemas — a field retyped from text to date can
|
|
1987
|
-
// hold anything — so an unparseable value is shown as stored
|
|
2397
|
+
// hold anything — so an unparseable value is shown as stored, and never reaches the
|
|
2398
|
+
// formatter.
|
|
1988
2399
|
const parsed = new Date(value);
|
|
1989
|
-
|
|
2400
|
+
if (Number.isNaN(parsed.getTime()))
|
|
2401
|
+
return value;
|
|
2402
|
+
return formatDisplayValue('date', undefined, value, this.language);
|
|
1990
2403
|
}
|
|
1991
2404
|
/**
|
|
1992
2405
|
* Resolved through `ValidationMessagesService`, so `provideNgxDynamicEntity({
|
|
@@ -1994,18 +2407,18 @@ class DateFieldComponent {
|
|
|
1994
2407
|
* "This field has an error", which made a documented, configurable feature work on three
|
|
1995
2408
|
* of fifteen field types.
|
|
1996
2409
|
*/
|
|
2410
|
+
/** Author help text, shown under the control and named by `aria-describedby`. */
|
|
2411
|
+
get hint() {
|
|
2412
|
+
return resolveLabel(this.field?.hint, this.language);
|
|
2413
|
+
}
|
|
2414
|
+
/** The ids of whatever is describing this control right now. */
|
|
2415
|
+
describedBy() {
|
|
2416
|
+
return fieldDescribedBy(s => this.domId(s), { hint: !!this.hint, error: !!this.errorMessage });
|
|
2417
|
+
}
|
|
1997
2418
|
get errorMessage() {
|
|
1998
2419
|
if (!this.control?.errors || !this.control.touched)
|
|
1999
2420
|
return '';
|
|
2000
|
-
return this.messages.
|
|
2001
|
-
'required',
|
|
2002
|
-
'email',
|
|
2003
|
-
'min',
|
|
2004
|
-
'max',
|
|
2005
|
-
'minlength',
|
|
2006
|
-
'maxlength',
|
|
2007
|
-
'pattern',
|
|
2008
|
-
]);
|
|
2421
|
+
return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
|
|
2009
2422
|
}
|
|
2010
2423
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: DateFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2011
2424
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: DateFieldComponent, isStandalone: true, selector: "ngx-date-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
@@ -2016,7 +2429,21 @@ class DateFieldComponent {
|
|
|
2016
2429
|
[class.ngx-field--readonly]="readonly"
|
|
2017
2430
|
[class.ngx-field--masked]="masked"
|
|
2018
2431
|
>
|
|
2019
|
-
<
|
|
2432
|
+
<div class="ngx-field__label-row">
|
|
2433
|
+
<label class="ngx-field__label" [attr.for]="domId()">{{ label }}</label>
|
|
2434
|
+
@if (hint) {
|
|
2435
|
+
<span class="ngx-field__hint-wrap">
|
|
2436
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
2437
|
+
<span
|
|
2438
|
+
class="ngx-field__hint"
|
|
2439
|
+
role="tooltip"
|
|
2440
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
2441
|
+
[id]="domId('-hint')"
|
|
2442
|
+
>{{ hint }}</span
|
|
2443
|
+
>
|
|
2444
|
+
</span>
|
|
2445
|
+
}
|
|
2446
|
+
</div>
|
|
2020
2447
|
@if (masked) {
|
|
2021
2448
|
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
2022
2449
|
maskedText
|
|
@@ -2032,10 +2459,13 @@ class DateFieldComponent {
|
|
|
2032
2459
|
[attr.data-testid]="'field-' + field.id + '-input'"
|
|
2033
2460
|
type="date"
|
|
2034
2461
|
[formControl]="$any(control)"
|
|
2462
|
+
[attr.aria-describedby]="describedBy()"
|
|
2035
2463
|
[attr.disabled]="field.disabled ? true : null"
|
|
2036
2464
|
/>
|
|
2037
2465
|
@if (errorMessage) {
|
|
2038
|
-
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">{{
|
|
2466
|
+
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'" [id]="domId('-error')">{{
|
|
2467
|
+
errorMessage
|
|
2468
|
+
}}</span>
|
|
2039
2469
|
}
|
|
2040
2470
|
}
|
|
2041
2471
|
</div>
|
|
@@ -2056,7 +2486,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
2056
2486
|
[class.ngx-field--readonly]="readonly"
|
|
2057
2487
|
[class.ngx-field--masked]="masked"
|
|
2058
2488
|
>
|
|
2059
|
-
<
|
|
2489
|
+
<div class="ngx-field__label-row">
|
|
2490
|
+
<label class="ngx-field__label" [attr.for]="domId()">{{ label }}</label>
|
|
2491
|
+
@if (hint) {
|
|
2492
|
+
<span class="ngx-field__hint-wrap">
|
|
2493
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
2494
|
+
<span
|
|
2495
|
+
class="ngx-field__hint"
|
|
2496
|
+
role="tooltip"
|
|
2497
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
2498
|
+
[id]="domId('-hint')"
|
|
2499
|
+
>{{ hint }}</span
|
|
2500
|
+
>
|
|
2501
|
+
</span>
|
|
2502
|
+
}
|
|
2503
|
+
</div>
|
|
2060
2504
|
@if (masked) {
|
|
2061
2505
|
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
2062
2506
|
maskedText
|
|
@@ -2072,10 +2516,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
2072
2516
|
[attr.data-testid]="'field-' + field.id + '-input'"
|
|
2073
2517
|
type="date"
|
|
2074
2518
|
[formControl]="$any(control)"
|
|
2519
|
+
[attr.aria-describedby]="describedBy()"
|
|
2075
2520
|
[attr.disabled]="field.disabled ? true : null"
|
|
2076
2521
|
/>
|
|
2077
2522
|
@if (errorMessage) {
|
|
2078
|
-
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">{{
|
|
2523
|
+
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'" [id]="domId('-error')">{{
|
|
2524
|
+
errorMessage
|
|
2525
|
+
}}</span>
|
|
2079
2526
|
}
|
|
2080
2527
|
}
|
|
2081
2528
|
</div>
|
|
@@ -2162,13 +2609,23 @@ class DateTimeFieldComponent {
|
|
|
2162
2609
|
this.control.markAsTouched();
|
|
2163
2610
|
}
|
|
2164
2611
|
/**
|
|
2165
|
-
* Displays date *and* time
|
|
2166
|
-
*
|
|
2167
|
-
*
|
|
2612
|
+
* Displays date *and* time, through the formatter the host configured.
|
|
2613
|
+
*
|
|
2614
|
+
* Two rounds of the same fault. First this called `toLocaleDateString()`, so a datetime
|
|
2615
|
+
* read back through the field showed no time while the record summary showed it. Then it
|
|
2616
|
+
* called `toLocaleString()` directly, which fixed the time but kept the field outside
|
|
2617
|
+
* `setDateFormatters` — so a host that configured formatters still saw them apply to the
|
|
2618
|
+
* summary and not to the field. Going through `formatDisplayValue` is what makes the two
|
|
2619
|
+
* paths agree by construction rather than by matching literals.
|
|
2620
|
+
*
|
|
2621
|
+
* `parseStored` accepts shapes `new Date(raw)` alone does not, so the *parsed* instant is
|
|
2622
|
+
* handed on as ISO rather than the raw value.
|
|
2168
2623
|
*/
|
|
2169
2624
|
formatDateTime(value) {
|
|
2170
2625
|
const d = parseStored(value);
|
|
2171
|
-
|
|
2626
|
+
if (!d)
|
|
2627
|
+
return '—';
|
|
2628
|
+
return formatDisplayValue('datetime', undefined, d.toISOString(), this.language);
|
|
2172
2629
|
}
|
|
2173
2630
|
/**
|
|
2174
2631
|
* Resolved through `ValidationMessagesService`, so `provideNgxDynamicEntity({
|
|
@@ -2176,18 +2633,18 @@ class DateTimeFieldComponent {
|
|
|
2176
2633
|
* "This field has an error", which made a documented, configurable feature work on three
|
|
2177
2634
|
* of fifteen field types.
|
|
2178
2635
|
*/
|
|
2636
|
+
/** Author help text, shown under the control and named by `aria-describedby`. */
|
|
2637
|
+
get hint() {
|
|
2638
|
+
return resolveLabel(this.field?.hint, this.language);
|
|
2639
|
+
}
|
|
2640
|
+
/** The ids of whatever is describing this control right now. */
|
|
2641
|
+
describedBy() {
|
|
2642
|
+
return fieldDescribedBy(s => this.domId(s), { hint: !!this.hint, error: !!this.errorMessage });
|
|
2643
|
+
}
|
|
2179
2644
|
get errorMessage() {
|
|
2180
2645
|
if (!this.control?.errors || !this.control.touched)
|
|
2181
2646
|
return '';
|
|
2182
|
-
return this.messages.
|
|
2183
|
-
'required',
|
|
2184
|
-
'email',
|
|
2185
|
-
'min',
|
|
2186
|
-
'max',
|
|
2187
|
-
'minlength',
|
|
2188
|
-
'maxlength',
|
|
2189
|
-
'pattern',
|
|
2190
|
-
]);
|
|
2647
|
+
return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
|
|
2191
2648
|
}
|
|
2192
2649
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: DateTimeFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2193
2650
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: DateTimeFieldComponent, isStandalone: true, selector: "ngx-date-time-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
@@ -2198,7 +2655,21 @@ class DateTimeFieldComponent {
|
|
|
2198
2655
|
[class.ngx-field--readonly]="readonly"
|
|
2199
2656
|
[class.ngx-field--masked]="masked"
|
|
2200
2657
|
>
|
|
2201
|
-
<
|
|
2658
|
+
<div class="ngx-field__label-row">
|
|
2659
|
+
<label class="ngx-field__label" [attr.for]="domId()">{{ label }}</label>
|
|
2660
|
+
@if (hint) {
|
|
2661
|
+
<span class="ngx-field__hint-wrap">
|
|
2662
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
2663
|
+
<span
|
|
2664
|
+
class="ngx-field__hint"
|
|
2665
|
+
role="tooltip"
|
|
2666
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
2667
|
+
[id]="domId('-hint')"
|
|
2668
|
+
>{{ hint }}</span
|
|
2669
|
+
>
|
|
2670
|
+
</span>
|
|
2671
|
+
}
|
|
2672
|
+
</div>
|
|
2202
2673
|
@if (masked) {
|
|
2203
2674
|
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
2204
2675
|
maskedText
|
|
@@ -2216,9 +2687,12 @@ class DateTimeFieldComponent {
|
|
|
2216
2687
|
[value]="inputValue"
|
|
2217
2688
|
(change)="onInput($any($event.target).value)"
|
|
2218
2689
|
[attr.disabled]="field.disabled ? true : null"
|
|
2690
|
+
[attr.aria-describedby]="describedBy()"
|
|
2219
2691
|
/>
|
|
2220
2692
|
@if (errorMessage) {
|
|
2221
|
-
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">{{
|
|
2693
|
+
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'" [id]="domId('-error')">{{
|
|
2694
|
+
errorMessage
|
|
2695
|
+
}}</span>
|
|
2222
2696
|
}
|
|
2223
2697
|
}
|
|
2224
2698
|
</div>
|
|
@@ -2239,7 +2713,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
2239
2713
|
[class.ngx-field--readonly]="readonly"
|
|
2240
2714
|
[class.ngx-field--masked]="masked"
|
|
2241
2715
|
>
|
|
2242
|
-
<
|
|
2716
|
+
<div class="ngx-field__label-row">
|
|
2717
|
+
<label class="ngx-field__label" [attr.for]="domId()">{{ label }}</label>
|
|
2718
|
+
@if (hint) {
|
|
2719
|
+
<span class="ngx-field__hint-wrap">
|
|
2720
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
2721
|
+
<span
|
|
2722
|
+
class="ngx-field__hint"
|
|
2723
|
+
role="tooltip"
|
|
2724
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
2725
|
+
[id]="domId('-hint')"
|
|
2726
|
+
>{{ hint }}</span
|
|
2727
|
+
>
|
|
2728
|
+
</span>
|
|
2729
|
+
}
|
|
2730
|
+
</div>
|
|
2243
2731
|
@if (masked) {
|
|
2244
2732
|
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
2245
2733
|
maskedText
|
|
@@ -2257,9 +2745,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
2257
2745
|
[value]="inputValue"
|
|
2258
2746
|
(change)="onInput($any($event.target).value)"
|
|
2259
2747
|
[attr.disabled]="field.disabled ? true : null"
|
|
2748
|
+
[attr.aria-describedby]="describedBy()"
|
|
2260
2749
|
/>
|
|
2261
2750
|
@if (errorMessage) {
|
|
2262
|
-
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">{{
|
|
2751
|
+
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'" [id]="domId('-error')">{{
|
|
2752
|
+
errorMessage
|
|
2753
|
+
}}</span>
|
|
2263
2754
|
}
|
|
2264
2755
|
}
|
|
2265
2756
|
</div>
|
|
@@ -2319,18 +2810,18 @@ class TimeFieldComponent {
|
|
|
2319
2810
|
* "This field has an error", which made a documented, configurable feature work on three
|
|
2320
2811
|
* of fifteen field types.
|
|
2321
2812
|
*/
|
|
2813
|
+
/** Author help text, shown under the control and named by `aria-describedby`. */
|
|
2814
|
+
get hint() {
|
|
2815
|
+
return resolveLabel(this.field?.hint, this.language);
|
|
2816
|
+
}
|
|
2817
|
+
/** The ids of whatever is describing this control right now. */
|
|
2818
|
+
describedBy() {
|
|
2819
|
+
return fieldDescribedBy(s => this.domId(s), { hint: !!this.hint, error: !!this.errorMessage });
|
|
2820
|
+
}
|
|
2322
2821
|
get errorMessage() {
|
|
2323
2822
|
if (!this.control?.errors || !this.control.touched)
|
|
2324
2823
|
return '';
|
|
2325
|
-
return this.messages.
|
|
2326
|
-
'required',
|
|
2327
|
-
'email',
|
|
2328
|
-
'min',
|
|
2329
|
-
'max',
|
|
2330
|
-
'minlength',
|
|
2331
|
-
'maxlength',
|
|
2332
|
-
'pattern',
|
|
2333
|
-
]);
|
|
2824
|
+
return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
|
|
2334
2825
|
}
|
|
2335
2826
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: TimeFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2336
2827
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: TimeFieldComponent, isStandalone: true, selector: "ngx-time-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
@@ -2341,7 +2832,21 @@ class TimeFieldComponent {
|
|
|
2341
2832
|
[class.ngx-field--readonly]="readonly"
|
|
2342
2833
|
[class.ngx-field--masked]="masked"
|
|
2343
2834
|
>
|
|
2344
|
-
<
|
|
2835
|
+
<div class="ngx-field__label-row">
|
|
2836
|
+
<label class="ngx-field__label" [attr.for]="domId()">{{ label }}</label>
|
|
2837
|
+
@if (hint) {
|
|
2838
|
+
<span class="ngx-field__hint-wrap">
|
|
2839
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
2840
|
+
<span
|
|
2841
|
+
class="ngx-field__hint"
|
|
2842
|
+
role="tooltip"
|
|
2843
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
2844
|
+
[id]="domId('-hint')"
|
|
2845
|
+
>{{ hint }}</span
|
|
2846
|
+
>
|
|
2847
|
+
</span>
|
|
2848
|
+
}
|
|
2849
|
+
</div>
|
|
2345
2850
|
@if (masked) {
|
|
2346
2851
|
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
2347
2852
|
maskedText
|
|
@@ -2357,10 +2862,13 @@ class TimeFieldComponent {
|
|
|
2357
2862
|
[attr.data-testid]="'field-' + field.id + '-input'"
|
|
2358
2863
|
type="time"
|
|
2359
2864
|
[formControl]="$any(control)"
|
|
2865
|
+
[attr.aria-describedby]="describedBy()"
|
|
2360
2866
|
[attr.disabled]="field.disabled ? true : null"
|
|
2361
2867
|
/>
|
|
2362
2868
|
@if (errorMessage) {
|
|
2363
|
-
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">{{
|
|
2869
|
+
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'" [id]="domId('-error')">{{
|
|
2870
|
+
errorMessage
|
|
2871
|
+
}}</span>
|
|
2364
2872
|
}
|
|
2365
2873
|
}
|
|
2366
2874
|
</div>
|
|
@@ -2381,7 +2889,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
2381
2889
|
[class.ngx-field--readonly]="readonly"
|
|
2382
2890
|
[class.ngx-field--masked]="masked"
|
|
2383
2891
|
>
|
|
2384
|
-
<
|
|
2892
|
+
<div class="ngx-field__label-row">
|
|
2893
|
+
<label class="ngx-field__label" [attr.for]="domId()">{{ label }}</label>
|
|
2894
|
+
@if (hint) {
|
|
2895
|
+
<span class="ngx-field__hint-wrap">
|
|
2896
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
2897
|
+
<span
|
|
2898
|
+
class="ngx-field__hint"
|
|
2899
|
+
role="tooltip"
|
|
2900
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
2901
|
+
[id]="domId('-hint')"
|
|
2902
|
+
>{{ hint }}</span
|
|
2903
|
+
>
|
|
2904
|
+
</span>
|
|
2905
|
+
}
|
|
2906
|
+
</div>
|
|
2385
2907
|
@if (masked) {
|
|
2386
2908
|
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
2387
2909
|
maskedText
|
|
@@ -2397,10 +2919,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
2397
2919
|
[attr.data-testid]="'field-' + field.id + '-input'"
|
|
2398
2920
|
type="time"
|
|
2399
2921
|
[formControl]="$any(control)"
|
|
2922
|
+
[attr.aria-describedby]="describedBy()"
|
|
2400
2923
|
[attr.disabled]="field.disabled ? true : null"
|
|
2401
2924
|
/>
|
|
2402
2925
|
@if (errorMessage) {
|
|
2403
|
-
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">{{
|
|
2926
|
+
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'" [id]="domId('-error')">{{
|
|
2927
|
+
errorMessage
|
|
2928
|
+
}}</span>
|
|
2404
2929
|
}
|
|
2405
2930
|
}
|
|
2406
2931
|
</div>
|
|
@@ -2494,18 +3019,18 @@ class MonthYearFieldComponent {
|
|
|
2494
3019
|
* "This field has an error", which made a documented, configurable feature work on three
|
|
2495
3020
|
* of fifteen field types.
|
|
2496
3021
|
*/
|
|
3022
|
+
/** Author help text, shown under the control and named by `aria-describedby`. */
|
|
3023
|
+
get hint() {
|
|
3024
|
+
return resolveLabel(this.field?.hint, this.language);
|
|
3025
|
+
}
|
|
3026
|
+
/** The ids of whatever is describing this control right now. */
|
|
3027
|
+
describedBy() {
|
|
3028
|
+
return fieldDescribedBy(s => this.domId(s), { hint: !!this.hint, error: !!this.errorMessage });
|
|
3029
|
+
}
|
|
2497
3030
|
get errorMessage() {
|
|
2498
3031
|
if (!this.control?.errors || !this.control.touched)
|
|
2499
3032
|
return '';
|
|
2500
|
-
return this.messages.
|
|
2501
|
-
'required',
|
|
2502
|
-
'email',
|
|
2503
|
-
'min',
|
|
2504
|
-
'max',
|
|
2505
|
-
'minlength',
|
|
2506
|
-
'maxlength',
|
|
2507
|
-
'pattern',
|
|
2508
|
-
]);
|
|
3033
|
+
return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
|
|
2509
3034
|
}
|
|
2510
3035
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MonthYearFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2511
3036
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: MonthYearFieldComponent, isStandalone: true, selector: "ngx-month-year-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
@@ -2522,7 +3047,21 @@ class MonthYearFieldComponent {
|
|
|
2522
3047
|
clicked. Each select then carries its own name below, because "Billing Cycle" alone
|
|
2523
3048
|
does not say which half of the pair the screen reader has landed on.
|
|
2524
3049
|
-->
|
|
2525
|
-
<
|
|
3050
|
+
<div class="ngx-field__label-row">
|
|
3051
|
+
<label class="ngx-field__label" [attr.for]="domId('-month')">{{ label }}</label>
|
|
3052
|
+
@if (hint) {
|
|
3053
|
+
<span class="ngx-field__hint-wrap">
|
|
3054
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
3055
|
+
<span
|
|
3056
|
+
class="ngx-field__hint"
|
|
3057
|
+
role="tooltip"
|
|
3058
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
3059
|
+
[id]="domId('-hint')"
|
|
3060
|
+
>{{ hint }}</span
|
|
3061
|
+
>
|
|
3062
|
+
</span>
|
|
3063
|
+
}
|
|
3064
|
+
</div>
|
|
2526
3065
|
@if (masked) {
|
|
2527
3066
|
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
2528
3067
|
maskedText
|
|
@@ -2541,6 +3080,7 @@ class MonthYearFieldComponent {
|
|
|
2541
3080
|
[value]="selectedMonth"
|
|
2542
3081
|
(change)="onMonthChange($any($event.target).value)"
|
|
2543
3082
|
[attr.disabled]="field.disabled ? true : null"
|
|
3083
|
+
[attr.aria-describedby]="describedBy()"
|
|
2544
3084
|
>
|
|
2545
3085
|
<option value="">{{ ui.text('month', language) }}</option>
|
|
2546
3086
|
@for (m of months; track m.value) {
|
|
@@ -2555,6 +3095,7 @@ class MonthYearFieldComponent {
|
|
|
2555
3095
|
[value]="selectedYear"
|
|
2556
3096
|
(change)="onYearChange($any($event.target).value)"
|
|
2557
3097
|
[attr.disabled]="field.disabled ? true : null"
|
|
3098
|
+
[attr.aria-describedby]="describedBy()"
|
|
2558
3099
|
>
|
|
2559
3100
|
<option value="">{{ ui.text('year', language) }}</option>
|
|
2560
3101
|
@for (y of years; track y) {
|
|
@@ -2563,7 +3104,9 @@ class MonthYearFieldComponent {
|
|
|
2563
3104
|
</select>
|
|
2564
3105
|
</div>
|
|
2565
3106
|
@if (errorMessage) {
|
|
2566
|
-
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">{{
|
|
3107
|
+
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'" [id]="domId('-error')">{{
|
|
3108
|
+
errorMessage
|
|
3109
|
+
}}</span>
|
|
2567
3110
|
}
|
|
2568
3111
|
}
|
|
2569
3112
|
</div>
|
|
@@ -2590,7 +3133,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
2590
3133
|
clicked. Each select then carries its own name below, because "Billing Cycle" alone
|
|
2591
3134
|
does not say which half of the pair the screen reader has landed on.
|
|
2592
3135
|
-->
|
|
2593
|
-
<
|
|
3136
|
+
<div class="ngx-field__label-row">
|
|
3137
|
+
<label class="ngx-field__label" [attr.for]="domId('-month')">{{ label }}</label>
|
|
3138
|
+
@if (hint) {
|
|
3139
|
+
<span class="ngx-field__hint-wrap">
|
|
3140
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
3141
|
+
<span
|
|
3142
|
+
class="ngx-field__hint"
|
|
3143
|
+
role="tooltip"
|
|
3144
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
3145
|
+
[id]="domId('-hint')"
|
|
3146
|
+
>{{ hint }}</span
|
|
3147
|
+
>
|
|
3148
|
+
</span>
|
|
3149
|
+
}
|
|
3150
|
+
</div>
|
|
2594
3151
|
@if (masked) {
|
|
2595
3152
|
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
2596
3153
|
maskedText
|
|
@@ -2609,6 +3166,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
2609
3166
|
[value]="selectedMonth"
|
|
2610
3167
|
(change)="onMonthChange($any($event.target).value)"
|
|
2611
3168
|
[attr.disabled]="field.disabled ? true : null"
|
|
3169
|
+
[attr.aria-describedby]="describedBy()"
|
|
2612
3170
|
>
|
|
2613
3171
|
<option value="">{{ ui.text('month', language) }}</option>
|
|
2614
3172
|
@for (m of months; track m.value) {
|
|
@@ -2623,6 +3181,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
2623
3181
|
[value]="selectedYear"
|
|
2624
3182
|
(change)="onYearChange($any($event.target).value)"
|
|
2625
3183
|
[attr.disabled]="field.disabled ? true : null"
|
|
3184
|
+
[attr.aria-describedby]="describedBy()"
|
|
2626
3185
|
>
|
|
2627
3186
|
<option value="">{{ ui.text('year', language) }}</option>
|
|
2628
3187
|
@for (y of years; track y) {
|
|
@@ -2631,7 +3190,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
2631
3190
|
</select>
|
|
2632
3191
|
</div>
|
|
2633
3192
|
@if (errorMessage) {
|
|
2634
|
-
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">{{
|
|
3193
|
+
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'" [id]="domId('-error')">{{
|
|
3194
|
+
errorMessage
|
|
3195
|
+
}}</span>
|
|
2635
3196
|
}
|
|
2636
3197
|
}
|
|
2637
3198
|
</div>
|
|
@@ -2961,12 +3522,20 @@ class DropdownFieldComponent {
|
|
|
2961
3522
|
return cached;
|
|
2962
3523
|
return typeof value === 'object' ? resolveLabel(value, this.language) : String(value ?? '—');
|
|
2963
3524
|
}
|
|
3525
|
+
/** Author help text, shown under the control and named by `aria-describedby`. */
|
|
3526
|
+
get hint() {
|
|
3527
|
+
return resolveLabel(this.field?.hint, this.language);
|
|
3528
|
+
}
|
|
3529
|
+
/** The ids of whatever is describing this control right now. */
|
|
3530
|
+
describedBy() {
|
|
3531
|
+
return fieldDescribedBy(s => this.domId(s), { hint: !!this.hint, error: !!this.errorMessage });
|
|
3532
|
+
}
|
|
2964
3533
|
get errorMessage() {
|
|
2965
3534
|
if (!this.control || !this.control.errors || !this.control.touched)
|
|
2966
3535
|
return '';
|
|
2967
3536
|
// `requiredSelection` rather than `required`: "Please select an option" reads better on a
|
|
2968
3537
|
// dropdown than "This field is required", and both stay independently overridable.
|
|
2969
|
-
return this.messages.
|
|
3538
|
+
return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
|
|
2970
3539
|
}
|
|
2971
3540
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: DropdownFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2972
3541
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: DropdownFieldComponent, isStandalone: true, selector: "ngx-dropdown-field", inputs: { field: "field", language: "language", control: "control", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
@@ -2978,12 +3547,26 @@ class DropdownFieldComponent {
|
|
|
2978
3547
|
[class.ngx-field--masked]="masked"
|
|
2979
3548
|
[class.ngx-field--invalid]="control && control.invalid && control.touched"
|
|
2980
3549
|
>
|
|
2981
|
-
<
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
3550
|
+
<div class="ngx-field__label-row">
|
|
3551
|
+
<label class="ngx-field__label" [attr.for]="domId()">
|
|
3552
|
+
{{ label }}
|
|
3553
|
+
@if (field.validators?.required) {
|
|
3554
|
+
<span class="ngx-field__req">*</span>
|
|
3555
|
+
}
|
|
3556
|
+
</label>
|
|
3557
|
+
@if (hint) {
|
|
3558
|
+
<span class="ngx-field__hint-wrap">
|
|
3559
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
3560
|
+
<span
|
|
3561
|
+
class="ngx-field__hint"
|
|
3562
|
+
role="tooltip"
|
|
3563
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
3564
|
+
[id]="domId('-hint')"
|
|
3565
|
+
>{{ hint }}</span
|
|
3566
|
+
>
|
|
3567
|
+
</span>
|
|
2985
3568
|
}
|
|
2986
|
-
</
|
|
3569
|
+
</div>
|
|
2987
3570
|
@if (masked) {
|
|
2988
3571
|
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
2989
3572
|
maskedText
|
|
@@ -3001,7 +3584,7 @@ class DropdownFieldComponent {
|
|
|
3001
3584
|
[compareWith]="compareFn"
|
|
3002
3585
|
[attr.disabled]="field.disabled ? true : null"
|
|
3003
3586
|
[attr.aria-invalid]="control.invalid && control.touched"
|
|
3004
|
-
[attr.aria-describedby]="
|
|
3587
|
+
[attr.aria-describedby]="describedBy()"
|
|
3005
3588
|
>
|
|
3006
3589
|
<option [value]="''">{{ placeholder || ui.text('selectPlaceholder', language) }}</option>
|
|
3007
3590
|
@for (option of options(); track getOptLabel(option)) {
|
|
@@ -3041,12 +3624,26 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
3041
3624
|
[class.ngx-field--masked]="masked"
|
|
3042
3625
|
[class.ngx-field--invalid]="control && control.invalid && control.touched"
|
|
3043
3626
|
>
|
|
3044
|
-
<
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3627
|
+
<div class="ngx-field__label-row">
|
|
3628
|
+
<label class="ngx-field__label" [attr.for]="domId()">
|
|
3629
|
+
{{ label }}
|
|
3630
|
+
@if (field.validators?.required) {
|
|
3631
|
+
<span class="ngx-field__req">*</span>
|
|
3632
|
+
}
|
|
3633
|
+
</label>
|
|
3634
|
+
@if (hint) {
|
|
3635
|
+
<span class="ngx-field__hint-wrap">
|
|
3636
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
3637
|
+
<span
|
|
3638
|
+
class="ngx-field__hint"
|
|
3639
|
+
role="tooltip"
|
|
3640
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
3641
|
+
[id]="domId('-hint')"
|
|
3642
|
+
>{{ hint }}</span
|
|
3643
|
+
>
|
|
3644
|
+
</span>
|
|
3048
3645
|
}
|
|
3049
|
-
</
|
|
3646
|
+
</div>
|
|
3050
3647
|
@if (masked) {
|
|
3051
3648
|
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
3052
3649
|
maskedText
|
|
@@ -3064,7 +3661,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
3064
3661
|
[compareWith]="compareFn"
|
|
3065
3662
|
[attr.disabled]="field.disabled ? true : null"
|
|
3066
3663
|
[attr.aria-invalid]="control.invalid && control.touched"
|
|
3067
|
-
[attr.aria-describedby]="
|
|
3664
|
+
[attr.aria-describedby]="describedBy()"
|
|
3068
3665
|
>
|
|
3069
3666
|
<option [value]="''">{{ placeholder || ui.text('selectPlaceholder', language) }}</option>
|
|
3070
3667
|
@for (option of options(); track getOptLabel(option)) {
|
|
@@ -3189,18 +3786,18 @@ class RadioFieldComponent {
|
|
|
3189
3786
|
* "This field has an error", which made a documented, configurable feature work on three
|
|
3190
3787
|
* of fifteen field types.
|
|
3191
3788
|
*/
|
|
3789
|
+
/** Author help text, shown under the control and named by `aria-describedby`. */
|
|
3790
|
+
get hint() {
|
|
3791
|
+
return resolveLabel(this.field?.hint, this.language);
|
|
3792
|
+
}
|
|
3793
|
+
/** The ids of whatever is describing this control right now. */
|
|
3794
|
+
describedBy() {
|
|
3795
|
+
return fieldDescribedBy(s => this.domId(s), { hint: !!this.hint, error: !!this.errorMessage });
|
|
3796
|
+
}
|
|
3192
3797
|
get errorMessage() {
|
|
3193
3798
|
if (!this.control?.errors || !this.control.touched)
|
|
3194
3799
|
return '';
|
|
3195
|
-
return this.messages.
|
|
3196
|
-
'required',
|
|
3197
|
-
'email',
|
|
3198
|
-
'min',
|
|
3199
|
-
'max',
|
|
3200
|
-
'minlength',
|
|
3201
|
-
'maxlength',
|
|
3202
|
-
'pattern',
|
|
3203
|
-
]);
|
|
3800
|
+
return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
|
|
3204
3801
|
}
|
|
3205
3802
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: RadioFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3206
3803
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: RadioFieldComponent, isStandalone: true, selector: "ngx-radio-field", inputs: { field: "field", language: "language", control: "control", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
@@ -3212,7 +3809,25 @@ class RadioFieldComponent {
|
|
|
3212
3809
|
[class.ngx-field--masked]="masked"
|
|
3213
3810
|
>
|
|
3214
3811
|
<fieldset class="ngx-field__fieldset">
|
|
3215
|
-
<legend class="ngx-field__label">
|
|
3812
|
+
<legend class="ngx-field__label">
|
|
3813
|
+
{{ label }}
|
|
3814
|
+
@if (hint) {
|
|
3815
|
+
<!--
|
|
3816
|
+
Inside the legend rather than beside it: a fieldset allows exactly one legend
|
|
3817
|
+
and it must be the first child, so a sibling row would be invalid here.
|
|
3818
|
+
-->
|
|
3819
|
+
<span class="ngx-field__hint-wrap">
|
|
3820
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
3821
|
+
<span
|
|
3822
|
+
class="ngx-field__hint"
|
|
3823
|
+
role="tooltip"
|
|
3824
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
3825
|
+
[id]="domId('-hint')"
|
|
3826
|
+
>{{ hint }}</span
|
|
3827
|
+
>
|
|
3828
|
+
</span>
|
|
3829
|
+
}
|
|
3830
|
+
</legend>
|
|
3216
3831
|
@if (masked) {
|
|
3217
3832
|
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
3218
3833
|
maskedText
|
|
@@ -3229,6 +3844,7 @@ class RadioFieldComponent {
|
|
|
3229
3844
|
type="radio"
|
|
3230
3845
|
class="ngx-field__radio-input"
|
|
3231
3846
|
[formControl]="$any(control)"
|
|
3847
|
+
[attr.aria-describedby]="describedBy()"
|
|
3232
3848
|
[value]="getOptStoredVal(option)"
|
|
3233
3849
|
[attr.disabled]="field.disabled ? true : null"
|
|
3234
3850
|
/>
|
|
@@ -3237,7 +3853,9 @@ class RadioFieldComponent {
|
|
|
3237
3853
|
}
|
|
3238
3854
|
</div>
|
|
3239
3855
|
@if (errorMessage) {
|
|
3240
|
-
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">{{
|
|
3856
|
+
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'" [id]="domId('-error')">{{
|
|
3857
|
+
errorMessage
|
|
3858
|
+
}}</span>
|
|
3241
3859
|
}
|
|
3242
3860
|
}
|
|
3243
3861
|
</fieldset>
|
|
@@ -3260,7 +3878,25 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
3260
3878
|
[class.ngx-field--masked]="masked"
|
|
3261
3879
|
>
|
|
3262
3880
|
<fieldset class="ngx-field__fieldset">
|
|
3263
|
-
<legend class="ngx-field__label">
|
|
3881
|
+
<legend class="ngx-field__label">
|
|
3882
|
+
{{ label }}
|
|
3883
|
+
@if (hint) {
|
|
3884
|
+
<!--
|
|
3885
|
+
Inside the legend rather than beside it: a fieldset allows exactly one legend
|
|
3886
|
+
and it must be the first child, so a sibling row would be invalid here.
|
|
3887
|
+
-->
|
|
3888
|
+
<span class="ngx-field__hint-wrap">
|
|
3889
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
3890
|
+
<span
|
|
3891
|
+
class="ngx-field__hint"
|
|
3892
|
+
role="tooltip"
|
|
3893
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
3894
|
+
[id]="domId('-hint')"
|
|
3895
|
+
>{{ hint }}</span
|
|
3896
|
+
>
|
|
3897
|
+
</span>
|
|
3898
|
+
}
|
|
3899
|
+
</legend>
|
|
3264
3900
|
@if (masked) {
|
|
3265
3901
|
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
3266
3902
|
maskedText
|
|
@@ -3277,6 +3913,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
3277
3913
|
type="radio"
|
|
3278
3914
|
class="ngx-field__radio-input"
|
|
3279
3915
|
[formControl]="$any(control)"
|
|
3916
|
+
[attr.aria-describedby]="describedBy()"
|
|
3280
3917
|
[value]="getOptStoredVal(option)"
|
|
3281
3918
|
[attr.disabled]="field.disabled ? true : null"
|
|
3282
3919
|
/>
|
|
@@ -3285,7 +3922,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
3285
3922
|
}
|
|
3286
3923
|
</div>
|
|
3287
3924
|
@if (errorMessage) {
|
|
3288
|
-
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">{{
|
|
3925
|
+
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'" [id]="domId('-error')">{{
|
|
3926
|
+
errorMessage
|
|
3927
|
+
}}</span>
|
|
3289
3928
|
}
|
|
3290
3929
|
}
|
|
3291
3930
|
</fieldset>
|
|
@@ -3375,18 +4014,18 @@ class MultiSelectFieldComponent {
|
|
|
3375
4014
|
* "This field has an error", which made a documented, configurable feature work on three
|
|
3376
4015
|
* of fifteen field types.
|
|
3377
4016
|
*/
|
|
4017
|
+
/** Author help text, shown under the control and named by `aria-describedby`. */
|
|
4018
|
+
get hint() {
|
|
4019
|
+
return resolveLabel(this.field?.hint, this.language);
|
|
4020
|
+
}
|
|
4021
|
+
/** The ids of whatever is describing this control right now. */
|
|
4022
|
+
describedBy() {
|
|
4023
|
+
return fieldDescribedBy(s => this.domId(s), { hint: !!this.hint, error: !!this.errorMessage });
|
|
4024
|
+
}
|
|
3378
4025
|
get errorMessage() {
|
|
3379
4026
|
if (!this.control?.errors || !this.control.touched)
|
|
3380
4027
|
return '';
|
|
3381
|
-
return this.messages.
|
|
3382
|
-
'required',
|
|
3383
|
-
'email',
|
|
3384
|
-
'min',
|
|
3385
|
-
'max',
|
|
3386
|
-
'minlength',
|
|
3387
|
-
'maxlength',
|
|
3388
|
-
'pattern',
|
|
3389
|
-
]);
|
|
4028
|
+
return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
|
|
3390
4029
|
}
|
|
3391
4030
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MultiSelectFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3392
4031
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: MultiSelectFieldComponent, isStandalone: true, selector: "ngx-multi-select-field", inputs: { field: "field", language: "language", control: "control", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
@@ -3397,7 +4036,21 @@ class MultiSelectFieldComponent {
|
|
|
3397
4036
|
[class.ngx-field--readonly]="readonly"
|
|
3398
4037
|
[class.ngx-field--masked]="masked"
|
|
3399
4038
|
>
|
|
3400
|
-
<
|
|
4039
|
+
<div class="ngx-field__label-row">
|
|
4040
|
+
<label class="ngx-field__label" [attr.for]="domId()">{{ label }}</label>
|
|
4041
|
+
@if (hint) {
|
|
4042
|
+
<span class="ngx-field__hint-wrap">
|
|
4043
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
4044
|
+
<span
|
|
4045
|
+
class="ngx-field__hint"
|
|
4046
|
+
role="tooltip"
|
|
4047
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
4048
|
+
[id]="domId('-hint')"
|
|
4049
|
+
>{{ hint }}</span
|
|
4050
|
+
>
|
|
4051
|
+
</span>
|
|
4052
|
+
}
|
|
4053
|
+
</div>
|
|
3401
4054
|
@if (masked) {
|
|
3402
4055
|
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
3403
4056
|
maskedText
|
|
@@ -3412,6 +4065,7 @@ class MultiSelectFieldComponent {
|
|
|
3412
4065
|
class="ngx-field__input"
|
|
3413
4066
|
[attr.data-testid]="'field-' + field.id + '-input'"
|
|
3414
4067
|
[formControl]="$any(control)"
|
|
4068
|
+
[attr.aria-describedby]="describedBy()"
|
|
3415
4069
|
[compareWith]="compareFn"
|
|
3416
4070
|
[attr.disabled]="field.disabled ? true : null"
|
|
3417
4071
|
multiple
|
|
@@ -3426,7 +4080,9 @@ class MultiSelectFieldComponent {
|
|
|
3426
4080
|
}
|
|
3427
4081
|
</select>
|
|
3428
4082
|
@if (errorMessage) {
|
|
3429
|
-
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">{{
|
|
4083
|
+
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'" [id]="domId('-error')">{{
|
|
4084
|
+
errorMessage
|
|
4085
|
+
}}</span>
|
|
3430
4086
|
}
|
|
3431
4087
|
}
|
|
3432
4088
|
</div>
|
|
@@ -3447,7 +4103,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
3447
4103
|
[class.ngx-field--readonly]="readonly"
|
|
3448
4104
|
[class.ngx-field--masked]="masked"
|
|
3449
4105
|
>
|
|
3450
|
-
<
|
|
4106
|
+
<div class="ngx-field__label-row">
|
|
4107
|
+
<label class="ngx-field__label" [attr.for]="domId()">{{ label }}</label>
|
|
4108
|
+
@if (hint) {
|
|
4109
|
+
<span class="ngx-field__hint-wrap">
|
|
4110
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
4111
|
+
<span
|
|
4112
|
+
class="ngx-field__hint"
|
|
4113
|
+
role="tooltip"
|
|
4114
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
4115
|
+
[id]="domId('-hint')"
|
|
4116
|
+
>{{ hint }}</span
|
|
4117
|
+
>
|
|
4118
|
+
</span>
|
|
4119
|
+
}
|
|
4120
|
+
</div>
|
|
3451
4121
|
@if (masked) {
|
|
3452
4122
|
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
3453
4123
|
maskedText
|
|
@@ -3462,6 +4132,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
3462
4132
|
class="ngx-field__input"
|
|
3463
4133
|
[attr.data-testid]="'field-' + field.id + '-input'"
|
|
3464
4134
|
[formControl]="$any(control)"
|
|
4135
|
+
[attr.aria-describedby]="describedBy()"
|
|
3465
4136
|
[compareWith]="compareFn"
|
|
3466
4137
|
[attr.disabled]="field.disabled ? true : null"
|
|
3467
4138
|
multiple
|
|
@@ -3476,7 +4147,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
3476
4147
|
}
|
|
3477
4148
|
</select>
|
|
3478
4149
|
@if (errorMessage) {
|
|
3479
|
-
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">{{
|
|
4150
|
+
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'" [id]="domId('-error')">{{
|
|
4151
|
+
errorMessage
|
|
4152
|
+
}}</span>
|
|
3480
4153
|
}
|
|
3481
4154
|
}
|
|
3482
4155
|
</div>
|
|
@@ -3949,18 +4622,18 @@ class EntityRefFieldComponent {
|
|
|
3949
4622
|
* "This field has an error", which made a documented, configurable feature work on three
|
|
3950
4623
|
* of fifteen field types.
|
|
3951
4624
|
*/
|
|
4625
|
+
/** Author help text, shown under the control and named by `aria-describedby`. */
|
|
4626
|
+
get hint() {
|
|
4627
|
+
return resolveLabel(this.field?.hint, this.language);
|
|
4628
|
+
}
|
|
4629
|
+
/** The ids of whatever is describing this control right now. */
|
|
4630
|
+
describedBy() {
|
|
4631
|
+
return fieldDescribedBy(s => this.domId(s), { hint: !!this.hint, error: !!this.errorMessage });
|
|
4632
|
+
}
|
|
3952
4633
|
get errorMessage() {
|
|
3953
4634
|
if (!this.control?.errors || !this.control.touched)
|
|
3954
4635
|
return '';
|
|
3955
|
-
return this.messages.
|
|
3956
|
-
'required',
|
|
3957
|
-
'email',
|
|
3958
|
-
'min',
|
|
3959
|
-
'max',
|
|
3960
|
-
'minlength',
|
|
3961
|
-
'maxlength',
|
|
3962
|
-
'pattern',
|
|
3963
|
-
]);
|
|
4636
|
+
return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
|
|
3964
4637
|
}
|
|
3965
4638
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: EntityRefFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3966
4639
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: EntityRefFieldComponent, isStandalone: true, selector: "ngx-entity-ref-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
@@ -3971,7 +4644,21 @@ class EntityRefFieldComponent {
|
|
|
3971
4644
|
[class.ngx-field--readonly]="readonly"
|
|
3972
4645
|
[class.ngx-field--masked]="masked"
|
|
3973
4646
|
>
|
|
3974
|
-
<
|
|
4647
|
+
<div class="ngx-field__label-row">
|
|
4648
|
+
<label class="ngx-field__label" [attr.for]="domId()">{{ label }}</label>
|
|
4649
|
+
@if (hint) {
|
|
4650
|
+
<span class="ngx-field__hint-wrap">
|
|
4651
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
4652
|
+
<span
|
|
4653
|
+
class="ngx-field__hint"
|
|
4654
|
+
role="tooltip"
|
|
4655
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
4656
|
+
[id]="domId('-hint')"
|
|
4657
|
+
>{{ hint }}</span
|
|
4658
|
+
>
|
|
4659
|
+
</span>
|
|
4660
|
+
}
|
|
4661
|
+
</div>
|
|
3975
4662
|
@if (masked) {
|
|
3976
4663
|
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
3977
4664
|
maskedText
|
|
@@ -3991,6 +4678,7 @@ class EntityRefFieldComponent {
|
|
|
3991
4678
|
[attr.data-testid]="'field-' + field.id + '-input'"
|
|
3992
4679
|
[id]="domId()"
|
|
3993
4680
|
[formControl]="$any(control)"
|
|
4681
|
+
[attr.aria-describedby]="describedBy()"
|
|
3994
4682
|
[attr.disabled]="field.disabled ? true : null"
|
|
3995
4683
|
>
|
|
3996
4684
|
<option value="">{{ placeholder || ui.text('selectPlaceholder', language) }}</option>
|
|
@@ -4005,7 +4693,9 @@ class EntityRefFieldComponent {
|
|
|
4005
4693
|
}
|
|
4006
4694
|
}
|
|
4007
4695
|
@if (errorMessage) {
|
|
4008
|
-
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">{{
|
|
4696
|
+
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'" [id]="domId('-error')">{{
|
|
4697
|
+
errorMessage
|
|
4698
|
+
}}</span>
|
|
4009
4699
|
}
|
|
4010
4700
|
}
|
|
4011
4701
|
</div>
|
|
@@ -4026,7 +4716,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
4026
4716
|
[class.ngx-field--readonly]="readonly"
|
|
4027
4717
|
[class.ngx-field--masked]="masked"
|
|
4028
4718
|
>
|
|
4029
|
-
<
|
|
4719
|
+
<div class="ngx-field__label-row">
|
|
4720
|
+
<label class="ngx-field__label" [attr.for]="domId()">{{ label }}</label>
|
|
4721
|
+
@if (hint) {
|
|
4722
|
+
<span class="ngx-field__hint-wrap">
|
|
4723
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
4724
|
+
<span
|
|
4725
|
+
class="ngx-field__hint"
|
|
4726
|
+
role="tooltip"
|
|
4727
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
4728
|
+
[id]="domId('-hint')"
|
|
4729
|
+
>{{ hint }}</span
|
|
4730
|
+
>
|
|
4731
|
+
</span>
|
|
4732
|
+
}
|
|
4733
|
+
</div>
|
|
4030
4734
|
@if (masked) {
|
|
4031
4735
|
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
4032
4736
|
maskedText
|
|
@@ -4046,6 +4750,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
4046
4750
|
[attr.data-testid]="'field-' + field.id + '-input'"
|
|
4047
4751
|
[id]="domId()"
|
|
4048
4752
|
[formControl]="$any(control)"
|
|
4753
|
+
[attr.aria-describedby]="describedBy()"
|
|
4049
4754
|
[attr.disabled]="field.disabled ? true : null"
|
|
4050
4755
|
>
|
|
4051
4756
|
<option value="">{{ placeholder || ui.text('selectPlaceholder', language) }}</option>
|
|
@@ -4060,7 +4765,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
4060
4765
|
}
|
|
4061
4766
|
}
|
|
4062
4767
|
@if (errorMessage) {
|
|
4063
|
-
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">{{
|
|
4768
|
+
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'" [id]="domId('-error')">{{
|
|
4769
|
+
errorMessage
|
|
4770
|
+
}}</span>
|
|
4064
4771
|
}
|
|
4065
4772
|
}
|
|
4066
4773
|
</div>
|
|
@@ -4330,6 +5037,10 @@ class GroupFieldComponent {
|
|
|
4330
5037
|
readonly = false;
|
|
4331
5038
|
masked = false;
|
|
4332
5039
|
userRoles = [];
|
|
5040
|
+
/** Author help text for the section as a whole. */
|
|
5041
|
+
get hint() {
|
|
5042
|
+
return resolveLabel(this.field?.hint, this.language);
|
|
5043
|
+
}
|
|
4333
5044
|
get label() {
|
|
4334
5045
|
return resolveLabel(this.field?.label, this.language);
|
|
4335
5046
|
}
|
|
@@ -4339,11 +5050,27 @@ class GroupFieldComponent {
|
|
|
4339
5050
|
}
|
|
4340
5051
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: GroupFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
4341
5052
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", 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: `
|
|
4342
|
-
<fieldset
|
|
4343
|
-
|
|
4344
|
-
|
|
5053
|
+
<fieldset
|
|
5054
|
+
class="ngx-field ngx-field--group"
|
|
5055
|
+
[attr.data-testid]="'field-' + field.id"
|
|
5056
|
+
[attr.data-field-type]="field.type"
|
|
5057
|
+
[class.ngx-field--readonly]="readonly"
|
|
5058
|
+
[class.ngx-field--masked]="masked"
|
|
5059
|
+
>
|
|
5060
|
+
<legend class="ngx-field__legend">
|
|
5061
|
+
{{ label }}
|
|
5062
|
+
@if (hint) {
|
|
5063
|
+
<!-- A group's hint describes the whole section, so it hangs off the section's name. -->
|
|
5064
|
+
<span class="ngx-field__hint-wrap">
|
|
5065
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
5066
|
+
<span class="ngx-field__hint" role="tooltip" [attr.data-testid]="'field-' + field.id + '-hint'">{{ hint }}</span>
|
|
5067
|
+
</span>
|
|
5068
|
+
}
|
|
5069
|
+
</legend>
|
|
4345
5070
|
@if (masked) {
|
|
4346
|
-
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
5071
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
5072
|
+
maskedText
|
|
5073
|
+
}}</span>
|
|
4347
5074
|
} @else {
|
|
4348
5075
|
<div class="ngx-field__group-children">
|
|
4349
5076
|
@for (child of field.children ?? []; track child.id) {
|
|
@@ -4366,11 +5093,27 @@ class GroupFieldComponent {
|
|
|
4366
5093
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: GroupFieldComponent, decorators: [{
|
|
4367
5094
|
type: Component,
|
|
4368
5095
|
args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'ngx-group-field', standalone: true, imports: [ReactiveFormsModule, forwardRef(() => DynamicFieldComponent)], template: `
|
|
4369
|
-
<fieldset
|
|
4370
|
-
|
|
4371
|
-
|
|
5096
|
+
<fieldset
|
|
5097
|
+
class="ngx-field ngx-field--group"
|
|
5098
|
+
[attr.data-testid]="'field-' + field.id"
|
|
5099
|
+
[attr.data-field-type]="field.type"
|
|
5100
|
+
[class.ngx-field--readonly]="readonly"
|
|
5101
|
+
[class.ngx-field--masked]="masked"
|
|
5102
|
+
>
|
|
5103
|
+
<legend class="ngx-field__legend">
|
|
5104
|
+
{{ label }}
|
|
5105
|
+
@if (hint) {
|
|
5106
|
+
<!-- A group's hint describes the whole section, so it hangs off the section's name. -->
|
|
5107
|
+
<span class="ngx-field__hint-wrap">
|
|
5108
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
5109
|
+
<span class="ngx-field__hint" role="tooltip" [attr.data-testid]="'field-' + field.id + '-hint'">{{ hint }}</span>
|
|
5110
|
+
</span>
|
|
5111
|
+
}
|
|
5112
|
+
</legend>
|
|
4372
5113
|
@if (masked) {
|
|
4373
|
-
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
5114
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
5115
|
+
maskedText
|
|
5116
|
+
}}</span>
|
|
4374
5117
|
} @else {
|
|
4375
5118
|
<div class="ngx-field__group-children">
|
|
4376
5119
|
@for (child of field.children ?? []; track child.id) {
|
|
@@ -4521,6 +5264,10 @@ class ArrayFieldComponent {
|
|
|
4521
5264
|
userRoles = [];
|
|
4522
5265
|
fb = inject(FormBuilder);
|
|
4523
5266
|
validatorRegistry = inject(ValidatorRegistryService);
|
|
5267
|
+
/** Author help text for the section as a whole. */
|
|
5268
|
+
get hint() {
|
|
5269
|
+
return resolveLabel(this.field?.hint, this.language);
|
|
5270
|
+
}
|
|
4524
5271
|
get label() {
|
|
4525
5272
|
return resolveLabel(this.field?.label, this.language);
|
|
4526
5273
|
}
|
|
@@ -4554,7 +5301,18 @@ class ArrayFieldComponent {
|
|
|
4554
5301
|
[class.ngx-field--masked]="masked"
|
|
4555
5302
|
>
|
|
4556
5303
|
<div class="ngx-field__array-header">
|
|
4557
|
-
<label class="ngx-field__label">
|
|
5304
|
+
<label class="ngx-field__label">
|
|
5305
|
+
{{ label }}
|
|
5306
|
+
@if (hint) {
|
|
5307
|
+
<!-- Explains the set of rows, so it hangs off their name rather than any one row. -->
|
|
5308
|
+
<span class="ngx-field__hint-wrap">
|
|
5309
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
5310
|
+
<span class="ngx-field__hint" role="tooltip" [attr.data-testid]="'field-' + field.id + '-hint'">{{
|
|
5311
|
+
hint
|
|
5312
|
+
}}</span>
|
|
5313
|
+
</span>
|
|
5314
|
+
}
|
|
5315
|
+
</label>
|
|
4558
5316
|
@if (!readonly && !masked) {
|
|
4559
5317
|
<button
|
|
4560
5318
|
type="button"
|
|
@@ -4566,7 +5324,6 @@ class ArrayFieldComponent {
|
|
|
4566
5324
|
</button>
|
|
4567
5325
|
}
|
|
4568
5326
|
</div>
|
|
4569
|
-
|
|
4570
5327
|
@if (masked) {
|
|
4571
5328
|
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
4572
5329
|
maskedText
|
|
@@ -4625,7 +5382,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
4625
5382
|
[class.ngx-field--masked]="masked"
|
|
4626
5383
|
>
|
|
4627
5384
|
<div class="ngx-field__array-header">
|
|
4628
|
-
<label class="ngx-field__label">
|
|
5385
|
+
<label class="ngx-field__label">
|
|
5386
|
+
{{ label }}
|
|
5387
|
+
@if (hint) {
|
|
5388
|
+
<!-- Explains the set of rows, so it hangs off their name rather than any one row. -->
|
|
5389
|
+
<span class="ngx-field__hint-wrap">
|
|
5390
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
5391
|
+
<span class="ngx-field__hint" role="tooltip" [attr.data-testid]="'field-' + field.id + '-hint'">{{
|
|
5392
|
+
hint
|
|
5393
|
+
}}</span>
|
|
5394
|
+
</span>
|
|
5395
|
+
}
|
|
5396
|
+
</label>
|
|
4629
5397
|
@if (!readonly && !masked) {
|
|
4630
5398
|
<button
|
|
4631
5399
|
type="button"
|
|
@@ -4637,7 +5405,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
4637
5405
|
</button>
|
|
4638
5406
|
}
|
|
4639
5407
|
</div>
|
|
4640
|
-
|
|
4641
5408
|
@if (masked) {
|
|
4642
5409
|
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
4643
5410
|
maskedText
|
|
@@ -4853,10 +5620,18 @@ class ImageFieldComponent {
|
|
|
4853
5620
|
this.uploads.revokePreviewUrl(this.previewCache);
|
|
4854
5621
|
}
|
|
4855
5622
|
/** Validation, as distinct from the upload failures this field already reported. */
|
|
5623
|
+
/** Author help text, shown under the control and named by `aria-describedby`. */
|
|
5624
|
+
get hint() {
|
|
5625
|
+
return resolveLabel(this.field?.hint, this.language);
|
|
5626
|
+
}
|
|
5627
|
+
/** The ids of whatever is describing this control right now. */
|
|
5628
|
+
describedBy() {
|
|
5629
|
+
return fieldDescribedBy(s => this.domId(s), { hint: !!this.hint, error: !!this.errorMessage });
|
|
5630
|
+
}
|
|
4856
5631
|
get errorMessage() {
|
|
4857
5632
|
if (!this.control?.errors || !this.control.touched)
|
|
4858
5633
|
return '';
|
|
4859
|
-
return this.messages.
|
|
5634
|
+
return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
|
|
4860
5635
|
}
|
|
4861
5636
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: ImageFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
4862
5637
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: ImageFieldComponent, isStandalone: true, selector: "ngx-image-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
@@ -4867,7 +5642,21 @@ class ImageFieldComponent {
|
|
|
4867
5642
|
[class.ngx-field--readonly]="readonly"
|
|
4868
5643
|
[class.ngx-field--masked]="masked"
|
|
4869
5644
|
>
|
|
4870
|
-
<
|
|
5645
|
+
<div class="ngx-field__label-row">
|
|
5646
|
+
<label class="ngx-field__label" [attr.for]="domId()">{{ label }}</label>
|
|
5647
|
+
@if (hint) {
|
|
5648
|
+
<span class="ngx-field__hint-wrap">
|
|
5649
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
5650
|
+
<span
|
|
5651
|
+
class="ngx-field__hint"
|
|
5652
|
+
role="tooltip"
|
|
5653
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
5654
|
+
[id]="domId('-hint')"
|
|
5655
|
+
>{{ hint }}</span
|
|
5656
|
+
>
|
|
5657
|
+
</span>
|
|
5658
|
+
}
|
|
5659
|
+
</div>
|
|
4871
5660
|
@if (masked) {
|
|
4872
5661
|
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
4873
5662
|
maskedText
|
|
@@ -4896,6 +5685,7 @@ class ImageFieldComponent {
|
|
|
4896
5685
|
accept="image/*"
|
|
4897
5686
|
[disabled]="field.disabled || uploading()"
|
|
4898
5687
|
(change)="onFileSelect($any($event.target).files[0])"
|
|
5688
|
+
[attr.aria-describedby]="describedBy()"
|
|
4899
5689
|
/>
|
|
4900
5690
|
}
|
|
4901
5691
|
</label>
|
|
@@ -4911,9 +5701,13 @@ class ImageFieldComponent {
|
|
|
4911
5701
|
ever showed the first. A required file left unchosen said nothing, and a
|
|
4912
5702
|
configured required message had no element to appear in. -->
|
|
4913
5703
|
@if (uploadError() || errorMessage) {
|
|
4914
|
-
<span
|
|
4915
|
-
|
|
4916
|
-
|
|
5704
|
+
<span
|
|
5705
|
+
class="ngx-field__error"
|
|
5706
|
+
[attr.data-testid]="'field-' + field.id + '-error'"
|
|
5707
|
+
[id]="domId('-error')"
|
|
5708
|
+
role="alert"
|
|
5709
|
+
>{{ uploadError() || errorMessage }}</span
|
|
5710
|
+
>
|
|
4917
5711
|
}
|
|
4918
5712
|
}
|
|
4919
5713
|
</div>
|
|
@@ -4934,7 +5728,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
4934
5728
|
[class.ngx-field--readonly]="readonly"
|
|
4935
5729
|
[class.ngx-field--masked]="masked"
|
|
4936
5730
|
>
|
|
4937
|
-
<
|
|
5731
|
+
<div class="ngx-field__label-row">
|
|
5732
|
+
<label class="ngx-field__label" [attr.for]="domId()">{{ label }}</label>
|
|
5733
|
+
@if (hint) {
|
|
5734
|
+
<span class="ngx-field__hint-wrap">
|
|
5735
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
5736
|
+
<span
|
|
5737
|
+
class="ngx-field__hint"
|
|
5738
|
+
role="tooltip"
|
|
5739
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
5740
|
+
[id]="domId('-hint')"
|
|
5741
|
+
>{{ hint }}</span
|
|
5742
|
+
>
|
|
5743
|
+
</span>
|
|
5744
|
+
}
|
|
5745
|
+
</div>
|
|
4938
5746
|
@if (masked) {
|
|
4939
5747
|
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
4940
5748
|
maskedText
|
|
@@ -4963,6 +5771,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
4963
5771
|
accept="image/*"
|
|
4964
5772
|
[disabled]="field.disabled || uploading()"
|
|
4965
5773
|
(change)="onFileSelect($any($event.target).files[0])"
|
|
5774
|
+
[attr.aria-describedby]="describedBy()"
|
|
4966
5775
|
/>
|
|
4967
5776
|
}
|
|
4968
5777
|
</label>
|
|
@@ -4978,9 +5787,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
4978
5787
|
ever showed the first. A required file left unchosen said nothing, and a
|
|
4979
5788
|
configured required message had no element to appear in. -->
|
|
4980
5789
|
@if (uploadError() || errorMessage) {
|
|
4981
|
-
<span
|
|
4982
|
-
|
|
4983
|
-
|
|
5790
|
+
<span
|
|
5791
|
+
class="ngx-field__error"
|
|
5792
|
+
[attr.data-testid]="'field-' + field.id + '-error'"
|
|
5793
|
+
[id]="domId('-error')"
|
|
5794
|
+
role="alert"
|
|
5795
|
+
>{{ uploadError() || errorMessage }}</span
|
|
5796
|
+
>
|
|
4984
5797
|
}
|
|
4985
5798
|
}
|
|
4986
5799
|
</div>
|
|
@@ -5057,10 +5870,18 @@ class FileFieldComponent {
|
|
|
5057
5870
|
this.control.markAsTouched();
|
|
5058
5871
|
}
|
|
5059
5872
|
/** Validation, as distinct from the upload failures this field already reported. */
|
|
5873
|
+
/** Author help text, shown under the control and named by `aria-describedby`. */
|
|
5874
|
+
get hint() {
|
|
5875
|
+
return resolveLabel(this.field?.hint, this.language);
|
|
5876
|
+
}
|
|
5877
|
+
/** The ids of whatever is describing this control right now. */
|
|
5878
|
+
describedBy() {
|
|
5879
|
+
return fieldDescribedBy(s => this.domId(s), { hint: !!this.hint, error: !!this.errorMessage });
|
|
5880
|
+
}
|
|
5060
5881
|
get errorMessage() {
|
|
5061
5882
|
if (!this.control?.errors || !this.control.touched)
|
|
5062
5883
|
return '';
|
|
5063
|
-
return this.messages.
|
|
5884
|
+
return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
|
|
5064
5885
|
}
|
|
5065
5886
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: FileFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
5066
5887
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: FileFieldComponent, isStandalone: true, selector: "ngx-file-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
@@ -5071,7 +5892,21 @@ class FileFieldComponent {
|
|
|
5071
5892
|
[class.ngx-field--readonly]="readonly"
|
|
5072
5893
|
[class.ngx-field--masked]="masked"
|
|
5073
5894
|
>
|
|
5074
|
-
<
|
|
5895
|
+
<div class="ngx-field__label-row">
|
|
5896
|
+
<label class="ngx-field__label" [attr.for]="domId()">{{ label }}</label>
|
|
5897
|
+
@if (hint) {
|
|
5898
|
+
<span class="ngx-field__hint-wrap">
|
|
5899
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
5900
|
+
<span
|
|
5901
|
+
class="ngx-field__hint"
|
|
5902
|
+
role="tooltip"
|
|
5903
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
5904
|
+
[id]="domId('-hint')"
|
|
5905
|
+
>{{ hint }}</span
|
|
5906
|
+
>
|
|
5907
|
+
</span>
|
|
5908
|
+
}
|
|
5909
|
+
</div>
|
|
5075
5910
|
@if (masked) {
|
|
5076
5911
|
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
5077
5912
|
maskedText
|
|
@@ -5110,6 +5945,7 @@ class FileFieldComponent {
|
|
|
5110
5945
|
[id]="domId()"
|
|
5111
5946
|
[disabled]="field.disabled || uploading()"
|
|
5112
5947
|
(change)="onFileSelect($any($event.target).files[0])"
|
|
5948
|
+
[attr.aria-describedby]="describedBy()"
|
|
5113
5949
|
/>
|
|
5114
5950
|
}
|
|
5115
5951
|
</label>
|
|
@@ -5118,9 +5954,13 @@ class FileFieldComponent {
|
|
|
5118
5954
|
ever showed the first. A required file left unchosen said nothing, and a
|
|
5119
5955
|
configured required message had no element to appear in. -->
|
|
5120
5956
|
@if (uploadError() || errorMessage) {
|
|
5121
|
-
<span
|
|
5122
|
-
|
|
5123
|
-
|
|
5957
|
+
<span
|
|
5958
|
+
class="ngx-field__error"
|
|
5959
|
+
[attr.data-testid]="'field-' + field.id + '-error'"
|
|
5960
|
+
[id]="domId('-error')"
|
|
5961
|
+
role="alert"
|
|
5962
|
+
>{{ uploadError() || errorMessage }}</span
|
|
5963
|
+
>
|
|
5124
5964
|
}
|
|
5125
5965
|
}
|
|
5126
5966
|
</div>
|
|
@@ -5141,7 +5981,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
5141
5981
|
[class.ngx-field--readonly]="readonly"
|
|
5142
5982
|
[class.ngx-field--masked]="masked"
|
|
5143
5983
|
>
|
|
5144
|
-
<
|
|
5984
|
+
<div class="ngx-field__label-row">
|
|
5985
|
+
<label class="ngx-field__label" [attr.for]="domId()">{{ label }}</label>
|
|
5986
|
+
@if (hint) {
|
|
5987
|
+
<span class="ngx-field__hint-wrap">
|
|
5988
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
5989
|
+
<span
|
|
5990
|
+
class="ngx-field__hint"
|
|
5991
|
+
role="tooltip"
|
|
5992
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
5993
|
+
[id]="domId('-hint')"
|
|
5994
|
+
>{{ hint }}</span
|
|
5995
|
+
>
|
|
5996
|
+
</span>
|
|
5997
|
+
}
|
|
5998
|
+
</div>
|
|
5145
5999
|
@if (masked) {
|
|
5146
6000
|
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
5147
6001
|
maskedText
|
|
@@ -5180,6 +6034,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
5180
6034
|
[id]="domId()"
|
|
5181
6035
|
[disabled]="field.disabled || uploading()"
|
|
5182
6036
|
(change)="onFileSelect($any($event.target).files[0])"
|
|
6037
|
+
[attr.aria-describedby]="describedBy()"
|
|
5183
6038
|
/>
|
|
5184
6039
|
}
|
|
5185
6040
|
</label>
|
|
@@ -5188,9 +6043,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
5188
6043
|
ever showed the first. A required file left unchosen said nothing, and a
|
|
5189
6044
|
configured required message had no element to appear in. -->
|
|
5190
6045
|
@if (uploadError() || errorMessage) {
|
|
5191
|
-
<span
|
|
5192
|
-
|
|
5193
|
-
|
|
6046
|
+
<span
|
|
6047
|
+
class="ngx-field__error"
|
|
6048
|
+
[attr.data-testid]="'field-' + field.id + '-error'"
|
|
6049
|
+
[id]="domId('-error')"
|
|
6050
|
+
role="alert"
|
|
6051
|
+
>{{ uploadError() || errorMessage }}</span
|
|
6052
|
+
>
|
|
5194
6053
|
}
|
|
5195
6054
|
}
|
|
5196
6055
|
</div>
|
|
@@ -5372,6 +6231,37 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
5372
6231
|
args: [CONFIG_SOURCE]
|
|
5373
6232
|
}] }] });
|
|
5374
6233
|
|
|
6234
|
+
/**
|
|
6235
|
+
* Grid width per field type, used only under `layout="auto"` and only where the field itself
|
|
6236
|
+
* names no `colSpan`.
|
|
6237
|
+
*
|
|
6238
|
+
* The rule behind the numbers is how much *content* the control holds, not how much room it
|
|
6239
|
+
* would happily fill: a date is eight characters and a currency amount is rarely more, so
|
|
6240
|
+
* giving either a full row buys nothing but scrolling. Anything that holds a paragraph, a
|
|
6241
|
+
* picture, or a form of its own keeps all twelve.
|
|
6242
|
+
*
|
|
6243
|
+
* A type missing from this table falls back to twelve, which is what an unrecognised custom
|
|
6244
|
+
* field type should get: the layout is conservative about controls it knows nothing about.
|
|
6245
|
+
*/
|
|
6246
|
+
const AUTO_COL_SPAN = {
|
|
6247
|
+
text: 6,
|
|
6248
|
+
email: 6,
|
|
6249
|
+
password: 6,
|
|
6250
|
+
number: 4,
|
|
6251
|
+
currency: 4,
|
|
6252
|
+
date: 4,
|
|
6253
|
+
datetime: 4,
|
|
6254
|
+
time: 4,
|
|
6255
|
+
monthYear: 4,
|
|
6256
|
+
dropdown: 6,
|
|
6257
|
+
radio: 6,
|
|
6258
|
+
multiSelect: 6,
|
|
6259
|
+
boolean: 4,
|
|
6260
|
+
checkbox: 4,
|
|
6261
|
+
'entity-ref': 6,
|
|
6262
|
+
// Left at the full width deliberately — textarea, markdown, image, file, group and array
|
|
6263
|
+
// each hold something a half-row would crop.
|
|
6264
|
+
};
|
|
5375
6265
|
/**
|
|
5376
6266
|
* DynamicFormComponent — the main form component.
|
|
5377
6267
|
* Renders a reactive form from EntityFormConfig with tab support, responsive 12-col grid,
|
|
@@ -5429,6 +6319,19 @@ class DynamicFormComponent {
|
|
|
5429
6319
|
* disable the form. Used by the builder's live preview.
|
|
5430
6320
|
*/
|
|
5431
6321
|
preview = false;
|
|
6322
|
+
/**
|
|
6323
|
+
* How a field with no `colSpan` of its own is sized in the 12-column grid.
|
|
6324
|
+
*
|
|
6325
|
+
* `stack` — the historic behaviour — gives every such field the full twelve, so a form of
|
|
6326
|
+
* eight short text fields renders as an eight-row ladder with two thirds of its width
|
|
6327
|
+
* empty. `auto` sizes them by field type instead: a date or a number is half a row, a
|
|
6328
|
+
* textarea or a nested group still takes all twelve.
|
|
6329
|
+
*
|
|
6330
|
+
* An explicit `colSpan` always wins in both modes, so turning `auto` on cannot override a
|
|
6331
|
+
* layout somebody authored. The default stays `stack` because changing how existing configs
|
|
6332
|
+
* render is not this input's business — opting in is.
|
|
6333
|
+
*/
|
|
6334
|
+
layout = 'stack';
|
|
5432
6335
|
// ─── Outputs ──────────────────────────────────────────────────────────────
|
|
5433
6336
|
formSubmit = new EventEmitter();
|
|
5434
6337
|
formChange = new EventEmitter();
|
|
@@ -5447,9 +6350,15 @@ class DynamicFormComponent {
|
|
|
5447
6350
|
hookRegistry = inject(HookRegistryService);
|
|
5448
6351
|
rbacService = inject(RbacService);
|
|
5449
6352
|
rulesEvaluation = inject(RulesEvaluationService);
|
|
6353
|
+
/** Resolves what is wrong with a field, for the error summary — see `resolveForField`. */
|
|
6354
|
+
messages = inject(ValidationMessagesService);
|
|
5450
6355
|
entityRefSelection = inject(EntityRefSelectionService);
|
|
5451
6356
|
commonModulesRegistry = inject(COMMON_MODULES_REGISTRY, { optional: true });
|
|
5452
6357
|
migrations = inject(RECORD_MIGRATIONS, { optional: true }) ?? [];
|
|
6358
|
+
/** Scopes the field-slot lookup a jump does, so the library never touches global `document`. */
|
|
6359
|
+
host = inject(ElementRef);
|
|
6360
|
+
/** Ties `afterNextRender` to this component, so destroying it cancels a pending jump. */
|
|
6361
|
+
injector = inject(Injector);
|
|
5453
6362
|
Object = Object;
|
|
5454
6363
|
// ─── Signals (local reactive state) ───────────────────────────────────────
|
|
5455
6364
|
activeTab = signal('', ...(ngDevMode ? [{ debugName: "activeTab" }] : /* istanbul ignore next */ []));
|
|
@@ -5475,9 +6384,12 @@ class DynamicFormComponent {
|
|
|
5475
6384
|
handleKeyboardEvent(event) {
|
|
5476
6385
|
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 's') {
|
|
5477
6386
|
event.preventDefault();
|
|
5478
|
-
|
|
6387
|
+
// No `form.valid` check: the shortcut is the Save button, and pressing Save on an
|
|
6388
|
+
// invalid form is what produces the error summary. Guarding here instead meant Ctrl+S
|
|
6389
|
+
// on an incomplete form did nothing at all — and unlike the button, a shortcut cannot
|
|
6390
|
+
// even look disabled.
|
|
6391
|
+
if (!this.readonly && this.canSubmit)
|
|
5479
6392
|
this.submit();
|
|
5480
|
-
}
|
|
5481
6393
|
}
|
|
5482
6394
|
}
|
|
5483
6395
|
// ─── Computed ─────────────────────────────────────────────────────────────
|
|
@@ -5628,10 +6540,35 @@ class DynamicFormComponent {
|
|
|
5628
6540
|
get isValidating() {
|
|
5629
6541
|
return this.form.pending;
|
|
5630
6542
|
}
|
|
5631
|
-
/**
|
|
6543
|
+
/**
|
|
6544
|
+
* Whether a submit would be refused. The guard `submit()` checks — not the button's state.
|
|
6545
|
+
*
|
|
6546
|
+
* See `submitDisabled` for why those are no longer the same question.
|
|
6547
|
+
*/
|
|
5632
6548
|
get submitBlocked() {
|
|
5633
6549
|
return this.form.invalid || this.form.pending || this.hasRuleErrors;
|
|
5634
6550
|
}
|
|
6551
|
+
/**
|
|
6552
|
+
* Whether the Save button is unavailable.
|
|
6553
|
+
*
|
|
6554
|
+
* This used to be `submitBlocked`, so an invalid form greyed Save out — and a greyed button
|
|
6555
|
+
* is the worst possible answer to "why can't I save?". It cannot be clicked, so there is no
|
|
6556
|
+
* moment at which the form gets to say which field is at fault or which tab it is on; the
|
|
6557
|
+
* user is left comparing a disabled button against a form that looks, to them, filled in.
|
|
6558
|
+
* On a tabbed form the offending field is usually not even on screen.
|
|
6559
|
+
*
|
|
6560
|
+
* Save now stays available while the form is merely invalid. Clicking it still saves
|
|
6561
|
+
* nothing — `submit()` checks `submitBlocked` and refuses — but the refusal is where the
|
|
6562
|
+
* error summary, the tab badges and the jump to the first bad field come from, so pressing
|
|
6563
|
+
* Save produces an explanation instead of silence.
|
|
6564
|
+
*
|
|
6565
|
+
* What is still disabled is what a *retry cannot fix*: a save already in flight, and an
|
|
6566
|
+
* async validator whose answer has not come back. Those are moments where the right answer
|
|
6567
|
+
* is genuinely "wait", and a button that accepts a click would be lying.
|
|
6568
|
+
*/
|
|
6569
|
+
get submitDisabled() {
|
|
6570
|
+
return this.isSaving() || this.loading || this.form.pending;
|
|
6571
|
+
}
|
|
5635
6572
|
ngOnInit() {
|
|
5636
6573
|
this.selectionSub = this.entityRefSelection.selection$.subscribe(({ fieldId, option }) => {
|
|
5637
6574
|
this.runAutoPatch(fieldId, option?.record);
|
|
@@ -5732,9 +6669,113 @@ class DynamicFormComponent {
|
|
|
5732
6669
|
this.focusActivePanel();
|
|
5733
6670
|
}
|
|
5734
6671
|
getFieldSpan(field) {
|
|
5735
|
-
const span = field.colSpan ?? 12;
|
|
6672
|
+
const span = field.colSpan ?? (this.layout === 'auto' ? (AUTO_COL_SPAN[field.type] ?? 12) : 12);
|
|
5736
6673
|
return `span ${Math.min(12, Math.max(1, span))}`;
|
|
5737
6674
|
}
|
|
6675
|
+
// ─── Validation recovery ──────────────────────────────────────────────────
|
|
6676
|
+
/**
|
|
6677
|
+
* Set by a submit that validation refused, cleared by one that went through.
|
|
6678
|
+
*
|
|
6679
|
+
* Gates the error summary rather than `form.invalid` doing it: a form opened on an empty
|
|
6680
|
+
* required field is invalid from the first render, and leading with a block of red before
|
|
6681
|
+
* the user has typed anything is an accusation, not help. The summary is the answer to
|
|
6682
|
+
* "why did Save do nothing", so it appears when Save does nothing.
|
|
6683
|
+
*/
|
|
6684
|
+
submitAttempted = signal(false, ...(ngDevMode ? [{ debugName: "submitAttempted" }] : /* istanbul ignore next */ []));
|
|
6685
|
+
/**
|
|
6686
|
+
* Every rendered field whose control is currently invalid, with the tab it lives on.
|
|
6687
|
+
*
|
|
6688
|
+
* Read by the tab badges, the error summary and the jump, so all three agree on what
|
|
6689
|
+
* counts. Hidden fields are excluded because `syncHiddenFieldState` disables them and a
|
|
6690
|
+
* disabled control is not part of validity — pointing the user at one would send them to a
|
|
6691
|
+
* field that is not on screen, or worse, switch tabs to show them nothing.
|
|
6692
|
+
*
|
|
6693
|
+
* Cached per change-detection pass. The walk calls `getControl` once per field and that
|
|
6694
|
+
* falls back to a recursive search of the whole form; the template reads this three times
|
|
6695
|
+
* for the summary and once per tab for the badges, so recomputing it each time turns one
|
|
6696
|
+
* traversal into a dozen on every keystroke. `form.statusChanges` is the only thing that
|
|
6697
|
+
* can alter the answer, and it already runs `markForCheck` — dropping the cache there keeps
|
|
6698
|
+
* it honest without a second subscription.
|
|
6699
|
+
*/
|
|
6700
|
+
invalidFields() {
|
|
6701
|
+
if (this.invalidFieldsCache)
|
|
6702
|
+
return this.invalidFieldsCache;
|
|
6703
|
+
const out = [];
|
|
6704
|
+
const collect = (tab, parentId) => {
|
|
6705
|
+
for (const field of tab.fields ?? []) {
|
|
6706
|
+
const control = this.getControl(field.id, tab.id);
|
|
6707
|
+
if (!control || control.disabled || control.valid || control.pending)
|
|
6708
|
+
continue;
|
|
6709
|
+
// The same message the field renders under itself — see `resolveForField`. Saying
|
|
6710
|
+
// only *which* field is wrong leaves the user to go and look at each one; saying what
|
|
6711
|
+
// is wrong with it is usually enough to fix it without leaving the summary.
|
|
6712
|
+
const message = this.messages.resolveForField(control.errors, this.language, field.type);
|
|
6713
|
+
out.push(parentId
|
|
6714
|
+
? { field, message, tabId: parentId, subTabId: tab.id }
|
|
6715
|
+
: { field, message, tabId: tab.id });
|
|
6716
|
+
}
|
|
6717
|
+
for (const child of tab.children ?? [])
|
|
6718
|
+
collect(child, parentId ?? tab.id);
|
|
6719
|
+
};
|
|
6720
|
+
for (const tab of this.visibleTabs)
|
|
6721
|
+
collect(tab);
|
|
6722
|
+
return (this.invalidFieldsCache = out);
|
|
6723
|
+
}
|
|
6724
|
+
invalidFieldsCache;
|
|
6725
|
+
/** How many invalid fields sit on a tab — its own and its sub-tabs'. */
|
|
6726
|
+
tabErrorCount(tab) {
|
|
6727
|
+
if (!this.submitAttempted())
|
|
6728
|
+
return 0;
|
|
6729
|
+
return this.invalidFields().filter(entry => entry.tabId === tab.id).length;
|
|
6730
|
+
}
|
|
6731
|
+
/** The same count for a sub-tab, which is addressed by its own id rather than its parent's. */
|
|
6732
|
+
subTabErrorCount(subTab) {
|
|
6733
|
+
if (!this.submitAttempted())
|
|
6734
|
+
return 0;
|
|
6735
|
+
return this.invalidFields().filter(entry => entry.subTabId === subTab.id).length;
|
|
6736
|
+
}
|
|
6737
|
+
/**
|
|
6738
|
+
* Switch to the tab holding `fieldId`, scroll it into view, and move focus onto it.
|
|
6739
|
+
*
|
|
6740
|
+
* `afterNextRender` rather than a timer: the panel the field lives in is rendered by the
|
|
6741
|
+
* change detection this call is part of, so the element does not exist yet — and a timeout
|
|
6742
|
+
* is both a guess at how long that takes and something that keeps running after the
|
|
6743
|
+
* component is destroyed. The query is scoped to this component's own element, so the
|
|
6744
|
+
* library never reaches for the global `document`.
|
|
6745
|
+
*/
|
|
6746
|
+
jumpToField(fieldId) {
|
|
6747
|
+
const location = this.invalidFields().find(entry => entry.field.id === fieldId) ?? this.locateField(fieldId);
|
|
6748
|
+
if (!location)
|
|
6749
|
+
return;
|
|
6750
|
+
// The panel must not take focus back — this jump is going to focus the field itself.
|
|
6751
|
+
this.setActiveTab(location.tabId, { focusPanel: false });
|
|
6752
|
+
// `setActiveTab` resets to a tab's first child, so the sub-tab is selected after it.
|
|
6753
|
+
if (location.subTabId)
|
|
6754
|
+
this.setActiveSubTab(location.subTabId, { focusPanel: false });
|
|
6755
|
+
afterNextRender(() => {
|
|
6756
|
+
// The id comes from config, so it never goes into a selector string: no escaping to
|
|
6757
|
+
// get wrong, and no need for `CSS.escape`, which jsdom does not provide.
|
|
6758
|
+
const wanted = `field-container-${fieldId}`;
|
|
6759
|
+
const el = Array.from(this.host.nativeElement.querySelectorAll('[id^="field-container-"]')).find(slot => slot.id === wanted);
|
|
6760
|
+
if (!el)
|
|
6761
|
+
return;
|
|
6762
|
+
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
6763
|
+
// The slot carries tabindex="-1", so this actually moves focus rather than only
|
|
6764
|
+
// scrolling and leaving focus behind on the link that was clicked.
|
|
6765
|
+
el.focus();
|
|
6766
|
+
}, { injector: this.injector });
|
|
6767
|
+
}
|
|
6768
|
+
locateField(fieldId) {
|
|
6769
|
+
for (const tab of this.tabs) {
|
|
6770
|
+
if ((tab.fields ?? []).some(f => f.id === fieldId))
|
|
6771
|
+
return { tabId: tab.id };
|
|
6772
|
+
for (const child of tab.children ?? []) {
|
|
6773
|
+
if ((child.fields ?? []).some(f => f.id === fieldId))
|
|
6774
|
+
return { tabId: tab.id, subTabId: child.id };
|
|
6775
|
+
}
|
|
6776
|
+
}
|
|
6777
|
+
return null;
|
|
6778
|
+
}
|
|
5738
6779
|
// ─── criticalField locking ────────────────────────────────────────────────
|
|
5739
6780
|
/** A critical field stays read-only until the user deliberately unlocks it. */
|
|
5740
6781
|
isFieldLocked(field) {
|
|
@@ -5767,8 +6808,15 @@ class DynamicFormComponent {
|
|
|
5767
6808
|
next.add(field.id);
|
|
5768
6809
|
this.unlockedFields.set(next);
|
|
5769
6810
|
}
|
|
6811
|
+
/**
|
|
6812
|
+
* A field's label in the form's language, falling back to its id.
|
|
6813
|
+
*
|
|
6814
|
+
* The fallback matters wherever the label stands alone rather than beside its control: an
|
|
6815
|
+
* error-summary chip or a critical-change banner naming a field with no label for this
|
|
6816
|
+
* language would otherwise be blank, and a blank chip is worse than a raw id.
|
|
6817
|
+
*/
|
|
5770
6818
|
resolveFieldLabel(field) {
|
|
5771
|
-
return resolveLabel(field.label, this.language);
|
|
6819
|
+
return resolveLabel(field.label, this.language) || field.id;
|
|
5772
6820
|
}
|
|
5773
6821
|
/**
|
|
5774
6822
|
* The changed critical fields as one comma-separated string.
|
|
@@ -5838,7 +6886,13 @@ class DynamicFormComponent {
|
|
|
5838
6886
|
// never re-enable. submitBlocked reads form.pending and form.invalid, so this is what
|
|
5839
6887
|
// keeps it honest.
|
|
5840
6888
|
this.statusSub?.unsubscribe();
|
|
5841
|
-
|
|
6889
|
+
// A rebuild replaces every control, so anything cached against the old ones is stale.
|
|
6890
|
+
this.invalidFieldsCache = undefined;
|
|
6891
|
+
this.statusSub = this.form.statusChanges.subscribe(() => {
|
|
6892
|
+
// The only thing that can change which fields are invalid — see `invalidFields`.
|
|
6893
|
+
this.invalidFieldsCache = undefined;
|
|
6894
|
+
this.cdr.markForCheck();
|
|
6895
|
+
});
|
|
5842
6896
|
this.valueSub = changes$.subscribe(() => {
|
|
5843
6897
|
const flattened = this.flattenFormValues();
|
|
5844
6898
|
this.formValues.set(flattened);
|
|
@@ -6347,9 +7401,23 @@ class DynamicFormComponent {
|
|
|
6347
7401
|
}
|
|
6348
7402
|
async submit() {
|
|
6349
7403
|
if (!this.canSubmit || this.submitBlocked) {
|
|
7404
|
+
/*
|
|
7405
|
+
* A refused save used to mark every control touched and stop there.
|
|
7406
|
+
*
|
|
7407
|
+
* On a tabbed form that is indistinguishable from a broken button: the errors appear on
|
|
7408
|
+
* whichever tabs hold them, the user is looking at a different one, and nothing on
|
|
7409
|
+
* screen changes. Three things fix it, and all three are needed — the summary says how
|
|
7410
|
+
* many and which, the tab badges say where, and this jump takes the user to the first.
|
|
7411
|
+
*/
|
|
6350
7412
|
this.markAllTouched();
|
|
7413
|
+
this.submitAttempted.set(true);
|
|
7414
|
+
const first = this.invalidFields()[0];
|
|
7415
|
+
if (first)
|
|
7416
|
+
this.jumpToField(first.field.id);
|
|
6351
7417
|
return;
|
|
6352
7418
|
}
|
|
7419
|
+
// Whatever the last attempt complained about is settled, so the summary goes away.
|
|
7420
|
+
this.submitAttempted.set(false);
|
|
6353
7421
|
const rawData = this.extractRecord();
|
|
6354
7422
|
let processedData = rawData;
|
|
6355
7423
|
this.isSaving.set(true);
|
|
@@ -6387,14 +7455,16 @@ class DynamicFormComponent {
|
|
|
6387
7455
|
this.formValues.set(values);
|
|
6388
7456
|
this.previousValues = { ...values };
|
|
6389
7457
|
this.unlockedFields.set(new Set());
|
|
7458
|
+
// Nothing has been attempted against the restored values, so the old complaint is stale.
|
|
7459
|
+
this.submitAttempted.set(false);
|
|
6390
7460
|
this.formReset.emit();
|
|
6391
7461
|
}
|
|
6392
7462
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: DynamicFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
6393
|
-
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" }, outputs: { formSubmit: "formSubmit", formChange: "formChange", formReset: "formReset", activeTabChange: "activeTabChange", saveRejected: "saveRejected" }, host: { listeners: { "window:keydown": "handleKeyboardEvent($event)" } }, providers: [EntityRefSelectionService], viewQueries: [{ propertyName: "formPanel", first: true, predicate: ["formPanel"], descendants: true }, { propertyName: "fieldHosts", predicate: DynamicFieldComponent, descendants: true }], usesOnChanges: true, ngImport: i0, template: "@if (canView) {\n <form [formGroup]=\"form\" (ngSubmit)=\"submit()\" class=\"ngx-form\" novalidate>\n <!-- Error message banner -->\n @if (error) {\n <div class=\"ngx-form__error-banner\" role=\"alert\" data-testid=\"form-error\">{{ error }}</div>\n }\n\n <!-- Rules Info Banners -->\n @for (infoKey of showInfoBanners ? Object.keys(ruleResult().infoBanners) : []; track infoKey) {\n <div class=\"ngx-form__info-banner\" role=\"status\" [attr.data-testid]=\"'info-banner-' + infoKey\">\n \u2139\uFE0F {{ ruleResult().infoBanners[infoKey] }}\n </div>\n }\n\n <!-- Rules Validation Errors & Warnings -->\n @for (errKey of Object.keys(ruleResult().validationErrors); track errKey) {\n <div class=\"ngx-form__error-banner\" role=\"alert\" [attr.data-testid]=\"'rule-error-' + errKey\">\n \u26A0\uFE0F {{ ruleResult().validationErrors[errKey] }}\n </div>\n }\n @for (warnKey of Object.keys(ruleResult().validationWarnings); track warnKey) {\n <div class=\"ngx-form__warning-banner\" role=\"status\" [attr.data-testid]=\"'rule-warning-' + warnKey\">\n \u26A1 {{ ruleResult().validationWarnings[warnKey] }}\n </div>\n }\n\n <!-- Deferred VALUE_CHANGED notice for criticalField edits (vs session baseline) -->\n @if (changedCriticalFields().length > 0) {\n <div class=\"ngx-form__warning-banner\" role=\"status\" data-testid=\"critical-change-banner\">\n {{ 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 </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 </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 <button\n type=\"submit\"\n class=\"ngx-form__submit\"\n data-testid=\"form-submit\"\n [disabled]=\"submitBlocked\"\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:16px 20px;align-items:start}.ngx-form__field-slot{display:flex;align-items:flex-start;gap:6px;min-width:0}.ngx-form__field-slot ngx-dynamic-field{flex:1;min-width:0}.ngx-form__lock{flex:0 0 auto;margin-top:22px;background:none;border:1px solid transparent;border-radius:6px;cursor:pointer;font-size:14px;line-height:1;padding:4px 6px}.ngx-form__lock:hover{border-color:#d1d5db;background:#f9fafb}@media(max-width:768px){.ngx-form__panel{grid-template-columns:1fr}.ngx-form__field-slot{grid-column:span 12!important}}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[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 });
|
|
7463
|
+
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], 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--readonly .ngx-form__panel{grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:2px var(--ngx-gap, 18px)}.ngx-form--readonly .ngx-form__field-slot{grid-column:auto}.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,.ngx-form--readonly .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 });
|
|
6394
7464
|
}
|
|
6395
7465
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: DynamicFormComponent, decorators: [{
|
|
6396
7466
|
type: Component,
|
|
6397
|
-
args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'ngx-dynamic-form', standalone: true, imports: [ReactiveFormsModule, DynamicFieldComponent, NgComponentOutlet], providers: [EntityRefSelectionService], template: "@if (canView) {\n <form [formGroup]=\"form\" (ngSubmit)=\"submit()\" class=\"ngx-form\" novalidate>\n <!-- Error message banner -->\n @if (error) {\n <div class=\"ngx-form__error-banner\" role=\"alert\" data-testid=\"form-error\">{{ error }}</div>\n }\n\n <!-- Rules Info Banners -->\n @for (infoKey of showInfoBanners ? Object.keys(ruleResult().infoBanners) : []; track infoKey) {\n <div class=\"ngx-form__info-banner\" role=\"status\" [attr.data-testid]=\"'info-banner-' + infoKey\">\n \u2139\uFE0F {{ ruleResult().infoBanners[infoKey] }}\n </div>\n }\n\n <!-- Rules Validation Errors & Warnings -->\n @for (errKey of Object.keys(ruleResult().validationErrors); track errKey) {\n <div class=\"ngx-form__error-banner\" role=\"alert\" [attr.data-testid]=\"'rule-error-' + errKey\">\n \u26A0\uFE0F {{ ruleResult().validationErrors[errKey] }}\n </div>\n }\n @for (warnKey of Object.keys(ruleResult().validationWarnings); track warnKey) {\n <div class=\"ngx-form__warning-banner\" role=\"status\" [attr.data-testid]=\"'rule-warning-' + warnKey\">\n \u26A1 {{ ruleResult().validationWarnings[warnKey] }}\n </div>\n }\n\n <!-- Deferred VALUE_CHANGED notice for criticalField edits (vs session baseline) -->\n @if (changedCriticalFields().length > 0) {\n <div class=\"ngx-form__warning-banner\" role=\"status\" data-testid=\"critical-change-banner\">\n {{ 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 </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 </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 <button\n type=\"submit\"\n class=\"ngx-form__submit\"\n data-testid=\"form-submit\"\n [disabled]=\"submitBlocked\"\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:16px 20px;align-items:start}.ngx-form__field-slot{display:flex;align-items:flex-start;gap:6px;min-width:0}.ngx-form__field-slot ngx-dynamic-field{flex:1;min-width:0}.ngx-form__lock{flex:0 0 auto;margin-top:22px;background:none;border:1px solid transparent;border-radius:6px;cursor:pointer;font-size:14px;line-height:1;padding:4px 6px}.ngx-form__lock:hover{border-color:#d1d5db;background:#f9fafb}@media(max-width:768px){.ngx-form__panel{grid-template-columns:1fr}.ngx-form__field-slot{grid-column:span 12!important}}\n"] }]
|
|
7467
|
+
args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'ngx-dynamic-form', standalone: true, imports: [ReactiveFormsModule, DynamicFieldComponent, NgComponentOutlet], providers: [EntityRefSelectionService], 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--readonly .ngx-form__panel{grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:2px var(--ngx-gap, 18px)}.ngx-form--readonly .ngx-form__field-slot{grid-column:auto}.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,.ngx-form--readonly .ngx-form__panel{grid-template-columns:minmax(0,1fr)}.ngx-form__field-slot{grid-column:1 / -1!important}}\n"] }]
|
|
6398
7468
|
}], propDecorators: { config: [{
|
|
6399
7469
|
type: Input
|
|
6400
7470
|
}], rules: [{
|
|
@@ -6421,6 +7491,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
6421
7491
|
type: Input
|
|
6422
7492
|
}], preview: [{
|
|
6423
7493
|
type: Input
|
|
7494
|
+
}], layout: [{
|
|
7495
|
+
type: Input
|
|
6424
7496
|
}], formSubmit: [{
|
|
6425
7497
|
type: Output
|
|
6426
7498
|
}], formChange: [{
|
|
@@ -6467,6 +7539,8 @@ class DynamicRecordFormComponent {
|
|
|
6467
7539
|
isReadOnly = false;
|
|
6468
7540
|
/** Specific field ids forced read-only while the rest of the record stays editable. */
|
|
6469
7541
|
readOnlyFields = [];
|
|
7542
|
+
/** Forwarded verbatim to the hosted form — see `DynamicFormComponent.layout`. */
|
|
7543
|
+
layout = 'stack';
|
|
6470
7544
|
/**
|
|
6471
7545
|
* Open the record read-only, with a per-tab "Edit section" flow — one tab is edited and
|
|
6472
7546
|
* validated at a time. This is the framework's `EntityRecordComponent` model and the
|
|
@@ -6480,11 +7554,19 @@ class DynamicRecordFormComponent {
|
|
|
6480
7554
|
formReset = new EventEmitter();
|
|
6481
7555
|
/** One tab section was saved. Carries the whole record, plus which tab was edited. */
|
|
6482
7556
|
sectionSave = new EventEmitter();
|
|
7557
|
+
/**
|
|
7558
|
+
* A `beforeSave` hook aborted the save — by returning false or throwing.
|
|
7559
|
+
*
|
|
7560
|
+
* Re-exposed from the embedded `ngx-dynamic-form`, and emitted by `saveSection` too. The
|
|
7561
|
+
* inner form already ran the hook and already refused the save, but nothing carried that
|
|
7562
|
+
* out to a host binding this component: the veto worked and looked exactly like a Save
|
|
7563
|
+
* button that did nothing, which is the failure `saveRejected` exists to prevent.
|
|
7564
|
+
*/
|
|
7565
|
+
saveRejected = new EventEmitter();
|
|
6483
7566
|
dynamicFormComp;
|
|
6484
7567
|
rulesEvaluation = inject(RulesEvaluationService);
|
|
6485
7568
|
rbacService = inject(RbacService);
|
|
6486
|
-
|
|
6487
|
-
injector = inject(Injector);
|
|
7569
|
+
hookRegistry = inject(HookRegistryService);
|
|
6488
7570
|
currentData = signal({}, ...(ngDevMode ? [{ debugName: "currentData" }] : /* istanbul ignore next */ []));
|
|
6489
7571
|
originalBaseline = signal({}, ...(ngDevMode ? [{ debugName: "originalBaseline" }] : /* istanbul ignore next */ []));
|
|
6490
7572
|
/**
|
|
@@ -6704,7 +7786,7 @@ class DynamicRecordFormComponent {
|
|
|
6704
7786
|
* validity of the tab's own controls, and rules filtered to this tab — a rule targeting
|
|
6705
7787
|
* a hidden tab must not block the save (the reference's OV0-968 fix).
|
|
6706
7788
|
*/
|
|
6707
|
-
saveSection() {
|
|
7789
|
+
async saveSection() {
|
|
6708
7790
|
const tabId = this.editingTabId();
|
|
6709
7791
|
const form = this.dynamicFormComp;
|
|
6710
7792
|
if (!tabId || !form)
|
|
@@ -6726,10 +7808,50 @@ class DynamicRecordFormComponent {
|
|
|
6726
7808
|
return;
|
|
6727
7809
|
}
|
|
6728
7810
|
this.sectionErrors.set({});
|
|
6729
|
-
this.editingTabId.set(null);
|
|
6730
7811
|
const record = form.extractRecord();
|
|
6731
|
-
|
|
6732
|
-
this
|
|
7812
|
+
// Only await when there is something to await. An async function runs synchronously up
|
|
7813
|
+
// to its first `await`, so this keeps `sectionSave` firing in the same tick for the
|
|
7814
|
+
// hosts that register no hook — which is how it has always behaved, and which a host
|
|
7815
|
+
// may well depend on. Awaiting an already-resolved promise would defer the emit by a
|
|
7816
|
+
// microtask for everybody, to no purpose.
|
|
7817
|
+
const hookKey = `${this.config?.entity}:beforeSave`;
|
|
7818
|
+
const reviewed = this.hookRegistry.has(hookKey) ? await this.runBeforeSave(record) : record;
|
|
7819
|
+
// Vetoed: the section stays open, exactly as the whole-record form stays open, so the
|
|
7820
|
+
// user is looking at the values the hook refused rather than at a closed section.
|
|
7821
|
+
if (reviewed === null)
|
|
7822
|
+
return;
|
|
7823
|
+
this.editingTabId.set(null);
|
|
7824
|
+
this.currentData.set(reviewed);
|
|
7825
|
+
this.sectionSave.emit({ tabId, record: reviewed });
|
|
7826
|
+
}
|
|
7827
|
+
/**
|
|
7828
|
+
* Put a record to the entity's `beforeSave` hook.
|
|
7829
|
+
*
|
|
7830
|
+
* `sectionSave` carries the *whole* record — `extractRecord()`, not the tab's slice — so
|
|
7831
|
+
* the same payload reached persistence through two buttons, one of which asked the hook
|
|
7832
|
+
* and one of which did not. A hook registered to veto a save is a data-integrity
|
|
7833
|
+
* mechanism; having a second route around it made it advisory.
|
|
7834
|
+
*
|
|
7835
|
+
* @returns the record to emit, or `null` when the hook aborted (already reported).
|
|
7836
|
+
*/
|
|
7837
|
+
async runBeforeSave(record) {
|
|
7838
|
+
const hookKey = `${this.config?.entity}:beforeSave`;
|
|
7839
|
+
// Defensive: `saveSection` checks this before awaiting, but a later caller may not.
|
|
7840
|
+
if (!this.hookRegistry.has(hookKey))
|
|
7841
|
+
return record;
|
|
7842
|
+
try {
|
|
7843
|
+
const result = await this.hookRegistry.run(hookKey, record);
|
|
7844
|
+
if (result === false) {
|
|
7845
|
+
this.saveRejected.emit({ reason: 'beforeSave returned false' });
|
|
7846
|
+
return null;
|
|
7847
|
+
}
|
|
7848
|
+
// Anything else is the payload to save; `undefined`/`true` mean "unchanged".
|
|
7849
|
+
return result === undefined || result === true ? record : result;
|
|
7850
|
+
}
|
|
7851
|
+
catch (err) {
|
|
7852
|
+
this.saveRejected.emit({ reason: err instanceof Error ? err.message : String(err), error: err });
|
|
7853
|
+
return null;
|
|
7854
|
+
}
|
|
6733
7855
|
}
|
|
6734
7856
|
/**
|
|
6735
7857
|
* Seed the session baseline from the loaded record so `VALUE_CHANGED` rules and the
|
|
@@ -6755,8 +7877,56 @@ class DynamicRecordFormComponent {
|
|
|
6755
7877
|
return 'Record';
|
|
6756
7878
|
return this.config.name ? resolveLabel(this.config.name, this.language) : this.config.entity;
|
|
6757
7879
|
}
|
|
7880
|
+
/**
|
|
7881
|
+
* What this record is called, as opposed to what kind of thing it is.
|
|
7882
|
+
*
|
|
7883
|
+
* The header used to show the entity's name twice — "clients", with "Entity: clients"
|
|
7884
|
+
* underneath — so the one question a record header exists to answer, *which* record am I
|
|
7885
|
+
* looking at, was the one thing it did not say. The entity keeps the subtitle; this takes
|
|
7886
|
+
* the heading.
|
|
7887
|
+
*
|
|
7888
|
+
* The order is deliberate. `showOnMinimize` is the author saying "this is what identifies a
|
|
7889
|
+
* record" — it is what the summary panel and the collapsed view already show — so it is
|
|
7890
|
+
* asked first. A text or email field is the fallback because those are what a name is
|
|
7891
|
+
* stored in; a number or a date is a property of the record, not its identity, and would
|
|
7892
|
+
* give a header reading "42". When neither exists there is nothing to name it with and the
|
|
7893
|
+
* entity label stands in, which is the behaviour this replaced.
|
|
7894
|
+
*
|
|
7895
|
+
* A masked field is never a candidate. Masking is presentational (see SECURITY.md), but
|
|
7896
|
+
* that is exactly why: the whole point is that this role does not get to read the value, and
|
|
7897
|
+
* promoting it into the page heading would print in full what every field renders as
|
|
7898
|
+
* `XXXXXXXXX`. A record whose only text field is masked keeps the entity label.
|
|
7899
|
+
*
|
|
7900
|
+
* One pass rather than two filtered ones: this is a getter the header reads on every change
|
|
7901
|
+
* detection, and the preferred candidate is usually the first field it meets.
|
|
7902
|
+
*/
|
|
7903
|
+
get recordHeading() {
|
|
7904
|
+
const named = (field) => {
|
|
7905
|
+
const value = this.fieldValue(field.id);
|
|
7906
|
+
if (typeof value !== 'string' && typeof value !== 'number')
|
|
7907
|
+
return null;
|
|
7908
|
+
const text = String(value).trim();
|
|
7909
|
+
return text.length ? text : null;
|
|
7910
|
+
};
|
|
7911
|
+
let fallback = null;
|
|
7912
|
+
for (const field of this.allFields()) {
|
|
7913
|
+
if (field.visibility === false)
|
|
7914
|
+
continue;
|
|
7915
|
+
if (this.rbacService.shouldMaskField(field, undefined, this.config, this.userRoles))
|
|
7916
|
+
continue;
|
|
7917
|
+
if (field.showOnMinimize) {
|
|
7918
|
+
const preferred = named(field);
|
|
7919
|
+
if (preferred)
|
|
7920
|
+
return preferred;
|
|
7921
|
+
}
|
|
7922
|
+
else if (!fallback && (field.type === 'text' || field.type === 'email')) {
|
|
7923
|
+
fallback = named(field);
|
|
7924
|
+
}
|
|
7925
|
+
}
|
|
7926
|
+
return fallback ?? this.recordTitle;
|
|
7927
|
+
}
|
|
6758
7928
|
get avatarLetter() {
|
|
6759
|
-
return (this.
|
|
7929
|
+
return (this.recordHeading || 'R').charAt(0).toUpperCase();
|
|
6760
7930
|
}
|
|
6761
7931
|
// ─── Header fields (isProfileImage / isHeaderToggle) ────────────────────────
|
|
6762
7932
|
/** Every field in the config, at any depth. */
|
|
@@ -6900,61 +8070,24 @@ class DynamicRecordFormComponent {
|
|
|
6900
8070
|
* field. The walk used to check top-level `fields` only, so every field in a sub-tab was
|
|
6901
8071
|
* simply not found and the jump did nothing at all.
|
|
6902
8072
|
*/
|
|
6903
|
-
locateField(fieldId) {
|
|
6904
|
-
for (const tab of this.config?.tabs || []) {
|
|
6905
|
-
if ((tab.fields || []).some(f => f.id === fieldId))
|
|
6906
|
-
return { tabId: tab.id };
|
|
6907
|
-
for (const child of tab.children || []) {
|
|
6908
|
-
if ((child.fields || []).some(f => f.id === fieldId)) {
|
|
6909
|
-
return { tabId: tab.id, subTabId: child.id };
|
|
6910
|
-
}
|
|
6911
|
-
}
|
|
6912
|
-
}
|
|
6913
|
-
return null;
|
|
6914
|
-
}
|
|
6915
8073
|
/**
|
|
6916
8074
|
* Switches to the tab holding `fieldId`, then scrolls it into view and moves focus to it.
|
|
6917
8075
|
*
|
|
6918
|
-
*
|
|
6919
|
-
*
|
|
6920
|
-
*
|
|
6921
|
-
*
|
|
6922
|
-
*
|
|
6923
|
-
* destroying the component cancels it. The query is scoped to this component's own element,
|
|
6924
|
-
* so the library no longer reaches for the global `document` at all.
|
|
8076
|
+
* Delegated rather than implemented twice. The hosted form grew the same jump when a
|
|
8077
|
+
* refused save had to be able to take the user to the first invalid field, and the form is
|
|
8078
|
+
* where it belongs: it owns the tab state, the panel that renders the field, and the
|
|
8079
|
+
* element the jump is looking for. This is kept as the record editor's public entry point
|
|
8080
|
+
* because the summary panel's quick-jump calls it and a host may too.
|
|
6925
8081
|
*/
|
|
6926
8082
|
jumpToField(fieldId) {
|
|
6927
|
-
|
|
6928
|
-
if (!location)
|
|
6929
|
-
return;
|
|
6930
|
-
// The panel must not take the focus back: this jump is going to focus the field itself.
|
|
6931
|
-
this.dynamicFormComp?.setActiveTab(location.tabId, { focusPanel: false });
|
|
6932
|
-
// `setActiveTab` resets to a tab's first child, so the sub-tab has to be selected after
|
|
6933
|
-
// it, not before. Without this the walk found sub-tab fields and then rendered the wrong
|
|
6934
|
-
// panel, so the element the callback below looks for never existed.
|
|
6935
|
-
if (location.subTabId) {
|
|
6936
|
-
this.dynamicFormComp?.setActiveSubTab(location.subTabId, { focusPanel: false });
|
|
6937
|
-
}
|
|
6938
|
-
afterNextRender(() => {
|
|
6939
|
-
// The field id comes from config, so it never goes into a selector string: no
|
|
6940
|
-
// escaping to get wrong, and no need for `CSS.escape`, which jsdom and older
|
|
6941
|
-
// browsers do not provide and whose absence throws silently inside a render hook.
|
|
6942
|
-
const wanted = `field-container-${fieldId}`;
|
|
6943
|
-
const el = Array.from(this.host.nativeElement.querySelectorAll('[id^="field-container-"]')).find(slot => slot.id === wanted);
|
|
6944
|
-
if (!el)
|
|
6945
|
-
return;
|
|
6946
|
-
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
6947
|
-
// The slot carries tabindex="-1" so this actually moves focus. Without it the jump
|
|
6948
|
-
// scrolled the field into view and left focus behind on the link.
|
|
6949
|
-
el.focus();
|
|
6950
|
-
}, { injector: this.injector });
|
|
8083
|
+
this.dynamicFormComp?.jumpToField(fieldId);
|
|
6951
8084
|
}
|
|
6952
8085
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: DynamicRecordFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
6953
|
-
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", viewMode: "viewMode" }, outputs: { formSubmit: "formSubmit", formChange: "formChange", formReset: "formReset", sectionSave: "sectionSave" }, viewQueries: [{ propertyName: "dynamicFormComp", first: true, predicate: DynamicFormComponent, descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"ngx-record-editor\">\n <!-- Record Header -->\n <div class=\"ngx-record-editor__header\">\n <div class=\"ngx-record-editor__title-group\">\n @if (profileImageUrl) {\n <img\n class=\"ngx-record-editor__avatar\"\n data-testid=\"header-profile-image\"\n [src]=\"profileImageUrl\"\n [alt]=\"recordTitle\"\n />\n } @else {\n <div class=\"ngx-record-editor__avatar\" data-testid=\"header-avatar-initial\">\n {{ avatarLetter }}\n </div>\n }\n <div>\n <h2 class=\"ngx-record-editor__title\" data-testid=\"record-title\">\n {{ recordTitle }}\n </h2>\n <span class=\"ngx-record-editor__subtitle\">{{ ui.text('entityLabel', language, { entity: config.entity }) }}</span>\n </div>\n </div>\n\n <!-- Record-level status switch, outside section editing -->\n @if (headerToggleField; as toggleField) {\n <label class=\"ngx-record-editor__header-toggle\">\n <span>{{ formatFieldLabel(toggleField) }}</span>\n <input\n type=\"checkbox\"\n role=\"switch\"\n data-testid=\"header-toggle\"\n [checked]=\"headerToggleValue\"\n [disabled]=\"recordReadOnly\"\n [attr.aria-checked]=\"headerToggleValue\"\n (change)=\"toggleHeaderStatus()\"\n />\n <span class=\"ngx-record-editor__header-toggle-text\">{{ 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 [showInfoBanners]=\"false\"\n [loading]=\"loading\"\n [error]=\"error\"\n (formChange)=\"onFormChange($event)\"\n (formSubmit)=\"onFormSubmit($event)\"\n (formReset)=\"onFormReset()\"\n (activeTabChange)=\"onActiveTabChange($event)\"\n />\n</div>\n", styles: [".ngx-record-editor{display:flex;flex-direction:column;gap:16px;background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:20px;box-shadow:0 4px 6px -1px #0000000d}.ngx-record-editor__header{display:flex;align-items:center;justify-content:space-between;padding-bottom:16px;border-bottom:1px solid #f3f4f6}.ngx-record-editor__title-group{display:flex;align-items:center;gap:12px}.ngx-record-editor__avatar{width:48px;height:48px;border-radius:50%;object-fit:cover;background:#e0e7ff;color:#4f46e5;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:18px}.ngx-record-editor__title{font-size:20px;font-weight:700;color:#111827;margin:0}.ngx-record-editor__subtitle{font-size:13px;color:#6b7280}.ngx-record-editor__header-toggle{display:flex;align-items:center;gap:8px;font-size:13px;color:#374151}.ngx-record-editor__header-toggle-text{font-weight:600}.ngx-record-editor__banner{padding:10px 14px;border-radius:8px;font-size:13px;font-weight:500}.ngx-record-editor__banner--info{background:#eff6ff;border:1px solid #dbeafe;color:#1d4ed8;display:flex;align-items:center;justify-content:space-between;gap:12px}.ngx-record-editor__banner-dismiss{background:none;border:none;cursor:pointer;color:inherit;font-size:14px;line-height:1;padding:2px 6px;border-radius:4px}.ngx-record-editor__banner-dismiss:hover{background:#0000000f}.ngx-record-editor__banner--error{background:#fef2f2;border:1px solid #fecaca;color:#b91c1c}.ngx-record-editor__section-bar{display:flex;align-items:center;justify-content:flex-end;gap:8px}.ngx-record-editor__section-hint{margin-right:auto;font-size:12px;color:#6b7280}.ngx-record-editor__banner--warning{background:#fffbeb;border:1px solid #fef3c7;color:#b45309}.ngx-record-editor__rows{border:1px solid #f3f4f6;border-radius:8px;padding:12px 14px;display:flex;flex-direction:column;gap:6px}.ngx-record-editor__rows-head{display:flex;align-items:center;justify-content:space-between;gap:8px}.ngx-record-editor__row{display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:6px;background:#f9fafb}.ngx-record-editor__row-text{flex:1;font-size:13px;overflow-wrap:anywhere}.ngx-record-editor__row-btn{background:none;border:1px solid #e5e7eb;border-radius:6px;cursor:pointer;font-size:12px;padding:2px 8px}.ngx-record-editor__row-empty{font-size:12px;color:#6b7280;margin:0}.ngx-record-editor__drawer{border:1px solid #dbeafe;background:#f8fafc;border-radius:8px;padding:14px;display:flex;flex-direction:column;gap:12px}.ngx-record-editor__summary-panel{background:#f9fafb;border:1px solid #f3f4f6;border-radius:8px;padding:12px 16px;display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px}.ngx-record-editor__summary-item{display:flex;flex-direction:column;cursor:pointer;padding:6px 10px;border-radius:6px;transition:background .15s}.ngx-record-editor__summary-item:hover{background:#eff6ff}.ngx-record-editor__summary-label{font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:#6b7280;font-weight:600}.ngx-record-editor__summary-val{font-size:14px;font-weight:600;color:#1f2937}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[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"], 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 });
|
|
8086
|
+
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 });
|
|
6954
8087
|
}
|
|
6955
8088
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: DynamicRecordFormComponent, decorators: [{
|
|
6956
8089
|
type: Component,
|
|
6957
|
-
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]=\"recordTitle\"\n />\n } @else {\n <div class=\"ngx-record-editor__avatar\" data-testid=\"header-avatar-initial\">\n {{ avatarLetter }}\n </div>\n }\n <div>\n <h2 class=\"ngx-record-editor__title\" data-testid=\"record-title\">\n {{ recordTitle }}\n </h2>\n <span class=\"ngx-record-editor__subtitle\">{{ ui.text('entityLabel', language, { entity: config.entity }) }}</span>\n </div>\n </div>\n\n <!-- Record-level status switch, outside section editing -->\n @if (headerToggleField; as toggleField) {\n <label class=\"ngx-record-editor__header-toggle\">\n <span>{{ formatFieldLabel(toggleField) }}</span>\n <input\n type=\"checkbox\"\n role=\"switch\"\n data-testid=\"header-toggle\"\n [checked]=\"headerToggleValue\"\n [disabled]=\"recordReadOnly\"\n [attr.aria-checked]=\"headerToggleValue\"\n (change)=\"toggleHeaderStatus()\"\n />\n <span class=\"ngx-record-editor__header-toggle-text\">{{ 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 [showInfoBanners]=\"false\"\n [loading]=\"loading\"\n [error]=\"error\"\n (formChange)=\"onFormChange($event)\"\n (formSubmit)=\"onFormSubmit($event)\"\n (formReset)=\"onFormReset()\"\n (activeTabChange)=\"onActiveTabChange($event)\"\n />\n</div>\n", styles: [".ngx-record-editor{display:flex;flex-direction:column;gap:16px;background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:20px;box-shadow:0 4px 6px -1px #0000000d}.ngx-record-editor__header{display:flex;align-items:center;justify-content:space-between;padding-bottom:16px;border-bottom:1px solid #f3f4f6}.ngx-record-editor__title-group{display:flex;align-items:center;gap:12px}.ngx-record-editor__avatar{width:48px;height:48px;border-radius:50%;object-fit:cover;background:#e0e7ff;color:#4f46e5;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:18px}.ngx-record-editor__title{font-size:20px;font-weight:700;color:#111827;margin:0}.ngx-record-editor__subtitle{font-size:13px;color:#6b7280}.ngx-record-editor__header-toggle{display:flex;align-items:center;gap:8px;font-size:13px;color:#374151}.ngx-record-editor__header-toggle-text{font-weight:600}.ngx-record-editor__banner{padding:10px 14px;border-radius:8px;font-size:13px;font-weight:500}.ngx-record-editor__banner--info{background:#eff6ff;border:1px solid #dbeafe;color:#1d4ed8;display:flex;align-items:center;justify-content:space-between;gap:12px}.ngx-record-editor__banner-dismiss{background:none;border:none;cursor:pointer;color:inherit;font-size:14px;line-height:1;padding:2px 6px;border-radius:4px}.ngx-record-editor__banner-dismiss:hover{background:#0000000f}.ngx-record-editor__banner--error{background:#fef2f2;border:1px solid #fecaca;color:#b91c1c}.ngx-record-editor__section-bar{display:flex;align-items:center;justify-content:flex-end;gap:8px}.ngx-record-editor__section-hint{margin-right:auto;font-size:12px;color:#6b7280}.ngx-record-editor__banner--warning{background:#fffbeb;border:1px solid #fef3c7;color:#b45309}.ngx-record-editor__rows{border:1px solid #f3f4f6;border-radius:8px;padding:12px 14px;display:flex;flex-direction:column;gap:6px}.ngx-record-editor__rows-head{display:flex;align-items:center;justify-content:space-between;gap:8px}.ngx-record-editor__row{display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:6px;background:#f9fafb}.ngx-record-editor__row-text{flex:1;font-size:13px;overflow-wrap:anywhere}.ngx-record-editor__row-btn{background:none;border:1px solid #e5e7eb;border-radius:6px;cursor:pointer;font-size:12px;padding:2px 8px}.ngx-record-editor__row-empty{font-size:12px;color:#6b7280;margin:0}.ngx-record-editor__drawer{border:1px solid #dbeafe;background:#f8fafc;border-radius:8px;padding:14px;display:flex;flex-direction:column;gap:12px}.ngx-record-editor__summary-panel{background:#f9fafb;border:1px solid #f3f4f6;border-radius:8px;padding:12px 16px;display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px}.ngx-record-editor__summary-item{display:flex;flex-direction:column;cursor:pointer;padding:6px 10px;border-radius:6px;transition:background .15s}.ngx-record-editor__summary-item:hover{background:#eff6ff}.ngx-record-editor__summary-label{font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:#6b7280;font-weight:600}.ngx-record-editor__summary-val{font-size:14px;font-weight:600;color:#1f2937}\n"] }]
|
|
8090
|
+
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"] }]
|
|
6958
8091
|
}], propDecorators: { config: [{
|
|
6959
8092
|
type: Input
|
|
6960
8093
|
}], rules: [{
|
|
@@ -6975,6 +8108,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
6975
8108
|
type: Input
|
|
6976
8109
|
}], readOnlyFields: [{
|
|
6977
8110
|
type: Input
|
|
8111
|
+
}], layout: [{
|
|
8112
|
+
type: Input
|
|
6978
8113
|
}], viewMode: [{
|
|
6979
8114
|
type: Input
|
|
6980
8115
|
}], formSubmit: [{
|
|
@@ -6985,6 +8120,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
6985
8120
|
type: Output
|
|
6986
8121
|
}], sectionSave: [{
|
|
6987
8122
|
type: Output
|
|
8123
|
+
}], saveRejected: [{
|
|
8124
|
+
type: Output
|
|
6988
8125
|
}], dynamicFormComp: [{
|
|
6989
8126
|
type: ViewChild,
|
|
6990
8127
|
args: [DynamicFormComponent]
|
|
@@ -6996,5 +8133,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
6996
8133
|
* Generated bundle index. Do not edit.
|
|
6997
8134
|
*/
|
|
6998
8135
|
|
|
6999
|
-
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, EntityRefFieldComponent, EntityRefRegistryService, EntityRefSelectionService, EntityReferenceService, FIELD_TYPE_REGISTRY, FIELD_TYPE_SETS, FieldRegistryService, FileFieldComponent, GroupFieldComponent, HOOK_REGISTRY, HookRegistryService, ImageFieldComponent, InMemoryEntityRefCacheStore, LOOKUP_REGISTRY, LookupRegistryService, MARKDOWN_RENDERER, MASKED_PLACEHOLDER, MASKED_ROLES, MarkdownFieldComponent, MonthYearFieldComponent, MultiSelectFieldComponent, NumberFieldComponent, PasswordFieldComponent, RECORD_MIGRATIONS, RadioFieldComponent, RbacService, RulesEvaluationService, SYSTEM_DEFAULT_CAN_EDIT, TextFieldComponent, TextareaFieldComponent, TimeFieldComponent, UI_TEXT, UPLOAD_HANDLER, UiTextService, VALIDATION_MESSAGES, VALIDATOR_REGISTRY, ValidationMessagesService, ValidatorRegistryService, builtInFieldTypes, fieldDomId, nextFieldInstanceId, provideBuiltInFieldTypes, provideFieldTypes, provideNgxDynamicEntity, refreshChoiceOptions, resolveUiText };
|
|
8136
|
+
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, EntityRefFieldComponent, EntityRefRegistryService, EntityRefSelectionService, EntityReferenceService, FIELD_TYPE_REGISTRY, FIELD_TYPE_SETS, FieldRegistryService, FileFieldComponent, GroupFieldComponent, HOOK_REGISTRY, HookRegistryService, ImageFieldComponent, InMemoryEntityRefCacheStore, LOOKUP_REGISTRY, LookupRegistryService, MARKDOWN_RENDERER, MASKED_PLACEHOLDER, MASKED_ROLES, MarkdownFieldComponent, MonthYearFieldComponent, MultiSelectFieldComponent, NumberFieldComponent, PasswordFieldComponent, RECORD_MIGRATIONS, RadioFieldComponent, RbacService, RulesEvaluationService, SYSTEM_DEFAULT_CAN_EDIT, TextFieldComponent, TextareaFieldComponent, TimeFieldComponent, UI_TEXT, UPLOAD_HANDLER, UiTextService, VALIDATION_MESSAGES, VALIDATOR_REGISTRY, ValidationMessagesService, ValidatorRegistryService, builtInFieldTypes, fieldDescribedBy, fieldDomId, nextFieldInstanceId, provideBuiltInFieldTypes, provideFieldTypes, provideNgxDynamicEntity, refreshChoiceOptions, resolveUiText };
|
|
7000
8137
|
//# sourceMappingURL=ngx-dynamic-entity.mjs.map
|