@spartan-ng/cli 0.0.1-alpha.657 → 0.0.1-alpha.658
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/package.json +1 -1
- package/src/generators/healthcheck/generator.js +2 -0
- package/src/generators/healthcheck/generator.js.map +1 -1
- package/src/generators/healthcheck/healthchecks/hlm-form-field.d.ts +2 -0
- package/src/generators/healthcheck/healthchecks/hlm-form-field.js +35 -0
- package/src/generators/healthcheck/healthchecks/hlm-form-field.js.map +1 -0
- package/src/generators/migrate-form-field/compat.d.ts +2 -0
- package/src/generators/migrate-form-field/compat.js +6 -0
- package/src/generators/migrate-form-field/compat.js.map +1 -0
- package/src/generators/migrate-form-field/generator.d.ts +4 -0
- package/src/generators/migrate-form-field/generator.js +60 -0
- package/src/generators/migrate-form-field/generator.js.map +1 -0
- package/src/generators/migrate-form-field/schema.d.ts +4 -0
- package/src/generators/migrate-form-field/schema.json +19 -0
- package/src/generators/ui/libs/autocomplete/files/lib/hlm-autocomplete-input.ts.template +9 -4
- package/src/generators/ui/libs/badge/files/lib/hlm-badge.ts.template +7 -8
- package/src/generators/ui/libs/card/files/lib/hlm-card-action.ts.template +1 -1
- package/src/generators/ui/libs/card/files/lib/hlm-card-content.ts.template +1 -1
- package/src/generators/ui/libs/card/files/lib/hlm-card-description.ts.template +1 -1
- package/src/generators/ui/libs/card/files/lib/hlm-card-footer.ts.template +1 -4
- package/src/generators/ui/libs/card/files/lib/hlm-card-header.ts.template +1 -1
- package/src/generators/ui/libs/card/files/lib/hlm-card-title.ts.template +1 -1
- package/src/generators/ui/libs/card/files/lib/hlm-card.ts.template +1 -4
- package/src/generators/ui/libs/checkbox/files/lib/hlm-checkbox.ts.template +18 -2
- package/src/generators/ui/libs/combobox/files/lib/hlm-combobox-chip-input.ts.template +1 -1
- package/src/generators/ui/libs/combobox/files/lib/hlm-combobox-chips.ts.template +22 -7
- package/src/generators/ui/libs/combobox/files/lib/hlm-combobox-input.ts.template +8 -4
- package/src/generators/ui/libs/combobox/files/lib/hlm-combobox-trigger.ts.template +10 -1
- package/src/generators/ui/libs/date-picker/files/lib/hlm-date-picker-multi.ts.template +38 -5
- package/src/generators/ui/libs/date-picker/files/lib/hlm-date-picker.ts.template +36 -6
- package/src/generators/ui/libs/date-picker/files/lib/hlm-date-range-picker.ts.template +33 -4
- package/src/generators/ui/libs/field/files/index.ts.template +3 -3
- package/src/generators/ui/libs/field/files/lib/hlm-field-content.ts.template +1 -1
- package/src/generators/ui/libs/field/files/lib/hlm-field-description.ts.template +40 -2
- package/src/generators/ui/libs/field/files/lib/hlm-field-error.ts.template +89 -27
- package/src/generators/ui/libs/field/files/lib/hlm-field-group.ts.template +1 -1
- package/src/generators/ui/libs/field/files/lib/hlm-field-legend.ts.template +2 -2
- package/src/generators/ui/libs/field/files/lib/hlm-field.ts.template +9 -6
- package/src/generators/ui/libs/input/files/lib/hlm-input.ts.template +10 -69
- package/src/generators/ui/libs/input-group/files/lib/hlm-input-group.ts.template +15 -5
- package/src/generators/ui/libs/label/files/lib/hlm-label.ts.template +4 -1
- package/src/generators/ui/libs/native-select/files/lib/hlm-native-select.ts.template +32 -13
- package/src/generators/ui/libs/radio-group/files/lib/hlm-radio-group.ts.template +18 -2
- package/src/generators/ui/libs/radio-group/files/lib/hlm-radio.ts.template +16 -1
- package/src/generators/ui/libs/select/files/lib/hlm-select-trigger.ts.template +5 -2
- package/src/generators/ui/libs/slider/files/lib/hlm-slider.ts.template +3 -4
- package/src/generators/ui/libs/textarea/files/lib/hlm-textarea.ts.template +10 -71
- package/src/generators/ui/primitive-deps.js +0 -1
- package/src/generators/ui/primitive-deps.js.map +1 -1
- package/src/generators/ui/primitives.d.ts +1 -1
- package/src/generators/ui/style-lyra.css +16 -16
- package/src/generators/ui/style-maia.css +16 -16
- package/src/generators/ui/style-mira.css +16 -16
- package/src/generators/ui/style-nova.css +16 -16
- package/src/generators/ui/style-vega.css +16 -16
- package/src/generators/ui/supported-ui-libraries.json +54 -56
- package/src/generators/ui/libs/form-field/files/index.ts.template +0 -9
- package/src/generators/ui/libs/form-field/files/lib/hlm-error.ts.template +0 -12
- package/src/generators/ui/libs/form-field/files/lib/hlm-form-field.ts.template +0 -39
- package/src/generators/ui/libs/form-field/files/lib/hlm-hint.ts.template +0 -12
- package/src/generators/ui/libs/form-field/generator.d.ts +0 -3
- package/src/generators/ui/libs/form-field/generator.js +0 -9
- package/src/generators/ui/libs/form-field/generator.js.map +0 -1
|
@@ -5,18 +5,20 @@ import {
|
|
|
5
5
|
Component,
|
|
6
6
|
computed,
|
|
7
7
|
forwardRef,
|
|
8
|
+
inject,
|
|
8
9
|
input,
|
|
9
10
|
linkedSignal,
|
|
10
11
|
output,
|
|
11
12
|
signal,
|
|
12
13
|
} from '@angular/core';
|
|
13
14
|
import { type ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
|
14
|
-
import { provideIcons } from '@ng-icons/core';
|
|
15
|
+
import { NgIcon, provideIcons } from '@ng-icons/core';
|
|
15
16
|
import { lucideChevronDown } from '@ng-icons/lucide';
|
|
16
17
|
import type { BrnDialogState } from '@spartan-ng/brain/dialog';
|
|
18
|
+
import { BrnFieldControl, BrnFieldControlDescribedBy, provideBrnLabelable } from '@spartan-ng/brain/field';
|
|
17
19
|
import type { ChangeFn, TouchFn } from '@spartan-ng/brain/forms';
|
|
18
20
|
import { HlmCalendar } from '<%- importAlias %>/calendar';
|
|
19
|
-
import {
|
|
21
|
+
import { HlmIcon } from '<%- importAlias %>/icon';
|
|
20
22
|
import { HlmPopoverImports } from '<%- importAlias %>/popover';
|
|
21
23
|
import { hlm } from '<%- importAlias %>/utils';
|
|
22
24
|
import type { ClassValue } from 'clsx';
|
|
@@ -32,20 +34,32 @@ let nextId = 0;
|
|
|
32
34
|
|
|
33
35
|
@Component({
|
|
34
36
|
selector: 'hlm-date-picker',
|
|
35
|
-
imports: [
|
|
36
|
-
providers: [HLM_DATE_PICKER_VALUE_ACCESSOR, provideIcons({ lucideChevronDown })],
|
|
37
|
+
imports: [NgIcon, HlmIcon, BrnFieldControlDescribedBy, HlmPopoverImports, HlmCalendar],
|
|
38
|
+
providers: [HLM_DATE_PICKER_VALUE_ACCESSOR, provideIcons({ lucideChevronDown }), provideBrnLabelable(HlmDatePicker)],
|
|
37
39
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
40
|
+
hostDirectives: [BrnFieldControl],
|
|
38
41
|
host: {
|
|
39
42
|
class: 'block',
|
|
40
43
|
},
|
|
41
44
|
template: `
|
|
42
|
-
<hlm-popover
|
|
45
|
+
<hlm-popover
|
|
46
|
+
sideOffset="5"
|
|
47
|
+
[state]="_popoverState()"
|
|
48
|
+
(stateChanged)="_popoverState.set($event)"
|
|
49
|
+
(closed)="_onTouched?.()"
|
|
50
|
+
>
|
|
43
51
|
<button
|
|
44
52
|
[id]="buttonId()"
|
|
45
53
|
type="button"
|
|
46
54
|
[class]="_computedClass()"
|
|
47
55
|
[disabled]="_mutableDisabled()"
|
|
56
|
+
[attr.aria-invalid]="_ariaInvalid()"
|
|
57
|
+
[attr.data-invalid]="_ariaInvalid()"
|
|
58
|
+
[attr.data-touched]="_touched?.() ? 'true' : null"
|
|
59
|
+
[attr.data-dirty]="_dirty?.() ? 'true' : null"
|
|
60
|
+
[attr.data-matches-spartan-invalid]="_spartanInvalid?.() ? 'true' : null"
|
|
48
61
|
hlmPopoverTrigger
|
|
62
|
+
brnFieldControlDescribedBy
|
|
49
63
|
>
|
|
50
64
|
<span class="truncate">
|
|
51
65
|
@if (_formattedDate(); as formattedDate) {
|
|
@@ -74,14 +88,28 @@ let nextId = 0;
|
|
|
74
88
|
})
|
|
75
89
|
export class HlmDatePicker<T> implements ControlValueAccessor {
|
|
76
90
|
private readonly _config = injectHlmDatePickerConfig<T>();
|
|
91
|
+
private readonly _fieldControl = inject(BrnFieldControl, { optional: true });
|
|
92
|
+
|
|
93
|
+
private readonly _invalid = this._fieldControl?.invalid;
|
|
94
|
+
protected readonly _spartanInvalid = this._fieldControl?.spartanInvalid;
|
|
95
|
+
protected readonly _dirty = this._fieldControl?.dirty;
|
|
96
|
+
protected readonly _touched = this._fieldControl?.touched;
|
|
97
|
+
|
|
98
|
+
protected readonly _errorStateClass = computed(() =>
|
|
99
|
+
this._spartanInvalid?.()
|
|
100
|
+
? 'border-destructive focus-visible:border-destructive focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40'
|
|
101
|
+
: '',
|
|
102
|
+
);
|
|
103
|
+
protected readonly _ariaInvalid = computed(() => (this._invalid?.() ? 'true' : null));
|
|
77
104
|
|
|
78
105
|
public readonly userClass = input<ClassValue>('', { alias: 'class' });
|
|
79
106
|
protected readonly _computedClass = computed(() =>
|
|
80
107
|
hlm(
|
|
81
|
-
'ring-offset-background border-input bg-background hover:bg-accent dark:bg-input/30 dark:hover:bg-input/50
|
|
108
|
+
'ring-offset-background border-input bg-background hover:bg-accent dark:bg-input/30 dark:hover:bg-input/50 inline-flex h-9 w-[280px] cursor-default items-center justify-between gap-2 rounded-md border px-3 py-2 text-left text-sm font-normal whitespace-nowrap transition-all disabled:pointer-events-none disabled:opacity-50',
|
|
82
109
|
'focus-visible:ring-ring focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none',
|
|
83
110
|
'disabled:pointer-events-none disabled:opacity-50',
|
|
84
111
|
'[&_ng-icon]:pointer-events-none [&_ng-icon]:shrink-0',
|
|
112
|
+
this._errorStateClass(),
|
|
85
113
|
this.userClass(),
|
|
86
114
|
),
|
|
87
115
|
);
|
|
@@ -130,6 +158,8 @@ export class HlmDatePicker<T> implements ControlValueAccessor {
|
|
|
130
158
|
|
|
131
159
|
public readonly dateChange = output<T>();
|
|
132
160
|
|
|
161
|
+
public readonly labelableId = this.buttonId;
|
|
162
|
+
|
|
133
163
|
protected _onChange?: ChangeFn<T>;
|
|
134
164
|
protected _onTouched?: TouchFn;
|
|
135
165
|
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
Component,
|
|
6
6
|
computed,
|
|
7
7
|
forwardRef,
|
|
8
|
+
inject,
|
|
8
9
|
input,
|
|
9
10
|
linkedSignal,
|
|
10
11
|
output,
|
|
@@ -15,6 +16,7 @@ import { type ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
|
|
15
16
|
import { provideIcons } from '@ng-icons/core';
|
|
16
17
|
import { lucideChevronDown } from '@ng-icons/lucide';
|
|
17
18
|
import type { BrnDialogState } from '@spartan-ng/brain/dialog';
|
|
19
|
+
import { BrnFieldControl, BrnFieldControlDescribedBy, provideBrnLabelable } from '@spartan-ng/brain/field';
|
|
18
20
|
import type { ChangeFn, TouchFn } from '@spartan-ng/brain/forms';
|
|
19
21
|
import { HlmCalendarRange } from '<%- importAlias %>/calendar';
|
|
20
22
|
import { HlmIconImports } from '<%- importAlias %>/icon';
|
|
@@ -33,9 +35,14 @@ let nextId = 0;
|
|
|
33
35
|
|
|
34
36
|
@Component({
|
|
35
37
|
selector: 'hlm-date-range-picker',
|
|
36
|
-
imports: [HlmIconImports, HlmPopoverImports, HlmCalendarRange],
|
|
37
|
-
providers: [
|
|
38
|
+
imports: [HlmIconImports, HlmPopoverImports, HlmCalendarRange, BrnFieldControlDescribedBy],
|
|
39
|
+
providers: [
|
|
40
|
+
HLM_DATE_RANGE_PICKER_VALUE_ACCESSOR,
|
|
41
|
+
provideIcons({ lucideChevronDown }),
|
|
42
|
+
provideBrnLabelable(HlmDateRangePicker),
|
|
43
|
+
],
|
|
38
44
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
45
|
+
hostDirectives: [BrnFieldControl],
|
|
39
46
|
host: {
|
|
40
47
|
class: 'block',
|
|
41
48
|
},
|
|
@@ -44,14 +51,20 @@ let nextId = 0;
|
|
|
44
51
|
sideOffset="5"
|
|
45
52
|
[state]="_popoverState()"
|
|
46
53
|
(stateChanged)="_popoverState.set($event)"
|
|
47
|
-
(closed)="_onClose()"
|
|
54
|
+
(closed)="_onClose(); _onTouched?.()"
|
|
48
55
|
>
|
|
49
56
|
<button
|
|
50
57
|
[id]="buttonId()"
|
|
51
58
|
type="button"
|
|
52
59
|
[class]="_computedClass()"
|
|
53
60
|
[disabled]="_mutableDisabled()"
|
|
61
|
+
[attr.aria-invalid]="_ariaInvalid()"
|
|
62
|
+
[attr.data-invalid]="_ariaInvalid()"
|
|
63
|
+
[attr.data-touched]="_touched?.() ? 'true' : null"
|
|
64
|
+
[attr.data-dirty]="_dirty?.() ? 'true' : null"
|
|
65
|
+
[attr.data-matches-spartan-invalid]="_spartanInvalid?.() ? 'true' : null"
|
|
54
66
|
hlmPopoverTrigger
|
|
67
|
+
brnFieldControlDescribedBy
|
|
55
68
|
>
|
|
56
69
|
<span class="truncate">
|
|
57
70
|
@if (_formattedDate(); as formattedDate) {
|
|
@@ -82,14 +95,28 @@ let nextId = 0;
|
|
|
82
95
|
})
|
|
83
96
|
export class HlmDateRangePicker<T> implements ControlValueAccessor {
|
|
84
97
|
private readonly _config = injectHlmDateRangePickerConfig<T>();
|
|
98
|
+
private readonly _fieldControl = inject(BrnFieldControl, { optional: true });
|
|
99
|
+
|
|
100
|
+
protected readonly _spartanInvalid = this._fieldControl?.spartanInvalid;
|
|
101
|
+
protected readonly _touched = this._fieldControl?.touched;
|
|
102
|
+
protected readonly _dirty = this._fieldControl?.dirty;
|
|
103
|
+
protected readonly _invalid = this._fieldControl?.invalid;
|
|
104
|
+
|
|
105
|
+
protected readonly _errorStateClass = computed(() =>
|
|
106
|
+
this._spartanInvalid?.()
|
|
107
|
+
? 'border-destructive focus-visible:border-destructive focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40'
|
|
108
|
+
: '',
|
|
109
|
+
);
|
|
110
|
+
protected readonly _ariaInvalid = computed(() => (this._invalid?.() ? 'true' : null));
|
|
85
111
|
|
|
86
112
|
public readonly userClass = input<ClassValue>('', { alias: 'class' });
|
|
87
113
|
protected readonly _computedClass = computed(() =>
|
|
88
114
|
hlm(
|
|
89
|
-
'ring-offset-background border-input bg-background hover:bg-accent dark:bg-input/30 dark:hover:bg-input/50
|
|
115
|
+
'ring-offset-background border-input bg-background hover:bg-accent dark:bg-input/30 dark:hover:bg-input/50 inline-flex h-9 w-[280px] cursor-default items-center justify-between gap-2 rounded-md border px-3 py-2 text-left text-sm font-normal whitespace-nowrap transition-all disabled:pointer-events-none disabled:opacity-50',
|
|
90
116
|
'focus-visible:ring-ring focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none',
|
|
91
117
|
'disabled:pointer-events-none disabled:opacity-50',
|
|
92
118
|
'[&_ng-icon]:pointer-events-none [&_ng-icon]:shrink-0',
|
|
119
|
+
this._errorStateClass(),
|
|
93
120
|
this.userClass(),
|
|
94
121
|
),
|
|
95
122
|
);
|
|
@@ -142,6 +169,8 @@ export class HlmDateRangePicker<T> implements ControlValueAccessor {
|
|
|
142
169
|
|
|
143
170
|
public readonly dateChange = output<[T, T] | null>();
|
|
144
171
|
|
|
172
|
+
public readonly labelableId = this.buttonId;
|
|
173
|
+
|
|
145
174
|
protected _onChange?: ChangeFn<[T, T] | null>;
|
|
146
175
|
protected _onTouched?: TouchFn;
|
|
147
176
|
|
|
@@ -22,13 +22,13 @@ export * from './lib/hlm-field-title';
|
|
|
22
22
|
|
|
23
23
|
export const HlmFieldImports = [
|
|
24
24
|
HlmField,
|
|
25
|
-
HlmFieldTitle,
|
|
26
25
|
HlmFieldContent,
|
|
27
26
|
HlmFieldDescription,
|
|
28
27
|
HlmFieldError,
|
|
29
|
-
HlmFieldLabel,
|
|
30
|
-
HlmFieldSeparator,
|
|
31
28
|
HlmFieldGroup,
|
|
29
|
+
HlmFieldLabel,
|
|
32
30
|
HlmFieldLegend,
|
|
31
|
+
HlmFieldSeparator,
|
|
33
32
|
HlmFieldSet,
|
|
33
|
+
HlmFieldTitle,
|
|
34
34
|
] as const;
|
|
@@ -9,6 +9,6 @@ import { classes } from '<%- importAlias %>/utils';
|
|
|
9
9
|
})
|
|
10
10
|
export class HlmFieldContent {
|
|
11
11
|
constructor() {
|
|
12
|
-
classes(() => 'group/field-content flex flex-1 flex-col gap-1
|
|
12
|
+
classes(() => 'group/field-content flex flex-1 flex-col gap-1 leading-snug');
|
|
13
13
|
}
|
|
14
14
|
}
|
|
@@ -1,18 +1,56 @@
|
|
|
1
|
-
import { Directive } from '@angular/core';
|
|
1
|
+
import { Directive, effect, EffectRef, inject, input, OnDestroy } from '@angular/core';
|
|
2
|
+
import { BrnFieldA11yService } from '@spartan-ng/brain/field';
|
|
2
3
|
import { classes } from '<%- importAlias %>/utils';
|
|
4
|
+
import type { ClassValue } from 'clsx';
|
|
3
5
|
|
|
4
6
|
@Directive({
|
|
5
7
|
selector: '[hlmFieldDescription],hlm-field-description',
|
|
6
8
|
host: {
|
|
7
9
|
'data-slot': 'field-description',
|
|
10
|
+
'[attr.id]': 'id()',
|
|
8
11
|
},
|
|
9
12
|
})
|
|
10
|
-
export class HlmFieldDescription {
|
|
13
|
+
export class HlmFieldDescription implements OnDestroy {
|
|
14
|
+
private static _id = 0;
|
|
15
|
+
|
|
16
|
+
private readonly _a11y = inject(BrnFieldA11yService, { optional: true, host: true });
|
|
17
|
+
|
|
18
|
+
public readonly userClass = input<ClassValue>('', { alias: 'class' });
|
|
19
|
+
public readonly id = input<string>(`hlm-field-description-${HlmFieldDescription._id++}`);
|
|
20
|
+
|
|
21
|
+
private _registeredId?: string;
|
|
22
|
+
|
|
23
|
+
private readonly _cleanup: EffectRef | null = this._a11y
|
|
24
|
+
? effect(() => {
|
|
25
|
+
const a11y = this._a11y;
|
|
26
|
+
if (!a11y) return;
|
|
27
|
+
|
|
28
|
+
const id = this.id();
|
|
29
|
+
if (this._registeredId && this._registeredId !== id) {
|
|
30
|
+
a11y.unregisterDescription(this._registeredId);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (this._registeredId !== id) {
|
|
34
|
+
a11y.registerDescription(id);
|
|
35
|
+
this._registeredId = id;
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
: null;
|
|
39
|
+
|
|
11
40
|
constructor() {
|
|
12
41
|
classes(() => [
|
|
13
42
|
'text-muted-foreground text-sm leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance',
|
|
14
43
|
'last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5',
|
|
15
44
|
'[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4',
|
|
45
|
+
this.userClass(),
|
|
16
46
|
]);
|
|
17
47
|
}
|
|
48
|
+
|
|
49
|
+
ngOnDestroy() {
|
|
50
|
+
this._cleanup?.destroy();
|
|
51
|
+
|
|
52
|
+
if (this._registeredId) {
|
|
53
|
+
this._a11y?.unregisterDescription(this._registeredId);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
18
56
|
}
|
|
@@ -1,40 +1,102 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
import { BooleanInput } from '@angular/cdk/coercion';
|
|
2
|
+
import {
|
|
3
|
+
booleanAttribute,
|
|
4
|
+
ChangeDetectionStrategy,
|
|
5
|
+
Component,
|
|
6
|
+
computed,
|
|
7
|
+
effect,
|
|
8
|
+
EffectRef,
|
|
9
|
+
inject,
|
|
10
|
+
input,
|
|
11
|
+
OnDestroy,
|
|
12
|
+
} from '@angular/core';
|
|
13
|
+
import { BrnField, BrnFieldA11yService } from '@spartan-ng/brain/field';
|
|
14
|
+
import { classes } from '<%- importAlias %>/utils';
|
|
15
|
+
import { ClassValue } from 'clsx';
|
|
4
16
|
|
|
5
17
|
@Component({
|
|
6
18
|
selector: 'hlm-field-error',
|
|
7
19
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
20
|
+
host: {
|
|
21
|
+
role: 'alert',
|
|
22
|
+
'data-slot': 'field-error',
|
|
23
|
+
'[attr.id]': 'id()',
|
|
24
|
+
'[hidden]': '!_display()',
|
|
25
|
+
},
|
|
8
26
|
template: `
|
|
9
|
-
|
|
10
|
-
<ng-content
|
|
11
|
-
|
|
12
|
-
{{ _uniqueErrors()[0]?.message }}
|
|
13
|
-
} @else if (_uniqueErrors().length > 1) {
|
|
14
|
-
<ul class="ml-4 flex list-disc flex-col gap-1">
|
|
15
|
-
@for (error of _uniqueErrors(); track $index) {
|
|
16
|
-
@if (error?.message) {
|
|
17
|
-
<li>{{ error?.message }}</li>
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
</ul>
|
|
21
|
-
}
|
|
22
|
-
</ng-content>
|
|
23
|
-
</div>
|
|
27
|
+
@if (_display()) {
|
|
28
|
+
<ng-content />
|
|
29
|
+
}
|
|
24
30
|
`,
|
|
25
31
|
})
|
|
26
|
-
export class HlmFieldError {
|
|
32
|
+
export class HlmFieldError implements OnDestroy {
|
|
33
|
+
private static _id = 0;
|
|
34
|
+
|
|
35
|
+
private readonly _field = inject(BrnField, { optional: true });
|
|
36
|
+
private readonly _a11y = inject(BrnFieldA11yService, { optional: true, host: true });
|
|
37
|
+
|
|
38
|
+
private _registeredId?: string;
|
|
39
|
+
|
|
40
|
+
private readonly _hasParentField = !!this._field;
|
|
41
|
+
|
|
42
|
+
/** The unique ID for the field error. If none is supplied, it will be auto-generated. */
|
|
43
|
+
public readonly id = input<string>(`hlm-field-error-${HlmFieldError._id++}`);
|
|
44
|
+
|
|
45
|
+
/** Additional CSS classes to apply to the field error element. */
|
|
27
46
|
public readonly userClass = input<ClassValue>('', { alias: 'class' });
|
|
28
|
-
public readonly error = input<Array<{ message: string } | undefined>>();
|
|
29
47
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
48
|
+
/**
|
|
49
|
+
* The name of the specific validator error key to match (e.g. 'required').
|
|
50
|
+
* When omitted, the error is shown if any validation error is present.
|
|
51
|
+
*/
|
|
52
|
+
public readonly validator = input<string>();
|
|
53
|
+
|
|
54
|
+
/** Forces the error message to be visible regardless of the control's validation state. */
|
|
55
|
+
public readonly forceShow = input<boolean, BooleanInput>(false, { transform: booleanAttribute });
|
|
56
|
+
|
|
57
|
+
protected readonly _display = computed(() => !this._hasParentField || this.forceShow() || this._hasError());
|
|
58
|
+
|
|
59
|
+
protected readonly _hasError = computed(() => {
|
|
60
|
+
const errors = this._field?.errors();
|
|
61
|
+
if (!errors) return false;
|
|
35
62
|
|
|
36
|
-
|
|
63
|
+
const validator = this.validator();
|
|
64
|
+
const spartanInvalid = this._field?.controlState()?.spartanInvalid;
|
|
65
|
+
|
|
66
|
+
if (!spartanInvalid) return false;
|
|
67
|
+
|
|
68
|
+
return validator ? validator in errors : Object.keys(errors).length > 0;
|
|
37
69
|
});
|
|
38
70
|
|
|
39
|
-
|
|
71
|
+
private readonly _cleanup: EffectRef | null = this._a11y
|
|
72
|
+
? effect(() => {
|
|
73
|
+
const a11y = this._a11y;
|
|
74
|
+
if (!a11y) return;
|
|
75
|
+
|
|
76
|
+
const id = this.id();
|
|
77
|
+
const hasError = this._hasError();
|
|
78
|
+
|
|
79
|
+
if (this._registeredId && (this._registeredId !== id || !hasError)) {
|
|
80
|
+
a11y.unregisterError(this._registeredId);
|
|
81
|
+
this._registeredId = undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (hasError && this._registeredId !== id) {
|
|
85
|
+
a11y.registerError(id);
|
|
86
|
+
this._registeredId = id;
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
: null;
|
|
90
|
+
|
|
91
|
+
constructor() {
|
|
92
|
+
classes(() => ['text-destructive text-sm font-normal', this.userClass()]);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
ngOnDestroy() {
|
|
96
|
+
this._cleanup?.destroy();
|
|
97
|
+
|
|
98
|
+
if (this._registeredId) {
|
|
99
|
+
this._a11y?.unregisterError(this._registeredId);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
40
102
|
}
|
|
@@ -11,7 +11,7 @@ export class HlmFieldGroup {
|
|
|
11
11
|
constructor() {
|
|
12
12
|
classes(
|
|
13
13
|
() =>
|
|
14
|
-
'group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3
|
|
14
|
+
'group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 *:data-[slot=field-group]:gap-4',
|
|
15
15
|
);
|
|
16
16
|
}
|
|
17
17
|
}
|
|
@@ -9,9 +9,9 @@ import { classes } from '<%- importAlias %>/utils';
|
|
|
9
9
|
},
|
|
10
10
|
})
|
|
11
11
|
export class HlmFieldLegend {
|
|
12
|
+
public readonly variant = input<'label' | 'legend'>('legend');
|
|
13
|
+
|
|
12
14
|
constructor() {
|
|
13
15
|
classes(() => 'mb-3 font-medium data-[variant=label]:text-sm data-[variant=legend]:text-base');
|
|
14
16
|
}
|
|
15
|
-
|
|
16
|
-
public readonly variant = input<'label' | 'legend'>('legend');
|
|
17
17
|
}
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
import { Directive, input } from '@angular/core';
|
|
2
|
+
import { BrnField } from '@spartan-ng/brain/field';
|
|
2
3
|
import { classes } from '<%- importAlias %>/utils';
|
|
3
|
-
import { cva,
|
|
4
|
+
import { cva, VariantProps } from 'class-variance-authority';
|
|
4
5
|
|
|
5
|
-
const fieldVariants = cva('group/field data-[invalid=true]:text-destructive flex w-full gap-3', {
|
|
6
|
+
const fieldVariants = cva('group/field data-[matches-spartan-invalid=true]:text-destructive flex w-full gap-3', {
|
|
6
7
|
variants: {
|
|
7
8
|
orientation: {
|
|
8
|
-
vertical:
|
|
9
|
+
vertical: 'flex-col *:w-full [&>.sr-only]:w-auto',
|
|
9
10
|
horizontal: [
|
|
10
11
|
'flex-row items-center',
|
|
11
|
-
'
|
|
12
|
+
'*:data-[slot=field-label]:flex-auto',
|
|
12
13
|
'has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px',
|
|
13
14
|
],
|
|
14
15
|
responsive: [
|
|
15
|
-
'flex-col @md/field-group:flex-row @md/field-group:items-center
|
|
16
|
-
'@md/field-group
|
|
16
|
+
'flex-col *:w-full @md/field-group:flex-row @md/field-group:items-center @md/field-group:*:w-auto [&>.sr-only]:w-auto',
|
|
17
|
+
'@md/field-group:*:data-[slot=field-label]:flex-auto',
|
|
17
18
|
'@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px',
|
|
18
19
|
],
|
|
19
20
|
},
|
|
@@ -27,6 +28,7 @@ export type FieldVariants = VariantProps<typeof fieldVariants>;
|
|
|
27
28
|
|
|
28
29
|
@Directive({
|
|
29
30
|
selector: '[hlmField],hlm-field',
|
|
31
|
+
hostDirectives: [{ directive: BrnField, inputs: ['data-invalid', 'forceInvalid'] }],
|
|
30
32
|
host: {
|
|
31
33
|
role: 'group',
|
|
32
34
|
'data-slot': 'field',
|
|
@@ -35,6 +37,7 @@ export type FieldVariants = VariantProps<typeof fieldVariants>;
|
|
|
35
37
|
})
|
|
36
38
|
export class HlmField {
|
|
37
39
|
public readonly orientation = input<FieldVariants['orientation']>('vertical');
|
|
40
|
+
|
|
38
41
|
constructor() {
|
|
39
42
|
classes(() => fieldVariants({ orientation: this.orientation() }));
|
|
40
43
|
}
|
|
@@ -1,29 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
effect,
|
|
5
|
-
forwardRef,
|
|
6
|
-
inject,
|
|
7
|
-
Injector,
|
|
8
|
-
input,
|
|
9
|
-
linkedSignal,
|
|
10
|
-
signal,
|
|
11
|
-
untracked,
|
|
12
|
-
type DoCheck,
|
|
13
|
-
} from '@angular/core';
|
|
14
|
-
import { FormGroupDirective, NgControl, NgForm } from '@angular/forms';
|
|
15
|
-
import { BrnFormFieldControl } from '@spartan-ng/brain/form-field';
|
|
16
|
-
import { ErrorStateMatcher, ErrorStateTracker } from '@spartan-ng/brain/forms';
|
|
1
|
+
import { Directive, input, linkedSignal } from '@angular/core';
|
|
2
|
+
import { BrnFieldControlDescribedBy } from '@spartan-ng/brain/field';
|
|
3
|
+
import { BrnInput } from '@spartan-ng/brain/input';
|
|
17
4
|
import { classes } from '<%- importAlias %>/utils';
|
|
18
5
|
import { cva, type VariantProps } from 'class-variance-authority';
|
|
19
|
-
import type { ClassValue } from 'clsx';
|
|
20
6
|
|
|
21
7
|
export const inputVariants = cva(
|
|
22
8
|
'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-[3px] disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
|
23
9
|
{
|
|
24
10
|
variants: {
|
|
25
11
|
error: {
|
|
26
|
-
auto: '[
|
|
12
|
+
auto: 'data-[matches-spartan-invalid=true]:border-destructive data-[matches-spartan-invalid=true]:ring-destructive/20 dark:data-[matches-spartan-invalid=true]:ring-destructive/40',
|
|
27
13
|
true: 'border-destructive focus-visible:border-destructive focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40',
|
|
28
14
|
},
|
|
29
15
|
},
|
|
@@ -36,62 +22,17 @@ type InputVariants = VariantProps<typeof inputVariants>;
|
|
|
36
22
|
|
|
37
23
|
@Directive({
|
|
38
24
|
selector: '[hlmInput]',
|
|
39
|
-
|
|
40
|
-
{
|
|
41
|
-
provide: BrnFormFieldControl,
|
|
42
|
-
useExisting: forwardRef(() => HlmInput),
|
|
43
|
-
},
|
|
44
|
-
],
|
|
25
|
+
hostDirectives: [{ directive: BrnInput, inputs: ['id'] }, BrnFieldControlDescribedBy],
|
|
45
26
|
})
|
|
46
|
-
export class HlmInput
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
private readonly _errorStateTracker: ErrorStateTracker;
|
|
51
|
-
|
|
52
|
-
private readonly _defaultErrorStateMatcher = inject(ErrorStateMatcher);
|
|
53
|
-
private readonly _parentForm = inject(NgForm, { optional: true });
|
|
54
|
-
private readonly _parentFormGroup = inject(FormGroupDirective, { optional: true });
|
|
55
|
-
|
|
27
|
+
export class HlmInput {
|
|
28
|
+
/** Controls the error visual state of the input.
|
|
29
|
+
* Defaults to 'auto', which infers the state from the associated form control.
|
|
30
|
+
*/
|
|
56
31
|
public readonly error = input<InputVariants['error']>('auto');
|
|
57
32
|
|
|
58
33
|
protected readonly _state = linkedSignal(() => ({ error: this.error() }));
|
|
59
34
|
|
|
60
|
-
public readonly ngControl: NgControl | null = this._injector.get(NgControl, null);
|
|
61
|
-
|
|
62
|
-
public readonly errorState = computed(() => this._errorStateTracker.errorState());
|
|
63
|
-
|
|
64
35
|
constructor() {
|
|
65
|
-
|
|
66
|
-
this._defaultErrorStateMatcher,
|
|
67
|
-
this.ngControl,
|
|
68
|
-
this._parentFormGroup,
|
|
69
|
-
this._parentForm,
|
|
70
|
-
);
|
|
71
|
-
|
|
72
|
-
classes(() => [inputVariants({ error: this._state().error }), this._additionalClasses()]);
|
|
73
|
-
|
|
74
|
-
effect(() => {
|
|
75
|
-
const error = this._errorStateTracker.errorState();
|
|
76
|
-
untracked(() => {
|
|
77
|
-
if (this.ngControl) {
|
|
78
|
-
const shouldShowError = error && this.ngControl.invalid && (this.ngControl.touched || this.ngControl.dirty);
|
|
79
|
-
this._errorStateTracker.errorState.set(shouldShowError ? true : false);
|
|
80
|
-
this.setError(shouldShowError ? true : 'auto');
|
|
81
|
-
}
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
ngDoCheck() {
|
|
87
|
-
this._errorStateTracker.updateErrorState();
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
setError(error: InputVariants['error']) {
|
|
91
|
-
this._state.set({ error });
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
setClass(classes: string): void {
|
|
95
|
-
this._additionalClasses.set(classes);
|
|
36
|
+
classes(() => inputVariants({ error: this._state().error }));
|
|
96
37
|
}
|
|
97
38
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { Directive } from '@angular/core';
|
|
1
|
+
import { computed, contentChild, Directive, inject } from '@angular/core';
|
|
2
|
+
import { BrnFieldControl } from '@spartan-ng/brain/field';
|
|
2
3
|
import { classes } from '<%- importAlias %>/utils';
|
|
3
4
|
|
|
4
5
|
@Directive({
|
|
@@ -9,6 +10,15 @@ import { classes } from '<%- importAlias %>/utils';
|
|
|
9
10
|
},
|
|
10
11
|
})
|
|
11
12
|
export class HlmInputGroup {
|
|
13
|
+
private readonly _fieldControl = inject(BrnFieldControl, { optional: true });
|
|
14
|
+
private readonly _fieldControlChild = contentChild(BrnFieldControl);
|
|
15
|
+
|
|
16
|
+
private readonly _spartanInvalid = computed(() => {
|
|
17
|
+
if (this._fieldControl) return this._fieldControl.spartanInvalid();
|
|
18
|
+
|
|
19
|
+
return this._fieldControlChild()?.spartanInvalid();
|
|
20
|
+
});
|
|
21
|
+
|
|
12
22
|
constructor() {
|
|
13
23
|
classes(() => [
|
|
14
24
|
'group/input-group border-input dark:bg-input/30 relative flex w-full items-center rounded-md border shadow-xs transition-[color,box-shadow] outline-none',
|
|
@@ -18,10 +28,10 @@ export class HlmInputGroup {
|
|
|
18
28
|
'has-[>[data-align=inline-end]]:[&>input]:pe-2',
|
|
19
29
|
'has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3',
|
|
20
30
|
'has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3',
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
31
|
+
'has-[[data-slot=input-group-control]:focus-visible]:ring-[3px]',
|
|
32
|
+
this._spartanInvalid?.()
|
|
33
|
+
? 'has-[>[data-matches-spartan-invalid=true]]:ring-destructive/20 has-[>[data-matches-spartan-invalid=true]]:border-destructive dark:has-[>[data-matches-spartan-invalid=true]]:ring-destructive/40'
|
|
34
|
+
: 'has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50',
|
|
25
35
|
]);
|
|
26
36
|
}
|
|
27
37
|
}
|