list-selection-input 15.0.4 → 15.0.5
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 +1069 -13
- package/fesm2022/list-selection-input.mjs +34 -34
- package/fesm2022/list-selection-input.mjs.map +1 -1
- package/list-selection-input-15.0.5.tgz +0 -0
- package/package.json +12 -5
- package/types/list-selection-input.d.ts +215 -0
- package/esm2022/lib/list-selection-demo/list-selection-demo.component.mjs +0 -167
- package/esm2022/lib/list-selection-input/list-selection-input.component.mjs +0 -265
- package/esm2022/lib/list-selection-input.module.mjs +0 -101
- package/esm2022/lib/models/index.mjs +0 -6
- package/esm2022/lib/models/selection-basic.model.mjs +0 -10
- package/esm2022/lib/models/selection-item.model.mjs +0 -12
- package/esm2022/lib/multi-selection-demo/multi-selection-demo.component.mjs +0 -167
- package/esm2022/lib/pipes/remove-underscore.pipe.mjs +0 -17
- package/esm2022/list-selection-input.mjs +0 -5
- package/esm2022/public-api.mjs +0 -10
- package/index.d.ts +0 -5
- package/lib/list-selection-demo/list-selection-demo.component.d.ts +0 -54
- package/lib/list-selection-input/list-selection-input.component.d.ts +0 -53
- package/lib/list-selection-input.module.d.ts +0 -27
- package/lib/models/index.d.ts +0 -2
- package/lib/models/selection-basic.model.d.ts +0 -10
- package/lib/models/selection-item.model.d.ts +0 -14
- package/lib/multi-selection-demo/multi-selection-demo.component.d.ts +0 -54
- package/lib/pipes/remove-underscore.pipe.d.ts +0 -7
- package/list-selection-input-15.0.4.tgz +0 -0
- package/public-api.d.ts +0 -6
package/README.md
CHANGED
|
@@ -1,24 +1,1080 @@
|
|
|
1
|
-
#
|
|
1
|
+
# List Selection Input Component
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## Overview
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
The `list-selection-input` library provides a Material Design dropdown selection component that allows users to select from a list of options in a space-efficient manner. It supports both single and multiple selection modes, validation limits, disabled items, and works with both string and object data formats. Built with Angular Material select components and implementing `ControlValueAccessor` for seamless form integration.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
> Note: Don't forget to add `--project list-selection-input` or else it will be added to the default project in your `angular.json` file.
|
|
7
|
+
### Core Capabilities
|
|
9
8
|
|
|
10
|
-
|
|
9
|
+
#### 📋 Dropdown List Selection Interface
|
|
11
10
|
|
|
12
|
-
|
|
11
|
+
- **Single/Multiple Selection**: Support for both single selection and multi-select modes
|
|
12
|
+
- **Form Validation**: Built-in min/max selection validation with error handling
|
|
13
|
+
- **Flexible Data Support**: Handles both string arrays and complex object arrays
|
|
14
|
+
- **Disabled State Support**: Mark individual items as non-selectable
|
|
15
|
+
- **Material Design**: Built on Angular Material select foundation
|
|
16
|
+
- **Form Control Integration**: Implements `ControlValueAccessor` for reactive forms
|
|
17
|
+
- **Validation Integration**: Native Angular validation system compatibility
|
|
18
|
+
- **Space Efficient**: Dropdown interface saves screen space
|
|
13
19
|
|
|
14
|
-
|
|
20
|
+
#### 🔧 Features
|
|
15
21
|
|
|
16
|
-
|
|
22
|
+
✅ **ControlValueAccessor Implementation** - Works with Angular forms
|
|
23
|
+
✅ **Material Design Integration** - Uses Angular Material components
|
|
24
|
+
✅ **Single & Multiple Selection** - Flexible selection modes
|
|
25
|
+
✅ **Min/Max Validation** - Configurable selection limits
|
|
26
|
+
✅ **Disabled Items** - Mark individual options as non-selectable
|
|
27
|
+
✅ **Flexible Data Types** - Support for strings and objects
|
|
28
|
+
✅ **Pre-selection** - Initialize with selected items
|
|
29
|
+
✅ **Form Validation** - Native validation integration
|
|
30
|
+
✅ **Dropdown Interface** - Space-efficient selection display
|
|
17
31
|
|
|
18
|
-
|
|
32
|
+
### Key Benefits
|
|
19
33
|
|
|
20
|
-
|
|
34
|
+
| Feature | Description |
|
|
35
|
+
|---------|-------------|
|
|
36
|
+
| **Space Efficient** | Compact dropdown interface vs. visible lists |
|
|
37
|
+
| **Flexible Selection** | Support for single or multiple item selection |
|
|
38
|
+
| **Rich Validation** | Built-in min/max selection limits with error states |
|
|
39
|
+
| **Data Format Support** | Works with simple strings or complex objects |
|
|
40
|
+
| **State Management** | Disabled states and pre-selection capabilities |
|
|
41
|
+
| **Form Integration** | Seamless Angular form control and validation integration |
|
|
21
42
|
|
|
22
|
-
|
|
43
|
+
---
|
|
23
44
|
|
|
24
|
-
|
|
45
|
+
## Summary
|
|
46
|
+
|
|
47
|
+
The `list-selection-input` library provides a space-efficient dropdown selection component with comprehensive form integration, validation support, and multiple selection capabilities for Angular applications.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Quick Start Guide
|
|
52
|
+
|
|
53
|
+
### Installation & Setup (2 minutes)
|
|
54
|
+
|
|
55
|
+
#### 1. Import Module
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
// app.module.ts
|
|
59
|
+
import { ListSelectionInputModule } from 'list-selection-input';
|
|
60
|
+
|
|
61
|
+
@NgModule({
|
|
62
|
+
imports: [
|
|
63
|
+
ListSelectionInputModule
|
|
64
|
+
]
|
|
65
|
+
})
|
|
66
|
+
export class AppModule { }
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
#### 2. No Module Configuration Required
|
|
70
|
+
|
|
71
|
+
The `ListSelectionInputModule` does not require global configuration. Components can be used immediately after module import.
|
|
72
|
+
|
|
73
|
+
### Quick Examples
|
|
74
|
+
|
|
75
|
+
#### Example 1: Basic Single Selection
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
import { Component } from '@angular/core';
|
|
79
|
+
import { FormControl } from '@angular/forms';
|
|
80
|
+
|
|
81
|
+
@Component({
|
|
82
|
+
selector: 'app-basic-list-select',
|
|
83
|
+
template: `
|
|
84
|
+
<app-list-selection-input
|
|
85
|
+
[formControl]="selectionControl"
|
|
86
|
+
[data]="options">
|
|
87
|
+
</app-list-selection-input>
|
|
88
|
+
|
|
89
|
+
<div>Selected: {{ selectionControl.value }}</div>
|
|
90
|
+
`
|
|
91
|
+
})
|
|
92
|
+
export class BasicListSelectComponent {
|
|
93
|
+
selectionControl = new FormControl();
|
|
94
|
+
|
|
95
|
+
options = ['Option 1', 'Option 2', 'Option 3', 'Option 4'];
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
#### Example 2: Multiple Selection with Validation
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
import { Component } from '@angular/core';
|
|
103
|
+
import { FormControl, Validators } from '@angular/forms';
|
|
104
|
+
|
|
105
|
+
@Component({
|
|
106
|
+
selector: 'app-multi-list-select',
|
|
107
|
+
template: `
|
|
108
|
+
<app-list-selection-input
|
|
109
|
+
[formControl]="multiControl"
|
|
110
|
+
[data]="skills"
|
|
111
|
+
[multiple]="true"
|
|
112
|
+
[minSelection]="2"
|
|
113
|
+
[maxSelection]="4"
|
|
114
|
+
label="Select Skills"
|
|
115
|
+
placeholder="Choose your skills">
|
|
116
|
+
</app-list-selection-input>
|
|
117
|
+
|
|
118
|
+
<div class="errors" *ngIf="multiControl.errors && multiControl.touched">
|
|
119
|
+
<div *ngIf="multiControl.hasError('minRequired')">
|
|
120
|
+
Please select at least 2 skills
|
|
121
|
+
</div>
|
|
122
|
+
<div *ngIf="multiControl.hasError('maxExceeded')">
|
|
123
|
+
You can select maximum 4 skills
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
`,
|
|
127
|
+
styles: [`
|
|
128
|
+
.errors {
|
|
129
|
+
color: #f44336;
|
|
130
|
+
font-size: 0.875rem;
|
|
131
|
+
margin-top: 0.5rem;
|
|
132
|
+
}
|
|
133
|
+
`]
|
|
134
|
+
})
|
|
135
|
+
export class MultiListSelectComponent {
|
|
136
|
+
multiControl = new FormControl([], [
|
|
137
|
+
Validators.required,
|
|
138
|
+
Validators.minLength(2),
|
|
139
|
+
Validators.maxLength(4)
|
|
140
|
+
]);
|
|
141
|
+
|
|
142
|
+
skills = [
|
|
143
|
+
'JavaScript', 'TypeScript', 'Python', 'Java', 'C#', 'PHP', 'Ruby', 'Go',
|
|
144
|
+
'Rust', 'Swift', 'Kotlin', 'Dart', 'Scala', 'Clojure', 'Elixir', 'Haskell'
|
|
145
|
+
];
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
#### Example 3: Object Data with Disabled Items
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
import { Component } from '@angular/core';
|
|
153
|
+
import { FormControl } from '@angular/forms';
|
|
154
|
+
|
|
155
|
+
@Component({
|
|
156
|
+
selector: 'app-object-list-select',
|
|
157
|
+
template: `
|
|
158
|
+
<app-list-selection-input
|
|
159
|
+
[formControl]="userControl"
|
|
160
|
+
[data]="userOptions"
|
|
161
|
+
label="Assign User"
|
|
162
|
+
placeholder="Select a user"
|
|
163
|
+
displayField="label">
|
|
164
|
+
</app-list-selection-input>
|
|
165
|
+
|
|
166
|
+
<div *ngIf="userControl.value" class="user-info">
|
|
167
|
+
<h4>{{ userControl.value.label }}</h4>
|
|
168
|
+
<p>{{ userControl.value.email }}</p>
|
|
169
|
+
<p>Role: {{ userControl.value.role }}</p>
|
|
170
|
+
</div>
|
|
171
|
+
`,
|
|
172
|
+
styles: [`
|
|
173
|
+
.user-info {
|
|
174
|
+
margin-top: 1rem;
|
|
175
|
+
padding: 1rem;
|
|
176
|
+
border: 1px solid #ddd;
|
|
177
|
+
border-radius: 4px;
|
|
178
|
+
background: #f9f9f9;
|
|
179
|
+
}
|
|
180
|
+
.user-info h4 {
|
|
181
|
+
margin: 0 0 0.5rem 0;
|
|
182
|
+
color: #333;
|
|
183
|
+
}
|
|
184
|
+
.user-info p {
|
|
185
|
+
margin: 0.25rem 0;
|
|
186
|
+
font-size: 0.9rem;
|
|
187
|
+
color: #666;
|
|
188
|
+
}
|
|
189
|
+
`]
|
|
190
|
+
})
|
|
191
|
+
export class ObjectListSelectComponent {
|
|
192
|
+
userControl = new FormControl();
|
|
193
|
+
|
|
194
|
+
userOptions = [
|
|
195
|
+
{ value: 'user1', label: 'John Doe', email: 'john@example.com', role: 'Admin', disabled: false },
|
|
196
|
+
{ value: 'user2', label: 'Jane Smith', email: 'jane@example.com', role: 'Editor', disabled: false },
|
|
197
|
+
{ value: 'user3', label: 'Bob Johnson', email: 'bob@example.com', role: 'Viewer', disabled: true },
|
|
198
|
+
{ value: 'user4', label: 'Alice Brown', email: 'alice@example.com', role: 'Editor', disabled: false },
|
|
199
|
+
{ value: 'user5', label: 'Charlie Wilson', email: 'charlie@example.com', role: 'Admin', disabled: false }
|
|
200
|
+
];
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
#### Example 4: Multi-Select with Pre-selection
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
import { Component } from '@angular/core';
|
|
208
|
+
import { FormControl } from '@angular/forms';
|
|
209
|
+
|
|
210
|
+
@Component({
|
|
211
|
+
selector: 'app-pre-selected-list',
|
|
212
|
+
template: `
|
|
213
|
+
<app-list-selection-input
|
|
214
|
+
[formControl]="preSelectedControl"
|
|
215
|
+
[data]="preSelectedOptions"
|
|
216
|
+
[multiple]="true"
|
|
217
|
+
label="Favorite Languages"
|
|
218
|
+
placeholder="Select your favorite programming languages">
|
|
219
|
+
</app-list-selection-input>
|
|
220
|
+
|
|
221
|
+
<div class="selection-info">
|
|
222
|
+
Selected ({{ preSelectedControl.value?.length || 0 }}):
|
|
223
|
+
{{ preSelectedControl.value?.join(', ') || 'None' }}
|
|
224
|
+
</div>
|
|
225
|
+
`,
|
|
226
|
+
styles: [`
|
|
227
|
+
.selection-info {
|
|
228
|
+
margin-top: 1rem;
|
|
229
|
+
padding: 0.75rem;
|
|
230
|
+
background: #e8f5e8;
|
|
231
|
+
border: 1px solid #4caf50;
|
|
232
|
+
border-radius: 4px;
|
|
233
|
+
color: #2e7d32;
|
|
234
|
+
font-size: 0.9rem;
|
|
235
|
+
}
|
|
236
|
+
`]
|
|
237
|
+
})
|
|
238
|
+
export class PreSelectedListComponent {
|
|
239
|
+
preSelectedControl = new FormControl(['JavaScript', 'TypeScript']);
|
|
240
|
+
|
|
241
|
+
preSelectedOptions = [
|
|
242
|
+
{ value: 'JavaScript', label: 'JavaScript', selected: true },
|
|
243
|
+
{ value: 'TypeScript', label: 'TypeScript', selected: true },
|
|
244
|
+
{ value: 'Python', label: 'Python', selected: false },
|
|
245
|
+
{ value: 'Java', label: 'Java', selected: false },
|
|
246
|
+
{ value: 'C#', label: 'C#', selected: false },
|
|
247
|
+
{ value: 'PHP', label: 'PHP', selected: false },
|
|
248
|
+
{ value: 'Ruby', label: 'Ruby', selected: false },
|
|
249
|
+
{ value: 'Go', label: 'Go', selected: false }
|
|
250
|
+
];
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
#### Example 5: Form Integration with Dynamic Options
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
import { Component } from '@angular/core';
|
|
258
|
+
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
|
259
|
+
|
|
260
|
+
@Component({
|
|
261
|
+
selector: 'app-dynamic-list-form',
|
|
262
|
+
template: `
|
|
263
|
+
<form [formGroup]="dynamicForm">
|
|
264
|
+
<app-list-selection-input
|
|
265
|
+
formControlName="category"
|
|
266
|
+
[data]="categoryOptions"
|
|
267
|
+
label="Product Category"
|
|
268
|
+
placeholder="Select a category"
|
|
269
|
+
[required]="true">
|
|
270
|
+
</app-list-selection-input>
|
|
271
|
+
|
|
272
|
+
<app-list-selection-input
|
|
273
|
+
formControlName="subcategories"
|
|
274
|
+
[data]="subcategoryOptions"
|
|
275
|
+
label="Subcategories"
|
|
276
|
+
placeholder="Select subcategories"
|
|
277
|
+
[multiple]="true"
|
|
278
|
+
[minSelection]="1"
|
|
279
|
+
[maxSelection]="3"
|
|
280
|
+
[disabled]="!dynamicForm.get('category')?.value">
|
|
281
|
+
</app-list-selection-input>
|
|
282
|
+
</form>
|
|
283
|
+
|
|
284
|
+
<div class="form-status">
|
|
285
|
+
<div>Form Valid: {{ dynamicForm.valid }}</div>
|
|
286
|
+
<div>Category: {{ dynamicForm.get('category')?.value }}</div>
|
|
287
|
+
<div>Subcategories: {{ dynamicForm.get('subcategories')?.value?.length || 0 }} selected</div>
|
|
288
|
+
</div>
|
|
289
|
+
|
|
290
|
+
<div class="controls">
|
|
291
|
+
<button (click)="resetForm()">Reset</button>
|
|
292
|
+
<button (click)="setDefaults()">Set Defaults</button>
|
|
293
|
+
<button (click)="submitForm()" [disabled]="dynamicForm.invalid">Submit</button>
|
|
294
|
+
</div>
|
|
295
|
+
`,
|
|
296
|
+
styles: [`
|
|
297
|
+
form {
|
|
298
|
+
display: flex;
|
|
299
|
+
flex-direction: column;
|
|
300
|
+
gap: 1rem;
|
|
301
|
+
max-width: 400px;
|
|
302
|
+
}
|
|
303
|
+
.form-status {
|
|
304
|
+
margin-top: 1rem;
|
|
305
|
+
padding: 1rem;
|
|
306
|
+
background: #f5f5f5;
|
|
307
|
+
border-radius: 4px;
|
|
308
|
+
font-family: monospace;
|
|
309
|
+
font-size: 0.9rem;
|
|
310
|
+
}
|
|
311
|
+
.controls {
|
|
312
|
+
margin-top: 1rem;
|
|
313
|
+
display: flex;
|
|
314
|
+
gap: 0.5rem;
|
|
315
|
+
}
|
|
316
|
+
button {
|
|
317
|
+
padding: 0.5rem 1rem;
|
|
318
|
+
border: 1px solid #ccc;
|
|
319
|
+
border-radius: 4px;
|
|
320
|
+
background: white;
|
|
321
|
+
cursor: pointer;
|
|
322
|
+
}
|
|
323
|
+
button:disabled {
|
|
324
|
+
opacity: 0.5;
|
|
325
|
+
cursor: not-allowed;
|
|
326
|
+
}
|
|
327
|
+
`]
|
|
328
|
+
})
|
|
329
|
+
export class DynamicListFormComponent {
|
|
330
|
+
dynamicForm: FormGroup;
|
|
331
|
+
|
|
332
|
+
categoryOptions = [
|
|
333
|
+
'Electronics', 'Clothing', 'Books', 'Home & Garden', 'Sports & Outdoors',
|
|
334
|
+
'Automotive', 'Health & Beauty', 'Toys & Games', 'Food & Beverages'
|
|
335
|
+
];
|
|
336
|
+
|
|
337
|
+
subcategoryOptions: string[] = [];
|
|
338
|
+
|
|
339
|
+
constructor(private fb: FormBuilder) {
|
|
340
|
+
this.dynamicForm = this.fb.group({
|
|
341
|
+
category: ['', Validators.required],
|
|
342
|
+
subcategories: [[], [Validators.required, Validators.minLength(1), Validators.maxLength(3)]]
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// Watch for category changes to update subcategories
|
|
346
|
+
this.dynamicForm.get('category')?.valueChanges.subscribe(category => {
|
|
347
|
+
this.updateSubcategories(category);
|
|
348
|
+
this.dynamicForm.get('subcategories')?.reset();
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
updateSubcategories(category: string) {
|
|
353
|
+
const subcategoryMap: { [key: string]: string[] } = {
|
|
354
|
+
'Electronics': ['Smartphones', 'Laptops', 'Tablets', 'Headphones', 'Cameras'],
|
|
355
|
+
'Clothing': ['Men\'s Wear', 'Women\'s Wear', 'Kids\' Wear', 'Accessories', 'Footwear'],
|
|
356
|
+
'Books': ['Fiction', 'Non-Fiction', 'Educational', 'Children', 'Comics'],
|
|
357
|
+
'Home & Garden': ['Furniture', 'Decor', 'Kitchen', 'Tools', 'Plants'],
|
|
358
|
+
'Sports & Outdoors': ['Exercise', 'Outdoor Gear', 'Sports Equipment', 'Team Sports', 'Water Sports']
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
this.subcategoryOptions = subcategoryMap[category] || [];
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
resetForm() {
|
|
365
|
+
this.dynamicForm.reset();
|
|
366
|
+
this.subcategoryOptions = [];
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
setDefaults() {
|
|
370
|
+
this.dynamicForm.patchValue({
|
|
371
|
+
category: 'Electronics',
|
|
372
|
+
subcategories: ['Smartphones', 'Laptops']
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
submitForm() {
|
|
377
|
+
if (this.dynamicForm.valid) {
|
|
378
|
+
console.log('Form submitted:', this.dynamicForm.value);
|
|
379
|
+
alert('Form submitted successfully!');
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
---
|
|
386
|
+
|
|
387
|
+
## Component API
|
|
388
|
+
|
|
389
|
+
### Inputs
|
|
390
|
+
|
|
391
|
+
| Input | Type | Description | Default |
|
|
392
|
+
| :--- | :--- | :--- | :--- |
|
|
393
|
+
| `data` | `any[] \| string[]` | Array of options to select from | (Required) |
|
|
394
|
+
| `multiple` | `boolean` | Allow multiple selections | `false` |
|
|
395
|
+
| `label` | `string` | Label text for the select field | `undefined` |
|
|
396
|
+
| `placeholder` | `string` | Placeholder text for the select field | `undefined` |
|
|
397
|
+
| `displayField` | `string` | Property name to display for object arrays | `undefined` |
|
|
398
|
+
| `minSelection` | `number` | Minimum number of selections required | `0` |
|
|
399
|
+
| `maxSelection` | `number` | Maximum number of selections allowed | `0` |
|
|
400
|
+
| `required` | `boolean` | Whether selection is required | `false` |
|
|
401
|
+
| `disabled` | `boolean` | Whether the select is disabled | `false` |
|
|
402
|
+
|
|
403
|
+
### Outputs
|
|
404
|
+
|
|
405
|
+
| Output | Type | Description |
|
|
406
|
+
|--------|------|-------------|
|
|
407
|
+
| `selectionChange` | `EventEmitter<any[]>` | Emits array of selected values when selection changes |
|
|
408
|
+
|
|
409
|
+
---
|
|
410
|
+
|
|
411
|
+
## Form Integration (ControlValueAccessor)
|
|
412
|
+
|
|
413
|
+
The component implements Angular's `ControlValueAccessor` interface for seamless form integration.
|
|
414
|
+
|
|
415
|
+
### ControlValueAccessor Implementation
|
|
416
|
+
|
|
417
|
+
```typescript
|
|
418
|
+
// writeValue(value: any[]): void
|
|
419
|
+
// Sets the selected values
|
|
420
|
+
writeValue(value: any[]): void {
|
|
421
|
+
this.selectedValues = value || [];
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// registerOnChange(fn: any): void
|
|
425
|
+
// Registers a callback for value changes
|
|
426
|
+
registerOnChange(fn: any): void {
|
|
427
|
+
this.onChange = fn;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// registerOnTouched(fn: any): void
|
|
431
|
+
// Registers a callback for touch events
|
|
432
|
+
registerOnTouched(fn: any): void {
|
|
433
|
+
this.onTouch = fn;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// setDisabledState(isDisabled: boolean): void
|
|
437
|
+
// Sets disabled state
|
|
438
|
+
setDisabledState?(isDisabled: boolean): void {
|
|
439
|
+
this.disabled = isDisabled;
|
|
440
|
+
}
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
### Form Integration Examples
|
|
444
|
+
|
|
445
|
+
#### Reactive Forms
|
|
446
|
+
|
|
447
|
+
```typescript
|
|
448
|
+
import { Component } from '@angular/core';
|
|
449
|
+
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
|
450
|
+
|
|
451
|
+
@Component({
|
|
452
|
+
selector: 'app-reactive-list-form',
|
|
453
|
+
template: `
|
|
454
|
+
<form [formGroup]="listForm">
|
|
455
|
+
<app-list-selection-input
|
|
456
|
+
formControlName="country"
|
|
457
|
+
[data]="countries"
|
|
458
|
+
label="Country"
|
|
459
|
+
placeholder="Select a country"
|
|
460
|
+
[required]="true">
|
|
461
|
+
</app-list-selection-input>
|
|
462
|
+
|
|
463
|
+
<app-list-selection-input
|
|
464
|
+
formControlName="languages"
|
|
465
|
+
[data]="languages"
|
|
466
|
+
[multiple]="true"
|
|
467
|
+
label="Programming Languages"
|
|
468
|
+
placeholder="Select languages"
|
|
469
|
+
[minSelection]="1"
|
|
470
|
+
[maxSelection]="5">
|
|
471
|
+
</app-list-selection-input>
|
|
472
|
+
</form>
|
|
473
|
+
`
|
|
474
|
+
})
|
|
475
|
+
export class ReactiveListFormComponent {
|
|
476
|
+
listForm = new FormGroup({
|
|
477
|
+
country: new FormControl('', Validators.required),
|
|
478
|
+
languages: new FormControl([], [Validators.required, Validators.minLength(1), Validators.maxLength(5)])
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
countries = ['United States', 'Canada', 'United Kingdom', 'Germany', 'France', 'Japan'];
|
|
482
|
+
languages = ['JavaScript', 'Python', 'Java', 'C#', 'PHP', 'Ruby', 'Go', 'Rust'];
|
|
483
|
+
}
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
#### Template-Driven Forms
|
|
487
|
+
|
|
488
|
+
```typescript
|
|
489
|
+
import { Component } from '@angular/core';
|
|
490
|
+
|
|
491
|
+
@Component({
|
|
492
|
+
selector: 'app-template-list-form',
|
|
493
|
+
template: `
|
|
494
|
+
<app-list-selection-input
|
|
495
|
+
[(ngModel)]="selectedValue"
|
|
496
|
+
name="templateSelection"
|
|
497
|
+
[data]="templateOptions"
|
|
498
|
+
label="Template Selection"
|
|
499
|
+
placeholder="Choose an option"
|
|
500
|
+
[multiple]="true"
|
|
501
|
+
required>
|
|
502
|
+
</app-list-selection-input>
|
|
503
|
+
|
|
504
|
+
<div *ngIf="selectedValue">
|
|
505
|
+
Selected: {{ selectedValue.join(', ') }}
|
|
506
|
+
</div>
|
|
507
|
+
`
|
|
508
|
+
})
|
|
509
|
+
export class TemplateListFormComponent {
|
|
510
|
+
selectedValue: string[] = [];
|
|
511
|
+
|
|
512
|
+
templateOptions = ['Option A', 'Option B', 'Option C', 'Option D', 'Option E'];
|
|
513
|
+
}
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
---
|
|
517
|
+
|
|
518
|
+
## Model Structures
|
|
519
|
+
|
|
520
|
+
### Selection Option Interface
|
|
521
|
+
|
|
522
|
+
```typescript
|
|
523
|
+
export interface SelectionOptionInterface {
|
|
524
|
+
value: any; // The value to be selected/returned
|
|
525
|
+
label?: string; // Optional display label
|
|
526
|
+
disabled?: boolean; // Whether this option is disabled
|
|
527
|
+
selected?: boolean; // Whether this option is pre-selected
|
|
528
|
+
}
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
### Selection Option Class
|
|
532
|
+
|
|
533
|
+
```typescript
|
|
534
|
+
export class SelectionOption implements SelectionOptionInterface {
|
|
535
|
+
constructor(
|
|
536
|
+
public value: any,
|
|
537
|
+
public label?: string,
|
|
538
|
+
public disabled?: boolean = false,
|
|
539
|
+
public selected?: boolean = false,
|
|
540
|
+
) {}
|
|
541
|
+
|
|
542
|
+
static adapt(item?: any): SelectionOption {
|
|
543
|
+
return new SelectionOption(
|
|
544
|
+
item?.value ?? item,
|
|
545
|
+
item?.label,
|
|
546
|
+
item?.disabled || false,
|
|
547
|
+
item?.selected || false,
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
### Usage Examples
|
|
554
|
+
|
|
555
|
+
```typescript
|
|
556
|
+
// String array data (automatically converted)
|
|
557
|
+
const stringData = ['Option 1', 'Option 2', 'Option 3'];
|
|
558
|
+
|
|
559
|
+
// Object array data
|
|
560
|
+
const objectData = [
|
|
561
|
+
{ value: 'opt1', label: 'Option 1', disabled: false, selected: true },
|
|
562
|
+
{ value: 'opt2', label: 'Option 2', disabled: true, selected: false },
|
|
563
|
+
{ value: 'opt3', label: 'Option 3', disabled: false, selected: false }
|
|
564
|
+
];
|
|
565
|
+
|
|
566
|
+
// Manual SelectionOption creation
|
|
567
|
+
const manualOptions = [
|
|
568
|
+
new SelectionOption('value1', 'Label 1', false, true),
|
|
569
|
+
new SelectionOption('value2', 'Label 2', true, false),
|
|
570
|
+
new SelectionOption('value3', 'Label 3', false, false)
|
|
571
|
+
];
|
|
572
|
+
|
|
573
|
+
// Using adapt method for flexible data
|
|
574
|
+
const adaptedOptions = [
|
|
575
|
+
SelectionOption.adapt({ value: 'item1', label: 'Item 1', selected: true }),
|
|
576
|
+
SelectionOption.adapt({ value: 'item2', label: 'Item 2', disabled: true }),
|
|
577
|
+
SelectionOption.adapt('item3') // String fallback
|
|
578
|
+
];
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
---
|
|
582
|
+
|
|
583
|
+
## Validation System
|
|
584
|
+
|
|
585
|
+
### Built-in Validation
|
|
586
|
+
|
|
587
|
+
The component provides built-in validation for:
|
|
588
|
+
|
|
589
|
+
#### Min Selection Validation
|
|
590
|
+
```typescript
|
|
591
|
+
// Requires at least X selections
|
|
592
|
+
[minSelection]="2" // Must select at least 2 items
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
#### Max Selection Validation
|
|
596
|
+
```typescript
|
|
597
|
+
// Allows maximum X selections
|
|
598
|
+
[maxSelection]="3" // Cannot select more than 3 items
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
#### Required Validation
|
|
602
|
+
```typescript
|
|
603
|
+
// Marks field as required
|
|
604
|
+
[required]="true" // At least one selection required
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
### Validation Error Types
|
|
608
|
+
|
|
609
|
+
| Error Type | Condition | Description |
|
|
610
|
+
|------------|-----------|-------------|
|
|
611
|
+
| `minRequired` | `selectedCount < minSelection` | Not enough selections made |
|
|
612
|
+
| `maxExceeded` | `selectedCount >= maxSelection` | Too many selections made |
|
|
613
|
+
| `required` | Control has required validator and no selections | Control is required but empty |
|
|
614
|
+
|
|
615
|
+
### Custom Validation Examples
|
|
616
|
+
|
|
617
|
+
```typescript
|
|
618
|
+
// Complex validation scenarios
|
|
619
|
+
const complexForm = new FormGroup({
|
|
620
|
+
permissions: new FormControl([], [
|
|
621
|
+
Validators.required,
|
|
622
|
+
Validators.minLength(1), // At least 1 selection
|
|
623
|
+
Validators.maxLength(5), // Maximum 5 selections
|
|
624
|
+
customPermissionValidator() // Custom validation logic
|
|
625
|
+
])
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
// Custom validator example
|
|
629
|
+
function customPermissionValidator() {
|
|
630
|
+
return (control: AbstractControl): ValidationErrors | null => {
|
|
631
|
+
const value = control.value;
|
|
632
|
+
if (value && value.includes('admin') && value.includes('delete')) {
|
|
633
|
+
return { adminDeleteConflict: true };
|
|
634
|
+
}
|
|
635
|
+
return null;
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
---
|
|
641
|
+
|
|
642
|
+
## Module Configuration
|
|
643
|
+
|
|
644
|
+
### ListSelectionInputModule
|
|
645
|
+
|
|
646
|
+
**No Global Configuration Required**
|
|
647
|
+
|
|
648
|
+
The `ListSelectionInputModule` does not provide a `forRoot()` method or global configuration options. All configuration is done at the component level through input properties.
|
|
649
|
+
|
|
650
|
+
#### Dependencies
|
|
651
|
+
|
|
652
|
+
- **@angular/core**: Core Angular functionality
|
|
653
|
+
- **@angular/forms**: Form control integration (FormsModule, ReactiveFormsModule)
|
|
654
|
+
- **@angular/material**: Material Design components (MatSelectModule, MatFormFieldModule, etc.)
|
|
655
|
+
|
|
656
|
+
---
|
|
657
|
+
|
|
658
|
+
## Advanced Usage Patterns
|
|
659
|
+
|
|
660
|
+
### Cascading Selections
|
|
661
|
+
|
|
662
|
+
```typescript
|
|
663
|
+
import { Component } from '@angular/core';
|
|
664
|
+
import { FormBuilder, FormGroup } from '@angular/forms';
|
|
665
|
+
|
|
666
|
+
@Component({
|
|
667
|
+
selector: 'app-cascading-selects',
|
|
668
|
+
template: `
|
|
669
|
+
<form [formGroup]="cascadingForm">
|
|
670
|
+
<app-list-selection-input
|
|
671
|
+
formControlName="country"
|
|
672
|
+
[data]="countries"
|
|
673
|
+
label="Country"
|
|
674
|
+
placeholder="Select a country">
|
|
675
|
+
</app-list-selection-input>
|
|
676
|
+
|
|
677
|
+
<app-list-selection-input
|
|
678
|
+
formControlName="state"
|
|
679
|
+
[data]="states"
|
|
680
|
+
label="State/Province"
|
|
681
|
+
placeholder="Select a state"
|
|
682
|
+
[disabled]="!cascadingForm.get('country')?.value">
|
|
683
|
+
</app-list-selection-input>
|
|
684
|
+
|
|
685
|
+
<app-list-selection-input
|
|
686
|
+
formControlName="city"
|
|
687
|
+
[data]="cities"
|
|
688
|
+
label="City"
|
|
689
|
+
placeholder="Select a city"
|
|
690
|
+
[disabled]="!cascadingForm.get('state')?.value">
|
|
691
|
+
</app-list-selection-input>
|
|
692
|
+
</form>
|
|
693
|
+
`
|
|
694
|
+
})
|
|
695
|
+
export class CascadingSelectsComponent {
|
|
696
|
+
cascadingForm: FormGroup;
|
|
697
|
+
states: string[] = [];
|
|
698
|
+
cities: string[] = [];
|
|
699
|
+
|
|
700
|
+
countries = ['United States', 'Canada', 'United Kingdom'];
|
|
701
|
+
|
|
702
|
+
stateData = {
|
|
703
|
+
'United States': ['California', 'New York', 'Texas', 'Florida'],
|
|
704
|
+
'Canada': ['Ontario', 'Quebec', 'British Columbia', 'Alberta'],
|
|
705
|
+
'United Kingdom': ['England', 'Scotland', 'Wales', 'Northern Ireland']
|
|
706
|
+
};
|
|
707
|
+
|
|
708
|
+
cityData = {
|
|
709
|
+
'California': ['Los Angeles', 'San Francisco', 'San Diego', 'Sacramento'],
|
|
710
|
+
'New York': ['New York City', 'Buffalo', 'Rochester', 'Albany'],
|
|
711
|
+
'Ontario': ['Toronto', 'Ottawa', 'Mississauga', 'Hamilton']
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
constructor(private fb: FormBuilder) {
|
|
715
|
+
this.cascadingForm = this.fb.group({
|
|
716
|
+
country: [''],
|
|
717
|
+
state: [''],
|
|
718
|
+
city: ['']
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
// Watch for country changes
|
|
722
|
+
this.cascadingForm.get('country')?.valueChanges.subscribe(country => {
|
|
723
|
+
this.states = this.stateData[country as keyof typeof this.stateData] || [];
|
|
724
|
+
this.cascadingForm.get('state')?.reset();
|
|
725
|
+
this.cascadingForm.get('city')?.reset();
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
// Watch for state changes
|
|
729
|
+
this.cascadingForm.get('state')?.valueChanges.subscribe(state => {
|
|
730
|
+
this.cities = this.cityData[state as keyof typeof this.cityData] || [];
|
|
731
|
+
this.cascadingForm.get('city')?.reset();
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
### Async Data Loading
|
|
738
|
+
|
|
739
|
+
```typescript
|
|
740
|
+
import { Component } from '@angular/core';
|
|
741
|
+
import { HttpClient } from '@angular/common/http';
|
|
742
|
+
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
|
|
743
|
+
import { FormControl } from '@angular/forms';
|
|
744
|
+
import { Observable } from 'rxjs';
|
|
745
|
+
|
|
746
|
+
@Component({
|
|
747
|
+
selector: 'app-async-list-select',
|
|
748
|
+
template: `
|
|
749
|
+
<app-list-selection-input
|
|
750
|
+
[formControl]="searchControl"
|
|
751
|
+
[data]="searchResults$ | async"
|
|
752
|
+
label="Search Products"
|
|
753
|
+
placeholder="Start typing to search..."
|
|
754
|
+
displayField="name"
|
|
755
|
+
[loading]="loading">
|
|
756
|
+
</app-list-selection-input>
|
|
757
|
+
|
|
758
|
+
<div *ngIf="loading" class="loading-indicator">
|
|
759
|
+
Loading products...
|
|
760
|
+
</div>
|
|
761
|
+
`
|
|
762
|
+
})
|
|
763
|
+
export class AsyncListSelectComponent {
|
|
764
|
+
searchControl = new FormControl();
|
|
765
|
+
searchResults$!: Observable<any[]>;
|
|
766
|
+
loading = false;
|
|
767
|
+
|
|
768
|
+
constructor(private http: HttpClient) {
|
|
769
|
+
this.searchResults$ = this.searchControl.valueChanges.pipe(
|
|
770
|
+
debounceTime(300),
|
|
771
|
+
distinctUntilChanged(),
|
|
772
|
+
switchMap(query => this.searchProducts(query))
|
|
773
|
+
);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
private searchProducts(query: string): Observable<any[]> {
|
|
777
|
+
this.loading = true;
|
|
778
|
+
return this.http.get<any[]>(`/api/products/search?q=${encodeURIComponent(query)}`).pipe(
|
|
779
|
+
// Simulate loading state
|
|
780
|
+
switchMap(results => {
|
|
781
|
+
this.loading = false;
|
|
782
|
+
return [results];
|
|
783
|
+
})
|
|
784
|
+
);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
```
|
|
788
|
+
|
|
789
|
+
### Performance Optimization
|
|
790
|
+
|
|
791
|
+
```typescript
|
|
792
|
+
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
|
793
|
+
import { FormControl } from '@angular/forms';
|
|
794
|
+
|
|
795
|
+
@Component({
|
|
796
|
+
selector: 'app-optimized-list-select',
|
|
797
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
798
|
+
template: `
|
|
799
|
+
<app-list-selection-input
|
|
800
|
+
[formControl]="optimizedControl"
|
|
801
|
+
[data]="largeDataset"
|
|
802
|
+
label="Large Dataset Selection"
|
|
803
|
+
placeholder="Select from large dataset..."
|
|
804
|
+
[filterFunction]="customFilter">
|
|
805
|
+
</app-list-selection-input>
|
|
806
|
+
`
|
|
807
|
+
})
|
|
808
|
+
export class OptimizedListSelectComponent {
|
|
809
|
+
optimizedControl = new FormControl();
|
|
810
|
+
|
|
811
|
+
// Large dataset with efficient filtering
|
|
812
|
+
largeDataset = Array.from({ length: 10000 }, (_, i) => ({
|
|
813
|
+
id: i,
|
|
814
|
+
name: `Product ${i}`,
|
|
815
|
+
category: `Category ${Math.floor(i / 100)}`,
|
|
816
|
+
price: Math.random() * 1000
|
|
817
|
+
}));
|
|
818
|
+
|
|
819
|
+
customFilter(data: any[], filterText: string): any[] {
|
|
820
|
+
if (!filterText) return data;
|
|
821
|
+
|
|
822
|
+
const lowerFilter = filterText.toLowerCase();
|
|
823
|
+
return data.filter(item =>
|
|
824
|
+
item.name.toLowerCase().includes(lowerFilter) ||
|
|
825
|
+
item.category.toLowerCase().includes(lowerFilter)
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
```
|
|
830
|
+
|
|
831
|
+
---
|
|
832
|
+
|
|
833
|
+
## Integration Examples
|
|
834
|
+
|
|
835
|
+
### With Other UI Components
|
|
836
|
+
|
|
837
|
+
```typescript
|
|
838
|
+
import { Component } from '@angular/core';
|
|
839
|
+
|
|
840
|
+
@Component({
|
|
841
|
+
selector: 'app-integrated-list-select',
|
|
842
|
+
template: `
|
|
843
|
+
<app-display-card title="User Assignment">
|
|
844
|
+
<app-list-selection-input
|
|
845
|
+
[formControl]="userControl"
|
|
846
|
+
[data]="users"
|
|
847
|
+
label="Assign to User"
|
|
848
|
+
placeholder="Select a user"
|
|
849
|
+
displayField="name">
|
|
850
|
+
</app-list-selection-input>
|
|
851
|
+
|
|
852
|
+
<div *ngIf="userControl.value" class="assignment-info">
|
|
853
|
+
<h4>Assigned to: {{ userControl.value.name }}</h4>
|
|
854
|
+
<p>{{ userControl.value.email }}</p>
|
|
855
|
+
<p>Role: {{ userControl.value.role }}</p>
|
|
856
|
+
<p>Department: {{ userControl.value.department }}</p>
|
|
857
|
+
</div>
|
|
858
|
+
</app-display-card>
|
|
859
|
+
`
|
|
860
|
+
})
|
|
861
|
+
export class IntegratedListSelectComponent {
|
|
862
|
+
userControl = new FormControl();
|
|
863
|
+
|
|
864
|
+
users = [
|
|
865
|
+
{
|
|
866
|
+
id: 1,
|
|
867
|
+
name: 'John Doe',
|
|
868
|
+
email: 'john@example.com',
|
|
869
|
+
role: 'Admin',
|
|
870
|
+
department: 'Engineering'
|
|
871
|
+
},
|
|
872
|
+
{
|
|
873
|
+
id: 2,
|
|
874
|
+
name: 'Jane Smith',
|
|
875
|
+
email: 'jane@example.com',
|
|
876
|
+
role: 'Editor',
|
|
877
|
+
department: 'Marketing'
|
|
878
|
+
}
|
|
879
|
+
];
|
|
880
|
+
}
|
|
881
|
+
```
|
|
882
|
+
|
|
883
|
+
### With State Management
|
|
884
|
+
|
|
885
|
+
```typescript
|
|
886
|
+
import { Component } from '@angular/core';
|
|
887
|
+
import { Store } from '@ngrx/store';
|
|
888
|
+
|
|
889
|
+
@Component({
|
|
890
|
+
selector: 'app-state-list-select',
|
|
891
|
+
template: `
|
|
892
|
+
<app-list-selection-input
|
|
893
|
+
[formControl]="categoryControl"
|
|
894
|
+
[data]="categories$ | async"
|
|
895
|
+
label="Product Category"
|
|
896
|
+
placeholder="Select a category"
|
|
897
|
+
(selectionChange)="onCategorySelected($event)">
|
|
898
|
+
</app-list-selection-input>
|
|
899
|
+
`
|
|
900
|
+
})
|
|
901
|
+
export class StateListSelectComponent {
|
|
902
|
+
categoryControl = new FormControl();
|
|
903
|
+
categories$ = this.store.select(state => state.categories);
|
|
904
|
+
|
|
905
|
+
constructor(private store: Store) {}
|
|
906
|
+
|
|
907
|
+
onCategorySelected(category: any) {
|
|
908
|
+
this.store.dispatch(selectCategory({ category }));
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
```
|
|
912
|
+
|
|
913
|
+
---
|
|
914
|
+
|
|
915
|
+
## Testing
|
|
916
|
+
|
|
917
|
+
### Unit Testing Example
|
|
918
|
+
|
|
919
|
+
```typescript
|
|
920
|
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
921
|
+
import { ListSelectionInputComponent } from './list-selection-input.component';
|
|
922
|
+
import { ReactiveFormsModule } from '@angular/forms';
|
|
923
|
+
|
|
924
|
+
describe('ListSelectionInputComponent', () => {
|
|
925
|
+
let component: ListSelectionInputComponent;
|
|
926
|
+
let fixture: ComponentFixture<ListSelectionInputComponent>;
|
|
927
|
+
|
|
928
|
+
beforeEach(async () => {
|
|
929
|
+
await TestBed.configureTestingModule({
|
|
930
|
+
declarations: [ ListSelectionInputComponent ],
|
|
931
|
+
imports: [ ReactiveFormsModule ]
|
|
932
|
+
}).compileComponents();
|
|
933
|
+
|
|
934
|
+
fixture = TestBed.createComponent(ListSelectionInputComponent);
|
|
935
|
+
component = fixture.componentInstance;
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
it('should create', () => {
|
|
939
|
+
expect(component).toBeTruthy();
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
it('should display options from data input', () => {
|
|
943
|
+
component.data = ['Option 1', 'Option 2', 'Option 3'];
|
|
944
|
+
fixture.detectChanges();
|
|
945
|
+
|
|
946
|
+
const compiled = fixture.nativeElement;
|
|
947
|
+
expect(compiled.textContent).toContain('Option 1');
|
|
948
|
+
expect(compiled.textContent).toContain('Option 2');
|
|
949
|
+
expect(compiled.textContent).toContain('Option 3');
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
it('should handle multiple selection mode', () => {
|
|
953
|
+
component.multiple = true;
|
|
954
|
+
component.data = ['Option 1', 'Option 2', 'Option 3'];
|
|
955
|
+
|
|
956
|
+
expect(component.multiple).toBe(true);
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
it('should emit selection changes', () => {
|
|
960
|
+
spyOn(component.selectionChange, 'emit');
|
|
961
|
+
|
|
962
|
+
component.onSelectionChange(['Option 1', 'Option 2']);
|
|
963
|
+
|
|
964
|
+
expect(component.selectionChange.emit).toHaveBeenCalledWith(['Option 1', 'Option 2']);
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
it('should validate minimum selections', () => {
|
|
968
|
+
component.minSelection = 2;
|
|
969
|
+
component.data = ['Option 1', 'Option 2', 'Option 3'];
|
|
970
|
+
|
|
971
|
+
// Simulate selecting only 1 item
|
|
972
|
+
component.writeValue(['Option 1']);
|
|
973
|
+
|
|
974
|
+
const validationResult = component.validate({} as AbstractControl);
|
|
975
|
+
expect(validationResult?.minRequired).toBe(true);
|
|
976
|
+
});
|
|
977
|
+
});
|
|
978
|
+
```
|
|
979
|
+
|
|
980
|
+
---
|
|
981
|
+
|
|
982
|
+
## Troubleshooting
|
|
983
|
+
|
|
984
|
+
### Common Issues
|
|
985
|
+
|
|
986
|
+
1. **Form control not working**: Ensure ReactiveFormsModule is imported
|
|
987
|
+
2. **Validation not triggering**: Check that validators are properly configured
|
|
988
|
+
3. **Selection not updating**: Verify data format matches expected structure
|
|
989
|
+
4. **Multiple selection not working**: Check that `multiple` input is set to `true`
|
|
990
|
+
5. **Object display issues**: Set `displayField` property for object arrays
|
|
991
|
+
6. **Styling issues**: Ensure Material theme is properly configured
|
|
992
|
+
|
|
993
|
+
### Debug Mode
|
|
994
|
+
|
|
995
|
+
```typescript
|
|
996
|
+
@Component({
|
|
997
|
+
template: `
|
|
998
|
+
<div class="debug-info">
|
|
999
|
+
Form Control Value: {{ formControl.value | json }}<br>
|
|
1000
|
+
Form Control Valid: {{ formControl.valid }}<br>
|
|
1001
|
+
Form Control Errors: {{ formControl.errors | json }}<br>
|
|
1002
|
+
Data Length: {{ data?.length || 0 }}<br>
|
|
1003
|
+
Multiple Mode: {{ multiple }}<br>
|
|
1004
|
+
Min Selection: {{ minSelection }}<br>
|
|
1005
|
+
Max Selection: {{ maxSelection }}
|
|
1006
|
+
</div>
|
|
1007
|
+
|
|
1008
|
+
<app-list-selection-input
|
|
1009
|
+
[formControl]="formControl"
|
|
1010
|
+
[data]="data"
|
|
1011
|
+
[multiple]="multiple"
|
|
1012
|
+
[minSelection]="minSelection"
|
|
1013
|
+
[maxSelection]="maxSelection"
|
|
1014
|
+
(selectionChange)="onSelectionChange($event)">
|
|
1015
|
+
</app-list-selection-input>
|
|
1016
|
+
`
|
|
1017
|
+
})
|
|
1018
|
+
export class DebugListSelectComponent {
|
|
1019
|
+
formControl = new FormControl();
|
|
1020
|
+
data: any[] = [];
|
|
1021
|
+
multiple = false;
|
|
1022
|
+
minSelection = 0;
|
|
1023
|
+
maxSelection = 0;
|
|
1024
|
+
|
|
1025
|
+
constructor() {
|
|
1026
|
+
this.formControl.valueChanges.subscribe(value => {
|
|
1027
|
+
console.log('List selection value changed:', value);
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
this.formControl.statusChanges.subscribe(status => {
|
|
1031
|
+
console.log('Form control status:', status);
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
onSelectionChange(selection: any[]) {
|
|
1036
|
+
console.log('Selection changed:', selection);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
```
|
|
1040
|
+
|
|
1041
|
+
### Performance Debugging
|
|
1042
|
+
|
|
1043
|
+
```typescript
|
|
1044
|
+
@Component({
|
|
1045
|
+
template: `
|
|
1046
|
+
<div class="performance-info">
|
|
1047
|
+
Data Size: {{ data?.length || 0 }} items<br>
|
|
1048
|
+
Render Time: {{ renderTime }}ms<br>
|
|
1049
|
+
Filter Operations: {{ filterOperationCount }}
|
|
1050
|
+
</div>
|
|
1051
|
+
|
|
1052
|
+
<app-list-selection-input
|
|
1053
|
+
[formControl]="formControl"
|
|
1054
|
+
[data]="data"
|
|
1055
|
+
(selectionChange)="onSelectionChange($event)">
|
|
1056
|
+
</app-list-selection-input>
|
|
1057
|
+
`
|
|
1058
|
+
})
|
|
1059
|
+
export class PerformanceDebugComponent {
|
|
1060
|
+
formControl = new FormControl();
|
|
1061
|
+
data: any[] = [];
|
|
1062
|
+
renderTime = 0;
|
|
1063
|
+
filterOperationCount = 0;
|
|
1064
|
+
|
|
1065
|
+
onSelectionChange(selection: any[]) {
|
|
1066
|
+
const start = performance.now();
|
|
1067
|
+
|
|
1068
|
+
// Perform selection operation
|
|
1069
|
+
this.processSelection(selection);
|
|
1070
|
+
|
|
1071
|
+
const end = performance.now();
|
|
1072
|
+
this.renderTime = end - start;
|
|
1073
|
+
this.filterOperationCount++;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
private processSelection(selection: any[]) {
|
|
1077
|
+
// Your selection processing logic
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
```
|