@egose/shadcn-theme-ng-tw 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/accordion/README.md +405 -2
- package/alert/README.md +372 -2
- package/alert/fesm2022/alert.mjs +1 -1
- package/alert-dialog/README.md +471 -5
- package/aspect-ratio/README.md +272 -5
- package/autocomplete/README.md +502 -2
- package/autocomplete/fesm2022/autocomplete.mjs +1 -1
- package/avatar/README.md +357 -5
- package/badge/README.md +318 -2
- package/basic-alert/README.md +353 -2
- package/breadcrumb/README.md +406 -5
- package/button/README.md +482 -2
- package/button/fesm2022/button.mjs +85 -107
- package/button/types/button.d.ts +5 -8
- package/button-group/README.md +318 -5
- package/button-group/fesm2022/button-group.mjs +1 -1
- package/calendar/README.md +357 -2
- package/card/README.md +331 -5
- package/carousel/README.md +333 -5
- package/carousel/fesm2022/carousel.mjs +4 -1
- package/checkbox/README.md +320 -2
- package/checkbox/fesm2022/checkbox.mjs +6 -7
- package/checkbox/types/checkbox.d.ts +1 -1
- package/collapsible/README.md +332 -5
- package/combobox/README.md +507 -5
- package/combobox/fesm2022/combobox.mjs +5 -2
- package/command/README.md +435 -5
- package/confirmation-dialog/README.md +301 -2
- package/context-menu/README.md +366 -5
- package/date-picker/README.md +469 -2
- package/date-picker/fesm2022/date-picker.mjs +150 -32
- package/date-picker/types/date-picker.d.ts +102 -9
- package/dialog/README.md +448 -2
- package/drawer/README.md +395 -5
- package/dropdown-menu/README.md +417 -5
- package/empty/README.md +329 -5
- package/field/README.md +385 -5
- package/form-autocomplete/README.md +177 -0
- package/form-autocomplete/fesm2022/form-autocomplete.mjs +211 -0
- package/form-autocomplete/package.json +24 -0
- package/form-autocomplete/types/form-autocomplete.d.ts +61 -0
- package/form-checkbox/README.md +322 -2
- package/form-checkbox/fesm2022/form-checkbox.mjs +22 -9
- package/form-checkbox/types/form-checkbox.d.ts +23 -4
- package/form-combobox/README.md +202 -0
- package/form-combobox/fesm2022/form-combobox.mjs +273 -0
- package/form-combobox/package.json +24 -0
- package/form-combobox/types/form-combobox.d.ts +73 -0
- package/form-date-picker/README.md +348 -2
- package/form-date-picker/fesm2022/form-date-picker.mjs +60 -16
- package/form-date-picker/types/form-date-picker.d.ts +17 -1
- package/form-date-picker-multi/README.md +191 -0
- package/form-date-picker-multi/fesm2022/form-date-picker-multi.mjs +239 -0
- package/form-date-picker-multi/package.json +24 -0
- package/form-date-picker-multi/types/form-date-picker-multi.d.ts +58 -0
- package/form-date-range-picker/README.md +253 -0
- package/form-date-range-picker/fesm2022/form-date-range-picker.mjs +236 -0
- package/form-date-range-picker/package.json +24 -0
- package/form-date-range-picker/types/form-date-range-picker.d.ts +55 -0
- package/form-field/README.md +356 -2
- package/form-field-simple/README.md +340 -2
- package/form-input-otp/README.md +194 -0
- package/form-input-otp/fesm2022/form-input-otp.mjs +194 -0
- package/form-input-otp/package.json +24 -0
- package/form-input-otp/types/form-input-otp.d.ts +58 -0
- package/form-month-year-picker/README.md +188 -0
- package/form-month-year-picker/fesm2022/form-month-year-picker.mjs +229 -0
- package/form-month-year-picker/package.json +24 -0
- package/form-month-year-picker/types/form-month-year-picker.d.ts +53 -0
- package/form-native-select/README.md +205 -0
- package/form-native-select/fesm2022/form-native-select.mjs +187 -0
- package/form-native-select/package.json +24 -0
- package/form-native-select/types/form-native-select.d.ts +57 -0
- package/form-phone-input/README.md +188 -0
- package/form-phone-input/fesm2022/form-phone-input.mjs +203 -0
- package/form-phone-input/package.json +24 -0
- package/form-phone-input/types/form-phone-input.d.ts +58 -0
- package/form-radio-group/README.md +198 -0
- package/form-radio-group/fesm2022/form-radio-group.mjs +211 -0
- package/form-radio-group/package.json +24 -0
- package/form-radio-group/types/form-radio-group.d.ts +63 -0
- package/form-searchable-multiselect/README.md +371 -2
- package/form-searchable-multiselect/fesm2022/form-searchable-multiselect.mjs +19 -10
- package/form-searchable-multiselect/types/form-searchable-multiselect.d.ts +18 -1
- package/form-select/README.md +360 -2
- package/form-select/fesm2022/form-select.mjs +24 -15
- package/form-select/types/form-select.d.ts +18 -1
- package/form-slider/README.md +182 -0
- package/form-slider/fesm2022/form-slider.mjs +186 -0
- package/form-slider/package.json +24 -0
- package/form-slider/types/form-slider.d.ts +60 -0
- package/form-switch/README.md +173 -0
- package/form-switch/fesm2022/form-switch.mjs +178 -0
- package/form-switch/package.json +24 -0
- package/form-switch/types/form-switch.d.ts +50 -0
- package/form-text-input/README.md +381 -2
- package/form-text-input/fesm2022/form-text-input.mjs +19 -10
- package/form-text-input/types/form-text-input.d.ts +18 -1
- package/form-textarea/README.md +357 -2
- package/form-textarea/fesm2022/form-textarea.mjs +19 -10
- package/form-textarea/types/form-textarea.d.ts +18 -1
- package/form-toggle/README.md +186 -0
- package/form-toggle/fesm2022/form-toggle.mjs +241 -0
- package/form-toggle/package.json +24 -0
- package/form-toggle/types/form-toggle.d.ts +82 -0
- package/form-toggle-group/README.md +176 -0
- package/form-toggle-group/fesm2022/form-toggle-group.mjs +216 -0
- package/form-toggle-group/package.json +24 -0
- package/form-toggle-group/types/form-toggle-group.d.ts +65 -0
- package/hover-card/README.md +256 -5
- package/icon/README.md +239 -2
- package/input/README.md +269 -2
- package/input-group/README.md +335 -5
- package/input-group/fesm2022/input-group.mjs +23 -13
- package/input-group/types/input-group.d.ts +4 -1
- package/input-otp/README.md +375 -5
- package/item/README.md +385 -5
- package/item/fesm2022/item.mjs +3 -3
- package/kbd/README.md +291 -5
- package/label/README.md +272 -2
- package/layout-simple/README.md +193 -2
- package/layout-simple/fesm2022/layout-simple.mjs +877 -409
- package/layout-simple/types/layout-simple.d.ts +174 -137
- package/menu/README.md +417 -2
- package/menubar/README.md +343 -5
- package/native-select/README.md +323 -5
- package/native-select/fesm2022/native-select.mjs +18 -6
- package/native-select/types/native-select.d.ts +7 -2
- package/navigation-menu/README.md +369 -5
- package/package.json +57 -1
- package/pagination/README.md +388 -5
- package/phone-input/README.md +114 -0
- package/phone-input/fesm2022/phone-input.mjs +191 -0
- package/phone-input/package.json +24 -0
- package/phone-input/types/phone-input.d.ts +67 -0
- package/popover/README.md +331 -2
- package/progress/README.md +311 -5
- package/radio-group/README.md +364 -2
- package/radio-group/fesm2022/radio-group.mjs +5 -1
- package/resizable/README.md +269 -5
- package/scroll-area/README.md +233 -5
- package/searchable-multiselect/README.md +323 -2
- package/select/README.md +437 -2
- package/separator/README.md +222 -2
- package/sheet/README.md +311 -2
- package/sheet/fesm2022/sheet.mjs +1 -1
- package/sidebar/README.md +457 -5
- package/skeleton/README.md +217 -5
- package/slider/README.md +273 -5
- package/slider/fesm2022/slider.mjs +17 -13
- package/sonner/README.md +346 -2
- package/spinner/README.md +284 -2
- package/switch/README.md +310 -2
- package/switch/fesm2022/switch.mjs +7 -5
- package/switch/types/switch.d.ts +2 -1
- package/table/README.md +423 -5
- package/tabs/README.md +411 -2
- package/tabs/fesm2022/tabs.mjs +12 -2
- package/textarea/README.md +282 -5
- package/toggle/README.md +270 -5
- package/toggle-group/README.md +340 -5
- package/tooltip/README.md +269 -2
- package/typography/README.md +271 -5
- package/utils/README.md +303 -2
|
@@ -1,3 +1,324 @@
|
|
|
1
|
-
# Searchable Multiselect
|
|
1
|
+
# Searchable Multiselect (`@egose/shadcn-theme-ng/searchable-multiselect`)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A shadcn/ui-style **multi-select with chips + popover picker** — selected values render as removable chips, and a popover holds the checkbox option list. There is no exact single shadcn/ui counterpart; it composes the `Popover`, `Button`, and `Checkbox` patterns into one opinionated control.
|
|
4
|
+
|
|
5
|
+
It is a **standalone `ControlValueAccessor` component** (`EgSearchableMultiselect`), so it binds directly to Angular reactive forms (`formControlName`) and template-driven forms (`ngModel`) with a `string[]` value. Internally it reuses `HlmPopover`/`HlmCheckbox`/`HlmButton` — you do not import those yourself.
|
|
6
|
+
|
|
7
|
+
> **Ships as:** `@egose/shadcn-theme-ng/searchable-multiselect` and `@egose/shadcn-theme-ng-tw/searchable-multiselect` (the `tw:`-prefixed Tailwind variant — same API, class strings prefixed with `tw:`).
|
|
8
|
+
> See the [package README](../../README.md) for installation, peer dependencies, and Tailwind setup. Do not publish this project directory independently.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
# Plain Tailwind (no prefix)
|
|
14
|
+
npm install @egose/shadcn-theme-ng
|
|
15
|
+
|
|
16
|
+
# Or the tw:-prefixed variant
|
|
17
|
+
npm install @egose/shadcn-theme-ng-tw
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { EgSearchableMultiselect } from '@egose/shadcn-theme-ng/searchable-multiselect';
|
|
22
|
+
// tw variant:
|
|
23
|
+
// import { EgSearchableMultiselect } from '@egose/shadcn-theme-ng-tw/searchable-multiselect';
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Peer dependencies (see [package README](../../README.md) for versions): `@angular/core`, `@angular/common`. (No `@spartan-ng/brain` peer — popover/checkbox/button come along as regular library dependencies.)
|
|
27
|
+
|
|
28
|
+
## Imports
|
|
29
|
+
|
|
30
|
+
Real exported symbols (from `src/public-api.ts`):
|
|
31
|
+
|
|
32
|
+
| Symbol | Kind | Description |
|
|
33
|
+
| ------------------------- | -------------------------------------------------- | ---------------------------------------------------------- |
|
|
34
|
+
| `EgSearchableMultiselect` | Standalone component (`eg-searchable-multiselect`) | The whole control; provides `NG_VALUE_ACCESSOR` for itself |
|
|
35
|
+
| `SelectOption` | Interface | `{ label: string; value: string }` |
|
|
36
|
+
|
|
37
|
+
> Note: unlike most subpaths, this one exposes **no `*Imports` array and no `*Module`**. Import `EgSearchableMultiselect` directly — it is standalone.
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { Component } from '@angular/core';
|
|
41
|
+
import { EgSearchableMultiselect } from '@egose/shadcn-theme-ng/searchable-multiselect';
|
|
42
|
+
|
|
43
|
+
@Component({
|
|
44
|
+
selector: 'app-demo',
|
|
45
|
+
standalone: true,
|
|
46
|
+
imports: [EgSearchableMultiselect],
|
|
47
|
+
template: ` <eg-searchable-multiselect [options]="options" [(value)]="selected" /> `,
|
|
48
|
+
})
|
|
49
|
+
export class DemoComponent {}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Anatomy / Structure
|
|
53
|
+
|
|
54
|
+
```html
|
|
55
|
+
<eg-searchable-multiselect [options]="options" placeholder="Pick frameworks…" [(value)]="selected" />
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Renders (internally — you do not write this yourself):
|
|
59
|
+
|
|
60
|
+
```html
|
|
61
|
+
<eg-searchable-multiselect>
|
|
62
|
+
<!-- chip row: placeholder pill when empty, removable chips otherwise -->
|
|
63
|
+
<div>
|
|
64
|
+
<span><!-- {{ placeholder }} or chip {{ item.label }} + ✕ button --></span>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<!-- popover trigger + checkbox list -->
|
|
68
|
+
<hlm-popover>
|
|
69
|
+
<button hlmPopoverTrigger hlmButton>N selected</button>
|
|
70
|
+
<hlm-popover-content>
|
|
71
|
+
<label><!-- <hlm-checkbox> per option + label text --></label>
|
|
72
|
+
</hlm-popover-content>
|
|
73
|
+
</hlm-popover>
|
|
74
|
+
</eg-searchable-multiselect>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Real selector: `eg-searchable-multiselect` (element). The `✕` chip buttons and popover trigger honor the disabled state; the popover panel is `tw:w-64` with a `tw:max-h-60` scrolling option list.
|
|
78
|
+
|
|
79
|
+
## API reference
|
|
80
|
+
|
|
81
|
+
### EgSearchableMultiselect (component, `ControlValueAccessor`)
|
|
82
|
+
|
|
83
|
+
| Input | Type | Default | Description |
|
|
84
|
+
| --------------------- | --------------------- | ------------------------ | -------------------------------------------------------------------------------- |
|
|
85
|
+
| `options` | `SelectOption[]` | `[]` | Full option list (`{ label, value }`) |
|
|
86
|
+
| `value` | `string[]` | `[]` | Selected values (one-way in; pairs with `valueChange` for two-way `[(value)]`) |
|
|
87
|
+
| `placeholder` | `string` | `'Start typing to add…'` | Text of the pill shown when nothing is selected |
|
|
88
|
+
| `id` | `string` | `''` | `id` placed on the trigger button |
|
|
89
|
+
| `disabled` | `boolean` | `false` | Disables chips + trigger + checkboxes |
|
|
90
|
+
| `wrapperDisabled` | `boolean` | `false` | Second disable flag (e.g. set by wrapper form components); OR-ed with `disabled` |
|
|
91
|
+
| `ariaLabel` | `string \| undefined` | `undefined` | `aria-label` for the trigger button |
|
|
92
|
+
| `ariaDescribedby` | `string \| null` | `null` | `aria-describedby` for the trigger button |
|
|
93
|
+
| `class` (`userClass`) | `ClassValue` | `''` | Extra classes on the host |
|
|
94
|
+
|
|
95
|
+
| Output | Type | Description |
|
|
96
|
+
| ------------- | ---------- | ---------------------------------------------------- |
|
|
97
|
+
| `valueChange` | `string[]` | Emitted with the new value array on every add/remove |
|
|
98
|
+
|
|
99
|
+
`ControlValueAccessor` contract: `writeValue(values)`, `registerOnChange`, `registerOnTouched`, `setDisabledState` are implemented, so `formControl` / `formControlName` / `ngModel` all work. Effective disabled state = `disabled() \|\| wrapperDisabled() \|\| formDisabled()` (the last set by forms via `setDisabledState`).
|
|
100
|
+
|
|
101
|
+
API surprise worth knowing: despite the "searchable" name, the current template ships **no filter text field** — the popover shows the full checkbox list and empty state reads "No options". Treat `options` as the complete visible list (filter it yourself before passing it in if you need search). Also `value` is a plain `input`, not a `model`: form writes flow through `writeValue`, and user edits flow out through `valueChange` + the CVA `onChange` callback.
|
|
102
|
+
|
|
103
|
+
## Examples
|
|
104
|
+
|
|
105
|
+
### 1. Basic two-way binding
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
import { Component, signal } from '@angular/core';
|
|
109
|
+
import { EgSearchableMultiselect, type SelectOption } from '@egose/shadcn-theme-ng/searchable-multiselect';
|
|
110
|
+
|
|
111
|
+
@Component({
|
|
112
|
+
selector: 'app-basic-multi',
|
|
113
|
+
standalone: true,
|
|
114
|
+
imports: [EgSearchableMultiselect],
|
|
115
|
+
template: `
|
|
116
|
+
<eg-searchable-multiselect [options]="frameworks" placeholder="Pick frameworks…" [(value)]="selected" />
|
|
117
|
+
<p>Selected: {{ selected().join(', ') || 'none' }}</p>
|
|
118
|
+
`,
|
|
119
|
+
})
|
|
120
|
+
export class BasicMultiComponent {
|
|
121
|
+
readonly frameworks: SelectOption[] = [
|
|
122
|
+
{ label: 'Angular', value: 'angular' },
|
|
123
|
+
{ label: 'React', value: 'react' },
|
|
124
|
+
{ label: 'Vue', value: 'vue' },
|
|
125
|
+
{ label: 'Svelte', value: 'svelte' },
|
|
126
|
+
];
|
|
127
|
+
readonly selected = signal<string[]>(['angular']);
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### 2. Reactive forms
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
import { Component } from '@angular/core';
|
|
135
|
+
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
|
136
|
+
import { EgSearchableMultiselect, type SelectOption } from '@egose/shadcn-theme-ng/searchable-multiselect';
|
|
137
|
+
|
|
138
|
+
@Component({
|
|
139
|
+
selector: 'app-reactive-multi',
|
|
140
|
+
standalone: true,
|
|
141
|
+
imports: [EgSearchableMultiselect, ReactiveFormsModule],
|
|
142
|
+
template: `
|
|
143
|
+
<form [formGroup]="form" (ngSubmit)="submit()">
|
|
144
|
+
<label for="skills">Skills (pick at least one)</label>
|
|
145
|
+
<eg-searchable-multiselect id="skills" [options]="skills" formControlName="skillIds" />
|
|
146
|
+
@if (form.controls.skillIds.invalid && form.controls.skillIds.touched) {
|
|
147
|
+
<p class="tw:text-destructive tw:text-sm">Choose at least one skill.</p>
|
|
148
|
+
}
|
|
149
|
+
<button type="submit">Save</button>
|
|
150
|
+
</form>
|
|
151
|
+
`,
|
|
152
|
+
})
|
|
153
|
+
export class ReactiveMultiComponent {
|
|
154
|
+
readonly skills: SelectOption[] = [
|
|
155
|
+
{ label: 'TypeScript', value: 'ts' },
|
|
156
|
+
{ label: 'CSS', value: 'css' },
|
|
157
|
+
{ label: 'Testing', value: 'testing' },
|
|
158
|
+
];
|
|
159
|
+
readonly form = new FormGroup({
|
|
160
|
+
skillIds: new FormControl<string[]>(['ts'], { validators: Validators.required }),
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
submit(): void {
|
|
164
|
+
this.form.markAllAsTouched();
|
|
165
|
+
console.log(this.form.value);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### 3. Template-driven forms (`ngModel`)
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
import { Component } from '@angular/core';
|
|
174
|
+
import { FormsModule } from '@angular/forms';
|
|
175
|
+
import { EgSearchableMultiselect, type SelectOption } from '@egose/shadcn-theme-ng/searchable-multiselect';
|
|
176
|
+
|
|
177
|
+
@Component({
|
|
178
|
+
selector: 'app-ngmodel-multi',
|
|
179
|
+
standalone: true,
|
|
180
|
+
imports: [EgSearchableMultiselect, FormsModule],
|
|
181
|
+
template: `
|
|
182
|
+
<eg-searchable-multiselect name="tags" [options]="tags" [(ngModel)]="selected" #tagsCtrl="ngModel" />
|
|
183
|
+
@if (tagsCtrl.touched && !selected.length) {
|
|
184
|
+
<p class="tw:text-destructive tw:text-sm">Pick at least one tag.</p>
|
|
185
|
+
}
|
|
186
|
+
`,
|
|
187
|
+
})
|
|
188
|
+
export class NgModelMultiComponent {
|
|
189
|
+
readonly tags: SelectOption[] = [
|
|
190
|
+
{ label: 'Bug', value: 'bug' },
|
|
191
|
+
{ label: 'Feature', value: 'feature' },
|
|
192
|
+
{ label: 'Docs', value: 'docs' },
|
|
193
|
+
];
|
|
194
|
+
selected: string[] = [];
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### 4. Disabled / read-only states
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
import { Component } from '@angular/core';
|
|
202
|
+
import { EgSearchableMultiselect } from '@egose/shadcn-theme-ng/searchable-multiselect';
|
|
203
|
+
|
|
204
|
+
@Component({
|
|
205
|
+
selector: 'app-disabled-multi',
|
|
206
|
+
standalone: true,
|
|
207
|
+
imports: [EgSearchableMultiselect],
|
|
208
|
+
template: `
|
|
209
|
+
<!-- fully disabled: chips, ✕ buttons, trigger, checkboxes -->
|
|
210
|
+
<eg-searchable-multiselect [options]="options" [value]="['a']" disabled />
|
|
211
|
+
|
|
212
|
+
<!-- wrapper-driven disable (e.g. parent form section locked) -->
|
|
213
|
+
<eg-searchable-multiselect [options]="options" [value]="['a']" [wrapperDisabled]="locked" />
|
|
214
|
+
`,
|
|
215
|
+
})
|
|
216
|
+
export class DisabledMultiComponent {
|
|
217
|
+
readonly locked = true;
|
|
218
|
+
readonly options = [
|
|
219
|
+
{ label: 'Alpha', value: 'a' },
|
|
220
|
+
{ label: 'Beta', value: 'b' },
|
|
221
|
+
];
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Disabling via `formControl.disable()` works too — it flows through `setDisabledState`.
|
|
226
|
+
|
|
227
|
+
### 5. Client-side search (filter `options` yourself)
|
|
228
|
+
|
|
229
|
+
Since the popover lists exactly what you pass in `options`, implement search by filtering upstream:
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
import { Component, computed, signal } from '@angular/core';
|
|
233
|
+
import { EgSearchableMultiselect } from '@egose/shadcn-theme-ng/searchable-multiselect';
|
|
234
|
+
|
|
235
|
+
@Component({
|
|
236
|
+
selector: 'app-search-multi',
|
|
237
|
+
standalone: true,
|
|
238
|
+
imports: [EgSearchableMultiselect],
|
|
239
|
+
template: `
|
|
240
|
+
<input
|
|
241
|
+
type="search"
|
|
242
|
+
placeholder="Filter options…"
|
|
243
|
+
[value]="query()"
|
|
244
|
+
(input)="query.set($any($event.target).value)"
|
|
245
|
+
aria-label="Filter options"
|
|
246
|
+
/>
|
|
247
|
+
<eg-searchable-multiselect [options]="filtered()" [(value)]="selected" />
|
|
248
|
+
`,
|
|
249
|
+
})
|
|
250
|
+
export class SearchMultiComponent {
|
|
251
|
+
readonly query = signal('');
|
|
252
|
+
readonly selected = signal<string[]>([]);
|
|
253
|
+
private readonly all = [
|
|
254
|
+
{ label: 'Angular', value: 'angular' },
|
|
255
|
+
{ label: 'React', value: 'react' },
|
|
256
|
+
{ label: 'Vue', value: 'vue' },
|
|
257
|
+
{ label: 'Svelte', value: 'svelte' },
|
|
258
|
+
{ label: 'Solid', value: 'solid' },
|
|
259
|
+
];
|
|
260
|
+
readonly filtered = computed(() => {
|
|
261
|
+
const q = this.query().trim().toLowerCase();
|
|
262
|
+
return q ? this.all.filter((o) => o.label.toLowerCase().includes(q)) : this.all;
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Note: selections whose option is currently filtered out stay selected internally (chips still show) but have no checkbox row until the filter matches again. Unknown values passed via `value`/`writeValue` that match no option are dropped from the chip row.
|
|
268
|
+
|
|
269
|
+
### 6. Async options + reacting to changes
|
|
270
|
+
|
|
271
|
+
```ts
|
|
272
|
+
import { Component, resource, signal } from '@angular/core';
|
|
273
|
+
import { EgSearchableMultiselect, type SelectOption } from '@egose/shadcn-theme-ng/searchable-multiselect';
|
|
274
|
+
|
|
275
|
+
@Component({
|
|
276
|
+
selector: 'app-async-multi',
|
|
277
|
+
standalone: true,
|
|
278
|
+
imports: [EgSearchableMultiselect],
|
|
279
|
+
template: `
|
|
280
|
+
@if (users.isLoading()) {
|
|
281
|
+
<p>Loading users…</p>
|
|
282
|
+
} @else {
|
|
283
|
+
<eg-searchable-multiselect
|
|
284
|
+
[options]="users.value() ?? []"
|
|
285
|
+
[(value)]="assignees"
|
|
286
|
+
(valueChange)="onChange($event)"
|
|
287
|
+
ariaLabel="Assignees"
|
|
288
|
+
/>
|
|
289
|
+
}
|
|
290
|
+
`,
|
|
291
|
+
})
|
|
292
|
+
export class AsyncMultiComponent {
|
|
293
|
+
readonly assignees = signal<string[]>([]);
|
|
294
|
+
readonly users = resource({
|
|
295
|
+
loader: async (): Promise<SelectOption[]> => {
|
|
296
|
+
const res = await fetch('/api/users');
|
|
297
|
+
const list = (await res.json()) as Array<{ id: string; name: string }>;
|
|
298
|
+
return list.map((u) => ({ label: u.name, value: u.id }));
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
onChange(values: string[]): void {
|
|
303
|
+
console.log('assignees now:', values);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
## Accessibility notes
|
|
309
|
+
|
|
310
|
+
- The trigger is a real `<button>` — give it an accessible name via `ariaLabel` (or a visible `<label>` paired with `id`) and descriptions via `ariaDescribedby`.
|
|
311
|
+
- Options render as native checkbox-backed `hlm-checkbox` rows inside `<label>` elements, so they are keyboard-operable and announced per option.
|
|
312
|
+
- Chip `✕` buttons are real buttons and disabled along with the control; keep chip text concise so screen readers announce removals cleanly.
|
|
313
|
+
- The empty state is a plain text pill (not focusable) — the popover trigger remains the single keyboard entry point, which keeps tab order simple.
|
|
314
|
+
|
|
315
|
+
## Theming / CSS variables
|
|
316
|
+
|
|
317
|
+
Class-driven (chips, popover panel, checkbox rows). Extend via the `class` input on the host; inner popover width (`tw:w-64`) and list height (`tw:max-h-60`) are fixed in the template.
|
|
318
|
+
|
|
319
|
+
## Related subpaths
|
|
320
|
+
|
|
321
|
+
- `@egose/shadcn-theme-ng/form-searchable-multiselect` — form-field wrapper (label/description/error) around this control
|
|
322
|
+
- `@egose/shadcn-theme-ng/select` — single-select dropdown counterpart
|
|
323
|
+
- `@egose/shadcn-theme-ng/popover` — the underlying popover primitive
|
|
324
|
+
- `@egose/shadcn-theme-ng/checkbox` — the underlying option-row primitive
|