checkbox-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 +1245 -13
- package/checkbox-selection-input-15.0.5.tgz +0 -0
- package/fesm2022/checkbox-selection-input.mjs +28 -28
- package/fesm2022/checkbox-selection-input.mjs.map +1 -1
- package/package.json +12 -5
- package/types/checkbox-selection-input.d.ts +151 -0
- package/checkbox-selection-input-15.0.4.tgz +0 -0
- package/esm2022/checkbox-selection-input.mjs +0 -5
- package/esm2022/lib/checkbox-demo/checkbox-demo.component.mjs +0 -137
- package/esm2022/lib/checkbox-selection-input/checkbox-selection-input.component.mjs +0 -198
- package/esm2022/lib/checkbox-selection-input.module.mjs +0 -97
- 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/pipes/remove-underscore.pipe.mjs +0 -17
- package/esm2022/public-api.mjs +0 -9
- package/index.d.ts +0 -5
- package/lib/checkbox-demo/checkbox-demo.component.d.ts +0 -43
- package/lib/checkbox-selection-input/checkbox-selection-input.component.d.ts +0 -51
- package/lib/checkbox-selection-input.module.d.ts +0 -26
- 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/pipes/remove-underscore.pipe.d.ts +0 -7
- package/public-api.d.ts +0 -5
package/README.md
CHANGED
|
@@ -1,24 +1,1256 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Checkbox Selection Input Component
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## Overview
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
The `checkbox-selection-input` library provides a comprehensive Material Design checkbox component that works seamlessly with Angular forms. It supports multiple selection with validation limits (min/max), disabled states, and both string and object data formats. The component implements Angular's `ControlValueAccessor` and `NG_VALIDATORS` interfaces for seamless form integration and validation.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
> Note: Don't forget to add `--project checkbox-selection-input` or else it will be added to the default project in your `angular.json` file.
|
|
7
|
+
### Core Capabilities
|
|
9
8
|
|
|
10
|
-
|
|
9
|
+
#### ☑️ Advanced Checkbox Selection Interface
|
|
11
10
|
|
|
12
|
-
|
|
11
|
+
- **Multiple Selection**: Support for selecting multiple checkboxes simultaneously
|
|
12
|
+
- **Form Validation**: Built-in min/max selection validation with error handling
|
|
13
|
+
- **Flexible Data Support**: Accepts both string arrays and complex object arrays
|
|
14
|
+
- **State Management**: Disabled states, pre-selected items, and dynamic enabling/disabling
|
|
15
|
+
- **Material Design**: Built on Angular Material checkbox foundation
|
|
16
|
+
- **ControlValueAccessor Integration**: Full Angular form control support
|
|
17
|
+
- **Validation Integration**: Native Angular validation system compatibility
|
|
18
|
+
- **Dynamic Control**: Runtime data updates and validation constraint changes
|
|
13
19
|
|
|
14
|
-
|
|
20
|
+
#### 🔧 Features
|
|
15
21
|
|
|
16
|
-
|
|
22
|
+
✅ **ControlValueAccessor Implementation** - Works with Angular forms
|
|
23
|
+
✅ **NG_VALIDATORS Integration** - Native validation support
|
|
24
|
+
✅ **Material Design Integration** - Uses Angular Material components
|
|
25
|
+
✅ **Multiple Selection** - Select multiple items with checkboxes
|
|
26
|
+
✅ **Min/Max Validation selection limits
|
|
27
|
+
✅** - Configurable **Disable Max Behavior** - Auto-disable checkboxes when max reached
|
|
28
|
+
✅ **Flexible Data Types** - Support strings and objects
|
|
29
|
+
✅ **Pre-selection** - Initialize with selected items
|
|
30
|
+
✅ **Disabled Items** - Mark individual items as non-selectable
|
|
31
|
+
✅ **Change Detection** - Console logging for debugging
|
|
32
|
+
✅ **Form Integration** - Reactive and template-driven forms
|
|
17
33
|
|
|
18
|
-
|
|
34
|
+
### Key Benefits
|
|
19
35
|
|
|
20
|
-
|
|
36
|
+
| Feature | Description |
|
|
37
|
+
|---------|-------------|
|
|
38
|
+
| **Flexible Selection** | Support for multiple item selection with validation |
|
|
39
|
+
| **Rich Validation** | Built-in min/max selection limits with error states |
|
|
40
|
+
| **Data Format Support** | Works with simple strings or complex objects |
|
|
41
|
+
| **State Management** | Disabled states and pre-selection capabilities |
|
|
42
|
+
| **Form Integration** | Seamless Angular form control and validation integration |
|
|
43
|
+
| **Material Design** | Consistent Material Design styling and behavior |
|
|
21
44
|
|
|
22
|
-
|
|
45
|
+
---
|
|
23
46
|
|
|
24
|
-
|
|
47
|
+
## Demo Component (`CheckboxSelectionDemoComponent`)
|
|
48
|
+
|
|
49
|
+
The demo component showcases 6 different checkbox configurations demonstrating various use cases and validation scenarios.
|
|
50
|
+
|
|
51
|
+
### Usage
|
|
52
|
+
|
|
53
|
+
To use the demo component in your application:
|
|
54
|
+
|
|
55
|
+
```html
|
|
56
|
+
<app-checkbox-selection-demo></app-checkbox-selection-demo>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Demo Configurations
|
|
60
|
+
|
|
61
|
+
The demo includes 6 different checkbox setups:
|
|
62
|
+
|
|
63
|
+
#### Demo 1: Basic Configuration
|
|
64
|
+
- **No label/placeholder**
|
|
65
|
+
- **Not required**
|
|
66
|
+
- **Basic form control integration**
|
|
67
|
+
|
|
68
|
+
#### Demo 2: Labeled Configuration
|
|
69
|
+
- **With label and placeholder**
|
|
70
|
+
- **Required validation**
|
|
71
|
+
- **Error display toggle**
|
|
72
|
+
|
|
73
|
+
#### Demo 3: Minimum Selection
|
|
74
|
+
- **Label: "Providers"**
|
|
75
|
+
- **Minimum 2 selections required**
|
|
76
|
+
- **Error handling for insufficient selections**
|
|
77
|
+
|
|
78
|
+
#### Demo 4: Maximum Selection
|
|
79
|
+
- **Label: "Providers"**
|
|
80
|
+
- **Maximum 2 selections allowed**
|
|
81
|
+
- **Error handling for exceeding limit**
|
|
82
|
+
|
|
83
|
+
#### Demo 5: Auto-disable at Max
|
|
84
|
+
- **Label: "Providers"**
|
|
85
|
+
- **Maximum 2 selections**
|
|
86
|
+
- **Auto-disable remaining checkboxes when max reached**
|
|
87
|
+
|
|
88
|
+
#### Demo 6: Combined Min/Max
|
|
89
|
+
- **Label: "Providers"**
|
|
90
|
+
- **Minimum 1, Maximum 2 selections**
|
|
91
|
+
- **Auto-disable at max**
|
|
92
|
+
- **Complex validation scenarios**
|
|
93
|
+
|
|
94
|
+
### Demo Features
|
|
95
|
+
|
|
96
|
+
- **Data Type Toggle**: Switch between string arrays and object arrays
|
|
97
|
+
- **Patch Testing**: Programmatically set values for testing
|
|
98
|
+
- **Enable/Disable Controls**: Test form control state changes
|
|
99
|
+
- **Change Detection**: Console logging of value changes
|
|
100
|
+
- **Error Display**: Toggle error message visibility
|
|
101
|
+
- **Reset Functionality**: Clear all selections
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Summary
|
|
106
|
+
|
|
107
|
+
The `checkbox-selection-input` library provides a flexible, Material Design-compliant checkbox component with comprehensive form integration, validation support, and multiple selection capabilities for Angular applications.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Quick Start Guide
|
|
112
|
+
|
|
113
|
+
### Installation & Setup (2 minutes)
|
|
114
|
+
|
|
115
|
+
#### 1. Import Module
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
// app.module.ts
|
|
119
|
+
import { CheckboxSelectionInputModule } from 'checkbox-selection-input';
|
|
120
|
+
|
|
121
|
+
@NgModule({
|
|
122
|
+
imports: [
|
|
123
|
+
CheckboxSelectionInputModule
|
|
124
|
+
]
|
|
125
|
+
})
|
|
126
|
+
export class AppModule { }
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
#### 2. No Module Configuration Required
|
|
130
|
+
|
|
131
|
+
The `CheckboxSelectionInputModule` does not require global configuration. Components can be used immediately after module import.
|
|
132
|
+
|
|
133
|
+
### Quick Examples
|
|
134
|
+
|
|
135
|
+
#### Example 1: Basic Multiple Selection
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
import { Component } from '@angular/core';
|
|
139
|
+
import { FormControl } from '@angular/forms';
|
|
140
|
+
|
|
141
|
+
@Component({
|
|
142
|
+
selector: 'app-basic-checkbox',
|
|
143
|
+
template: `
|
|
144
|
+
<app-checkbox-selection-input
|
|
145
|
+
[formControl]="selectionControl"
|
|
146
|
+
[data]="options">
|
|
147
|
+
</app-checkbox-selection-input>
|
|
148
|
+
|
|
149
|
+
<div>Selected: {{ selectionControl.value | json }}</div>
|
|
150
|
+
`
|
|
151
|
+
})
|
|
152
|
+
export class BasicCheckboxComponent {
|
|
153
|
+
selectionControl = new FormControl();
|
|
154
|
+
|
|
155
|
+
options = ['Option 1', 'Option 2', 'Option 3', 'Option 4'];
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
#### Example 2: Required Selection with Validation
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
import { Component } from '@angular/core';
|
|
163
|
+
import { FormControl, Validators } from '@angular/forms';
|
|
164
|
+
|
|
165
|
+
@Component({
|
|
166
|
+
selector: 'app-required-checkbox',
|
|
167
|
+
template: `
|
|
168
|
+
<app-checkbox-selection-input
|
|
169
|
+
[formControl]="requiredControl"
|
|
170
|
+
[data]="providers"
|
|
171
|
+
label="Select Providers"
|
|
172
|
+
placeholder="Choose your providers"
|
|
173
|
+
[minSelection]="2"
|
|
174
|
+
[maxSelection]="3">
|
|
175
|
+
</app-checkbox-selection-input>
|
|
176
|
+
|
|
177
|
+
<div class="errors" *ngIf="requiredControl.errors">
|
|
178
|
+
<div *ngIf="requiredControl.hasError('minRequired')">
|
|
179
|
+
Please select at least 2 providers
|
|
180
|
+
</div>
|
|
181
|
+
<div *ngIf="requiredControl.hasError('maxExceeded')">
|
|
182
|
+
You can select maximum 3 providers
|
|
183
|
+
</div>
|
|
184
|
+
<div *ngIf="requiredControl.hasError('required')">
|
|
185
|
+
Provider selection is required
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
`
|
|
189
|
+
})
|
|
190
|
+
export class RequiredCheckboxComponent {
|
|
191
|
+
requiredControl = new FormControl([], [
|
|
192
|
+
Validators.required,
|
|
193
|
+
Validators.minLength(2),
|
|
194
|
+
Validators.maxLength(3)
|
|
195
|
+
]);
|
|
196
|
+
|
|
197
|
+
providers = ['Telus', 'AT&T', 'Bell', 'Rogers', 'Verizon'];
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
#### Example 3: Object Data with Disabled Items
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
import { Component } from '@angular/core';
|
|
205
|
+
import { FormControl } from '@angular/forms';
|
|
206
|
+
import { SelectionItem } from 'checkbox-selection-input';
|
|
207
|
+
|
|
208
|
+
@Component({
|
|
209
|
+
selector: 'app-object-checkbox',
|
|
210
|
+
template: `
|
|
211
|
+
<app-checkbox-selection-input
|
|
212
|
+
[formControl]="objectControl"
|
|
213
|
+
[data]="userOptions"
|
|
214
|
+
[disableMax]="true">
|
|
215
|
+
</app-checkbox-selection-input>
|
|
216
|
+
|
|
217
|
+
<div>Selected Users: {{ objectControl.value | json }}</div>
|
|
218
|
+
`
|
|
219
|
+
})
|
|
220
|
+
export class ObjectCheckboxComponent {
|
|
221
|
+
objectControl = new FormControl();
|
|
222
|
+
|
|
223
|
+
userOptions = [
|
|
224
|
+
{ id: 1, value: 'John Doe', selected: true },
|
|
225
|
+
{ id: 2, value: 'Jane Smith', disabled: true },
|
|
226
|
+
{ id: 3, value: 'Bob Johnson', selected: true },
|
|
227
|
+
{ id: 4, value: 'Alice Brown' }
|
|
228
|
+
];
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
#### Example 4: Auto-disable at Maximum
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
import { Component } from '@angular/core';
|
|
236
|
+
import { FormControl } from '@angular/forms';
|
|
237
|
+
|
|
238
|
+
@Component({
|
|
239
|
+
selector: 'app-auto-disable-checkbox',
|
|
240
|
+
template: `
|
|
241
|
+
<app-checkbox-selection-input
|
|
242
|
+
[formControl]="featuresControl"
|
|
243
|
+
[data]="features"
|
|
244
|
+
label="Select Features"
|
|
245
|
+
[maxSelection]="2"
|
|
246
|
+
[disableMax]="true">
|
|
247
|
+
</app-checkbox-selection-input>
|
|
248
|
+
|
|
249
|
+
<div class="info">
|
|
250
|
+
Selected: {{ featuresControl.value?.length || 0 }} / 2 maximum
|
|
251
|
+
</div>
|
|
252
|
+
`
|
|
253
|
+
})
|
|
254
|
+
export class AutoDisableCheckboxComponent {
|
|
255
|
+
featuresControl = new FormControl();
|
|
256
|
+
|
|
257
|
+
features = [
|
|
258
|
+
'Dark Mode',
|
|
259
|
+
'Notifications',
|
|
260
|
+
'Analytics',
|
|
261
|
+
'Export Data',
|
|
262
|
+
'API Access',
|
|
263
|
+
'Advanced Filters'
|
|
264
|
+
];
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
#### Example 5: Dynamic Data with Form Validation
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
import { Component } from '@angular/core';
|
|
272
|
+
import { FormBuilder, Validators } from '@angular/forms';
|
|
273
|
+
import { SelectionItem } from 'checkbox-selection-input';
|
|
274
|
+
|
|
275
|
+
@Component({
|
|
276
|
+
selector: 'app-dynamic-checkbox',
|
|
277
|
+
template: `
|
|
278
|
+
<form [formGroup]="dynamicForm">
|
|
279
|
+
<app-checkbox-selection-input
|
|
280
|
+
formControlName="permissions"
|
|
281
|
+
[data]="permissionOptions"
|
|
282
|
+
label="User Permissions"
|
|
283
|
+
[minSelection]="1"
|
|
284
|
+
[maxSelection]="4"
|
|
285
|
+
[disableMax]="true">
|
|
286
|
+
</app-checkbox-selection-input>
|
|
287
|
+
</form>
|
|
288
|
+
|
|
289
|
+
<div class="form-status">
|
|
290
|
+
<div>Valid: {{ dynamicForm.get('permissions')?.valid }}</div>
|
|
291
|
+
<div>Touched: {{ dynamicForm.get('permissions')?.touched }}</div>
|
|
292
|
+
<div>Selected Count: {{ dynamicForm.get('permissions')?.value?.length || 0 }}</div>
|
|
293
|
+
</div>
|
|
294
|
+
|
|
295
|
+
<div class="controls">
|
|
296
|
+
<button (click)="addPermission()">Add Permission</button>
|
|
297
|
+
<button (click)="removeLastPermission()">Remove Last</button>
|
|
298
|
+
<button (click)="resetForm()">Reset</button>
|
|
299
|
+
</div>
|
|
300
|
+
`,
|
|
301
|
+
styles: [`
|
|
302
|
+
.controls { margin-top: 1rem; display: flex; gap: 0.5rem; }
|
|
303
|
+
.form-status { margin-top: 1rem; padding: 0.5rem; background: #f5f5f5; }
|
|
304
|
+
`]
|
|
305
|
+
})
|
|
306
|
+
export class DynamicCheckboxComponent {
|
|
307
|
+
constructor(private fb: FormBuilder) {}
|
|
308
|
+
|
|
309
|
+
dynamicForm = this.fb.group({
|
|
310
|
+
permissions: [[], [Validators.required, Validators.minLength(1), Validators.maxLength(4)]]
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
permissionOptions = [
|
|
314
|
+
{ id: 1, value: 'Read Files' },
|
|
315
|
+
{ id: 2, value: 'Write Files' },
|
|
316
|
+
{ id: 3, value: 'Delete Files' },
|
|
317
|
+
{ id: 4, value: 'Share Files' },
|
|
318
|
+
{ id: 5, value: 'Admin Access' }
|
|
319
|
+
];
|
|
320
|
+
|
|
321
|
+
addPermission() {
|
|
322
|
+
const availablePermissions = this.permissionOptions.filter(
|
|
323
|
+
p => !this.dynamicForm.get('permissions')?.value?.includes(p.value)
|
|
324
|
+
);
|
|
325
|
+
if (availablePermissions.length > 0) {
|
|
326
|
+
const current = this.dynamicForm.get('permissions')?.value || [];
|
|
327
|
+
this.dynamicForm.get('permissions')?.setValue([...current, availablePermissions[0].value]);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
removeLastPermission() {
|
|
332
|
+
const current = this.dynamicForm.get('permissions')?.value || [];
|
|
333
|
+
if (current.length > 0) {
|
|
334
|
+
this.dynamicForm.get('permissions')?.setValue(current.slice(0, -1));
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
resetForm() {
|
|
339
|
+
this.dynamicForm.get('permissions')?.reset();
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
---
|
|
345
|
+
|
|
346
|
+
## Component API
|
|
347
|
+
|
|
348
|
+
### Inputs
|
|
349
|
+
|
|
350
|
+
| Input | Type | Description | Default |
|
|
351
|
+
| :--- | :--- | :--- | :--- |
|
|
352
|
+
| `data` | `any[] \| string[]` | Array of items or strings defining checkbox options | (Required) |
|
|
353
|
+
| `label` | `string` | Optional label text for the checkbox group | `undefined` |
|
|
354
|
+
| `placeholder` | `string` | Optional placeholder text | `undefined` |
|
|
355
|
+
| `error` | `string` | Optional error message to display | `undefined` |
|
|
356
|
+
| `disableMax` | `boolean` | If true, disables checkboxes when max selection is reached | `false` |
|
|
357
|
+
| `useDefaultReset` | `boolean` | Use default reset behavior | `false` |
|
|
358
|
+
| `minSelection` | `number` | Minimum number of selections required | `0` |
|
|
359
|
+
| `maxSelection` | `number` | Maximum number of selections allowed | `0` |
|
|
360
|
+
|
|
361
|
+
### Outputs
|
|
362
|
+
|
|
363
|
+
| Output | Type | Description |
|
|
364
|
+
| :--- | :--- | :--- |
|
|
365
|
+
| `selectionChange` | `EventEmitter<string[]>` | Emits array of selected values when selection changes |
|
|
366
|
+
|
|
367
|
+
### Form Control Integration
|
|
368
|
+
|
|
369
|
+
The component works with Angular form controls and emits:
|
|
370
|
+
|
|
371
|
+
- **Single array value**: Array of selected values (strings or objects based on input data)
|
|
372
|
+
- **Validation state**: Integrates with Angular's validation system
|
|
373
|
+
- **Touch/dirty states**: Properly tracks form control states
|
|
374
|
+
|
|
375
|
+
---
|
|
376
|
+
|
|
377
|
+
## Model Structures
|
|
378
|
+
|
|
379
|
+
### SelectionItem Interface
|
|
380
|
+
|
|
381
|
+
```typescript
|
|
382
|
+
export interface SelectionItemInterface {
|
|
383
|
+
id: number | string; // Unique identifier for the item
|
|
384
|
+
value: string; // The value to be selected/returned
|
|
385
|
+
disabled?: boolean; // Whether this item is disabled
|
|
386
|
+
selected?: boolean; // Whether this item is pre-selected
|
|
387
|
+
}
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
### SelectionItem Class
|
|
391
|
+
|
|
392
|
+
```typescript
|
|
393
|
+
export class SelectionItem implements SelectionItemInterface {
|
|
394
|
+
constructor(
|
|
395
|
+
public id = crypto.randomUUID(), // Auto-generates UUID if not provided
|
|
396
|
+
public value = '',
|
|
397
|
+
public disabled?: boolean = false,
|
|
398
|
+
public selected?: boolean = false,
|
|
399
|
+
) {}
|
|
400
|
+
|
|
401
|
+
static adapt(item?: any): SelectionItem {
|
|
402
|
+
return new SelectionItem(
|
|
403
|
+
item?.id, // Use provided ID or undefined
|
|
404
|
+
(item?.value) ? item.value : item, // Use value or fallback to item itself
|
|
405
|
+
(item?.disabled) ? true : false, // Convert to boolean
|
|
406
|
+
(item?.selected) ? true : false, // Convert to boolean
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
### Usage Examples
|
|
413
|
+
|
|
414
|
+
```typescript
|
|
415
|
+
// String array data (automatically converted to SelectionItem)
|
|
416
|
+
const stringData = ['Option 1', 'Option 2', 'Option 3'];
|
|
417
|
+
|
|
418
|
+
// Object array data
|
|
419
|
+
const objectData = [
|
|
420
|
+
{ id: 1, value: 'Telus', selected: true },
|
|
421
|
+
{ id: 2, value: 'AT&T', disabled: true },
|
|
422
|
+
{ id: 3, value: 'Bell' }
|
|
423
|
+
];
|
|
424
|
+
|
|
425
|
+
// Manual SelectionItem creation
|
|
426
|
+
const manualItems = [
|
|
427
|
+
new SelectionItem('1', 'Option 1', false, true),
|
|
428
|
+
new SelectionItem('2', 'Option 2', true, false),
|
|
429
|
+
new SelectionItem('3', 'Option 3', false, false)
|
|
430
|
+
];
|
|
431
|
+
|
|
432
|
+
// Using adapt method for flexible data
|
|
433
|
+
const adaptedItems = [
|
|
434
|
+
SelectionItem.adapt({ value: 'Item 1', selected: true }),
|
|
435
|
+
SelectionItem.adapt({ id: 'custom-id', value: 'Item 2', disabled: true }),
|
|
436
|
+
SelectionItem.adapt('Item 3') // String fallback
|
|
437
|
+
];
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
---
|
|
441
|
+
|
|
442
|
+
## Form Integration
|
|
443
|
+
|
|
444
|
+
### ControlValueAccessor Implementation
|
|
445
|
+
|
|
446
|
+
The component implements Angular's `ControlValueAccessor` interface:
|
|
447
|
+
|
|
448
|
+
```typescript
|
|
449
|
+
// writeValue(value: string[]): void
|
|
450
|
+
// Sets the value of the control (array of selected values)
|
|
451
|
+
writeValue(value: string[]): void {
|
|
452
|
+
// Handle incoming form control value
|
|
453
|
+
// Update component state and checkbox states
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// registerOnChange(fn: any): void
|
|
457
|
+
// Registers a callback for value changes
|
|
458
|
+
registerOnChange(fn: any): void {
|
|
459
|
+
this.onChange = fn;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// registerOnTouched(fn: any): void
|
|
463
|
+
// Registers a callback for touch events
|
|
464
|
+
registerOnTouched(fn: any): void {
|
|
465
|
+
this.onTouch = fn;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// setDisabledState(isDisabled: boolean): void
|
|
469
|
+
// Sets disabled state for entire component
|
|
470
|
+
setDisabledState(isDisabled: boolean): void {
|
|
471
|
+
this.disabled = isDisabled;
|
|
472
|
+
if (this.disabled) {
|
|
473
|
+
this.selectionControl.disable();
|
|
474
|
+
} else {
|
|
475
|
+
this.selectionControl.enable();
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
### Validation Integration
|
|
481
|
+
|
|
482
|
+
The component also implements `NG_VALIDATORS`:
|
|
483
|
+
|
|
484
|
+
```typescript
|
|
485
|
+
// validate(control: AbstractControl): ValidationErrors | null
|
|
486
|
+
// Performs validation and returns errors if any
|
|
487
|
+
validate(control: AbstractControl): ValidationErrors | null {
|
|
488
|
+
// Check min/max selection requirements
|
|
489
|
+
// Return validation errors or null
|
|
490
|
+
return { minRequired: true, maxExceeded: false };
|
|
491
|
+
}
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
### Form Integration Examples
|
|
495
|
+
|
|
496
|
+
#### Reactive Forms
|
|
497
|
+
|
|
498
|
+
```typescript
|
|
499
|
+
import { Component } from '@angular/core';
|
|
500
|
+
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
|
501
|
+
|
|
502
|
+
@Component({
|
|
503
|
+
selector: 'app-reactive-checkbox',
|
|
504
|
+
template: `
|
|
505
|
+
<form [formGroup]="checkboxForm">
|
|
506
|
+
<app-checkbox-selection-input
|
|
507
|
+
formControlName="selections"
|
|
508
|
+
[data]="options"
|
|
509
|
+
label="Select Options"
|
|
510
|
+
[minSelection]="2"
|
|
511
|
+
[maxSelection]="4"
|
|
512
|
+
[disableMax]="true">
|
|
513
|
+
</app-checkbox-selection-input>
|
|
514
|
+
|
|
515
|
+
<div class="errors" *ngIf="checkboxForm.get('selections')?.errors">
|
|
516
|
+
<div *ngIf="checkboxForm.get('selections')?.hasError('minRequired')">
|
|
517
|
+
Please select at least 2 options
|
|
518
|
+
</div>
|
|
519
|
+
<div *ngIf="checkboxForm.get('selections')?.hasError('maxExceeded')">
|
|
520
|
+
Maximum selections exceeded
|
|
521
|
+
</div>
|
|
522
|
+
</div>
|
|
523
|
+
</form>
|
|
524
|
+
`
|
|
525
|
+
})
|
|
526
|
+
export class ReactiveCheckboxComponent {
|
|
527
|
+
checkboxForm = new FormGroup({
|
|
528
|
+
selections: new FormControl([], [
|
|
529
|
+
Validators.required,
|
|
530
|
+
Validators.minLength(2),
|
|
531
|
+
Validators.maxLength(4)
|
|
532
|
+
])
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
options = ['Option 1', 'Option 2', 'Option 3', 'Option 4', 'Option 5'];
|
|
536
|
+
}
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
#### Template-Driven Forms
|
|
540
|
+
|
|
541
|
+
```typescript
|
|
542
|
+
import { Component } from '@angular/core';
|
|
543
|
+
|
|
544
|
+
@Component({
|
|
545
|
+
selector: 'app-template-checkbox',
|
|
546
|
+
template: `
|
|
547
|
+
<form #checkboxForm="ngForm">
|
|
548
|
+
<app-checkbox-selection-input
|
|
549
|
+
[(ngModel)]="selectedValues"
|
|
550
|
+
name="checkboxSelections"
|
|
551
|
+
[data]="options"
|
|
552
|
+
label="Template Driven Selection"
|
|
553
|
+
[minSelection]="1"
|
|
554
|
+
[maxSelection]="3"
|
|
555
|
+
required>
|
|
556
|
+
</app-checkbox-selection-input>
|
|
557
|
+
|
|
558
|
+
<div *ngIf="checkboxForm.controls.checkboxSelections?.invalid &&
|
|
559
|
+
checkboxForm.controls.checkboxSelections?.touched">
|
|
560
|
+
Please make a selection
|
|
561
|
+
</div>
|
|
562
|
+
</form>
|
|
563
|
+
`
|
|
564
|
+
})
|
|
565
|
+
export class TemplateCheckboxComponent {
|
|
566
|
+
selectedValues: string[] = [];
|
|
567
|
+
|
|
568
|
+
options = ['Choice 1', 'Choice 2', 'Choice 3', 'Choice 4'];
|
|
569
|
+
}
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
#### Programmatic Control
|
|
573
|
+
|
|
574
|
+
```typescript
|
|
575
|
+
// Setting values
|
|
576
|
+
this.formControl.setValue(['Option 1', 'Option 2']);
|
|
577
|
+
this.formControl.patchValue(['Option 1']); // Partial update
|
|
578
|
+
|
|
579
|
+
// Resetting
|
|
580
|
+
this.formControl.reset();
|
|
581
|
+
|
|
582
|
+
// Getting current value
|
|
583
|
+
const currentValue = this.formControl.value;
|
|
584
|
+
|
|
585
|
+
// Validation
|
|
586
|
+
const isValid = this.formControl.valid;
|
|
587
|
+
const errors = this.formControl.errors;
|
|
588
|
+
|
|
589
|
+
// Setting custom errors
|
|
590
|
+
this.formControl.setErrors({ customError: 'Custom error message' });
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
---
|
|
594
|
+
|
|
595
|
+
## Validation System
|
|
596
|
+
|
|
597
|
+
### Built-in Validation
|
|
598
|
+
|
|
599
|
+
The component provides built-in validation for:
|
|
600
|
+
|
|
601
|
+
#### Min Selection Validation
|
|
602
|
+
```typescript
|
|
603
|
+
// Requires at least X selections
|
|
604
|
+
[minSelection]="2" // Must select at least 2 items
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
#### Max Selection Validation
|
|
608
|
+
```typescript
|
|
609
|
+
// Allows maximum X selections
|
|
610
|
+
[maxSelection]="3" // Cannot select more than 3 items
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
#### Disable Max Behavior
|
|
614
|
+
```typescript
|
|
615
|
+
// Auto-disable remaining checkboxes when max reached
|
|
616
|
+
[disableMax]="true" // Disables unchecked boxes at max
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
### Validation Error Types
|
|
620
|
+
|
|
621
|
+
| Error Type | Condition | Description |
|
|
622
|
+
|------------|-----------|-------------|
|
|
623
|
+
| `minRequired` | `selectedCount < minSelection` | Not enough selections made |
|
|
624
|
+
| `maxExceeded` | `selectedCount >= maxSelection` | Too many selections made |
|
|
625
|
+
| `required` | Control has required validator and no selections | Control is required but empty |
|
|
626
|
+
|
|
627
|
+
### Custom Validation Examples
|
|
628
|
+
|
|
629
|
+
```typescript
|
|
630
|
+
// Complex validation scenarios
|
|
631
|
+
const complexForm = new FormGroup({
|
|
632
|
+
permissions: new FormControl([], [
|
|
633
|
+
Validators.required,
|
|
634
|
+
Validators.minLength(1), // At least 1 selection
|
|
635
|
+
Validators.maxLength(5), // Maximum 5 selections
|
|
636
|
+
customMaxValidator(3) // Custom: Cannot select admin + delete together
|
|
637
|
+
])
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
// Custom validator example
|
|
641
|
+
function customMaxValidator(maxCount: number) {
|
|
642
|
+
return (control: AbstractControl): ValidationErrors | null => {
|
|
643
|
+
const value = control.value;
|
|
644
|
+
if (value && value.length > maxCount) {
|
|
645
|
+
return { customMaxExceeded: true };
|
|
646
|
+
}
|
|
647
|
+
return null;
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
---
|
|
653
|
+
|
|
654
|
+
## Module Configuration
|
|
655
|
+
|
|
656
|
+
### CheckboxSelectionInputModule
|
|
657
|
+
|
|
658
|
+
**No Global Configuration Required**
|
|
659
|
+
|
|
660
|
+
The `CheckboxSelectionInputModule` does not provide a `forRoot()` method or global configuration options. All configuration is done at the component level through input properties.
|
|
661
|
+
|
|
662
|
+
#### Module Structure
|
|
663
|
+
|
|
664
|
+
```typescript
|
|
665
|
+
@NgModule({
|
|
666
|
+
imports: [
|
|
667
|
+
CommonModule,
|
|
668
|
+
FormsModule,
|
|
669
|
+
ReactiveFormsModule,
|
|
670
|
+
MatSliderModule,
|
|
671
|
+
MatButtonModule,
|
|
672
|
+
MatIconModule,
|
|
673
|
+
MatFormFieldModule,
|
|
674
|
+
MatToolbarModule,
|
|
675
|
+
MatCheckboxModule,
|
|
676
|
+
MatMenuModule,
|
|
677
|
+
MatButtonToggleModule,
|
|
678
|
+
MatDividerModule,
|
|
679
|
+
MatRadioModule,
|
|
680
|
+
MatInputModule,
|
|
681
|
+
MatAutocompleteModule,
|
|
682
|
+
RemoveUnderscorePipe,
|
|
683
|
+
MatSelectModule,
|
|
684
|
+
MatOptionModule,
|
|
685
|
+
MatSlideToggleModule,
|
|
686
|
+
],
|
|
687
|
+
declarations: [
|
|
688
|
+
CheckboxSelectionInputComponent,
|
|
689
|
+
CheckboxSelectionDemoComponent,
|
|
690
|
+
],
|
|
691
|
+
exports: [
|
|
692
|
+
CheckboxSelectionInputComponent,
|
|
693
|
+
CheckboxSelectionDemoComponent
|
|
694
|
+
]
|
|
695
|
+
})
|
|
696
|
+
export class CheckboxSelectionInputModule { }
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
#### Dependencies
|
|
700
|
+
|
|
701
|
+
- **@angular/common**: Core Angular functionality
|
|
702
|
+
- **@angular/forms**: Form control integration (FormsModule, ReactiveFormsModule)
|
|
703
|
+
- **@angular/material**: Material Design components
|
|
704
|
+
- MatCheckboxModule: Checkbox components
|
|
705
|
+
- MatFormFieldModule: Form field styling
|
|
706
|
+
- MatButtonModule: Button components
|
|
707
|
+
- MatIconModule: Icon display
|
|
708
|
+
- MatSlideToggleModule: Toggle switches for demo
|
|
709
|
+
- MatDividerModule: Visual dividers
|
|
710
|
+
- Additional Material modules for comprehensive UI support
|
|
711
|
+
|
|
712
|
+
---
|
|
713
|
+
|
|
714
|
+
## Styling and Customization
|
|
715
|
+
|
|
716
|
+
### CSS Classes and Styling
|
|
717
|
+
|
|
718
|
+
The component uses Material Design styling and can be customized using:
|
|
719
|
+
|
|
720
|
+
1. **Global Material Theme**: Configure colors in your Angular Material theme
|
|
721
|
+
2. **Component-specific Styles**: Add custom CSS classes
|
|
722
|
+
3. **Form Field Styling**: Style using Material form field classes
|
|
723
|
+
4. **Checkbox Styling**: Customize individual checkbox appearance
|
|
724
|
+
|
|
725
|
+
### Custom Styling Examples
|
|
726
|
+
|
|
727
|
+
```scss
|
|
728
|
+
// Custom checkbox group styling
|
|
729
|
+
:host ::ng-deep .checkbox-selection-input {
|
|
730
|
+
.mat-form-field {
|
|
731
|
+
.mat-form-field-label {
|
|
732
|
+
color: #2196f3;
|
|
733
|
+
font-weight: 500;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
.mat-checkbox {
|
|
738
|
+
margin-bottom: 8px;
|
|
739
|
+
|
|
740
|
+
&.mat-checkbox-disabled {
|
|
741
|
+
.mat-checkbox-label {
|
|
742
|
+
opacity: 0.6;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// Custom disabled state styling
|
|
749
|
+
:host ::ng-deep .checkbox-selection-input {
|
|
750
|
+
.mat-checkbox-disabled {
|
|
751
|
+
.mat-checkbox-frame {
|
|
752
|
+
border-color: #ccc;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
.mat-checkbox-label {
|
|
756
|
+
color: #999;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
### Layout Customization
|
|
763
|
+
|
|
764
|
+
```scss
|
|
765
|
+
// Horizontal layout
|
|
766
|
+
.horizontal-checkboxes {
|
|
767
|
+
app-checkbox-selection-input {
|
|
768
|
+
.mat-checkbox {
|
|
769
|
+
display: inline-block;
|
|
770
|
+
margin-right: 16px;
|
|
771
|
+
margin-bottom: 0;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// Compact layout
|
|
777
|
+
.compact-checkboxes {
|
|
778
|
+
app-checkbox-selection-input {
|
|
779
|
+
.mat-checkbox {
|
|
780
|
+
margin-bottom: 4px;
|
|
781
|
+
|
|
782
|
+
.mat-checkbox-label {
|
|
783
|
+
font-size: 0.875rem;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
```
|
|
789
|
+
|
|
790
|
+
---
|
|
791
|
+
|
|
792
|
+
## Accessibility
|
|
793
|
+
|
|
794
|
+
### ARIA Support
|
|
795
|
+
|
|
796
|
+
- Checkboxes include proper ARIA labels and roles
|
|
797
|
+
- Group labeling through label input property
|
|
798
|
+
- Keyboard navigation is fully supported (Tab, Space, Arrow keys)
|
|
799
|
+
- Screen reader friendly with appropriate descriptions
|
|
800
|
+
- Validation errors are announced to assistive technologies
|
|
801
|
+
- Disabled states are properly communicated
|
|
802
|
+
|
|
803
|
+
### Best Practices
|
|
804
|
+
|
|
805
|
+
1. **Provide meaningful labels** for the checkbox group
|
|
806
|
+
2. **Use descriptive placeholders** for additional context
|
|
807
|
+
3. **Set appropriate validation messages** for accessibility
|
|
808
|
+
4. **Consider keyboard navigation** order
|
|
809
|
+
5. **Test with screen readers** to ensure proper announcements
|
|
810
|
+
6. **Use logical grouping** for related checkbox options
|
|
811
|
+
|
|
812
|
+
### Keyboard Navigation
|
|
813
|
+
|
|
814
|
+
| Key | Action |
|
|
815
|
+
|-----|--------|
|
|
816
|
+
| `Tab` | Navigate to next checkbox |
|
|
817
|
+
| `Shift+Tab` | Navigate to previous checkbox |
|
|
818
|
+
| `Space` | Toggle checkbox selection |
|
|
819
|
+
| `Enter` | Toggle checkbox selection (in some contexts) |
|
|
820
|
+
|
|
821
|
+
---
|
|
822
|
+
|
|
823
|
+
## Integration Examples
|
|
824
|
+
|
|
825
|
+
### With Other UI Components
|
|
826
|
+
|
|
827
|
+
```typescript
|
|
828
|
+
// Integration with display-card
|
|
829
|
+
@Component({
|
|
830
|
+
template: `
|
|
831
|
+
<app-display-card title="User Permissions">
|
|
832
|
+
<app-checkbox-selection-input
|
|
833
|
+
[data]="permissionOptions"
|
|
834
|
+
[formControl]="permissionControl"
|
|
835
|
+
label="Select Permissions"
|
|
836
|
+
[minSelection]="1"
|
|
837
|
+
[maxSelection]="3"
|
|
838
|
+
[disableMax]="true">
|
|
839
|
+
</app-checkbox-selection-input>
|
|
840
|
+
</app-display-card>
|
|
841
|
+
`
|
|
842
|
+
})
|
|
843
|
+
export class CardWithCheckboxComponent {
|
|
844
|
+
permissionControl = new FormControl();
|
|
845
|
+
|
|
846
|
+
permissionOptions = [
|
|
847
|
+
{ id: 1, value: 'Read Access' },
|
|
848
|
+
{ id: 2, value: 'Write Access' },
|
|
849
|
+
{ id: 3, value: 'Delete Access' },
|
|
850
|
+
{ id: 4, value: 'Admin Access' }
|
|
851
|
+
];
|
|
852
|
+
}
|
|
853
|
+
```
|
|
854
|
+
|
|
855
|
+
### With State Management
|
|
856
|
+
|
|
857
|
+
```typescript
|
|
858
|
+
// Integration with HTTP Request Manager
|
|
859
|
+
@Component({
|
|
860
|
+
template: `
|
|
861
|
+
<app-checkbox-selection-input
|
|
862
|
+
[data]="categoryOptions$ | async"
|
|
863
|
+
[formControl]="categoryControl"
|
|
864
|
+
[multiple]="true"
|
|
865
|
+
(selectionChange)="handleSelectionChange($event)">
|
|
866
|
+
</app-checkbox-selection-input>
|
|
867
|
+
`
|
|
868
|
+
})
|
|
869
|
+
export class StateManagedCheckboxComponent {
|
|
870
|
+
categoryOptions$ = this.categoryStore.options$;
|
|
871
|
+
categoryControl = new FormControl();
|
|
872
|
+
|
|
873
|
+
constructor(private categoryStore: CategoryStore) {}
|
|
874
|
+
|
|
875
|
+
handleSelectionChange(selectedValues: string[]) {
|
|
876
|
+
this.categoryStore.updateSelection(selectedValues);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
```
|
|
880
|
+
|
|
881
|
+
### With Dynamic Forms
|
|
882
|
+
|
|
883
|
+
```typescript
|
|
884
|
+
@Component({
|
|
885
|
+
template: `
|
|
886
|
+
<div formArrayName="checkboxGroups">
|
|
887
|
+
<div *ngFor="let group of checkboxGroups.controls; let i = index">
|
|
888
|
+
<app-checkbox-selection-input
|
|
889
|
+
[formControlName]="i"
|
|
890
|
+
[data]="dynamicOptions[i]"
|
|
891
|
+
[label]="'Group ' + (i + 1)">
|
|
892
|
+
</app-checkbox-selection-input>
|
|
893
|
+
</div>
|
|
894
|
+
</div>
|
|
895
|
+
`
|
|
896
|
+
})
|
|
897
|
+
export class DynamicFormCheckboxComponent {
|
|
898
|
+
checkboxForm = this.fb.group({
|
|
899
|
+
checkboxGroups: this.fb.array([
|
|
900
|
+
this.fb.control(['option1']),
|
|
901
|
+
this.fb.control(['option2', 'option3']),
|
|
902
|
+
this.fb.control([])
|
|
903
|
+
])
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
get checkboxGroups() {
|
|
907
|
+
return this.checkboxForm.get('checkboxGroups') as FormArray;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
dynamicOptions = [
|
|
911
|
+
['Option 1A', 'Option 1B', 'Option 1C'],
|
|
912
|
+
['Option 2A', 'Option 2B'],
|
|
913
|
+
['Option 3A', 'Option 3B', 'Option 3C', 'Option 3D']
|
|
914
|
+
];
|
|
915
|
+
}
|
|
916
|
+
```
|
|
917
|
+
|
|
918
|
+
---
|
|
919
|
+
|
|
920
|
+
## Performance Optimization
|
|
921
|
+
|
|
922
|
+
### Performance Tips
|
|
923
|
+
|
|
924
|
+
1. **Use OnPush change detection** for better performance with large checkbox arrays
|
|
925
|
+
2. **Implement trackBy** for dynamic checkbox lists (if applicable)
|
|
926
|
+
3. **Avoid frequent data object recreation** to prevent unnecessary re-renders
|
|
927
|
+
4. **Use immutable data patterns** for checkbox option updates
|
|
928
|
+
5. **Consider virtual scrolling** for very large checkbox lists
|
|
929
|
+
6. **Optimize validation** to avoid expensive operations on every change
|
|
930
|
+
|
|
931
|
+
### Memory Management
|
|
932
|
+
|
|
933
|
+
```typescript
|
|
934
|
+
// Efficient data updates
|
|
935
|
+
updateOptions(newOptions: any[]) {
|
|
936
|
+
// Create new array reference to trigger change detection
|
|
937
|
+
this.options = [...newOptions];
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// Cleanup in ngOnDestroy
|
|
941
|
+
ngOnDestroy() {
|
|
942
|
+
this.subscriptions.forEach(sub => sub.unsubscribe());
|
|
943
|
+
}
|
|
944
|
+
```
|
|
945
|
+
|
|
946
|
+
---
|
|
947
|
+
|
|
948
|
+
## Testing
|
|
949
|
+
|
|
950
|
+
### Unit Testing Example
|
|
951
|
+
|
|
952
|
+
```typescript
|
|
953
|
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
954
|
+
import { CheckboxSelectionInputComponent } from './checkbox-selection-input.component';
|
|
955
|
+
import { ReactiveFormsModule } from '@angular/forms';
|
|
956
|
+
|
|
957
|
+
describe('CheckboxSelectionInputComponent', () => {
|
|
958
|
+
let component: CheckboxSelectionInputComponent;
|
|
959
|
+
let fixture: ComponentFixture<CheckboxSelectionInputComponent>;
|
|
960
|
+
|
|
961
|
+
beforeEach(async () => {
|
|
962
|
+
await TestBed.configureTestingModule({
|
|
963
|
+
declarations: [ CheckboxSelectionInputComponent ],
|
|
964
|
+
imports: [ ReactiveFormsModule ]
|
|
965
|
+
}).compileComponents();
|
|
966
|
+
|
|
967
|
+
fixture = TestBed.createComponent(CheckboxSelectionInputComponent);
|
|
968
|
+
component = fixture.componentInstance;
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
it('should create', () => {
|
|
972
|
+
expect(component).toBeTruthy();
|
|
973
|
+
});
|
|
974
|
+
|
|
975
|
+
it('should display checkboxes from data input', () => {
|
|
976
|
+
component.data = ['Option 1', 'Option 2', 'Option 3'];
|
|
977
|
+
fixture.detectChanges();
|
|
978
|
+
|
|
979
|
+
const compiled = fixture.nativeElement;
|
|
980
|
+
const checkboxes = compiled.querySelectorAll('.mat-checkbox');
|
|
981
|
+
expect(checkboxes.length).toBe(3);
|
|
982
|
+
});
|
|
983
|
+
|
|
984
|
+
it('should emit selection changes', () => {
|
|
985
|
+
spyOn(component.selectionChange, 'emit');
|
|
986
|
+
component.data = ['Option 1', 'Option 2'];
|
|
987
|
+
component.writeValue(['Option 1']);
|
|
988
|
+
fixture.detectChanges();
|
|
989
|
+
|
|
990
|
+
expect(component.selectionChange.emit).toHaveBeenCalledWith(['Option 1']);
|
|
991
|
+
});
|
|
992
|
+
|
|
993
|
+
it('should validate minimum selections', () => {
|
|
994
|
+
component.minSelection = 2;
|
|
995
|
+
component.data = ['Option 1', 'Option 2', 'Option 3'];
|
|
996
|
+
|
|
997
|
+
// Simulate selecting only 1 item
|
|
998
|
+
component.writeValue(['Option 1']);
|
|
999
|
+
|
|
1000
|
+
const validationResult = component.validate({} as AbstractControl);
|
|
1001
|
+
expect(validationResult?.minRequired).toBe(true);
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
it('should validate maximum selections', () => {
|
|
1005
|
+
component.maxSelection = 2;
|
|
1006
|
+
component.data = ['Option 1', 'Option 2', 'Option 3'];
|
|
1007
|
+
|
|
1008
|
+
// Simulate selecting 3 items when max is 2
|
|
1009
|
+
component.writeValue(['Option 1', 'Option 2', 'Option 3']);
|
|
1010
|
+
|
|
1011
|
+
const validationResult = component.validate({} as AbstractControl);
|
|
1012
|
+
expect(validationResult?.maxExceeded).toBe(true);
|
|
1013
|
+
});
|
|
1014
|
+
});
|
|
1015
|
+
```
|
|
1016
|
+
|
|
1017
|
+
### Integration Testing
|
|
1018
|
+
|
|
1019
|
+
```typescript
|
|
1020
|
+
import { TestBed, ComponentFixture } from '@angular/core/testing';
|
|
1021
|
+
import { CheckboxSelectionInputModule } from './checkbox-selection-input.module';
|
|
1022
|
+
|
|
1023
|
+
describe('CheckboxSelectionInput Integration', () => {
|
|
1024
|
+
let component: TestHostComponent;
|
|
1025
|
+
let fixture: ComponentFixture<TestHostComponent>;
|
|
1026
|
+
|
|
1027
|
+
@Component({
|
|
1028
|
+
template: `
|
|
1029
|
+
<app-checkbox-selection-input
|
|
1030
|
+
[formControl]="testControl"
|
|
1031
|
+
[data]="testData"
|
|
1032
|
+
[minSelection]="1"
|
|
1033
|
+
[maxSelection]="2">
|
|
1034
|
+
</app-checkbox-selection-input>
|
|
1035
|
+
`
|
|
1036
|
+
})
|
|
1037
|
+
class TestHostComponent {
|
|
1038
|
+
testControl = new FormControl();
|
|
1039
|
+
testData = ['Test 1', 'Test 2', 'Test 3'];
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
beforeEach(async () => {
|
|
1043
|
+
await TestBed.configureTestingModule({
|
|
1044
|
+
declarations: [ TestHostComponent ],
|
|
1045
|
+
imports: [ CheckboxSelectionInputModule ]
|
|
1046
|
+
}).compileComponents();
|
|
1047
|
+
|
|
1048
|
+
fixture = TestBed.createComponent(TestHostComponent);
|
|
1049
|
+
component = fixture.componentInstance;
|
|
1050
|
+
});
|
|
1051
|
+
|
|
1052
|
+
it('should integrate with form controls', () => {
|
|
1053
|
+
expect(component.testControl).toBeDefined();
|
|
1054
|
+
expect(component.testData.length).toBe(3);
|
|
1055
|
+
});
|
|
1056
|
+
|
|
1057
|
+
it('should update form control value when selection changes', () => {
|
|
1058
|
+
fixture.detectChanges();
|
|
1059
|
+
|
|
1060
|
+
// Simulate user interaction
|
|
1061
|
+
const checkboxes = fixture.nativeElement.querySelectorAll('.mat-checkbox');
|
|
1062
|
+
checkboxes[0].click(); // Select first checkbox
|
|
1063
|
+
|
|
1064
|
+
expect(component.testControl.value).toEqual(['Test 1']);
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
it('should enforce validation constraints', () => {
|
|
1068
|
+
fixture.detectChanges();
|
|
1069
|
+
|
|
1070
|
+
// Test minimum selection validation
|
|
1071
|
+
component.testControl.setValue([]);
|
|
1072
|
+
expect(component.testControl.valid).toBe(false);
|
|
1073
|
+
|
|
1074
|
+
// Test maximum selection validation
|
|
1075
|
+
component.testControl.setValue(['Test 1', 'Test 2', 'Test 3']);
|
|
1076
|
+
expect(component.testControl.valid).toBe(false);
|
|
1077
|
+
|
|
1078
|
+
// Test valid selection
|
|
1079
|
+
component.testControl.setValue(['Test 1', 'Test 2']);
|
|
1080
|
+
expect(component.testControl.valid).toBe(true);
|
|
1081
|
+
});
|
|
1082
|
+
});
|
|
1083
|
+
```
|
|
1084
|
+
|
|
1085
|
+
---
|
|
1086
|
+
|
|
1087
|
+
## Troubleshooting
|
|
1088
|
+
|
|
1089
|
+
### Common Issues
|
|
1090
|
+
|
|
1091
|
+
1. **Form control not working**: Ensure ReactiveFormsModule is imported
|
|
1092
|
+
2. **Validation not triggering**: Check that validators are properly configured
|
|
1093
|
+
3. **Selection not updating**: Verify data format matches expected structure
|
|
1094
|
+
4. **Styling issues**: Ensure Material theme is properly configured
|
|
1095
|
+
5. **Auto-disable not working**: Check disableMax input property
|
|
1096
|
+
6. **Performance issues**: Consider OnPush change detection for large datasets
|
|
1097
|
+
|
|
1098
|
+
### Debug Mode
|
|
1099
|
+
|
|
1100
|
+
```typescript
|
|
1101
|
+
// Add debugging to track form control changes and validation
|
|
1102
|
+
@Component({
|
|
1103
|
+
template: `
|
|
1104
|
+
<div class="debug-info">
|
|
1105
|
+
Form Control Value: {{ formControl.value | json }}<br>
|
|
1106
|
+
Form Control Valid: {{ formControl.valid }}<br>
|
|
1107
|
+
Form Control Errors: {{ formControl.errors | json }}<br>
|
|
1108
|
+
Data Length: {{ data?.length || 0 }}<br>
|
|
1109
|
+
Min Selection: {{ minSelection }}<br>
|
|
1110
|
+
Max Selection: {{ maxSelection }}<br>
|
|
1111
|
+
Disable Max: {{ disableMax }}
|
|
1112
|
+
</div>
|
|
1113
|
+
|
|
1114
|
+
<app-checkbox-selection-input
|
|
1115
|
+
[formControl]="formControl"
|
|
1116
|
+
[data]="data"
|
|
1117
|
+
[minSelection]="minSelection"
|
|
1118
|
+
[maxSelection]="maxSelection"
|
|
1119
|
+
[disableMax]="disableMax">
|
|
1120
|
+
</app-checkbox-selection-input>
|
|
1121
|
+
`
|
|
1122
|
+
})
|
|
1123
|
+
export class DebugCheckboxComponent {
|
|
1124
|
+
formControl = new FormControl();
|
|
1125
|
+
data: string[] = [];
|
|
1126
|
+
minSelection = 0;
|
|
1127
|
+
maxSelection = 0;
|
|
1128
|
+
disableMax = false;
|
|
1129
|
+
|
|
1130
|
+
constructor() {
|
|
1131
|
+
this.formControl.valueChanges.subscribe(value => {
|
|
1132
|
+
console.log('Checkbox value changed:', value);
|
|
1133
|
+
});
|
|
1134
|
+
|
|
1135
|
+
this.formControl.statusChanges.subscribe(status => {
|
|
1136
|
+
console.log('Form control status:', status);
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
```
|
|
1141
|
+
|
|
1142
|
+
### Validation Debugging
|
|
1143
|
+
|
|
1144
|
+
```typescript
|
|
1145
|
+
// Debug validation logic
|
|
1146
|
+
validate(control: AbstractControl): ValidationErrors | null {
|
|
1147
|
+
console.log('Validating with:', {
|
|
1148
|
+
selectedCount: this.selectedCheckboxes(this.selectionControl.value).length,
|
|
1149
|
+
minSelection: this.minSelection,
|
|
1150
|
+
maxSelection: this.maxSelection,
|
|
1151
|
+
currentValue: this.selectionControl.value
|
|
1152
|
+
});
|
|
1153
|
+
|
|
1154
|
+
// ... validation logic
|
|
1155
|
+
|
|
1156
|
+
const errors = this.selectedValues(this.selectionControl.value, true).length > 0 ? errors : null;
|
|
1157
|
+
console.log('Validation result:', errors);
|
|
1158
|
+
|
|
1159
|
+
return errors;
|
|
1160
|
+
}
|
|
1161
|
+
```
|
|
1162
|
+
|
|
1163
|
+
### Performance Debugging
|
|
1164
|
+
|
|
1165
|
+
```typescript
|
|
1166
|
+
// Monitor change detection performance
|
|
1167
|
+
ngAfterViewInit() {
|
|
1168
|
+
// Track rendering time
|
|
1169
|
+
const start = performance.now();
|
|
1170
|
+
|
|
1171
|
+
setTimeout(() => {
|
|
1172
|
+
const end = performance.now();
|
|
1173
|
+
console.log(`Checkbox rendering took ${end - start}ms`);
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
```
|
|
1177
|
+
|
|
1178
|
+
---
|
|
1179
|
+
|
|
1180
|
+
## Advanced Usage Patterns
|
|
1181
|
+
|
|
1182
|
+
### Conditional Validation
|
|
1183
|
+
|
|
1184
|
+
```typescript
|
|
1185
|
+
// Complex validation scenarios
|
|
1186
|
+
@Component({
|
|
1187
|
+
template: `
|
|
1188
|
+
<app-checkbox-selection-input
|
|
1189
|
+
[formControl]="conditionalControl"
|
|
1190
|
+
[data]="conditionalOptions"
|
|
1191
|
+
[minSelection]="getMinSelection()"
|
|
1192
|
+
[maxSelection]="getMaxSelection()"
|
|
1193
|
+
[disableMax]="shouldDisableMax()">
|
|
1194
|
+
</app-checkbox-selection-input>
|
|
1195
|
+
`
|
|
1196
|
+
})
|
|
1197
|
+
export class ConditionalCheckboxComponent {
|
|
1198
|
+
conditionalControl = new FormControl();
|
|
1199
|
+
|
|
1200
|
+
getMinSelection(): number {
|
|
1201
|
+
const userRole = this.getUserRole();
|
|
1202
|
+
return userRole === 'admin' ? 1 : 2;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
getMaxSelection(): number {
|
|
1206
|
+
const subscriptionLevel = this.getSubscriptionLevel();
|
|
1207
|
+
return subscriptionLevel === 'premium' ? 5 : 3;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
shouldDisableMax(): boolean {
|
|
1211
|
+
return true;
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
```
|
|
1215
|
+
|
|
1216
|
+
### Data Transformation
|
|
1217
|
+
|
|
1218
|
+
```typescript
|
|
1219
|
+
// Transform data before passing to component
|
|
1220
|
+
transformData(rawData: any[]): any[] {
|
|
1221
|
+
return rawData.map(item => {
|
|
1222
|
+
if (typeof item === 'string') {
|
|
1223
|
+
return {
|
|
1224
|
+
id: this.generateId(item),
|
|
1225
|
+
value: item,
|
|
1226
|
+
selected: this.isPreSelected(item),
|
|
1227
|
+
disabled: this.isDisabled(item)
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
return SelectionItem.adapt(item);
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
```
|
|
1234
|
+
|
|
1235
|
+
### Custom Validation Messages
|
|
1236
|
+
|
|
1237
|
+
```typescript
|
|
1238
|
+
// Dynamic error messages
|
|
1239
|
+
getErrorMessage(control: AbstractControl): string {
|
|
1240
|
+
if (control.hasError('minRequired')) {
|
|
1241
|
+
const min = this.minSelection;
|
|
1242
|
+
return `Please select at least ${min} option${min > 1 ? 's' : ''}`;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
if (control.hasError('maxExceeded')) {
|
|
1246
|
+
const max = this.maxSelection;
|
|
1247
|
+
return `You can select maximum ${max} option${max > 1 ? 's' : ''}`;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
if (control.hasError('required')) {
|
|
1251
|
+
return 'Please make a selection';
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
return 'Invalid selection';
|
|
1255
|
+
}
|
|
1256
|
+
```
|