ngx-dynamic-entity 1.10.0 → 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/fesm2022/ngx-dynamic-entity.mjs +1364 -303
- 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 +262 -15
|
@@ -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>
|
|
@@ -2008,18 +2407,18 @@ class DateFieldComponent {
|
|
|
2008
2407
|
* "This field has an error", which made a documented, configurable feature work on three
|
|
2009
2408
|
* of fifteen field types.
|
|
2010
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
|
+
}
|
|
2011
2418
|
get errorMessage() {
|
|
2012
2419
|
if (!this.control?.errors || !this.control.touched)
|
|
2013
2420
|
return '';
|
|
2014
|
-
return this.messages.
|
|
2015
|
-
'required',
|
|
2016
|
-
'email',
|
|
2017
|
-
'min',
|
|
2018
|
-
'max',
|
|
2019
|
-
'minlength',
|
|
2020
|
-
'maxlength',
|
|
2021
|
-
'pattern',
|
|
2022
|
-
]);
|
|
2421
|
+
return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
|
|
2023
2422
|
}
|
|
2024
2423
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: DateFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2025
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: `
|
|
@@ -2030,7 +2429,21 @@ class DateFieldComponent {
|
|
|
2030
2429
|
[class.ngx-field--readonly]="readonly"
|
|
2031
2430
|
[class.ngx-field--masked]="masked"
|
|
2032
2431
|
>
|
|
2033
|
-
<
|
|
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>
|
|
2034
2447
|
@if (masked) {
|
|
2035
2448
|
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
2036
2449
|
maskedText
|
|
@@ -2046,10 +2459,13 @@ class DateFieldComponent {
|
|
|
2046
2459
|
[attr.data-testid]="'field-' + field.id + '-input'"
|
|
2047
2460
|
type="date"
|
|
2048
2461
|
[formControl]="$any(control)"
|
|
2462
|
+
[attr.aria-describedby]="describedBy()"
|
|
2049
2463
|
[attr.disabled]="field.disabled ? true : null"
|
|
2050
2464
|
/>
|
|
2051
2465
|
@if (errorMessage) {
|
|
2052
|
-
<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>
|
|
2053
2469
|
}
|
|
2054
2470
|
}
|
|
2055
2471
|
</div>
|
|
@@ -2070,7 +2486,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
2070
2486
|
[class.ngx-field--readonly]="readonly"
|
|
2071
2487
|
[class.ngx-field--masked]="masked"
|
|
2072
2488
|
>
|
|
2073
|
-
<
|
|
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>
|
|
2074
2504
|
@if (masked) {
|
|
2075
2505
|
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
2076
2506
|
maskedText
|
|
@@ -2086,10 +2516,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
2086
2516
|
[attr.data-testid]="'field-' + field.id + '-input'"
|
|
2087
2517
|
type="date"
|
|
2088
2518
|
[formControl]="$any(control)"
|
|
2519
|
+
[attr.aria-describedby]="describedBy()"
|
|
2089
2520
|
[attr.disabled]="field.disabled ? true : null"
|
|
2090
2521
|
/>
|
|
2091
2522
|
@if (errorMessage) {
|
|
2092
|
-
<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>
|
|
2093
2526
|
}
|
|
2094
2527
|
}
|
|
2095
2528
|
</div>
|
|
@@ -2200,18 +2633,18 @@ class DateTimeFieldComponent {
|
|
|
2200
2633
|
* "This field has an error", which made a documented, configurable feature work on three
|
|
2201
2634
|
* of fifteen field types.
|
|
2202
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
|
+
}
|
|
2203
2644
|
get errorMessage() {
|
|
2204
2645
|
if (!this.control?.errors || !this.control.touched)
|
|
2205
2646
|
return '';
|
|
2206
|
-
return this.messages.
|
|
2207
|
-
'required',
|
|
2208
|
-
'email',
|
|
2209
|
-
'min',
|
|
2210
|
-
'max',
|
|
2211
|
-
'minlength',
|
|
2212
|
-
'maxlength',
|
|
2213
|
-
'pattern',
|
|
2214
|
-
]);
|
|
2647
|
+
return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
|
|
2215
2648
|
}
|
|
2216
2649
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: DateTimeFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2217
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: `
|
|
@@ -2222,7 +2655,21 @@ class DateTimeFieldComponent {
|
|
|
2222
2655
|
[class.ngx-field--readonly]="readonly"
|
|
2223
2656
|
[class.ngx-field--masked]="masked"
|
|
2224
2657
|
>
|
|
2225
|
-
<
|
|
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>
|
|
2226
2673
|
@if (masked) {
|
|
2227
2674
|
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
2228
2675
|
maskedText
|
|
@@ -2240,9 +2687,12 @@ class DateTimeFieldComponent {
|
|
|
2240
2687
|
[value]="inputValue"
|
|
2241
2688
|
(change)="onInput($any($event.target).value)"
|
|
2242
2689
|
[attr.disabled]="field.disabled ? true : null"
|
|
2690
|
+
[attr.aria-describedby]="describedBy()"
|
|
2243
2691
|
/>
|
|
2244
2692
|
@if (errorMessage) {
|
|
2245
|
-
<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>
|
|
2246
2696
|
}
|
|
2247
2697
|
}
|
|
2248
2698
|
</div>
|
|
@@ -2263,7 +2713,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
2263
2713
|
[class.ngx-field--readonly]="readonly"
|
|
2264
2714
|
[class.ngx-field--masked]="masked"
|
|
2265
2715
|
>
|
|
2266
|
-
<
|
|
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>
|
|
2267
2731
|
@if (masked) {
|
|
2268
2732
|
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
2269
2733
|
maskedText
|
|
@@ -2281,9 +2745,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
2281
2745
|
[value]="inputValue"
|
|
2282
2746
|
(change)="onInput($any($event.target).value)"
|
|
2283
2747
|
[attr.disabled]="field.disabled ? true : null"
|
|
2748
|
+
[attr.aria-describedby]="describedBy()"
|
|
2284
2749
|
/>
|
|
2285
2750
|
@if (errorMessage) {
|
|
2286
|
-
<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>
|
|
2287
2754
|
}
|
|
2288
2755
|
}
|
|
2289
2756
|
</div>
|
|
@@ -2343,18 +2810,18 @@ class TimeFieldComponent {
|
|
|
2343
2810
|
* "This field has an error", which made a documented, configurable feature work on three
|
|
2344
2811
|
* of fifteen field types.
|
|
2345
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
|
+
}
|
|
2346
2821
|
get errorMessage() {
|
|
2347
2822
|
if (!this.control?.errors || !this.control.touched)
|
|
2348
2823
|
return '';
|
|
2349
|
-
return this.messages.
|
|
2350
|
-
'required',
|
|
2351
|
-
'email',
|
|
2352
|
-
'min',
|
|
2353
|
-
'max',
|
|
2354
|
-
'minlength',
|
|
2355
|
-
'maxlength',
|
|
2356
|
-
'pattern',
|
|
2357
|
-
]);
|
|
2824
|
+
return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
|
|
2358
2825
|
}
|
|
2359
2826
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: TimeFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2360
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: `
|
|
@@ -2365,7 +2832,21 @@ class TimeFieldComponent {
|
|
|
2365
2832
|
[class.ngx-field--readonly]="readonly"
|
|
2366
2833
|
[class.ngx-field--masked]="masked"
|
|
2367
2834
|
>
|
|
2368
|
-
<
|
|
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>
|
|
2369
2850
|
@if (masked) {
|
|
2370
2851
|
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
2371
2852
|
maskedText
|
|
@@ -2381,10 +2862,13 @@ class TimeFieldComponent {
|
|
|
2381
2862
|
[attr.data-testid]="'field-' + field.id + '-input'"
|
|
2382
2863
|
type="time"
|
|
2383
2864
|
[formControl]="$any(control)"
|
|
2865
|
+
[attr.aria-describedby]="describedBy()"
|
|
2384
2866
|
[attr.disabled]="field.disabled ? true : null"
|
|
2385
2867
|
/>
|
|
2386
2868
|
@if (errorMessage) {
|
|
2387
|
-
<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>
|
|
2388
2872
|
}
|
|
2389
2873
|
}
|
|
2390
2874
|
</div>
|
|
@@ -2405,7 +2889,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
2405
2889
|
[class.ngx-field--readonly]="readonly"
|
|
2406
2890
|
[class.ngx-field--masked]="masked"
|
|
2407
2891
|
>
|
|
2408
|
-
<
|
|
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>
|
|
2409
2907
|
@if (masked) {
|
|
2410
2908
|
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
2411
2909
|
maskedText
|
|
@@ -2421,10 +2919,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
2421
2919
|
[attr.data-testid]="'field-' + field.id + '-input'"
|
|
2422
2920
|
type="time"
|
|
2423
2921
|
[formControl]="$any(control)"
|
|
2922
|
+
[attr.aria-describedby]="describedBy()"
|
|
2424
2923
|
[attr.disabled]="field.disabled ? true : null"
|
|
2425
2924
|
/>
|
|
2426
2925
|
@if (errorMessage) {
|
|
2427
|
-
<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>
|
|
2428
2929
|
}
|
|
2429
2930
|
}
|
|
2430
2931
|
</div>
|
|
@@ -2518,18 +3019,18 @@ class MonthYearFieldComponent {
|
|
|
2518
3019
|
* "This field has an error", which made a documented, configurable feature work on three
|
|
2519
3020
|
* of fifteen field types.
|
|
2520
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
|
+
}
|
|
2521
3030
|
get errorMessage() {
|
|
2522
3031
|
if (!this.control?.errors || !this.control.touched)
|
|
2523
3032
|
return '';
|
|
2524
|
-
return this.messages.
|
|
2525
|
-
'required',
|
|
2526
|
-
'email',
|
|
2527
|
-
'min',
|
|
2528
|
-
'max',
|
|
2529
|
-
'minlength',
|
|
2530
|
-
'maxlength',
|
|
2531
|
-
'pattern',
|
|
2532
|
-
]);
|
|
3033
|
+
return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
|
|
2533
3034
|
}
|
|
2534
3035
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MonthYearFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2535
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: `
|
|
@@ -2546,7 +3047,21 @@ class MonthYearFieldComponent {
|
|
|
2546
3047
|
clicked. Each select then carries its own name below, because "Billing Cycle" alone
|
|
2547
3048
|
does not say which half of the pair the screen reader has landed on.
|
|
2548
3049
|
-->
|
|
2549
|
-
<
|
|
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>
|
|
2550
3065
|
@if (masked) {
|
|
2551
3066
|
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
2552
3067
|
maskedText
|
|
@@ -2565,6 +3080,7 @@ class MonthYearFieldComponent {
|
|
|
2565
3080
|
[value]="selectedMonth"
|
|
2566
3081
|
(change)="onMonthChange($any($event.target).value)"
|
|
2567
3082
|
[attr.disabled]="field.disabled ? true : null"
|
|
3083
|
+
[attr.aria-describedby]="describedBy()"
|
|
2568
3084
|
>
|
|
2569
3085
|
<option value="">{{ ui.text('month', language) }}</option>
|
|
2570
3086
|
@for (m of months; track m.value) {
|
|
@@ -2579,6 +3095,7 @@ class MonthYearFieldComponent {
|
|
|
2579
3095
|
[value]="selectedYear"
|
|
2580
3096
|
(change)="onYearChange($any($event.target).value)"
|
|
2581
3097
|
[attr.disabled]="field.disabled ? true : null"
|
|
3098
|
+
[attr.aria-describedby]="describedBy()"
|
|
2582
3099
|
>
|
|
2583
3100
|
<option value="">{{ ui.text('year', language) }}</option>
|
|
2584
3101
|
@for (y of years; track y) {
|
|
@@ -2587,7 +3104,9 @@ class MonthYearFieldComponent {
|
|
|
2587
3104
|
</select>
|
|
2588
3105
|
</div>
|
|
2589
3106
|
@if (errorMessage) {
|
|
2590
|
-
<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>
|
|
2591
3110
|
}
|
|
2592
3111
|
}
|
|
2593
3112
|
</div>
|
|
@@ -2614,7 +3133,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
2614
3133
|
clicked. Each select then carries its own name below, because "Billing Cycle" alone
|
|
2615
3134
|
does not say which half of the pair the screen reader has landed on.
|
|
2616
3135
|
-->
|
|
2617
|
-
<
|
|
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>
|
|
2618
3151
|
@if (masked) {
|
|
2619
3152
|
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
2620
3153
|
maskedText
|
|
@@ -2633,6 +3166,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
2633
3166
|
[value]="selectedMonth"
|
|
2634
3167
|
(change)="onMonthChange($any($event.target).value)"
|
|
2635
3168
|
[attr.disabled]="field.disabled ? true : null"
|
|
3169
|
+
[attr.aria-describedby]="describedBy()"
|
|
2636
3170
|
>
|
|
2637
3171
|
<option value="">{{ ui.text('month', language) }}</option>
|
|
2638
3172
|
@for (m of months; track m.value) {
|
|
@@ -2647,6 +3181,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
2647
3181
|
[value]="selectedYear"
|
|
2648
3182
|
(change)="onYearChange($any($event.target).value)"
|
|
2649
3183
|
[attr.disabled]="field.disabled ? true : null"
|
|
3184
|
+
[attr.aria-describedby]="describedBy()"
|
|
2650
3185
|
>
|
|
2651
3186
|
<option value="">{{ ui.text('year', language) }}</option>
|
|
2652
3187
|
@for (y of years; track y) {
|
|
@@ -2655,7 +3190,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
2655
3190
|
</select>
|
|
2656
3191
|
</div>
|
|
2657
3192
|
@if (errorMessage) {
|
|
2658
|
-
<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>
|
|
2659
3196
|
}
|
|
2660
3197
|
}
|
|
2661
3198
|
</div>
|
|
@@ -2985,12 +3522,20 @@ class DropdownFieldComponent {
|
|
|
2985
3522
|
return cached;
|
|
2986
3523
|
return typeof value === 'object' ? resolveLabel(value, this.language) : String(value ?? '—');
|
|
2987
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
|
+
}
|
|
2988
3533
|
get errorMessage() {
|
|
2989
3534
|
if (!this.control || !this.control.errors || !this.control.touched)
|
|
2990
3535
|
return '';
|
|
2991
3536
|
// `requiredSelection` rather than `required`: "Please select an option" reads better on a
|
|
2992
3537
|
// dropdown than "This field is required", and both stay independently overridable.
|
|
2993
|
-
return this.messages.
|
|
3538
|
+
return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
|
|
2994
3539
|
}
|
|
2995
3540
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: DropdownFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2996
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: `
|
|
@@ -3002,12 +3547,26 @@ class DropdownFieldComponent {
|
|
|
3002
3547
|
[class.ngx-field--masked]="masked"
|
|
3003
3548
|
[class.ngx-field--invalid]="control && control.invalid && control.touched"
|
|
3004
3549
|
>
|
|
3005
|
-
<
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
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>
|
|
3009
3568
|
}
|
|
3010
|
-
</
|
|
3569
|
+
</div>
|
|
3011
3570
|
@if (masked) {
|
|
3012
3571
|
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
3013
3572
|
maskedText
|
|
@@ -3025,7 +3584,7 @@ class DropdownFieldComponent {
|
|
|
3025
3584
|
[compareWith]="compareFn"
|
|
3026
3585
|
[attr.disabled]="field.disabled ? true : null"
|
|
3027
3586
|
[attr.aria-invalid]="control.invalid && control.touched"
|
|
3028
|
-
[attr.aria-describedby]="
|
|
3587
|
+
[attr.aria-describedby]="describedBy()"
|
|
3029
3588
|
>
|
|
3030
3589
|
<option [value]="''">{{ placeholder || ui.text('selectPlaceholder', language) }}</option>
|
|
3031
3590
|
@for (option of options(); track getOptLabel(option)) {
|
|
@@ -3065,12 +3624,26 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
3065
3624
|
[class.ngx-field--masked]="masked"
|
|
3066
3625
|
[class.ngx-field--invalid]="control && control.invalid && control.touched"
|
|
3067
3626
|
>
|
|
3068
|
-
<
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
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>
|
|
3072
3645
|
}
|
|
3073
|
-
</
|
|
3646
|
+
</div>
|
|
3074
3647
|
@if (masked) {
|
|
3075
3648
|
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
3076
3649
|
maskedText
|
|
@@ -3088,7 +3661,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
3088
3661
|
[compareWith]="compareFn"
|
|
3089
3662
|
[attr.disabled]="field.disabled ? true : null"
|
|
3090
3663
|
[attr.aria-invalid]="control.invalid && control.touched"
|
|
3091
|
-
[attr.aria-describedby]="
|
|
3664
|
+
[attr.aria-describedby]="describedBy()"
|
|
3092
3665
|
>
|
|
3093
3666
|
<option [value]="''">{{ placeholder || ui.text('selectPlaceholder', language) }}</option>
|
|
3094
3667
|
@for (option of options(); track getOptLabel(option)) {
|
|
@@ -3213,18 +3786,18 @@ class RadioFieldComponent {
|
|
|
3213
3786
|
* "This field has an error", which made a documented, configurable feature work on three
|
|
3214
3787
|
* of fifteen field types.
|
|
3215
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
|
+
}
|
|
3216
3797
|
get errorMessage() {
|
|
3217
3798
|
if (!this.control?.errors || !this.control.touched)
|
|
3218
3799
|
return '';
|
|
3219
|
-
return this.messages.
|
|
3220
|
-
'required',
|
|
3221
|
-
'email',
|
|
3222
|
-
'min',
|
|
3223
|
-
'max',
|
|
3224
|
-
'minlength',
|
|
3225
|
-
'maxlength',
|
|
3226
|
-
'pattern',
|
|
3227
|
-
]);
|
|
3800
|
+
return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
|
|
3228
3801
|
}
|
|
3229
3802
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: RadioFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3230
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: `
|
|
@@ -3236,7 +3809,25 @@ class RadioFieldComponent {
|
|
|
3236
3809
|
[class.ngx-field--masked]="masked"
|
|
3237
3810
|
>
|
|
3238
3811
|
<fieldset class="ngx-field__fieldset">
|
|
3239
|
-
<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>
|
|
3240
3831
|
@if (masked) {
|
|
3241
3832
|
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
3242
3833
|
maskedText
|
|
@@ -3253,6 +3844,7 @@ class RadioFieldComponent {
|
|
|
3253
3844
|
type="radio"
|
|
3254
3845
|
class="ngx-field__radio-input"
|
|
3255
3846
|
[formControl]="$any(control)"
|
|
3847
|
+
[attr.aria-describedby]="describedBy()"
|
|
3256
3848
|
[value]="getOptStoredVal(option)"
|
|
3257
3849
|
[attr.disabled]="field.disabled ? true : null"
|
|
3258
3850
|
/>
|
|
@@ -3261,7 +3853,9 @@ class RadioFieldComponent {
|
|
|
3261
3853
|
}
|
|
3262
3854
|
</div>
|
|
3263
3855
|
@if (errorMessage) {
|
|
3264
|
-
<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>
|
|
3265
3859
|
}
|
|
3266
3860
|
}
|
|
3267
3861
|
</fieldset>
|
|
@@ -3284,7 +3878,25 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
3284
3878
|
[class.ngx-field--masked]="masked"
|
|
3285
3879
|
>
|
|
3286
3880
|
<fieldset class="ngx-field__fieldset">
|
|
3287
|
-
<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>
|
|
3288
3900
|
@if (masked) {
|
|
3289
3901
|
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
3290
3902
|
maskedText
|
|
@@ -3301,6 +3913,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
3301
3913
|
type="radio"
|
|
3302
3914
|
class="ngx-field__radio-input"
|
|
3303
3915
|
[formControl]="$any(control)"
|
|
3916
|
+
[attr.aria-describedby]="describedBy()"
|
|
3304
3917
|
[value]="getOptStoredVal(option)"
|
|
3305
3918
|
[attr.disabled]="field.disabled ? true : null"
|
|
3306
3919
|
/>
|
|
@@ -3309,7 +3922,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
3309
3922
|
}
|
|
3310
3923
|
</div>
|
|
3311
3924
|
@if (errorMessage) {
|
|
3312
|
-
<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>
|
|
3313
3928
|
}
|
|
3314
3929
|
}
|
|
3315
3930
|
</fieldset>
|
|
@@ -3399,18 +4014,18 @@ class MultiSelectFieldComponent {
|
|
|
3399
4014
|
* "This field has an error", which made a documented, configurable feature work on three
|
|
3400
4015
|
* of fifteen field types.
|
|
3401
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
|
+
}
|
|
3402
4025
|
get errorMessage() {
|
|
3403
4026
|
if (!this.control?.errors || !this.control.touched)
|
|
3404
4027
|
return '';
|
|
3405
|
-
return this.messages.
|
|
3406
|
-
'required',
|
|
3407
|
-
'email',
|
|
3408
|
-
'min',
|
|
3409
|
-
'max',
|
|
3410
|
-
'minlength',
|
|
3411
|
-
'maxlength',
|
|
3412
|
-
'pattern',
|
|
3413
|
-
]);
|
|
4028
|
+
return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
|
|
3414
4029
|
}
|
|
3415
4030
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MultiSelectFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3416
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: `
|
|
@@ -3421,7 +4036,21 @@ class MultiSelectFieldComponent {
|
|
|
3421
4036
|
[class.ngx-field--readonly]="readonly"
|
|
3422
4037
|
[class.ngx-field--masked]="masked"
|
|
3423
4038
|
>
|
|
3424
|
-
<
|
|
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>
|
|
3425
4054
|
@if (masked) {
|
|
3426
4055
|
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
3427
4056
|
maskedText
|
|
@@ -3436,6 +4065,7 @@ class MultiSelectFieldComponent {
|
|
|
3436
4065
|
class="ngx-field__input"
|
|
3437
4066
|
[attr.data-testid]="'field-' + field.id + '-input'"
|
|
3438
4067
|
[formControl]="$any(control)"
|
|
4068
|
+
[attr.aria-describedby]="describedBy()"
|
|
3439
4069
|
[compareWith]="compareFn"
|
|
3440
4070
|
[attr.disabled]="field.disabled ? true : null"
|
|
3441
4071
|
multiple
|
|
@@ -3450,7 +4080,9 @@ class MultiSelectFieldComponent {
|
|
|
3450
4080
|
}
|
|
3451
4081
|
</select>
|
|
3452
4082
|
@if (errorMessage) {
|
|
3453
|
-
<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>
|
|
3454
4086
|
}
|
|
3455
4087
|
}
|
|
3456
4088
|
</div>
|
|
@@ -3471,7 +4103,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
3471
4103
|
[class.ngx-field--readonly]="readonly"
|
|
3472
4104
|
[class.ngx-field--masked]="masked"
|
|
3473
4105
|
>
|
|
3474
|
-
<
|
|
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>
|
|
3475
4121
|
@if (masked) {
|
|
3476
4122
|
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
3477
4123
|
maskedText
|
|
@@ -3486,6 +4132,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
3486
4132
|
class="ngx-field__input"
|
|
3487
4133
|
[attr.data-testid]="'field-' + field.id + '-input'"
|
|
3488
4134
|
[formControl]="$any(control)"
|
|
4135
|
+
[attr.aria-describedby]="describedBy()"
|
|
3489
4136
|
[compareWith]="compareFn"
|
|
3490
4137
|
[attr.disabled]="field.disabled ? true : null"
|
|
3491
4138
|
multiple
|
|
@@ -3500,7 +4147,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
3500
4147
|
}
|
|
3501
4148
|
</select>
|
|
3502
4149
|
@if (errorMessage) {
|
|
3503
|
-
<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>
|
|
3504
4153
|
}
|
|
3505
4154
|
}
|
|
3506
4155
|
</div>
|
|
@@ -3973,18 +4622,18 @@ class EntityRefFieldComponent {
|
|
|
3973
4622
|
* "This field has an error", which made a documented, configurable feature work on three
|
|
3974
4623
|
* of fifteen field types.
|
|
3975
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
|
+
}
|
|
3976
4633
|
get errorMessage() {
|
|
3977
4634
|
if (!this.control?.errors || !this.control.touched)
|
|
3978
4635
|
return '';
|
|
3979
|
-
return this.messages.
|
|
3980
|
-
'required',
|
|
3981
|
-
'email',
|
|
3982
|
-
'min',
|
|
3983
|
-
'max',
|
|
3984
|
-
'minlength',
|
|
3985
|
-
'maxlength',
|
|
3986
|
-
'pattern',
|
|
3987
|
-
]);
|
|
4636
|
+
return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
|
|
3988
4637
|
}
|
|
3989
4638
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: EntityRefFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3990
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: `
|
|
@@ -3995,7 +4644,21 @@ class EntityRefFieldComponent {
|
|
|
3995
4644
|
[class.ngx-field--readonly]="readonly"
|
|
3996
4645
|
[class.ngx-field--masked]="masked"
|
|
3997
4646
|
>
|
|
3998
|
-
<
|
|
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>
|
|
3999
4662
|
@if (masked) {
|
|
4000
4663
|
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
4001
4664
|
maskedText
|
|
@@ -4015,6 +4678,7 @@ class EntityRefFieldComponent {
|
|
|
4015
4678
|
[attr.data-testid]="'field-' + field.id + '-input'"
|
|
4016
4679
|
[id]="domId()"
|
|
4017
4680
|
[formControl]="$any(control)"
|
|
4681
|
+
[attr.aria-describedby]="describedBy()"
|
|
4018
4682
|
[attr.disabled]="field.disabled ? true : null"
|
|
4019
4683
|
>
|
|
4020
4684
|
<option value="">{{ placeholder || ui.text('selectPlaceholder', language) }}</option>
|
|
@@ -4029,7 +4693,9 @@ class EntityRefFieldComponent {
|
|
|
4029
4693
|
}
|
|
4030
4694
|
}
|
|
4031
4695
|
@if (errorMessage) {
|
|
4032
|
-
<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>
|
|
4033
4699
|
}
|
|
4034
4700
|
}
|
|
4035
4701
|
</div>
|
|
@@ -4050,7 +4716,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
4050
4716
|
[class.ngx-field--readonly]="readonly"
|
|
4051
4717
|
[class.ngx-field--masked]="masked"
|
|
4052
4718
|
>
|
|
4053
|
-
<
|
|
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>
|
|
4054
4734
|
@if (masked) {
|
|
4055
4735
|
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
4056
4736
|
maskedText
|
|
@@ -4070,6 +4750,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
4070
4750
|
[attr.data-testid]="'field-' + field.id + '-input'"
|
|
4071
4751
|
[id]="domId()"
|
|
4072
4752
|
[formControl]="$any(control)"
|
|
4753
|
+
[attr.aria-describedby]="describedBy()"
|
|
4073
4754
|
[attr.disabled]="field.disabled ? true : null"
|
|
4074
4755
|
>
|
|
4075
4756
|
<option value="">{{ placeholder || ui.text('selectPlaceholder', language) }}</option>
|
|
@@ -4084,7 +4765,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
4084
4765
|
}
|
|
4085
4766
|
}
|
|
4086
4767
|
@if (errorMessage) {
|
|
4087
|
-
<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>
|
|
4088
4771
|
}
|
|
4089
4772
|
}
|
|
4090
4773
|
</div>
|
|
@@ -4354,6 +5037,10 @@ class GroupFieldComponent {
|
|
|
4354
5037
|
readonly = false;
|
|
4355
5038
|
masked = false;
|
|
4356
5039
|
userRoles = [];
|
|
5040
|
+
/** Author help text for the section as a whole. */
|
|
5041
|
+
get hint() {
|
|
5042
|
+
return resolveLabel(this.field?.hint, this.language);
|
|
5043
|
+
}
|
|
4357
5044
|
get label() {
|
|
4358
5045
|
return resolveLabel(this.field?.label, this.language);
|
|
4359
5046
|
}
|
|
@@ -4363,11 +5050,27 @@ class GroupFieldComponent {
|
|
|
4363
5050
|
}
|
|
4364
5051
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: GroupFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
4365
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: `
|
|
4366
|
-
<fieldset
|
|
4367
|
-
|
|
4368
|
-
|
|
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>
|
|
4369
5070
|
@if (masked) {
|
|
4370
|
-
<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>
|
|
4371
5074
|
} @else {
|
|
4372
5075
|
<div class="ngx-field__group-children">
|
|
4373
5076
|
@for (child of field.children ?? []; track child.id) {
|
|
@@ -4390,11 +5093,27 @@ class GroupFieldComponent {
|
|
|
4390
5093
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: GroupFieldComponent, decorators: [{
|
|
4391
5094
|
type: Component,
|
|
4392
5095
|
args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'ngx-group-field', standalone: true, imports: [ReactiveFormsModule, forwardRef(() => DynamicFieldComponent)], template: `
|
|
4393
|
-
<fieldset
|
|
4394
|
-
|
|
4395
|
-
|
|
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>
|
|
4396
5113
|
@if (masked) {
|
|
4397
|
-
<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>
|
|
4398
5117
|
} @else {
|
|
4399
5118
|
<div class="ngx-field__group-children">
|
|
4400
5119
|
@for (child of field.children ?? []; track child.id) {
|
|
@@ -4545,6 +5264,10 @@ class ArrayFieldComponent {
|
|
|
4545
5264
|
userRoles = [];
|
|
4546
5265
|
fb = inject(FormBuilder);
|
|
4547
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
|
+
}
|
|
4548
5271
|
get label() {
|
|
4549
5272
|
return resolveLabel(this.field?.label, this.language);
|
|
4550
5273
|
}
|
|
@@ -4578,7 +5301,18 @@ class ArrayFieldComponent {
|
|
|
4578
5301
|
[class.ngx-field--masked]="masked"
|
|
4579
5302
|
>
|
|
4580
5303
|
<div class="ngx-field__array-header">
|
|
4581
|
-
<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>
|
|
4582
5316
|
@if (!readonly && !masked) {
|
|
4583
5317
|
<button
|
|
4584
5318
|
type="button"
|
|
@@ -4590,7 +5324,6 @@ class ArrayFieldComponent {
|
|
|
4590
5324
|
</button>
|
|
4591
5325
|
}
|
|
4592
5326
|
</div>
|
|
4593
|
-
|
|
4594
5327
|
@if (masked) {
|
|
4595
5328
|
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
4596
5329
|
maskedText
|
|
@@ -4649,7 +5382,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
4649
5382
|
[class.ngx-field--masked]="masked"
|
|
4650
5383
|
>
|
|
4651
5384
|
<div class="ngx-field__array-header">
|
|
4652
|
-
<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>
|
|
4653
5397
|
@if (!readonly && !masked) {
|
|
4654
5398
|
<button
|
|
4655
5399
|
type="button"
|
|
@@ -4661,7 +5405,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
4661
5405
|
</button>
|
|
4662
5406
|
}
|
|
4663
5407
|
</div>
|
|
4664
|
-
|
|
4665
5408
|
@if (masked) {
|
|
4666
5409
|
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
4667
5410
|
maskedText
|
|
@@ -4877,10 +5620,18 @@ class ImageFieldComponent {
|
|
|
4877
5620
|
this.uploads.revokePreviewUrl(this.previewCache);
|
|
4878
5621
|
}
|
|
4879
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
|
+
}
|
|
4880
5631
|
get errorMessage() {
|
|
4881
5632
|
if (!this.control?.errors || !this.control.touched)
|
|
4882
5633
|
return '';
|
|
4883
|
-
return this.messages.
|
|
5634
|
+
return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
|
|
4884
5635
|
}
|
|
4885
5636
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: ImageFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
4886
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: `
|
|
@@ -4891,7 +5642,21 @@ class ImageFieldComponent {
|
|
|
4891
5642
|
[class.ngx-field--readonly]="readonly"
|
|
4892
5643
|
[class.ngx-field--masked]="masked"
|
|
4893
5644
|
>
|
|
4894
|
-
<
|
|
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>
|
|
4895
5660
|
@if (masked) {
|
|
4896
5661
|
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
4897
5662
|
maskedText
|
|
@@ -4920,6 +5685,7 @@ class ImageFieldComponent {
|
|
|
4920
5685
|
accept="image/*"
|
|
4921
5686
|
[disabled]="field.disabled || uploading()"
|
|
4922
5687
|
(change)="onFileSelect($any($event.target).files[0])"
|
|
5688
|
+
[attr.aria-describedby]="describedBy()"
|
|
4923
5689
|
/>
|
|
4924
5690
|
}
|
|
4925
5691
|
</label>
|
|
@@ -4935,9 +5701,13 @@ class ImageFieldComponent {
|
|
|
4935
5701
|
ever showed the first. A required file left unchosen said nothing, and a
|
|
4936
5702
|
configured required message had no element to appear in. -->
|
|
4937
5703
|
@if (uploadError() || errorMessage) {
|
|
4938
|
-
<span
|
|
4939
|
-
|
|
4940
|
-
|
|
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
|
+
>
|
|
4941
5711
|
}
|
|
4942
5712
|
}
|
|
4943
5713
|
</div>
|
|
@@ -4958,7 +5728,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
4958
5728
|
[class.ngx-field--readonly]="readonly"
|
|
4959
5729
|
[class.ngx-field--masked]="masked"
|
|
4960
5730
|
>
|
|
4961
|
-
<
|
|
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>
|
|
4962
5746
|
@if (masked) {
|
|
4963
5747
|
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
4964
5748
|
maskedText
|
|
@@ -4987,6 +5771,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
4987
5771
|
accept="image/*"
|
|
4988
5772
|
[disabled]="field.disabled || uploading()"
|
|
4989
5773
|
(change)="onFileSelect($any($event.target).files[0])"
|
|
5774
|
+
[attr.aria-describedby]="describedBy()"
|
|
4990
5775
|
/>
|
|
4991
5776
|
}
|
|
4992
5777
|
</label>
|
|
@@ -5002,9 +5787,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
5002
5787
|
ever showed the first. A required file left unchosen said nothing, and a
|
|
5003
5788
|
configured required message had no element to appear in. -->
|
|
5004
5789
|
@if (uploadError() || errorMessage) {
|
|
5005
|
-
<span
|
|
5006
|
-
|
|
5007
|
-
|
|
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
|
+
>
|
|
5008
5797
|
}
|
|
5009
5798
|
}
|
|
5010
5799
|
</div>
|
|
@@ -5081,10 +5870,18 @@ class FileFieldComponent {
|
|
|
5081
5870
|
this.control.markAsTouched();
|
|
5082
5871
|
}
|
|
5083
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
|
+
}
|
|
5084
5881
|
get errorMessage() {
|
|
5085
5882
|
if (!this.control?.errors || !this.control.touched)
|
|
5086
5883
|
return '';
|
|
5087
|
-
return this.messages.
|
|
5884
|
+
return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
|
|
5088
5885
|
}
|
|
5089
5886
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: FileFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
5090
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: `
|
|
@@ -5095,7 +5892,21 @@ class FileFieldComponent {
|
|
|
5095
5892
|
[class.ngx-field--readonly]="readonly"
|
|
5096
5893
|
[class.ngx-field--masked]="masked"
|
|
5097
5894
|
>
|
|
5098
|
-
<
|
|
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>
|
|
5099
5910
|
@if (masked) {
|
|
5100
5911
|
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
5101
5912
|
maskedText
|
|
@@ -5134,6 +5945,7 @@ class FileFieldComponent {
|
|
|
5134
5945
|
[id]="domId()"
|
|
5135
5946
|
[disabled]="field.disabled || uploading()"
|
|
5136
5947
|
(change)="onFileSelect($any($event.target).files[0])"
|
|
5948
|
+
[attr.aria-describedby]="describedBy()"
|
|
5137
5949
|
/>
|
|
5138
5950
|
}
|
|
5139
5951
|
</label>
|
|
@@ -5142,9 +5954,13 @@ class FileFieldComponent {
|
|
|
5142
5954
|
ever showed the first. A required file left unchosen said nothing, and a
|
|
5143
5955
|
configured required message had no element to appear in. -->
|
|
5144
5956
|
@if (uploadError() || errorMessage) {
|
|
5145
|
-
<span
|
|
5146
|
-
|
|
5147
|
-
|
|
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
|
+
>
|
|
5148
5964
|
}
|
|
5149
5965
|
}
|
|
5150
5966
|
</div>
|
|
@@ -5165,7 +5981,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
5165
5981
|
[class.ngx-field--readonly]="readonly"
|
|
5166
5982
|
[class.ngx-field--masked]="masked"
|
|
5167
5983
|
>
|
|
5168
|
-
<
|
|
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>
|
|
5169
5999
|
@if (masked) {
|
|
5170
6000
|
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
5171
6001
|
maskedText
|
|
@@ -5204,6 +6034,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
5204
6034
|
[id]="domId()"
|
|
5205
6035
|
[disabled]="field.disabled || uploading()"
|
|
5206
6036
|
(change)="onFileSelect($any($event.target).files[0])"
|
|
6037
|
+
[attr.aria-describedby]="describedBy()"
|
|
5207
6038
|
/>
|
|
5208
6039
|
}
|
|
5209
6040
|
</label>
|
|
@@ -5212,9 +6043,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
5212
6043
|
ever showed the first. A required file left unchosen said nothing, and a
|
|
5213
6044
|
configured required message had no element to appear in. -->
|
|
5214
6045
|
@if (uploadError() || errorMessage) {
|
|
5215
|
-
<span
|
|
5216
|
-
|
|
5217
|
-
|
|
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
|
+
>
|
|
5218
6053
|
}
|
|
5219
6054
|
}
|
|
5220
6055
|
</div>
|
|
@@ -5396,6 +6231,37 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
5396
6231
|
args: [CONFIG_SOURCE]
|
|
5397
6232
|
}] }] });
|
|
5398
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
|
+
};
|
|
5399
6265
|
/**
|
|
5400
6266
|
* DynamicFormComponent — the main form component.
|
|
5401
6267
|
* Renders a reactive form from EntityFormConfig with tab support, responsive 12-col grid,
|
|
@@ -5453,6 +6319,19 @@ class DynamicFormComponent {
|
|
|
5453
6319
|
* disable the form. Used by the builder's live preview.
|
|
5454
6320
|
*/
|
|
5455
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';
|
|
5456
6335
|
// ─── Outputs ──────────────────────────────────────────────────────────────
|
|
5457
6336
|
formSubmit = new EventEmitter();
|
|
5458
6337
|
formChange = new EventEmitter();
|
|
@@ -5471,9 +6350,15 @@ class DynamicFormComponent {
|
|
|
5471
6350
|
hookRegistry = inject(HookRegistryService);
|
|
5472
6351
|
rbacService = inject(RbacService);
|
|
5473
6352
|
rulesEvaluation = inject(RulesEvaluationService);
|
|
6353
|
+
/** Resolves what is wrong with a field, for the error summary — see `resolveForField`. */
|
|
6354
|
+
messages = inject(ValidationMessagesService);
|
|
5474
6355
|
entityRefSelection = inject(EntityRefSelectionService);
|
|
5475
6356
|
commonModulesRegistry = inject(COMMON_MODULES_REGISTRY, { optional: true });
|
|
5476
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);
|
|
5477
6362
|
Object = Object;
|
|
5478
6363
|
// ─── Signals (local reactive state) ───────────────────────────────────────
|
|
5479
6364
|
activeTab = signal('', ...(ngDevMode ? [{ debugName: "activeTab" }] : /* istanbul ignore next */ []));
|
|
@@ -5499,9 +6384,12 @@ class DynamicFormComponent {
|
|
|
5499
6384
|
handleKeyboardEvent(event) {
|
|
5500
6385
|
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 's') {
|
|
5501
6386
|
event.preventDefault();
|
|
5502
|
-
|
|
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)
|
|
5503
6392
|
this.submit();
|
|
5504
|
-
}
|
|
5505
6393
|
}
|
|
5506
6394
|
}
|
|
5507
6395
|
// ─── Computed ─────────────────────────────────────────────────────────────
|
|
@@ -5652,10 +6540,35 @@ class DynamicFormComponent {
|
|
|
5652
6540
|
get isValidating() {
|
|
5653
6541
|
return this.form.pending;
|
|
5654
6542
|
}
|
|
5655
|
-
/**
|
|
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
|
+
*/
|
|
5656
6548
|
get submitBlocked() {
|
|
5657
6549
|
return this.form.invalid || this.form.pending || this.hasRuleErrors;
|
|
5658
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
|
+
}
|
|
5659
6572
|
ngOnInit() {
|
|
5660
6573
|
this.selectionSub = this.entityRefSelection.selection$.subscribe(({ fieldId, option }) => {
|
|
5661
6574
|
this.runAutoPatch(fieldId, option?.record);
|
|
@@ -5756,9 +6669,113 @@ class DynamicFormComponent {
|
|
|
5756
6669
|
this.focusActivePanel();
|
|
5757
6670
|
}
|
|
5758
6671
|
getFieldSpan(field) {
|
|
5759
|
-
const span = field.colSpan ?? 12;
|
|
6672
|
+
const span = field.colSpan ?? (this.layout === 'auto' ? (AUTO_COL_SPAN[field.type] ?? 12) : 12);
|
|
5760
6673
|
return `span ${Math.min(12, Math.max(1, span))}`;
|
|
5761
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
|
+
}
|
|
5762
6779
|
// ─── criticalField locking ────────────────────────────────────────────────
|
|
5763
6780
|
/** A critical field stays read-only until the user deliberately unlocks it. */
|
|
5764
6781
|
isFieldLocked(field) {
|
|
@@ -5791,8 +6808,15 @@ class DynamicFormComponent {
|
|
|
5791
6808
|
next.add(field.id);
|
|
5792
6809
|
this.unlockedFields.set(next);
|
|
5793
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
|
+
*/
|
|
5794
6818
|
resolveFieldLabel(field) {
|
|
5795
|
-
return resolveLabel(field.label, this.language);
|
|
6819
|
+
return resolveLabel(field.label, this.language) || field.id;
|
|
5796
6820
|
}
|
|
5797
6821
|
/**
|
|
5798
6822
|
* The changed critical fields as one comma-separated string.
|
|
@@ -5862,7 +6886,13 @@ class DynamicFormComponent {
|
|
|
5862
6886
|
// never re-enable. submitBlocked reads form.pending and form.invalid, so this is what
|
|
5863
6887
|
// keeps it honest.
|
|
5864
6888
|
this.statusSub?.unsubscribe();
|
|
5865
|
-
|
|
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
|
+
});
|
|
5866
6896
|
this.valueSub = changes$.subscribe(() => {
|
|
5867
6897
|
const flattened = this.flattenFormValues();
|
|
5868
6898
|
this.formValues.set(flattened);
|
|
@@ -6371,9 +7401,23 @@ class DynamicFormComponent {
|
|
|
6371
7401
|
}
|
|
6372
7402
|
async submit() {
|
|
6373
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
|
+
*/
|
|
6374
7412
|
this.markAllTouched();
|
|
7413
|
+
this.submitAttempted.set(true);
|
|
7414
|
+
const first = this.invalidFields()[0];
|
|
7415
|
+
if (first)
|
|
7416
|
+
this.jumpToField(first.field.id);
|
|
6375
7417
|
return;
|
|
6376
7418
|
}
|
|
7419
|
+
// Whatever the last attempt complained about is settled, so the summary goes away.
|
|
7420
|
+
this.submitAttempted.set(false);
|
|
6377
7421
|
const rawData = this.extractRecord();
|
|
6378
7422
|
let processedData = rawData;
|
|
6379
7423
|
this.isSaving.set(true);
|
|
@@ -6411,14 +7455,16 @@ class DynamicFormComponent {
|
|
|
6411
7455
|
this.formValues.set(values);
|
|
6412
7456
|
this.previousValues = { ...values };
|
|
6413
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);
|
|
6414
7460
|
this.formReset.emit();
|
|
6415
7461
|
}
|
|
6416
7462
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: DynamicFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
6417
|
-
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 });
|
|
6418
7464
|
}
|
|
6419
7465
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: DynamicFormComponent, decorators: [{
|
|
6420
7466
|
type: Component,
|
|
6421
|
-
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"] }]
|
|
6422
7468
|
}], propDecorators: { config: [{
|
|
6423
7469
|
type: Input
|
|
6424
7470
|
}], rules: [{
|
|
@@ -6445,6 +7491,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
6445
7491
|
type: Input
|
|
6446
7492
|
}], preview: [{
|
|
6447
7493
|
type: Input
|
|
7494
|
+
}], layout: [{
|
|
7495
|
+
type: Input
|
|
6448
7496
|
}], formSubmit: [{
|
|
6449
7497
|
type: Output
|
|
6450
7498
|
}], formChange: [{
|
|
@@ -6491,6 +7539,8 @@ class DynamicRecordFormComponent {
|
|
|
6491
7539
|
isReadOnly = false;
|
|
6492
7540
|
/** Specific field ids forced read-only while the rest of the record stays editable. */
|
|
6493
7541
|
readOnlyFields = [];
|
|
7542
|
+
/** Forwarded verbatim to the hosted form — see `DynamicFormComponent.layout`. */
|
|
7543
|
+
layout = 'stack';
|
|
6494
7544
|
/**
|
|
6495
7545
|
* Open the record read-only, with a per-tab "Edit section" flow — one tab is edited and
|
|
6496
7546
|
* validated at a time. This is the framework's `EntityRecordComponent` model and the
|
|
@@ -6517,8 +7567,6 @@ class DynamicRecordFormComponent {
|
|
|
6517
7567
|
rulesEvaluation = inject(RulesEvaluationService);
|
|
6518
7568
|
rbacService = inject(RbacService);
|
|
6519
7569
|
hookRegistry = inject(HookRegistryService);
|
|
6520
|
-
host = inject(ElementRef);
|
|
6521
|
-
injector = inject(Injector);
|
|
6522
7570
|
currentData = signal({}, ...(ngDevMode ? [{ debugName: "currentData" }] : /* istanbul ignore next */ []));
|
|
6523
7571
|
originalBaseline = signal({}, ...(ngDevMode ? [{ debugName: "originalBaseline" }] : /* istanbul ignore next */ []));
|
|
6524
7572
|
/**
|
|
@@ -6829,8 +7877,56 @@ class DynamicRecordFormComponent {
|
|
|
6829
7877
|
return 'Record';
|
|
6830
7878
|
return this.config.name ? resolveLabel(this.config.name, this.language) : this.config.entity;
|
|
6831
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
|
+
}
|
|
6832
7928
|
get avatarLetter() {
|
|
6833
|
-
return (this.
|
|
7929
|
+
return (this.recordHeading || 'R').charAt(0).toUpperCase();
|
|
6834
7930
|
}
|
|
6835
7931
|
// ─── Header fields (isProfileImage / isHeaderToggle) ────────────────────────
|
|
6836
7932
|
/** Every field in the config, at any depth. */
|
|
@@ -6974,61 +8070,24 @@ class DynamicRecordFormComponent {
|
|
|
6974
8070
|
* field. The walk used to check top-level `fields` only, so every field in a sub-tab was
|
|
6975
8071
|
* simply not found and the jump did nothing at all.
|
|
6976
8072
|
*/
|
|
6977
|
-
locateField(fieldId) {
|
|
6978
|
-
for (const tab of this.config?.tabs || []) {
|
|
6979
|
-
if ((tab.fields || []).some(f => f.id === fieldId))
|
|
6980
|
-
return { tabId: tab.id };
|
|
6981
|
-
for (const child of tab.children || []) {
|
|
6982
|
-
if ((child.fields || []).some(f => f.id === fieldId)) {
|
|
6983
|
-
return { tabId: tab.id, subTabId: child.id };
|
|
6984
|
-
}
|
|
6985
|
-
}
|
|
6986
|
-
}
|
|
6987
|
-
return null;
|
|
6988
|
-
}
|
|
6989
8073
|
/**
|
|
6990
8074
|
* Switches to the tab holding `fieldId`, then scrolls it into view and moves focus to it.
|
|
6991
8075
|
*
|
|
6992
|
-
*
|
|
6993
|
-
*
|
|
6994
|
-
*
|
|
6995
|
-
*
|
|
6996
|
-
*
|
|
6997
|
-
* destroying the component cancels it. The query is scoped to this component's own element,
|
|
6998
|
-
* 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.
|
|
6999
8081
|
*/
|
|
7000
8082
|
jumpToField(fieldId) {
|
|
7001
|
-
|
|
7002
|
-
if (!location)
|
|
7003
|
-
return;
|
|
7004
|
-
// The panel must not take the focus back: this jump is going to focus the field itself.
|
|
7005
|
-
this.dynamicFormComp?.setActiveTab(location.tabId, { focusPanel: false });
|
|
7006
|
-
// `setActiveTab` resets to a tab's first child, so the sub-tab has to be selected after
|
|
7007
|
-
// it, not before. Without this the walk found sub-tab fields and then rendered the wrong
|
|
7008
|
-
// panel, so the element the callback below looks for never existed.
|
|
7009
|
-
if (location.subTabId) {
|
|
7010
|
-
this.dynamicFormComp?.setActiveSubTab(location.subTabId, { focusPanel: false });
|
|
7011
|
-
}
|
|
7012
|
-
afterNextRender(() => {
|
|
7013
|
-
// The field id comes from config, so it never goes into a selector string: no
|
|
7014
|
-
// escaping to get wrong, and no need for `CSS.escape`, which jsdom and older
|
|
7015
|
-
// browsers do not provide and whose absence throws silently inside a render hook.
|
|
7016
|
-
const wanted = `field-container-${fieldId}`;
|
|
7017
|
-
const el = Array.from(this.host.nativeElement.querySelectorAll('[id^="field-container-"]')).find(slot => slot.id === wanted);
|
|
7018
|
-
if (!el)
|
|
7019
|
-
return;
|
|
7020
|
-
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
7021
|
-
// The slot carries tabindex="-1" so this actually moves focus. Without it the jump
|
|
7022
|
-
// scrolled the field into view and left focus behind on the link.
|
|
7023
|
-
el.focus();
|
|
7024
|
-
}, { injector: this.injector });
|
|
8083
|
+
this.dynamicFormComp?.jumpToField(fieldId);
|
|
7025
8084
|
}
|
|
7026
8085
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: DynamicRecordFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
7027
|
-
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", 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]=\"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 (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: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 });
|
|
7028
8087
|
}
|
|
7029
8088
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: DynamicRecordFormComponent, decorators: [{
|
|
7030
8089
|
type: Component,
|
|
7031
|
-
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 (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: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"] }]
|
|
7032
8091
|
}], propDecorators: { config: [{
|
|
7033
8092
|
type: Input
|
|
7034
8093
|
}], rules: [{
|
|
@@ -7049,6 +8108,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
7049
8108
|
type: Input
|
|
7050
8109
|
}], readOnlyFields: [{
|
|
7051
8110
|
type: Input
|
|
8111
|
+
}], layout: [{
|
|
8112
|
+
type: Input
|
|
7052
8113
|
}], viewMode: [{
|
|
7053
8114
|
type: Input
|
|
7054
8115
|
}], formSubmit: [{
|
|
@@ -7072,5 +8133,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
7072
8133
|
* Generated bundle index. Do not edit.
|
|
7073
8134
|
*/
|
|
7074
8135
|
|
|
7075
|
-
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 };
|
|
7076
8137
|
//# sourceMappingURL=ngx-dynamic-entity.mjs.map
|