ngx-com 0.0.1 → 0.0.4
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-com-components-avatar.mjs +772 -0
- package/fesm2022/ngx-com-components-avatar.mjs.map +1 -0
- package/fesm2022/ngx-com-components-badge.mjs +138 -0
- package/fesm2022/ngx-com-components-badge.mjs.map +1 -0
- package/fesm2022/ngx-com-components-button.mjs +146 -0
- package/fesm2022/ngx-com-components-button.mjs.map +1 -0
- package/fesm2022/ngx-com-components-calendar.mjs +5046 -0
- package/fesm2022/ngx-com-components-calendar.mjs.map +1 -0
- package/fesm2022/ngx-com-components-card.mjs +590 -0
- package/fesm2022/ngx-com-components-card.mjs.map +1 -0
- package/fesm2022/ngx-com-components-checkbox.mjs +344 -0
- package/fesm2022/ngx-com-components-checkbox.mjs.map +1 -0
- package/fesm2022/ngx-com-components-collapsible.mjs +612 -0
- package/fesm2022/ngx-com-components-collapsible.mjs.map +1 -0
- package/fesm2022/ngx-com-components-confirm.mjs +562 -0
- package/fesm2022/ngx-com-components-confirm.mjs.map +1 -0
- package/fesm2022/ngx-com-components-dropdown-testing.mjs +255 -0
- package/fesm2022/ngx-com-components-dropdown-testing.mjs.map +1 -0
- package/fesm2022/ngx-com-components-dropdown.mjs +2692 -0
- package/fesm2022/ngx-com-components-dropdown.mjs.map +1 -0
- package/fesm2022/ngx-com-components-empty-state.mjs +382 -0
- package/fesm2022/ngx-com-components-empty-state.mjs.map +1 -0
- package/fesm2022/ngx-com-components-form-field.mjs +924 -0
- package/fesm2022/ngx-com-components-form-field.mjs.map +1 -0
- package/fesm2022/ngx-com-components-icon.mjs +183 -0
- package/fesm2022/ngx-com-components-icon.mjs.map +1 -0
- package/fesm2022/ngx-com-components-item.mjs +578 -0
- package/fesm2022/ngx-com-components-item.mjs.map +1 -0
- package/fesm2022/ngx-com-components-menu.mjs +1200 -0
- package/fesm2022/ngx-com-components-menu.mjs.map +1 -0
- package/fesm2022/ngx-com-components-paginator.mjs +823 -0
- package/fesm2022/ngx-com-components-paginator.mjs.map +1 -0
- package/fesm2022/ngx-com-components-popover.mjs +901 -0
- package/fesm2022/ngx-com-components-popover.mjs.map +1 -0
- package/fesm2022/ngx-com-components-radio.mjs +621 -0
- package/fesm2022/ngx-com-components-radio.mjs.map +1 -0
- package/fesm2022/ngx-com-components-segmented-control.mjs +538 -0
- package/fesm2022/ngx-com-components-segmented-control.mjs.map +1 -0
- package/fesm2022/ngx-com-components-sort.mjs +368 -0
- package/fesm2022/ngx-com-components-sort.mjs.map +1 -0
- package/fesm2022/ngx-com-components-spinner.mjs +189 -0
- package/fesm2022/ngx-com-components-spinner.mjs.map +1 -0
- package/fesm2022/ngx-com-components-tabs.mjs +1522 -0
- package/fesm2022/ngx-com-components-tabs.mjs.map +1 -0
- package/fesm2022/ngx-com-components-tooltip.mjs +625 -0
- package/fesm2022/ngx-com-components-tooltip.mjs.map +1 -0
- package/fesm2022/ngx-com-components.mjs +17 -0
- package/fesm2022/ngx-com-components.mjs.map +1 -0
- package/fesm2022/ngx-com-tokens.mjs +12 -0
- package/fesm2022/ngx-com-tokens.mjs.map +1 -0
- package/fesm2022/ngx-com-utils.mjs +601 -0
- package/fesm2022/ngx-com-utils.mjs.map +1 -0
- package/fesm2022/ngx-com.mjs +9 -23
- package/fesm2022/ngx-com.mjs.map +1 -1
- package/package.json +105 -1
- package/types/ngx-com-components-avatar.d.ts +409 -0
- package/types/ngx-com-components-badge.d.ts +97 -0
- package/types/ngx-com-components-button.d.ts +69 -0
- package/types/ngx-com-components-calendar.d.ts +1665 -0
- package/types/ngx-com-components-card.d.ts +373 -0
- package/types/ngx-com-components-checkbox.d.ts +116 -0
- package/types/ngx-com-components-collapsible.d.ts +379 -0
- package/types/ngx-com-components-confirm.d.ts +160 -0
- package/types/ngx-com-components-dropdown-testing.d.ts +116 -0
- package/types/ngx-com-components-dropdown.d.ts +938 -0
- package/types/ngx-com-components-empty-state.d.ts +269 -0
- package/types/ngx-com-components-form-field.d.ts +531 -0
- package/types/ngx-com-components-icon.d.ts +94 -0
- package/types/ngx-com-components-item.d.ts +336 -0
- package/types/ngx-com-components-menu.d.ts +479 -0
- package/types/ngx-com-components-paginator.d.ts +265 -0
- package/types/ngx-com-components-popover.d.ts +309 -0
- package/types/ngx-com-components-radio.d.ts +258 -0
- package/types/ngx-com-components-segmented-control.d.ts +274 -0
- package/types/ngx-com-components-sort.d.ts +133 -0
- package/types/ngx-com-components-spinner.d.ts +120 -0
- package/types/ngx-com-components-tabs.d.ts +396 -0
- package/types/ngx-com-components-tooltip.d.ts +200 -0
- package/types/ngx-com-components.d.ts +12 -0
- package/types/ngx-com-tokens.d.ts +7 -0
- package/types/ngx-com-utils.d.ts +424 -0
- package/types/ngx-com.d.ts +10 -7
|
@@ -0,0 +1,924 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { signal, Directive, input, computed, Injectable, inject, ElementRef, DestroyRef, booleanAttribute, forwardRef, InjectionToken, contentChild, contentChildren, effect, ViewEncapsulation, ChangeDetectionStrategy, Component } from '@angular/core';
|
|
3
|
+
import { cva } from 'class-variance-authority';
|
|
4
|
+
import { mergeClasses } from 'ngx-com/utils';
|
|
5
|
+
import { NgForm, FormGroupDirective, NgControl } from '@angular/forms';
|
|
6
|
+
import { AutofillMonitor } from '@angular/cdk/text-field';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Contract that any control inside a form field must implement.
|
|
10
|
+
*
|
|
11
|
+
* This allows the form field to read state from any inner control
|
|
12
|
+
* (input, textarea, custom controls) without knowing implementation details.
|
|
13
|
+
*
|
|
14
|
+
* @example Implementing for a custom phone input
|
|
15
|
+
* ```ts
|
|
16
|
+
* @Directive({
|
|
17
|
+
* selector: 'com-phone-input',
|
|
18
|
+
* providers: [{ provide: FormFieldControl, useExisting: PhoneInputComponent }],
|
|
19
|
+
* })
|
|
20
|
+
* export class PhoneInputComponent extends FormFieldControl<string> {
|
|
21
|
+
* // ... implement all abstract members
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
class FormFieldControl {
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Auto-incrementing ID counter. */
|
|
29
|
+
let nextId$3 = 0;
|
|
30
|
+
/**
|
|
31
|
+
* Directive to mark the label element inside a form field.
|
|
32
|
+
*
|
|
33
|
+
* The form field automatically associates this label with the inner control
|
|
34
|
+
* and positions it appropriately based on appearance and float state.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```html
|
|
38
|
+
* <com-form-field>
|
|
39
|
+
* <label comLabel>Email address</label>
|
|
40
|
+
* <input comInput formControlName="email" />
|
|
41
|
+
* </com-form-field>
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
class ComLabel {
|
|
45
|
+
labelId = `com-label-${nextId$3++}`;
|
|
46
|
+
_forId = signal(null, ...(ngDevMode ? [{ debugName: "_forId" }] : []));
|
|
47
|
+
forId = this._forId.asReadonly();
|
|
48
|
+
/** Sets the `for` attribute to link to the control. Called by form field. */
|
|
49
|
+
setForId(id) {
|
|
50
|
+
this._forId.set(id);
|
|
51
|
+
}
|
|
52
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComLabel, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
53
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.0", type: ComLabel, isStandalone: true, selector: "[comLabel]", host: { properties: { "attr.for": "forId()", "id": "labelId" } }, exportAs: ["comLabel"], ngImport: i0 });
|
|
54
|
+
}
|
|
55
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComLabel, decorators: [{
|
|
56
|
+
type: Directive,
|
|
57
|
+
args: [{
|
|
58
|
+
selector: '[comLabel]',
|
|
59
|
+
exportAs: 'comLabel',
|
|
60
|
+
host: {
|
|
61
|
+
'[attr.for]': 'forId()',
|
|
62
|
+
'[id]': 'labelId',
|
|
63
|
+
},
|
|
64
|
+
}]
|
|
65
|
+
}] });
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* CVA variants for the form field wrapper.
|
|
69
|
+
*
|
|
70
|
+
* @tokens `--color-foreground`
|
|
71
|
+
*/
|
|
72
|
+
const formFieldVariants = cva(['com-form-field', 'relative block w-full', 'text-foreground'], {
|
|
73
|
+
variants: {
|
|
74
|
+
disabled: {
|
|
75
|
+
true: 'pointer-events-none',
|
|
76
|
+
false: '',
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
defaultVariants: {
|
|
80
|
+
disabled: false,
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
/**
|
|
84
|
+
* CVA variants for the input container (the bordered/filled area).
|
|
85
|
+
*
|
|
86
|
+
* @tokens `--color-input-border`, `--color-input-background`, `--color-primary`,
|
|
87
|
+
* `--color-accent`, `--color-warn`, `--color-ring`, `--color-muted`,
|
|
88
|
+
* `--color-disabled`, `--radius-input`
|
|
89
|
+
*/
|
|
90
|
+
const formFieldContainerVariants = cva([
|
|
91
|
+
'com-form-field__container',
|
|
92
|
+
'relative flex items-center',
|
|
93
|
+
'w-full min-h-11',
|
|
94
|
+
'transition-all duration-150',
|
|
95
|
+
'rounded-input',
|
|
96
|
+
], {
|
|
97
|
+
variants: {
|
|
98
|
+
appearance: {
|
|
99
|
+
outline: [
|
|
100
|
+
'border border-input-border',
|
|
101
|
+
'bg-transparent',
|
|
102
|
+
],
|
|
103
|
+
fill: [
|
|
104
|
+
'rounded-b-none',
|
|
105
|
+
'bg-muted',
|
|
106
|
+
'border-b-2 border-input-border',
|
|
107
|
+
],
|
|
108
|
+
},
|
|
109
|
+
color: {
|
|
110
|
+
primary: '',
|
|
111
|
+
accent: '',
|
|
112
|
+
warn: '',
|
|
113
|
+
},
|
|
114
|
+
focused: {
|
|
115
|
+
true: '',
|
|
116
|
+
false: '',
|
|
117
|
+
},
|
|
118
|
+
error: {
|
|
119
|
+
true: '',
|
|
120
|
+
false: '',
|
|
121
|
+
},
|
|
122
|
+
disabled: {
|
|
123
|
+
true: 'bg-disabled border-disabled',
|
|
124
|
+
false: '',
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
compoundVariants: [
|
|
128
|
+
// Outline + focused (no error)
|
|
129
|
+
{ appearance: 'outline', focused: true, error: false, class: 'border-primary ring-1 ring-primary' },
|
|
130
|
+
{ appearance: 'outline', focused: true, error: false, color: 'accent', class: 'border-accent ring-accent' },
|
|
131
|
+
{ appearance: 'outline', focused: true, error: false, color: 'warn', class: 'border-warn ring-warn' },
|
|
132
|
+
// Outline + error
|
|
133
|
+
{ appearance: 'outline', error: true, class: 'border-warn ring-1 ring-warn' },
|
|
134
|
+
// Fill + focused (no error)
|
|
135
|
+
{ appearance: 'fill', focused: true, error: false, class: 'border-b-primary' },
|
|
136
|
+
{ appearance: 'fill', focused: true, error: false, color: 'accent', class: 'border-b-accent' },
|
|
137
|
+
{ appearance: 'fill', focused: true, error: false, color: 'warn', class: 'border-b-warn' },
|
|
138
|
+
// Fill + error
|
|
139
|
+
{ appearance: 'fill', error: true, class: 'border-b-warn' },
|
|
140
|
+
],
|
|
141
|
+
defaultVariants: {
|
|
142
|
+
appearance: 'outline',
|
|
143
|
+
color: 'primary',
|
|
144
|
+
focused: false,
|
|
145
|
+
error: false,
|
|
146
|
+
disabled: false,
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
/**
|
|
150
|
+
* CVA variants for the floating label.
|
|
151
|
+
*
|
|
152
|
+
* @tokens `--color-muted-foreground`, `--color-primary`, `--color-accent`,
|
|
153
|
+
* `--color-warn`, `--color-background`
|
|
154
|
+
*/
|
|
155
|
+
const formFieldLabelVariants = cva([
|
|
156
|
+
'com-form-field__label',
|
|
157
|
+
'pointer-events-none',
|
|
158
|
+
'text-muted-foreground text-sm',
|
|
159
|
+
'origin-top-left',
|
|
160
|
+
'transition-all duration-200 ease-in-out',
|
|
161
|
+
], {
|
|
162
|
+
variants: {
|
|
163
|
+
appearance: {
|
|
164
|
+
outline: '',
|
|
165
|
+
fill: '',
|
|
166
|
+
},
|
|
167
|
+
floating: {
|
|
168
|
+
// Floating: absolute positioning at container border
|
|
169
|
+
true: 'absolute',
|
|
170
|
+
// Not floating: relative positioning in flex flow (after prefix)
|
|
171
|
+
false: 'relative flex-shrink-0 pl-3',
|
|
172
|
+
},
|
|
173
|
+
color: {
|
|
174
|
+
primary: '',
|
|
175
|
+
accent: '',
|
|
176
|
+
warn: '',
|
|
177
|
+
},
|
|
178
|
+
error: {
|
|
179
|
+
true: 'text-warn',
|
|
180
|
+
false: '',
|
|
181
|
+
},
|
|
182
|
+
focused: {
|
|
183
|
+
true: '',
|
|
184
|
+
false: '',
|
|
185
|
+
},
|
|
186
|
+
disabled: {
|
|
187
|
+
true: 'text-disabled-foreground',
|
|
188
|
+
false: '',
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
compoundVariants: [
|
|
192
|
+
// Outline floating (label above border with notch effect)
|
|
193
|
+
{
|
|
194
|
+
appearance: 'outline',
|
|
195
|
+
floating: true,
|
|
196
|
+
class: 'left-2 -top-2.5 scale-75 bg-background px-1',
|
|
197
|
+
},
|
|
198
|
+
// Fill floating
|
|
199
|
+
{
|
|
200
|
+
appearance: 'fill',
|
|
201
|
+
floating: true,
|
|
202
|
+
class: 'left-3 top-1 scale-75 text-xs',
|
|
203
|
+
},
|
|
204
|
+
// Focused + floating colors (no error)
|
|
205
|
+
{ floating: true, focused: true, error: false, color: 'primary', class: 'text-primary' },
|
|
206
|
+
{ floating: true, focused: true, error: false, color: 'accent', class: 'text-accent' },
|
|
207
|
+
{ floating: true, focused: true, error: false, color: 'warn', class: 'text-warn' },
|
|
208
|
+
// Error overrides
|
|
209
|
+
{ floating: true, error: true, class: 'text-warn' },
|
|
210
|
+
],
|
|
211
|
+
defaultVariants: {
|
|
212
|
+
appearance: 'outline',
|
|
213
|
+
floating: false,
|
|
214
|
+
color: 'primary',
|
|
215
|
+
error: false,
|
|
216
|
+
focused: false,
|
|
217
|
+
disabled: false,
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
/**
|
|
221
|
+
* CVA variants for the subscript area (hints/errors).
|
|
222
|
+
*
|
|
223
|
+
* @tokens (inherits from children)
|
|
224
|
+
*/
|
|
225
|
+
const formFieldSubscriptVariants = cva(['com-form-field__subscript', 'text-xs mt-1 px-3'], {
|
|
226
|
+
variants: {
|
|
227
|
+
sizing: {
|
|
228
|
+
fixed: 'min-h-5',
|
|
229
|
+
dynamic: 'min-h-0',
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
defaultVariants: {
|
|
233
|
+
sizing: 'fixed',
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
/**
|
|
237
|
+
* CVA variants for hint text.
|
|
238
|
+
*
|
|
239
|
+
* @tokens `--color-muted-foreground`
|
|
240
|
+
*/
|
|
241
|
+
const hintVariants = cva(['com-form-field__hint', 'text-muted-foreground text-xs']);
|
|
242
|
+
/**
|
|
243
|
+
* CVA variants for error messages.
|
|
244
|
+
*
|
|
245
|
+
* @tokens `--color-warn`
|
|
246
|
+
*/
|
|
247
|
+
const errorVariants = cva(['com-form-field__error', 'text-warn text-xs']);
|
|
248
|
+
/**
|
|
249
|
+
* CVA variants for prefix slot.
|
|
250
|
+
*
|
|
251
|
+
* @tokens `--color-muted-foreground`
|
|
252
|
+
*/
|
|
253
|
+
const prefixVariants = cva([
|
|
254
|
+
'com-form-field__prefix',
|
|
255
|
+
'flex items-center text-muted-foreground pl-3',
|
|
256
|
+
]);
|
|
257
|
+
/**
|
|
258
|
+
* CVA variants for suffix slot.
|
|
259
|
+
*
|
|
260
|
+
* @tokens `--color-muted-foreground`
|
|
261
|
+
*/
|
|
262
|
+
const suffixVariants = cva([
|
|
263
|
+
'com-form-field__suffix',
|
|
264
|
+
'flex items-center text-muted-foreground pr-3',
|
|
265
|
+
]);
|
|
266
|
+
|
|
267
|
+
/** Auto-incrementing ID counter. */
|
|
268
|
+
let nextId$2 = 0;
|
|
269
|
+
/**
|
|
270
|
+
* Directive for hint text displayed below the form field.
|
|
271
|
+
*
|
|
272
|
+
* The hint provides supplementary information to help users fill out the field.
|
|
273
|
+
* It is automatically added to the control's `aria-describedby`.
|
|
274
|
+
*
|
|
275
|
+
* @tokens `--color-muted-foreground`
|
|
276
|
+
*
|
|
277
|
+
* @example Basic hint
|
|
278
|
+
* ```html
|
|
279
|
+
* <com-form-field>
|
|
280
|
+
* <label comLabel>Password</label>
|
|
281
|
+
* <input comInput formControlName="password" />
|
|
282
|
+
* <span comHint>At least 8 characters</span>
|
|
283
|
+
* </com-form-field>
|
|
284
|
+
* ```
|
|
285
|
+
*
|
|
286
|
+
* @example Right-aligned hint (e.g., character count)
|
|
287
|
+
* ```html
|
|
288
|
+
* <com-form-field>
|
|
289
|
+
* <label comLabel>Bio</label>
|
|
290
|
+
* <textarea comInput formControlName="bio"></textarea>
|
|
291
|
+
* <span comHint>Keep it brief</span>
|
|
292
|
+
* <span comHint align="end">{{ bioLength }}/150</span>
|
|
293
|
+
* </com-form-field>
|
|
294
|
+
* ```
|
|
295
|
+
*/
|
|
296
|
+
class ComHint {
|
|
297
|
+
id = `com-hint-${nextId$2++}`;
|
|
298
|
+
align = input('start', ...(ngDevMode ? [{ debugName: "align" }] : []));
|
|
299
|
+
hostClasses = computed(() => mergeClasses(hintVariants(), this.align() === 'end' && 'ml-auto'), ...(ngDevMode ? [{ debugName: "hostClasses" }] : []));
|
|
300
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComHint, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
301
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.0", type: ComHint, isStandalone: true, selector: "[comHint]", inputs: { align: { classPropertyName: "align", publicName: "align", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "id": "id", "class": "hostClasses()" } }, exportAs: ["comHint"], ngImport: i0 });
|
|
302
|
+
}
|
|
303
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComHint, decorators: [{
|
|
304
|
+
type: Directive,
|
|
305
|
+
args: [{
|
|
306
|
+
selector: '[comHint]',
|
|
307
|
+
exportAs: 'comHint',
|
|
308
|
+
host: {
|
|
309
|
+
'[id]': 'id',
|
|
310
|
+
'[class]': 'hostClasses()',
|
|
311
|
+
},
|
|
312
|
+
}]
|
|
313
|
+
}], propDecorators: { align: [{ type: i0.Input, args: [{ isSignal: true, alias: "align", required: false }] }] } });
|
|
314
|
+
|
|
315
|
+
/** Auto-incrementing ID counter. */
|
|
316
|
+
let nextId$1 = 0;
|
|
317
|
+
/**
|
|
318
|
+
* Directive for error messages displayed below the form field.
|
|
319
|
+
*
|
|
320
|
+
* Errors replace hints when the control is in an error state.
|
|
321
|
+
* Uses `role="alert"` and `aria-live="polite"` for screen reader announcement.
|
|
322
|
+
*
|
|
323
|
+
* The optional `match` input allows showing errors only for specific validation errors.
|
|
324
|
+
*
|
|
325
|
+
* @tokens `--color-warn`
|
|
326
|
+
*
|
|
327
|
+
* @example Basic error
|
|
328
|
+
* ```html
|
|
329
|
+
* <com-form-field>
|
|
330
|
+
* <label comLabel>Email</label>
|
|
331
|
+
* <input comInput formControlName="email" />
|
|
332
|
+
* <span comError>Please enter a valid email</span>
|
|
333
|
+
* </com-form-field>
|
|
334
|
+
* ```
|
|
335
|
+
*
|
|
336
|
+
* @example Matching specific errors
|
|
337
|
+
* ```html
|
|
338
|
+
* <com-form-field>
|
|
339
|
+
* <label comLabel>Email</label>
|
|
340
|
+
* <input comInput formControlName="email" />
|
|
341
|
+
* <span comError match="required">Email is required.</span>
|
|
342
|
+
* <span comError match="email">Must be a valid email address.</span>
|
|
343
|
+
* </com-form-field>
|
|
344
|
+
* ```
|
|
345
|
+
*/
|
|
346
|
+
class ComError {
|
|
347
|
+
id = `com-error-${nextId$1++}`;
|
|
348
|
+
/** Reference to the form control, set by the parent form field. */
|
|
349
|
+
_control = signal(null, ...(ngDevMode ? [{ debugName: "_control" }] : []));
|
|
350
|
+
/**
|
|
351
|
+
* Show this error only when a specific validation error key is present.
|
|
352
|
+
* If empty, the error is always shown when the control is in error state.
|
|
353
|
+
*/
|
|
354
|
+
match = input('', ...(ngDevMode ? [{ debugName: "match" }] : []));
|
|
355
|
+
errorVariants = errorVariants;
|
|
356
|
+
/**
|
|
357
|
+
* Whether this error should be displayed based on the match condition.
|
|
358
|
+
* Used by the form field to filter which errors to show.
|
|
359
|
+
*/
|
|
360
|
+
shouldShow = computed(() => {
|
|
361
|
+
const matchKey = this.match();
|
|
362
|
+
if (!matchKey)
|
|
363
|
+
return true;
|
|
364
|
+
const errors = this._control()?.errors;
|
|
365
|
+
return errors ? matchKey in errors : false;
|
|
366
|
+
}, ...(ngDevMode ? [{ debugName: "shouldShow" }] : []));
|
|
367
|
+
/**
|
|
368
|
+
* Sets the form control reference.
|
|
369
|
+
* Called by the parent form field component.
|
|
370
|
+
*/
|
|
371
|
+
setControl(control) {
|
|
372
|
+
this._control.set(control);
|
|
373
|
+
}
|
|
374
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComError, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
375
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.0", type: ComError, isStandalone: true, selector: "[comError]", inputs: { match: { classPropertyName: "match", publicName: "match", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "alert", "aria-live": "polite" }, properties: { "id": "id", "class": "errorVariants()", "class.hidden": "!shouldShow()" } }, exportAs: ["comError"], ngImport: i0 });
|
|
376
|
+
}
|
|
377
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComError, decorators: [{
|
|
378
|
+
type: Directive,
|
|
379
|
+
args: [{
|
|
380
|
+
selector: '[comError]',
|
|
381
|
+
exportAs: 'comError',
|
|
382
|
+
host: {
|
|
383
|
+
'[id]': 'id',
|
|
384
|
+
'[class]': 'errorVariants()',
|
|
385
|
+
'[class.hidden]': '!shouldShow()',
|
|
386
|
+
'role': 'alert',
|
|
387
|
+
'aria-live': 'polite',
|
|
388
|
+
},
|
|
389
|
+
}]
|
|
390
|
+
}], propDecorators: { match: [{ type: i0.Input, args: [{ isSignal: true, alias: "match", required: false }] }] } });
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Directive to mark content as the prefix slot in a form field.
|
|
394
|
+
*
|
|
395
|
+
* Prefix content appears before the input (e.g., currency symbol, icon).
|
|
396
|
+
*
|
|
397
|
+
* @tokens `--color-muted-foreground`
|
|
398
|
+
*
|
|
399
|
+
* @example Currency prefix
|
|
400
|
+
* ```html
|
|
401
|
+
* <com-form-field>
|
|
402
|
+
* <label comLabel>Amount</label>
|
|
403
|
+
* <span comPrefix>$</span>
|
|
404
|
+
* <input comInput type="number" formControlName="amount" />
|
|
405
|
+
* </com-form-field>
|
|
406
|
+
* ```
|
|
407
|
+
*
|
|
408
|
+
* @example Icon prefix
|
|
409
|
+
* ```html
|
|
410
|
+
* <com-form-field>
|
|
411
|
+
* <label comLabel>Search</label>
|
|
412
|
+
* <svg comPrefix class="size-4">...</svg>
|
|
413
|
+
* <input comInput formControlName="query" />
|
|
414
|
+
* </com-form-field>
|
|
415
|
+
* ```
|
|
416
|
+
*/
|
|
417
|
+
class ComPrefix {
|
|
418
|
+
prefixVariants = prefixVariants;
|
|
419
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComPrefix, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
420
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.0", type: ComPrefix, isStandalone: true, selector: "[comPrefix]", host: { properties: { "class": "prefixVariants()" } }, exportAs: ["comPrefix"], ngImport: i0 });
|
|
421
|
+
}
|
|
422
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComPrefix, decorators: [{
|
|
423
|
+
type: Directive,
|
|
424
|
+
args: [{
|
|
425
|
+
selector: '[comPrefix]',
|
|
426
|
+
exportAs: 'comPrefix',
|
|
427
|
+
host: {
|
|
428
|
+
'[class]': 'prefixVariants()',
|
|
429
|
+
},
|
|
430
|
+
}]
|
|
431
|
+
}] });
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Directive to mark content as the suffix slot in a form field.
|
|
435
|
+
*
|
|
436
|
+
* Suffix content appears after the input (e.g., unit, clear button, visibility toggle).
|
|
437
|
+
*
|
|
438
|
+
* @tokens `--color-muted-foreground`
|
|
439
|
+
*
|
|
440
|
+
* @example Unit suffix
|
|
441
|
+
* ```html
|
|
442
|
+
* <com-form-field>
|
|
443
|
+
* <label comLabel>Weight</label>
|
|
444
|
+
* <input comInput type="number" formControlName="weight" />
|
|
445
|
+
* <span comSuffix>kg</span>
|
|
446
|
+
* </com-form-field>
|
|
447
|
+
* ```
|
|
448
|
+
*
|
|
449
|
+
* @example Clear button suffix
|
|
450
|
+
* ```html
|
|
451
|
+
* <com-form-field>
|
|
452
|
+
* <label comLabel>Search</label>
|
|
453
|
+
* <input comInput formControlName="query" />
|
|
454
|
+
* <button comSuffix type="button" (click)="clearSearch()">
|
|
455
|
+
* <svg class="size-4">...</svg>
|
|
456
|
+
* </button>
|
|
457
|
+
* </com-form-field>
|
|
458
|
+
* ```
|
|
459
|
+
*
|
|
460
|
+
* @example Password visibility toggle
|
|
461
|
+
* ```html
|
|
462
|
+
* <com-form-field>
|
|
463
|
+
* <label comLabel>Password</label>
|
|
464
|
+
* <input comInput [type]="showPassword ? 'text' : 'password'" formControlName="password" />
|
|
465
|
+
* <button comSuffix type="button" (click)="showPassword = !showPassword">
|
|
466
|
+
* <svg class="size-4">...</svg>
|
|
467
|
+
* </button>
|
|
468
|
+
* </com-form-field>
|
|
469
|
+
* ```
|
|
470
|
+
*/
|
|
471
|
+
class ComSuffix {
|
|
472
|
+
suffixVariants = suffixVariants;
|
|
473
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComSuffix, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
474
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.0", type: ComSuffix, isStandalone: true, selector: "[comSuffix]", host: { properties: { "class": "suffixVariants()" } }, exportAs: ["comSuffix"], ngImport: i0 });
|
|
475
|
+
}
|
|
476
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComSuffix, decorators: [{
|
|
477
|
+
type: Directive,
|
|
478
|
+
args: [{
|
|
479
|
+
selector: '[comSuffix]',
|
|
480
|
+
exportAs: 'comSuffix',
|
|
481
|
+
host: {
|
|
482
|
+
'[class]': 'suffixVariants()',
|
|
483
|
+
},
|
|
484
|
+
}]
|
|
485
|
+
}] });
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Strategy for determining when to display errors in a form field.
|
|
489
|
+
*
|
|
490
|
+
* The default behavior shows errors when the control is invalid AND
|
|
491
|
+
* either touched OR the parent form has been submitted.
|
|
492
|
+
*
|
|
493
|
+
* @example Override globally with eager error display
|
|
494
|
+
* ```ts
|
|
495
|
+
* @Injectable()
|
|
496
|
+
* export class EagerErrorStateMatcher extends ErrorStateMatcher {
|
|
497
|
+
* override isErrorState(control: AbstractControl | null): boolean {
|
|
498
|
+
* return !!(control?.invalid && control.dirty);
|
|
499
|
+
* }
|
|
500
|
+
* }
|
|
501
|
+
*
|
|
502
|
+
* // In app config
|
|
503
|
+
* providers: [{ provide: ErrorStateMatcher, useClass: EagerErrorStateMatcher }]
|
|
504
|
+
* ```
|
|
505
|
+
*
|
|
506
|
+
* @example Per-field override
|
|
507
|
+
* ```html
|
|
508
|
+
* <com-form-field>
|
|
509
|
+
* <input comInput [errorStateMatcher]="eagerMatcher" />
|
|
510
|
+
* </com-form-field>
|
|
511
|
+
* ```
|
|
512
|
+
*/
|
|
513
|
+
class ErrorStateMatcher {
|
|
514
|
+
isErrorState(control, form) {
|
|
515
|
+
return !!(control?.invalid && (control.touched || form?.submitted));
|
|
516
|
+
}
|
|
517
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ErrorStateMatcher, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
518
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ErrorStateMatcher, providedIn: 'root' });
|
|
519
|
+
}
|
|
520
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ErrorStateMatcher, decorators: [{
|
|
521
|
+
type: Injectable,
|
|
522
|
+
args: [{ providedIn: 'root' }]
|
|
523
|
+
}] });
|
|
524
|
+
|
|
525
|
+
/** Auto-incrementing ID counter. */
|
|
526
|
+
let nextId = 0;
|
|
527
|
+
/**
|
|
528
|
+
* Directive applied to native `<input>` and `<textarea>` elements to bridge
|
|
529
|
+
* them to the form field wrapper. Implements the `FormFieldControl` contract.
|
|
530
|
+
*
|
|
531
|
+
* This directive does NOT implement `ControlValueAccessor` - it relies on
|
|
532
|
+
* Angular's built-in value accessors (`DefaultValueAccessor`, `NumberValueAccessor`, etc.)
|
|
533
|
+
* that are already applied to native elements with `formControlName` or `ngModel`.
|
|
534
|
+
*
|
|
535
|
+
* Instead, this directive reports state (focused, empty, disabled, error) to the
|
|
536
|
+
* parent form field for proper visual presentation.
|
|
537
|
+
*
|
|
538
|
+
* @tokens (styling is controlled by the parent form field)
|
|
539
|
+
*
|
|
540
|
+
* @example Basic usage
|
|
541
|
+
* ```html
|
|
542
|
+
* <com-form-field>
|
|
543
|
+
* <label comLabel>Email</label>
|
|
544
|
+
* <input comInput formControlName="email" placeholder="you@example.com" />
|
|
545
|
+
* </com-form-field>
|
|
546
|
+
* ```
|
|
547
|
+
*
|
|
548
|
+
* @example Textarea
|
|
549
|
+
* ```html
|
|
550
|
+
* <com-form-field>
|
|
551
|
+
* <label comLabel>Description</label>
|
|
552
|
+
* <textarea comInput formControlName="description"></textarea>
|
|
553
|
+
* </com-form-field>
|
|
554
|
+
* ```
|
|
555
|
+
*
|
|
556
|
+
* @example Custom error state matcher
|
|
557
|
+
* ```html
|
|
558
|
+
* <com-form-field>
|
|
559
|
+
* <label comLabel>Code</label>
|
|
560
|
+
* <input comInput formControlName="code" [errorStateMatcher]="eagerMatcher" />
|
|
561
|
+
* </com-form-field>
|
|
562
|
+
* ```
|
|
563
|
+
*/
|
|
564
|
+
class ComInput {
|
|
565
|
+
elementRef = inject(ElementRef);
|
|
566
|
+
destroyRef = inject(DestroyRef);
|
|
567
|
+
autofillMonitor = inject(AutofillMonitor);
|
|
568
|
+
defaultErrorStateMatcher = inject(ErrorStateMatcher);
|
|
569
|
+
parentForm = inject(NgForm, { optional: true });
|
|
570
|
+
parentFormGroup = inject(FormGroupDirective, { optional: true });
|
|
571
|
+
/** NgControl bound to this input (if using reactive forms). */
|
|
572
|
+
ngControl = inject(NgControl, { optional: true, self: true });
|
|
573
|
+
// Inputs
|
|
574
|
+
inputId = input(undefined, { ...(ngDevMode ? { debugName: "inputId" } : {}), alias: 'id' });
|
|
575
|
+
inputDisabled = input(false, { ...(ngDevMode ? { debugName: "inputDisabled" } : {}), alias: 'disabled',
|
|
576
|
+
transform: booleanAttribute });
|
|
577
|
+
inputRequired = input(false, { ...(ngDevMode ? { debugName: "inputRequired" } : {}), alias: 'required',
|
|
578
|
+
transform: booleanAttribute });
|
|
579
|
+
userAriaDescribedBy = input('', { ...(ngDevMode ? { debugName: "userAriaDescribedBy" } : {}), alias: 'aria-describedby' });
|
|
580
|
+
errorStateMatcher = input(...(ngDevMode ? [undefined, { debugName: "errorStateMatcher" }] : []));
|
|
581
|
+
// Internal state
|
|
582
|
+
_focused = signal(false, ...(ngDevMode ? [{ debugName: "_focused" }] : []));
|
|
583
|
+
_autofilled = signal(false, ...(ngDevMode ? [{ debugName: "_autofilled" }] : []));
|
|
584
|
+
_empty = signal(true, ...(ngDevMode ? [{ debugName: "_empty" }] : []));
|
|
585
|
+
_uniqueId = `com-input-${nextId++}`;
|
|
586
|
+
_appearance = signal('outline', ...(ngDevMode ? [{ debugName: "_appearance" }] : []));
|
|
587
|
+
// Public signals implementing FormFieldControl
|
|
588
|
+
focused = this._focused.asReadonly();
|
|
589
|
+
id = computed(() => this.inputId() ?? this._uniqueId, ...(ngDevMode ? [{ debugName: "id" }] : []));
|
|
590
|
+
shouldLabelFloat = computed(() => {
|
|
591
|
+
return this._focused() || !this._empty() || this._autofilled();
|
|
592
|
+
}, ...(ngDevMode ? [{ debugName: "shouldLabelFloat" }] : []));
|
|
593
|
+
disabled = computed(() => {
|
|
594
|
+
if (this.inputDisabled())
|
|
595
|
+
return true;
|
|
596
|
+
return this.ngControl?.control?.disabled ?? false;
|
|
597
|
+
}, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
|
|
598
|
+
required = computed(() => {
|
|
599
|
+
if (this.inputRequired())
|
|
600
|
+
return true;
|
|
601
|
+
const control = this.ngControl?.control;
|
|
602
|
+
if (control?.validator) {
|
|
603
|
+
const result = control.validator({ value: '' });
|
|
604
|
+
return result?.['required'] !== undefined;
|
|
605
|
+
}
|
|
606
|
+
return false;
|
|
607
|
+
}, ...(ngDevMode ? [{ debugName: "required" }] : []));
|
|
608
|
+
errorState = computed(() => {
|
|
609
|
+
// Read _focused to trigger re-evaluation on blur (when touched changes)
|
|
610
|
+
// Read _empty to trigger re-evaluation on input (when validity changes)
|
|
611
|
+
this._focused();
|
|
612
|
+
this._empty();
|
|
613
|
+
const matcher = this.errorStateMatcher() ?? this.defaultErrorStateMatcher;
|
|
614
|
+
const form = this.parentFormGroup ?? this.parentForm;
|
|
615
|
+
return matcher.isErrorState(this.ngControl?.control ?? null, form);
|
|
616
|
+
}, ...(ngDevMode ? [{ debugName: "errorState" }] : []));
|
|
617
|
+
/** Combined aria-describedby including user-provided and form-field-generated IDs. */
|
|
618
|
+
_describedByIds = signal('', ...(ngDevMode ? [{ debugName: "_describedByIds" }] : []));
|
|
619
|
+
ariaDescribedBy = computed(() => {
|
|
620
|
+
const userIds = this.userAriaDescribedBy();
|
|
621
|
+
const fieldIds = this._describedByIds();
|
|
622
|
+
return [userIds, fieldIds].filter(Boolean).join(' ');
|
|
623
|
+
}, ...(ngDevMode ? [{ debugName: "ariaDescribedBy" }] : []));
|
|
624
|
+
/** Computed host classes including appearance-based padding. */
|
|
625
|
+
hostClasses = computed(() => {
|
|
626
|
+
const base = 'peer w-full bg-transparent text-foreground placeholder:text-input-placeholder outline-none border-none disabled:cursor-not-allowed disabled:text-disabled-foreground px-3';
|
|
627
|
+
const padding = this._appearance() === 'fill' ? 'pt-5 pb-1.5' : 'py-2.5';
|
|
628
|
+
return `${base} ${padding}`;
|
|
629
|
+
}, ...(ngDevMode ? [{ debugName: "hostClasses" }] : []));
|
|
630
|
+
ngOnInit() {
|
|
631
|
+
this.updateEmpty();
|
|
632
|
+
const autofillSub = this.autofillMonitor.monitor(this.elementRef).subscribe((event) => {
|
|
633
|
+
this._autofilled.set(event.isAutofilled);
|
|
634
|
+
});
|
|
635
|
+
this.destroyRef.onDestroy(() => {
|
|
636
|
+
autofillSub.unsubscribe();
|
|
637
|
+
this.autofillMonitor.stopMonitoring(this.elementRef);
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
onFocus() {
|
|
641
|
+
this._focused.set(true);
|
|
642
|
+
}
|
|
643
|
+
onBlur() {
|
|
644
|
+
this._focused.set(false);
|
|
645
|
+
}
|
|
646
|
+
onInput() {
|
|
647
|
+
this.updateEmpty();
|
|
648
|
+
}
|
|
649
|
+
updateEmpty() {
|
|
650
|
+
this._empty.set(!this.elementRef.nativeElement.value);
|
|
651
|
+
}
|
|
652
|
+
// FormFieldControl methods
|
|
653
|
+
onContainerClick(event) {
|
|
654
|
+
if (!this.disabled() && event.target !== this.elementRef.nativeElement) {
|
|
655
|
+
this.elementRef.nativeElement.focus();
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Sets the describedBy IDs from the form field.
|
|
660
|
+
* Called by the parent form field component.
|
|
661
|
+
*/
|
|
662
|
+
setDescribedByIds(ids) {
|
|
663
|
+
this._describedByIds.set(ids);
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Sets the appearance for styling.
|
|
667
|
+
* Called by the parent form field component.
|
|
668
|
+
*/
|
|
669
|
+
setAppearance(appearance) {
|
|
670
|
+
this._appearance.set(appearance);
|
|
671
|
+
}
|
|
672
|
+
/** Focus the native element. */
|
|
673
|
+
focus() {
|
|
674
|
+
this.elementRef.nativeElement.focus();
|
|
675
|
+
}
|
|
676
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComInput, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
677
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.0", type: ComInput, isStandalone: true, selector: "input[comInput], textarea[comInput]", inputs: { inputId: { classPropertyName: "inputId", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, inputDisabled: { classPropertyName: "inputDisabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, inputRequired: { classPropertyName: "inputRequired", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, userAriaDescribedBy: { classPropertyName: "userAriaDescribedBy", publicName: "aria-describedby", isSignal: true, isRequired: false, transformFunction: null }, errorStateMatcher: { classPropertyName: "errorStateMatcher", publicName: "errorStateMatcher", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "focus": "onFocus()", "blur": "onBlur()", "input": "onInput()" }, properties: { "id": "id()", "disabled": "disabled()", "required": "required()", "attr.aria-invalid": "errorState() || null", "attr.aria-required": "required() || null", "attr.aria-describedby": "ariaDescribedBy() || null", "class": "hostClasses()" } }, providers: [{ provide: FormFieldControl, useExisting: forwardRef(() => ComInput) }], exportAs: ["comInput"], ngImport: i0 });
|
|
678
|
+
}
|
|
679
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComInput, decorators: [{
|
|
680
|
+
type: Directive,
|
|
681
|
+
args: [{
|
|
682
|
+
selector: 'input[comInput], textarea[comInput]',
|
|
683
|
+
exportAs: 'comInput',
|
|
684
|
+
providers: [{ provide: FormFieldControl, useExisting: forwardRef(() => ComInput) }],
|
|
685
|
+
host: {
|
|
686
|
+
'[id]': 'id()',
|
|
687
|
+
'[disabled]': 'disabled()',
|
|
688
|
+
'[required]': 'required()',
|
|
689
|
+
'[attr.aria-invalid]': 'errorState() || null',
|
|
690
|
+
'[attr.aria-required]': 'required() || null',
|
|
691
|
+
'[attr.aria-describedby]': 'ariaDescribedBy() || null',
|
|
692
|
+
'[class]': 'hostClasses()',
|
|
693
|
+
'(focus)': 'onFocus()',
|
|
694
|
+
'(blur)': 'onBlur()',
|
|
695
|
+
'(input)': 'onInput()',
|
|
696
|
+
},
|
|
697
|
+
}]
|
|
698
|
+
}], propDecorators: { inputId: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], inputDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], inputRequired: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], userAriaDescribedBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-describedby", required: false }] }], errorStateMatcher: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorStateMatcher", required: false }] }] } });
|
|
699
|
+
|
|
700
|
+
const FORM_FIELD_DEFAULTS = new InjectionToken('FORM_FIELD_DEFAULTS', {
|
|
701
|
+
providedIn: 'root',
|
|
702
|
+
factory: () => ({}),
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* Form field wrapper providing visual structure for form inputs.
|
|
707
|
+
*
|
|
708
|
+
* Provides floating labels, borders (outline/fill appearance), hints, errors,
|
|
709
|
+
* and prefix/suffix slots. Automatically wires ARIA attributes for accessibility.
|
|
710
|
+
*
|
|
711
|
+
* The form field reads state from its inner `FormFieldControl` child (typically
|
|
712
|
+
* a `ComInput` directive) and renders UI accordingly.
|
|
713
|
+
*
|
|
714
|
+
* @tokens `--color-foreground`, `--color-input-border`, `--color-input-background`,
|
|
715
|
+
* `--color-primary`, `--color-accent`, `--color-warn`, `--color-ring`,
|
|
716
|
+
* `--color-muted`, `--color-muted-foreground`, `--color-disabled`,
|
|
717
|
+
* `--color-disabled-foreground`, `--color-background`, `--radius-input`
|
|
718
|
+
*
|
|
719
|
+
* @example Basic outline field
|
|
720
|
+
* ```html
|
|
721
|
+
* <com-form-field>
|
|
722
|
+
* <label comLabel>Email</label>
|
|
723
|
+
* <input comInput formControlName="email" />
|
|
724
|
+
* <span comHint>We'll never share your email.</span>
|
|
725
|
+
* <span comError match="required">Email is required.</span>
|
|
726
|
+
* </com-form-field>
|
|
727
|
+
* ```
|
|
728
|
+
*
|
|
729
|
+
* @example Fill appearance
|
|
730
|
+
* ```html
|
|
731
|
+
* <com-form-field appearance="fill">
|
|
732
|
+
* <label comLabel>Username</label>
|
|
733
|
+
* <input comInput formControlName="username" />
|
|
734
|
+
* </com-form-field>
|
|
735
|
+
* ```
|
|
736
|
+
*
|
|
737
|
+
* @example With prefix and suffix
|
|
738
|
+
* ```html
|
|
739
|
+
* <com-form-field>
|
|
740
|
+
* <label comLabel>Amount</label>
|
|
741
|
+
* <span comPrefix>$</span>
|
|
742
|
+
* <input comInput type="number" formControlName="amount" />
|
|
743
|
+
* <span comSuffix>.00</span>
|
|
744
|
+
* </com-form-field>
|
|
745
|
+
* ```
|
|
746
|
+
*/
|
|
747
|
+
class ComFormField {
|
|
748
|
+
defaults = inject(FORM_FIELD_DEFAULTS, { optional: true });
|
|
749
|
+
// Content children
|
|
750
|
+
control = contentChild(FormFieldControl, ...(ngDevMode ? [{ debugName: "control" }] : []));
|
|
751
|
+
inputDirective = contentChild(ComInput, ...(ngDevMode ? [{ debugName: "inputDirective" }] : []));
|
|
752
|
+
labelChild = contentChild(ComLabel, ...(ngDevMode ? [{ debugName: "labelChild" }] : []));
|
|
753
|
+
hintChildren = contentChildren(ComHint, ...(ngDevMode ? [{ debugName: "hintChildren" }] : []));
|
|
754
|
+
errorChildren = contentChildren(ComError, ...(ngDevMode ? [{ debugName: "errorChildren" }] : []));
|
|
755
|
+
prefixChild = contentChild(ComPrefix, ...(ngDevMode ? [{ debugName: "prefixChild" }] : []));
|
|
756
|
+
suffixChild = contentChild(ComSuffix, ...(ngDevMode ? [{ debugName: "suffixChild" }] : []));
|
|
757
|
+
// Inputs with defaults fallback
|
|
758
|
+
appearance = input(this.defaults?.appearance ?? 'outline', ...(ngDevMode ? [{ debugName: "appearance" }] : []));
|
|
759
|
+
color = input(this.defaults?.color ?? 'primary', ...(ngDevMode ? [{ debugName: "color" }] : []));
|
|
760
|
+
floatLabel = input(this.defaults?.floatLabel ?? 'auto', ...(ngDevMode ? [{ debugName: "floatLabel" }] : []));
|
|
761
|
+
hideRequiredMarker = input(this.defaults?.hideRequiredMarker ?? false, { ...(ngDevMode ? { debugName: "hideRequiredMarker" } : {}), transform: booleanAttribute });
|
|
762
|
+
subscriptSizing = input(this.defaults?.subscriptSizing ?? 'fixed', ...(ngDevMode ? [{ debugName: "subscriptSizing" }] : []));
|
|
763
|
+
userClass = input('', { ...(ngDevMode ? { debugName: "userClass" } : {}), alias: 'class' });
|
|
764
|
+
// Derived state from inner control
|
|
765
|
+
shouldLabelFloat = computed(() => {
|
|
766
|
+
if (this.floatLabel() === 'always')
|
|
767
|
+
return true;
|
|
768
|
+
return this.control()?.shouldLabelFloat() ?? false;
|
|
769
|
+
}, ...(ngDevMode ? [{ debugName: "shouldLabelFloat" }] : []));
|
|
770
|
+
isFocused = computed(() => this.control()?.focused() ?? false, ...(ngDevMode ? [{ debugName: "isFocused" }] : []));
|
|
771
|
+
isDisabled = computed(() => this.control()?.disabled() ?? false, ...(ngDevMode ? [{ debugName: "isDisabled" }] : []));
|
|
772
|
+
hasError = computed(() => this.control()?.errorState() ?? false, ...(ngDevMode ? [{ debugName: "hasError" }] : []));
|
|
773
|
+
hasPrefix = computed(() => !!this.prefixChild(), ...(ngDevMode ? [{ debugName: "hasPrefix" }] : []));
|
|
774
|
+
hasSuffix = computed(() => !!this.suffixChild(), ...(ngDevMode ? [{ debugName: "hasSuffix" }] : []));
|
|
775
|
+
showRequiredMarker = computed(() => {
|
|
776
|
+
if (this.hideRequiredMarker())
|
|
777
|
+
return false;
|
|
778
|
+
return this.control()?.required() ?? false;
|
|
779
|
+
}, ...(ngDevMode ? [{ debugName: "showRequiredMarker" }] : []));
|
|
780
|
+
showErrors = computed(() => this.hasError() && this.errorChildren().length > 0, ...(ngDevMode ? [{ debugName: "showErrors" }] : []));
|
|
781
|
+
// Computed classes
|
|
782
|
+
hostClasses = computed(() => mergeClasses(formFieldVariants({ disabled: this.isDisabled() }), this.userClass()), ...(ngDevMode ? [{ debugName: "hostClasses" }] : []));
|
|
783
|
+
containerClasses = computed(() => formFieldContainerVariants({
|
|
784
|
+
appearance: this.appearance(),
|
|
785
|
+
color: this.color(),
|
|
786
|
+
focused: this.isFocused(),
|
|
787
|
+
error: this.hasError(),
|
|
788
|
+
disabled: this.isDisabled(),
|
|
789
|
+
}), ...(ngDevMode ? [{ debugName: "containerClasses" }] : []));
|
|
790
|
+
labelClasses = computed(() => formFieldLabelVariants({
|
|
791
|
+
appearance: this.appearance(),
|
|
792
|
+
floating: this.shouldLabelFloat(),
|
|
793
|
+
color: this.color(),
|
|
794
|
+
error: this.hasError(),
|
|
795
|
+
focused: this.isFocused(),
|
|
796
|
+
disabled: this.isDisabled(),
|
|
797
|
+
}), ...(ngDevMode ? [{ debugName: "labelClasses" }] : []));
|
|
798
|
+
subscriptClasses = computed(() => formFieldSubscriptVariants({ sizing: this.subscriptSizing() }), ...(ngDevMode ? [{ debugName: "subscriptClasses" }] : []));
|
|
799
|
+
constructor() {
|
|
800
|
+
// Wire up label to control
|
|
801
|
+
effect(() => {
|
|
802
|
+
const label = this.labelChild();
|
|
803
|
+
const ctrl = this.control();
|
|
804
|
+
if (label && ctrl) {
|
|
805
|
+
label.setForId(ctrl.id());
|
|
806
|
+
}
|
|
807
|
+
});
|
|
808
|
+
// Wire up aria-describedby on the control
|
|
809
|
+
effect(() => {
|
|
810
|
+
const ctrl = this.control();
|
|
811
|
+
if (!ctrl || !('setDescribedByIds' in ctrl))
|
|
812
|
+
return;
|
|
813
|
+
const ids = this.showErrors()
|
|
814
|
+
? this.errorChildren().filter((e) => e.shouldShow()).map((e) => e.id)
|
|
815
|
+
: this.hintChildren().map((h) => h.id);
|
|
816
|
+
ctrl.setDescribedByIds(ids.join(' '));
|
|
817
|
+
});
|
|
818
|
+
// Wire up appearance to control for proper styling
|
|
819
|
+
effect(() => {
|
|
820
|
+
const ctrl = this.control();
|
|
821
|
+
if (ctrl && 'setAppearance' in ctrl) {
|
|
822
|
+
ctrl.setAppearance(this.appearance());
|
|
823
|
+
}
|
|
824
|
+
});
|
|
825
|
+
// Wire up control reference to error directives (re-runs on error state change)
|
|
826
|
+
effect(() => {
|
|
827
|
+
// Read hasError to trigger re-evaluation when validation state changes
|
|
828
|
+
this.hasError();
|
|
829
|
+
const ctrl = this.control();
|
|
830
|
+
const ngControl = ctrl?.ngControl?.control ?? null;
|
|
831
|
+
this.errorChildren().forEach((error) => error.setControl(ngControl));
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
onContainerClick(event) {
|
|
835
|
+
this.control()?.onContainerClick(event);
|
|
836
|
+
}
|
|
837
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComFormField, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
838
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: ComFormField, isStandalone: true, selector: "com-form-field", inputs: { appearance: { classPropertyName: "appearance", publicName: "appearance", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, floatLabel: { classPropertyName: "floatLabel", publicName: "floatLabel", isSignal: true, isRequired: false, transformFunction: null }, hideRequiredMarker: { classPropertyName: "hideRequiredMarker", publicName: "hideRequiredMarker", isSignal: true, isRequired: false, transformFunction: null }, subscriptSizing: { classPropertyName: "subscriptSizing", publicName: "subscriptSizing", isSignal: true, isRequired: false, transformFunction: null }, userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "hostClasses()", "class.com-form-field--focused": "isFocused()", "class.com-form-field--disabled": "isDisabled()", "class.com-form-field--error": "hasError()", "class.com-form-field--floating": "shouldLabelFloat()", "class.com-form-field--has-label": "!!labelChild()" } }, queries: [{ propertyName: "control", first: true, predicate: FormFieldControl, descendants: true, isSignal: true }, { propertyName: "inputDirective", first: true, predicate: ComInput, descendants: true, isSignal: true }, { propertyName: "labelChild", first: true, predicate: ComLabel, descendants: true, isSignal: true }, { propertyName: "hintChildren", predicate: ComHint, isSignal: true }, { propertyName: "errorChildren", predicate: ComError, isSignal: true }, { propertyName: "prefixChild", first: true, predicate: ComPrefix, descendants: true, isSignal: true }, { propertyName: "suffixChild", first: true, predicate: ComSuffix, descendants: true, isSignal: true }], exportAs: ["comFormField"], ngImport: i0, template: `
|
|
839
|
+
<div [class]="containerClasses()" (click)="onContainerClick($event)">
|
|
840
|
+
<div class="flex items-center w-full">
|
|
841
|
+
@if (hasPrefix()) {
|
|
842
|
+
<ng-content select="[comPrefix]" />
|
|
843
|
+
}
|
|
844
|
+
@if (labelChild()) {
|
|
845
|
+
<span [class]="labelClasses()">
|
|
846
|
+
<ng-content select="[comLabel]" />
|
|
847
|
+
@if (showRequiredMarker()) {
|
|
848
|
+
<span class="text-warn" aria-hidden="true"> *</span>
|
|
849
|
+
}
|
|
850
|
+
</span>
|
|
851
|
+
}
|
|
852
|
+
<div class="flex-1 min-w-0">
|
|
853
|
+
<ng-content />
|
|
854
|
+
</div>
|
|
855
|
+
@if (hasSuffix()) {
|
|
856
|
+
<ng-content select="[comSuffix]" />
|
|
857
|
+
}
|
|
858
|
+
</div>
|
|
859
|
+
</div>
|
|
860
|
+
|
|
861
|
+
<div [class]="subscriptClasses()">
|
|
862
|
+
<div [class.hidden]="!showErrors()">
|
|
863
|
+
<ng-content select="[comError]" />
|
|
864
|
+
</div>
|
|
865
|
+
<div [class.hidden]="showErrors()" class="flex justify-between w-full gap-2">
|
|
866
|
+
<ng-content select="[comHint]:not([align='end'])" />
|
|
867
|
+
<ng-content select="[comHint][align='end']" />
|
|
868
|
+
</div>
|
|
869
|
+
</div>
|
|
870
|
+
`, isInline: true, styles: ["com-form-field.com-form-field--has-label:not(.com-form-field--floating) input::placeholder,com-form-field.com-form-field--has-label:not(.com-form-field--floating) textarea::placeholder{color:transparent}com-form-field.com-form-field--has-label:not(.com-form-field--floating) com-dropdown .text-placeholder{display:none}com-form-field com-dropdown{display:block;width:100%}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
871
|
+
}
|
|
872
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComFormField, decorators: [{
|
|
873
|
+
type: Component,
|
|
874
|
+
args: [{ selector: 'com-form-field', exportAs: 'comFormField', template: `
|
|
875
|
+
<div [class]="containerClasses()" (click)="onContainerClick($event)">
|
|
876
|
+
<div class="flex items-center w-full">
|
|
877
|
+
@if (hasPrefix()) {
|
|
878
|
+
<ng-content select="[comPrefix]" />
|
|
879
|
+
}
|
|
880
|
+
@if (labelChild()) {
|
|
881
|
+
<span [class]="labelClasses()">
|
|
882
|
+
<ng-content select="[comLabel]" />
|
|
883
|
+
@if (showRequiredMarker()) {
|
|
884
|
+
<span class="text-warn" aria-hidden="true"> *</span>
|
|
885
|
+
}
|
|
886
|
+
</span>
|
|
887
|
+
}
|
|
888
|
+
<div class="flex-1 min-w-0">
|
|
889
|
+
<ng-content />
|
|
890
|
+
</div>
|
|
891
|
+
@if (hasSuffix()) {
|
|
892
|
+
<ng-content select="[comSuffix]" />
|
|
893
|
+
}
|
|
894
|
+
</div>
|
|
895
|
+
</div>
|
|
896
|
+
|
|
897
|
+
<div [class]="subscriptClasses()">
|
|
898
|
+
<div [class.hidden]="!showErrors()">
|
|
899
|
+
<ng-content select="[comError]" />
|
|
900
|
+
</div>
|
|
901
|
+
<div [class.hidden]="showErrors()" class="flex justify-between w-full gap-2">
|
|
902
|
+
<ng-content select="[comHint]:not([align='end'])" />
|
|
903
|
+
<ng-content select="[comHint][align='end']" />
|
|
904
|
+
</div>
|
|
905
|
+
</div>
|
|
906
|
+
`, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, host: {
|
|
907
|
+
'[class]': 'hostClasses()',
|
|
908
|
+
'[class.com-form-field--focused]': 'isFocused()',
|
|
909
|
+
'[class.com-form-field--disabled]': 'isDisabled()',
|
|
910
|
+
'[class.com-form-field--error]': 'hasError()',
|
|
911
|
+
'[class.com-form-field--floating]': 'shouldLabelFloat()',
|
|
912
|
+
'[class.com-form-field--has-label]': '!!labelChild()',
|
|
913
|
+
}, styles: ["com-form-field.com-form-field--has-label:not(.com-form-field--floating) input::placeholder,com-form-field.com-form-field--has-label:not(.com-form-field--floating) textarea::placeholder{color:transparent}com-form-field.com-form-field--has-label:not(.com-form-field--floating) com-dropdown .text-placeholder{display:none}com-form-field com-dropdown{display:block;width:100%}\n"] }]
|
|
914
|
+
}], ctorParameters: () => [], propDecorators: { control: [{ type: i0.ContentChild, args: [i0.forwardRef(() => FormFieldControl), { isSignal: true }] }], inputDirective: [{ type: i0.ContentChild, args: [i0.forwardRef(() => ComInput), { isSignal: true }] }], labelChild: [{ type: i0.ContentChild, args: [i0.forwardRef(() => ComLabel), { isSignal: true }] }], hintChildren: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => ComHint), { isSignal: true }] }], errorChildren: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => ComError), { isSignal: true }] }], prefixChild: [{ type: i0.ContentChild, args: [i0.forwardRef(() => ComPrefix), { isSignal: true }] }], suffixChild: [{ type: i0.ContentChild, args: [i0.forwardRef(() => ComSuffix), { isSignal: true }] }], appearance: [{ type: i0.Input, args: [{ isSignal: true, alias: "appearance", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], floatLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "floatLabel", required: false }] }], hideRequiredMarker: [{ type: i0.Input, args: [{ isSignal: true, alias: "hideRequiredMarker", required: false }] }], subscriptSizing: [{ type: i0.Input, args: [{ isSignal: true, alias: "subscriptSizing", required: false }] }], userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }] } });
|
|
915
|
+
|
|
916
|
+
// Public API for the form-field component
|
|
917
|
+
// Main component
|
|
918
|
+
|
|
919
|
+
/**
|
|
920
|
+
* Generated bundle index. Do not edit.
|
|
921
|
+
*/
|
|
922
|
+
|
|
923
|
+
export { ComError, ComFormField, ComHint, ComInput, ComLabel, ComPrefix, ComSuffix, ErrorStateMatcher, FORM_FIELD_DEFAULTS, FormFieldControl, errorVariants, formFieldContainerVariants, formFieldLabelVariants, formFieldSubscriptVariants, formFieldVariants, hintVariants, prefixVariants, suffixVariants };
|
|
924
|
+
//# sourceMappingURL=ngx-com-components-form-field.mjs.map
|