radio-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 +1169 -13
- package/fesm2022/radio-selection-input.mjs +28 -28
- package/fesm2022/radio-selection-input.mjs.map +1 -1
- package/package.json +12 -5
- package/radio-selection-input-15.0.5.tgz +0 -0
- package/types/radio-selection-input.d.ts +127 -0
- 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/lib/radio-demo/radio-demo.component.mjs +0 -75
- package/esm2022/lib/radio-selection-input/radio-selection-input.component.mjs +0 -131
- package/esm2022/public-api.mjs +0 -9
- package/esm2022/radio-selection-input.mjs +0 -5
- package/index.d.ts +0 -5
- 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/lib/radio-demo/radio-demo.component.d.ts +0 -36
- package/lib/radio-selection-input/radio-selection-input.component.d.ts +0 -34
- package/public-api.d.ts +0 -5
- package/radio-selection-input-15.0.4.tgz +0 -0
package/README.md
CHANGED
|
@@ -1,24 +1,1180 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Radio Selection Input Component
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## Overview
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
The `radio-selection-input` library provides a Material Design radio button selection component that allows users to select exactly one option from a mutually exclusive set of choices. It supports both string and object data formats, implements `ControlValueAccessor` for seamless form integration, and provides native Angular validation system compatibility.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
> Note: Don't forget to add `--project radio-selection-input` or else it will be added to the default project in your `angular.json` file.
|
|
7
|
+
### Core Capabilities
|
|
9
8
|
|
|
10
|
-
|
|
9
|
+
#### 🔘 Radio Button Selection Interface
|
|
11
10
|
|
|
12
|
-
|
|
11
|
+
- **Exclusive Selection**: Only one option can be selected at a time (radio button behavior)
|
|
12
|
+
- **Form Control Integration**: Implements `ControlValueAccessor` for reactive forms
|
|
13
|
+
- **Flexible Data Support**: Handles both string arrays and complex object arrays
|
|
14
|
+
- **Material Design**: Built on Angular Material radio button foundation
|
|
15
|
+
- **Validation Support**: Native Angular validation system compatibility
|
|
16
|
+
- **Label Configuration**: Customizable labels and placeholders
|
|
17
|
+
- **Disabled State Support**: Mark individual options as non-selectable
|
|
18
|
+
- **Pre-selection**: Initialize with a selected option
|
|
13
19
|
|
|
14
|
-
|
|
20
|
+
#### 🔧 Features
|
|
15
21
|
|
|
16
|
-
|
|
22
|
+
✅ **ControlValueAccessor Implementation** - Works with Angular forms
|
|
23
|
+
✅ **Material Design Integration** - Uses Angular Material components
|
|
24
|
+
✅ **Exclusive Selection** - Single selection from mutually exclusive options
|
|
25
|
+
✅ **Flexible Data Types** - Support for strings and objects
|
|
26
|
+
✅ **Form Validation** - Native validation integration
|
|
27
|
+
✅ **Customizable Labels** - Configurable display labels
|
|
28
|
+
✅ **Disabled Options** - Mark individual options as non-selectable
|
|
29
|
+
✅ **Pre-selection** - Initialize with selected option
|
|
30
|
+
✅ **Required Validation** - Ensure selection is made
|
|
17
31
|
|
|
18
|
-
|
|
32
|
+
### Key Benefits
|
|
19
33
|
|
|
20
|
-
|
|
34
|
+
| Feature | Description |
|
|
35
|
+
|---------|-------------|
|
|
36
|
+
| **Exclusive Selection** | Ensures only one option can be selected |
|
|
37
|
+
| **Form Integration** | Seamless Angular form control integration |
|
|
38
|
+
| **Data Format Support** | Works with simple strings or complex objects |
|
|
39
|
+
| **Validation Ready** | Built-in validation support |
|
|
40
|
+
| **Material Design** | Consistent Material Design styling |
|
|
41
|
+
| **Accessibility** | Full ARIA support and keyboard navigation |
|
|
21
42
|
|
|
22
|
-
|
|
43
|
+
---
|
|
23
44
|
|
|
24
|
-
|
|
45
|
+
## Summary
|
|
46
|
+
|
|
47
|
+
The `radio-selection-input` library provides a traditional radio button selection component with comprehensive form integration and validation support, perfect for mutually exclusive selection scenarios in 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 { RadioSelectionInputModule } from 'radio-selection-input';
|
|
60
|
+
|
|
61
|
+
@NgModule({
|
|
62
|
+
imports: [
|
|
63
|
+
RadioSelectionInputModule
|
|
64
|
+
]
|
|
65
|
+
})
|
|
66
|
+
export class AppModule { }
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
#### 2. No Module Configuration Required
|
|
70
|
+
|
|
71
|
+
The `RadioSelectionInputModule` does not require global configuration. Components can be used immediately after module import.
|
|
72
|
+
|
|
73
|
+
### Quick Examples
|
|
74
|
+
|
|
75
|
+
#### Example 1: Basic String Array Selection
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
import { Component } from '@angular/core';
|
|
79
|
+
import { FormControl } from '@angular/forms';
|
|
80
|
+
|
|
81
|
+
@Component({
|
|
82
|
+
selector: 'app-basic-radio',
|
|
83
|
+
template: `
|
|
84
|
+
<app-radio-selection-input
|
|
85
|
+
[formControl]="selectionControl"
|
|
86
|
+
[data]="paymentMethods"
|
|
87
|
+
label="Payment Method"
|
|
88
|
+
placeholder="Choose payment method">
|
|
89
|
+
</app-radio-selection-input>
|
|
90
|
+
|
|
91
|
+
<div>Selected: {{ selectionControl.value }}</div>
|
|
92
|
+
`
|
|
93
|
+
})
|
|
94
|
+
export class BasicRadioComponent {
|
|
95
|
+
selectionControl = new FormControl();
|
|
96
|
+
|
|
97
|
+
paymentMethods = [
|
|
98
|
+
'Credit Card', 'Debit Card', 'PayPal', 'Bank Transfer', 'Cash on Delivery'
|
|
99
|
+
];
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
#### Example 2: Object Array Selection
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
import { Component } from '@angular/core';
|
|
107
|
+
import { FormControl } from '@angular/forms';
|
|
108
|
+
|
|
109
|
+
@Component({
|
|
110
|
+
selector: 'app-object-radio',
|
|
111
|
+
template: `
|
|
112
|
+
<app-radio-selection-input
|
|
113
|
+
[formControl]="userControl"
|
|
114
|
+
[data]="userOptions"
|
|
115
|
+
label="Select User"
|
|
116
|
+
placeholder="Choose a user"
|
|
117
|
+
displayField="label">
|
|
118
|
+
</app-radio-selection-input>
|
|
119
|
+
|
|
120
|
+
<div *ngIf="userControl.value" class="user-info">
|
|
121
|
+
<h4>{{ userControl.value.label }}</h4>
|
|
122
|
+
<p>{{ userControl.value.email }}</p>
|
|
123
|
+
<p>Role: {{ userControl.value.role }}</p>
|
|
124
|
+
</div>
|
|
125
|
+
`,
|
|
126
|
+
styles: [`
|
|
127
|
+
.user-info {
|
|
128
|
+
margin-top: 1rem;
|
|
129
|
+
padding: 1rem;
|
|
130
|
+
border: 1px solid #ddd;
|
|
131
|
+
border-radius: 4px;
|
|
132
|
+
background: #f9f9f9;
|
|
133
|
+
}
|
|
134
|
+
.user-info h4 {
|
|
135
|
+
margin: 0 0 0.5rem 0;
|
|
136
|
+
color: #333;
|
|
137
|
+
}
|
|
138
|
+
.user-info p {
|
|
139
|
+
margin: 0.25rem 0;
|
|
140
|
+
font-size: 0.9rem;
|
|
141
|
+
color: #666;
|
|
142
|
+
}
|
|
143
|
+
`]
|
|
144
|
+
})
|
|
145
|
+
export class ObjectRadioComponent {
|
|
146
|
+
userControl = new FormControl();
|
|
147
|
+
|
|
148
|
+
userOptions = [
|
|
149
|
+
{ value: 'user1', label: 'John Doe', email: 'john@example.com', role: 'Admin' },
|
|
150
|
+
{ value: 'user2', label: 'Jane Smith', email: 'jane@example.com', role: 'Editor' },
|
|
151
|
+
{ value: 'user3', label: 'Bob Johnson', email: 'bob@example.com', role: 'Viewer' },
|
|
152
|
+
{ value: 'user4', label: 'Alice Brown', email: 'alice@example.com', role: 'Editor' }
|
|
153
|
+
];
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
#### Example 3: Required Selection with Validation
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
import { Component } from '@angular/core';
|
|
161
|
+
import { FormControl, Validators } from '@angular/forms';
|
|
162
|
+
|
|
163
|
+
@Component({
|
|
164
|
+
selector: 'app-required-radio',
|
|
165
|
+
template: `
|
|
166
|
+
<app-radio-selection-input
|
|
167
|
+
[formControl]="sizeControl"
|
|
168
|
+
[data]="shirtSizes"
|
|
169
|
+
label="Shirt Size"
|
|
170
|
+
placeholder="Select your shirt size"
|
|
171
|
+
[required]="true">
|
|
172
|
+
</app-radio-selection-input>
|
|
173
|
+
|
|
174
|
+
<div class="errors" *ngIf="sizeControl.errors && sizeControl.touched">
|
|
175
|
+
<div *ngIf="sizeControl.hasError('required')">
|
|
176
|
+
Shirt size selection is required
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
<div>Selected Size: {{ sizeControl.value }}</div>
|
|
181
|
+
`,
|
|
182
|
+
styles: [`
|
|
183
|
+
.errors {
|
|
184
|
+
color: #f44336;
|
|
185
|
+
font-size: 0.875rem;
|
|
186
|
+
margin-top: 0.5rem;
|
|
187
|
+
}
|
|
188
|
+
`]
|
|
189
|
+
})
|
|
190
|
+
export class RequiredRadioComponent {
|
|
191
|
+
sizeControl = new FormControl('', Validators.required);
|
|
192
|
+
|
|
193
|
+
shirtSizes = ['XS', 'S', 'M', 'L', 'XL', 'XXL'];
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
#### Example 4: Radio Group with Pre-selection
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
import { Component } from '@angular/core';
|
|
201
|
+
import { FormControl } from '@angular/forms';
|
|
202
|
+
|
|
203
|
+
@Component({
|
|
204
|
+
selector: 'app-pre-selected-radio',
|
|
205
|
+
template: `
|
|
206
|
+
<app-radio-selection-input
|
|
207
|
+
[formControl]="themeControl"
|
|
208
|
+
[data]="themeOptions"
|
|
209
|
+
label="Theme Preference"
|
|
210
|
+
placeholder="Choose a theme">
|
|
211
|
+
</app-radio-selection-input>
|
|
212
|
+
|
|
213
|
+
<div class="theme-preview" [attr.data-theme]="themeControl.value">
|
|
214
|
+
<h4>Theme Preview</h4>
|
|
215
|
+
<p>This is how your selected theme would look.</p>
|
|
216
|
+
<button class="sample-button">Sample Button</button>
|
|
217
|
+
</div>
|
|
218
|
+
`,
|
|
219
|
+
styles: [`
|
|
220
|
+
.theme-preview {
|
|
221
|
+
margin-top: 1rem;
|
|
222
|
+
padding: 1rem;
|
|
223
|
+
border-radius: 8px;
|
|
224
|
+
transition: all 0.3s ease;
|
|
225
|
+
}
|
|
226
|
+
.theme-preview[data-theme="light"] {
|
|
227
|
+
background: #ffffff;
|
|
228
|
+
color: #333333;
|
|
229
|
+
border: 1px solid #e0e0e0;
|
|
230
|
+
}
|
|
231
|
+
.theme-preview[data-theme="dark"] {
|
|
232
|
+
background: #333333;
|
|
233
|
+
color: #ffffff;
|
|
234
|
+
border: 1px solid #555555;
|
|
235
|
+
}
|
|
236
|
+
.theme-preview[data-theme="auto"] {
|
|
237
|
+
background: #f5f5f5;
|
|
238
|
+
color: #666666;
|
|
239
|
+
border: 1px solid #d0d0d0;
|
|
240
|
+
}
|
|
241
|
+
.sample-button {
|
|
242
|
+
margin-top: 0.5rem;
|
|
243
|
+
padding: 0.5rem 1rem;
|
|
244
|
+
border: none;
|
|
245
|
+
border-radius: 4px;
|
|
246
|
+
cursor: pointer;
|
|
247
|
+
}
|
|
248
|
+
.theme-preview[data-theme="light"] .sample-button {
|
|
249
|
+
background: #2196f3;
|
|
250
|
+
color: white;
|
|
251
|
+
}
|
|
252
|
+
.theme-preview[data-theme="dark"] .sample-button {
|
|
253
|
+
background: #64b5f6;
|
|
254
|
+
color: #333333;
|
|
255
|
+
}
|
|
256
|
+
.theme-preview[data-theme="auto"] .sample-button {
|
|
257
|
+
background: #ff9800;
|
|
258
|
+
color: white;
|
|
259
|
+
}
|
|
260
|
+
`]
|
|
261
|
+
})
|
|
262
|
+
export class PreSelectedRadioComponent {
|
|
263
|
+
themeControl = new FormControl('light');
|
|
264
|
+
|
|
265
|
+
themeOptions = [
|
|
266
|
+
{ value: 'light', label: 'Light Theme', description: 'Bright and clean' },
|
|
267
|
+
{ value: 'dark', label: 'Dark Theme', description: 'Easy on the eyes' },
|
|
268
|
+
{ value: 'auto', label: 'Auto', description: 'Follow system preference' }
|
|
269
|
+
];
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
#### Example 5: Form Integration with Dynamic Options
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
import { Component } from '@angular/core';
|
|
277
|
+
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
|
278
|
+
|
|
279
|
+
@Component({
|
|
280
|
+
selector: 'app-dynamic-radio-form',
|
|
281
|
+
template: `
|
|
282
|
+
<form [formGroup]="dynamicForm">
|
|
283
|
+
<app-radio-selection-input
|
|
284
|
+
formControlName="plan"
|
|
285
|
+
[data]="subscriptionPlans"
|
|
286
|
+
label="Subscription Plan"
|
|
287
|
+
placeholder="Select a plan"
|
|
288
|
+
[required]="true">
|
|
289
|
+
</app-radio-selection-input>
|
|
290
|
+
|
|
291
|
+
<app-radio-selection-input
|
|
292
|
+
formControlName="billingCycle"
|
|
293
|
+
[data]="billingCycles"
|
|
294
|
+
label="Billing Cycle"
|
|
295
|
+
placeholder="Choose billing frequency"
|
|
296
|
+
[required]="true">
|
|
297
|
+
</app-radio-selection-input>
|
|
298
|
+
</form>
|
|
299
|
+
|
|
300
|
+
<div class="form-status">
|
|
301
|
+
<div>Form Valid: {{ dynamicForm.valid }}</div>
|
|
302
|
+
<div>Plan: {{ dynamicForm.get('plan')?.value }}</div>
|
|
303
|
+
<div>Billing: {{ dynamicForm.get('billingCycle')?.value }}</div>
|
|
304
|
+
</div>
|
|
305
|
+
|
|
306
|
+
<div class="summary" *ngIf="dynamicForm.valid">
|
|
307
|
+
<h4>Subscription Summary</h4>
|
|
308
|
+
<p><strong>Plan:</strong> {{ getPlanDetails(dynamicForm.get('plan')?.value)?.name }}</p>
|
|
309
|
+
<p><strong>Billing:</strong> {{ getPlanDetails(dynamicForm.get('plan')?.value)?.billing }}</p>
|
|
310
|
+
<p><strong>Price:</strong> ${{ getPlanPrice(dynamicForm.get('plan')?.value, dynamicForm.get('billingCycle')?.value) }}/month</p>
|
|
311
|
+
</div>
|
|
312
|
+
|
|
313
|
+
<div class="controls">
|
|
314
|
+
<button (click)="selectPopularPlan()">Select Popular Plan</button>
|
|
315
|
+
<button (click)="resetForm()">Reset</button>
|
|
316
|
+
<button (click)="submitForm()" [disabled]="dynamicForm.invalid">Subscribe</button>
|
|
317
|
+
</div>
|
|
318
|
+
`,
|
|
319
|
+
styles: [`
|
|
320
|
+
form {
|
|
321
|
+
display: flex;
|
|
322
|
+
flex-direction: column;
|
|
323
|
+
gap: 1.5rem;
|
|
324
|
+
max-width: 400px;
|
|
325
|
+
}
|
|
326
|
+
.form-status {
|
|
327
|
+
margin-top: 1rem;
|
|
328
|
+
padding: 1rem;
|
|
329
|
+
background: #f5f5f5;
|
|
330
|
+
border-radius: 4px;
|
|
331
|
+
font-family: monospace;
|
|
332
|
+
font-size: 0.9rem;
|
|
333
|
+
}
|
|
334
|
+
.summary {
|
|
335
|
+
margin-top: 1rem;
|
|
336
|
+
padding: 1rem;
|
|
337
|
+
border: 2px solid #4caf50;
|
|
338
|
+
border-radius: 8px;
|
|
339
|
+
background: #f1f8e9;
|
|
340
|
+
}
|
|
341
|
+
.summary h4 {
|
|
342
|
+
margin: 0 0 0.5rem 0;
|
|
343
|
+
color: #2e7d32;
|
|
344
|
+
}
|
|
345
|
+
.summary p {
|
|
346
|
+
margin: 0.25rem 0;
|
|
347
|
+
color: #388e3c;
|
|
348
|
+
}
|
|
349
|
+
.controls {
|
|
350
|
+
margin-top: 1rem;
|
|
351
|
+
display: flex;
|
|
352
|
+
gap: 0.5rem;
|
|
353
|
+
}
|
|
354
|
+
button {
|
|
355
|
+
padding: 0.5rem 1rem;
|
|
356
|
+
border: 1px solid #ccc;
|
|
357
|
+
border-radius: 4px;
|
|
358
|
+
background: white;
|
|
359
|
+
cursor: pointer;
|
|
360
|
+
}
|
|
361
|
+
button:disabled {
|
|
362
|
+
opacity: 0.5;
|
|
363
|
+
cursor: not-allowed;
|
|
364
|
+
}
|
|
365
|
+
`]
|
|
366
|
+
})
|
|
367
|
+
export class DynamicRadioFormComponent {
|
|
368
|
+
dynamicForm: FormGroup;
|
|
369
|
+
|
|
370
|
+
subscriptionPlans = [
|
|
371
|
+
{ value: 'basic', name: 'Basic Plan', price: 9.99, billing: 'Monthly' },
|
|
372
|
+
{ value: 'pro', name: 'Pro Plan', price: 19.99, billing: 'Monthly' },
|
|
373
|
+
{ value: 'enterprise', name: 'Enterprise Plan', price: 49.99, billing: 'Monthly' }
|
|
374
|
+
];
|
|
375
|
+
|
|
376
|
+
billingCycles = ['Monthly', 'Quarterly', 'Yearly'];
|
|
377
|
+
|
|
378
|
+
constructor(private fb: FormBuilder) {
|
|
379
|
+
this.dynamicForm = this.fb.group({
|
|
380
|
+
plan: ['', Validators.required],
|
|
381
|
+
billingCycle: ['', Validators.required]
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
getPlanDetails(planValue: string) {
|
|
386
|
+
return this.subscriptionPlans.find(plan => plan.value === planValue);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
getPlanPrice(planValue: string, billingCycle: string) {
|
|
390
|
+
const plan = this.getPlanDetails(planValue);
|
|
391
|
+
if (!plan) return 0;
|
|
392
|
+
|
|
393
|
+
const multipliers = {
|
|
394
|
+
'Monthly': 1,
|
|
395
|
+
'Quarterly': 0.9, // 10% discount
|
|
396
|
+
'Yearly': 0.8 // 20% discount
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
return (plan.price * (multipliers[billingCycle as keyof typeof multipliers] || 1)).toFixed(2);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
selectPopularPlan() {
|
|
403
|
+
this.dynamicForm.patchValue({
|
|
404
|
+
plan: 'pro',
|
|
405
|
+
billingCycle: 'Yearly'
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
resetForm() {
|
|
410
|
+
this.dynamicForm.reset();
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
submitForm() {
|
|
414
|
+
if (this.dynamicForm.valid) {
|
|
415
|
+
console.log('Subscription form submitted:', this.dynamicForm.value);
|
|
416
|
+
alert('Subscription created successfully!');
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
## Component API
|
|
425
|
+
|
|
426
|
+
### Inputs
|
|
427
|
+
|
|
428
|
+
| Input | Type | Description | Default |
|
|
429
|
+
| :--- | :--- | :--- | :--- |
|
|
430
|
+
| `data` | `any[] \| string[]` | Array of options to select from | (Required) |
|
|
431
|
+
| `label` | `string` | Label text for the radio group | `undefined` |
|
|
432
|
+
| `placeholder` | `string` | Placeholder text for the radio group | `undefined` |
|
|
433
|
+
| `displayField` | `string` | Property name to display for object arrays | `undefined` |
|
|
434
|
+
| `required` | `boolean` | Whether selection is required | `false` |
|
|
435
|
+
| `disabled` | `boolean` | Whether the radio group is disabled | `false` |
|
|
436
|
+
| `direction` | `'row' \| 'column'` | Layout direction for radio options | `'column'` |
|
|
437
|
+
|
|
438
|
+
### Outputs
|
|
439
|
+
|
|
440
|
+
| Output | Type | Description |
|
|
441
|
+
|--------|------|-------------|
|
|
442
|
+
| `selectionChange` | `EventEmitter<any>` | Emits selected value when selection changes |
|
|
443
|
+
|
|
444
|
+
---
|
|
445
|
+
|
|
446
|
+
## Form Integration (ControlValueAccessor)
|
|
447
|
+
|
|
448
|
+
The component implements Angular's `ControlValueAccessor` interface for seamless form integration.
|
|
449
|
+
|
|
450
|
+
### ControlValueAccessor Implementation
|
|
451
|
+
|
|
452
|
+
```typescript
|
|
453
|
+
// writeValue(value: any): void
|
|
454
|
+
// Sets the selected value
|
|
455
|
+
writeValue(value: any): void {
|
|
456
|
+
this.selectedValue = value;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// registerOnChange(fn: any): void
|
|
460
|
+
// Registers a callback for value changes
|
|
461
|
+
registerOnChange(fn: any): void {
|
|
462
|
+
this.onChange = fn;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// registerOnTouched(fn: any): void
|
|
466
|
+
// Registers a callback for touch events
|
|
467
|
+
registerOnTouched(fn: any): void {
|
|
468
|
+
this.onTouch = fn;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// setDisabledState(isDisabled: boolean): void
|
|
472
|
+
// Sets disabled state
|
|
473
|
+
setDisabledState?(isDisabled: boolean): void {
|
|
474
|
+
this.disabled = isDisabled;
|
|
475
|
+
}
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
### Form Integration Examples
|
|
479
|
+
|
|
480
|
+
#### Reactive Forms
|
|
481
|
+
|
|
482
|
+
```typescript
|
|
483
|
+
import { Component } from '@angular/core';
|
|
484
|
+
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
|
485
|
+
|
|
486
|
+
@Component({
|
|
487
|
+
selector: 'app-reactive-radio-form',
|
|
488
|
+
template: `
|
|
489
|
+
<form [formGroup]="radioForm">
|
|
490
|
+
<app-radio-selection-input
|
|
491
|
+
formControlName="gender"
|
|
492
|
+
[data]="genderOptions"
|
|
493
|
+
label="Gender"
|
|
494
|
+
placeholder="Select gender"
|
|
495
|
+
[required]="true">
|
|
496
|
+
</app-radio-selection-input>
|
|
497
|
+
|
|
498
|
+
<app-radio-selection-input
|
|
499
|
+
formControlName="preferredContact"
|
|
500
|
+
[data]="contactOptions"
|
|
501
|
+
label="Preferred Contact Method"
|
|
502
|
+
placeholder="Choose contact method">
|
|
503
|
+
</app-radio-selection-input>
|
|
504
|
+
</form>
|
|
505
|
+
`
|
|
506
|
+
})
|
|
507
|
+
export class ReactiveRadioFormComponent {
|
|
508
|
+
radioForm = new FormGroup({
|
|
509
|
+
gender: new FormControl('', Validators.required),
|
|
510
|
+
preferredContact: new FormControl('')
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
genderOptions = ['Male', 'Female', 'Other', 'Prefer not to say'];
|
|
514
|
+
contactOptions = ['Email', 'Phone', 'SMS', 'Mail'];
|
|
515
|
+
}
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
#### Template-Driven Forms
|
|
519
|
+
|
|
520
|
+
```typescript
|
|
521
|
+
import { Component } from '@angular/core';
|
|
522
|
+
|
|
523
|
+
@Component({
|
|
524
|
+
selector: 'app-template-radio-form',
|
|
525
|
+
template: `
|
|
526
|
+
<app-radio-selection-input
|
|
527
|
+
[(ngModel)]="selectedOption"
|
|
528
|
+
name="templateRadio"
|
|
529
|
+
[data]="templateOptions"
|
|
530
|
+
label="Template Radio Selection"
|
|
531
|
+
placeholder="Choose an option"
|
|
532
|
+
required>
|
|
533
|
+
</app-radio-selection-input>
|
|
534
|
+
|
|
535
|
+
<div *ngIf="selectedOption">
|
|
536
|
+
Selected: {{ selectedOption }}
|
|
537
|
+
</div>
|
|
538
|
+
`
|
|
539
|
+
})
|
|
540
|
+
export class TemplateRadioFormComponent {
|
|
541
|
+
selectedOption = '';
|
|
542
|
+
|
|
543
|
+
templateOptions = ['Option A', 'Option B', 'Option C', 'Option D'];
|
|
544
|
+
}
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
---
|
|
548
|
+
|
|
549
|
+
## Model Structures
|
|
550
|
+
|
|
551
|
+
### Radio Option Interface
|
|
552
|
+
|
|
553
|
+
```typescript
|
|
554
|
+
export interface RadioOptionInterface {
|
|
555
|
+
value: any; // The value to be selected/returned
|
|
556
|
+
label?: string; // Optional display label
|
|
557
|
+
disabled?: boolean; // Whether this option is disabled
|
|
558
|
+
selected?: boolean; // Whether this option is pre-selected
|
|
559
|
+
description?: string; // Optional description text
|
|
560
|
+
}
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
### Radio Option Class
|
|
564
|
+
|
|
565
|
+
```typescript
|
|
566
|
+
export class RadioOption implements RadioOptionInterface {
|
|
567
|
+
constructor(
|
|
568
|
+
public value: any,
|
|
569
|
+
public label?: string,
|
|
570
|
+
public disabled?: boolean = false,
|
|
571
|
+
public selected?: boolean = false,
|
|
572
|
+
public description?: string,
|
|
573
|
+
) {}
|
|
574
|
+
|
|
575
|
+
static adapt(item?: any): RadioOption {
|
|
576
|
+
return new RadioOption(
|
|
577
|
+
item?.value ?? item,
|
|
578
|
+
item?.label,
|
|
579
|
+
item?.disabled || false,
|
|
580
|
+
item?.selected || false,
|
|
581
|
+
item?.description,
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
### Usage Examples
|
|
588
|
+
|
|
589
|
+
```typescript
|
|
590
|
+
// String array data (automatically converted)
|
|
591
|
+
const stringData = ['Option 1', 'Option 2', 'Option 3'];
|
|
592
|
+
|
|
593
|
+
// Object array data
|
|
594
|
+
const objectData = [
|
|
595
|
+
{ value: 'opt1', label: 'Option 1', disabled: false, selected: false, description: 'First option' },
|
|
596
|
+
{ value: 'opt2', label: 'Option 2', disabled: true, selected: false, description: 'Second option (disabled)' },
|
|
597
|
+
{ value: 'opt3', label: 'Option 3', disabled: false, selected: true, description: 'Third option (pre-selected)' }
|
|
598
|
+
];
|
|
599
|
+
|
|
600
|
+
// Manual RadioOption creation
|
|
601
|
+
const manualOptions = [
|
|
602
|
+
new RadioOption('value1', 'Label 1', false, false, 'Description 1'),
|
|
603
|
+
new RadioOption('value2', 'Label 2', true, false, 'Description 2'),
|
|
604
|
+
new RadioOption('value3', 'Label 3', false, true, 'Description 3')
|
|
605
|
+
];
|
|
606
|
+
|
|
607
|
+
// Using adapt method for flexible data
|
|
608
|
+
const adaptedOptions = [
|
|
609
|
+
RadioOption.adapt({ value: 'item1', label: 'Item 1', selected: true }),
|
|
610
|
+
RadioOption.adapt({ value: 'item2', label: 'Item 2', disabled: true }),
|
|
611
|
+
RadioOption.adapt('item3') // String fallback
|
|
612
|
+
];
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
---
|
|
616
|
+
|
|
617
|
+
## Validation System
|
|
618
|
+
|
|
619
|
+
### Built-in Validation
|
|
620
|
+
|
|
621
|
+
The component supports Angular's validation system:
|
|
622
|
+
|
|
623
|
+
#### Required Validation
|
|
624
|
+
```typescript
|
|
625
|
+
// Marks field as required
|
|
626
|
+
[required]="true" // Selection must be made
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
#### Custom Validators
|
|
630
|
+
```typescript
|
|
631
|
+
// Add custom validation logic
|
|
632
|
+
const customForm = new FormGroup({
|
|
633
|
+
selection: new FormControl('', [
|
|
634
|
+
Validators.required,
|
|
635
|
+
customValidator
|
|
636
|
+
])
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
// Custom validator example
|
|
640
|
+
function customValidator(control: AbstractControl): ValidationErrors | null {
|
|
641
|
+
const value = control.value;
|
|
642
|
+
if (value === 'blocked-option') {
|
|
643
|
+
return { blockedOption: true };
|
|
644
|
+
}
|
|
645
|
+
return null;
|
|
646
|
+
}
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
### Validation Error Types
|
|
650
|
+
|
|
651
|
+
| Error Type | Condition | Description |
|
|
652
|
+
|------------|-----------|-------------|
|
|
653
|
+
| `required` | Control has required validator and no selection | Control is required but empty |
|
|
654
|
+
| `custom` | Custom validation logic fails | User-defined validation errors |
|
|
655
|
+
|
|
656
|
+
---
|
|
657
|
+
|
|
658
|
+
## Module Configuration
|
|
659
|
+
|
|
660
|
+
### RadioSelectionInputModule
|
|
661
|
+
|
|
662
|
+
**No Global Configuration Required**
|
|
663
|
+
|
|
664
|
+
The `RadioSelectionInputModule` does not provide a `forRoot()` method or global configuration options. All configuration is done at the component level through input properties.
|
|
665
|
+
|
|
666
|
+
#### Dependencies
|
|
667
|
+
|
|
668
|
+
- **@angular/core**: Core Angular functionality
|
|
669
|
+
- **@angular/forms**: Form control integration (FormsModule, ReactiveFormsModule)
|
|
670
|
+
- **@angular/material**: Material Design components (MatRadioModule, MatFormFieldModule, etc.)
|
|
671
|
+
|
|
672
|
+
---
|
|
673
|
+
|
|
674
|
+
## Advanced Usage Patterns
|
|
675
|
+
|
|
676
|
+
### Conditional Options
|
|
677
|
+
|
|
678
|
+
```typescript
|
|
679
|
+
import { Component } from '@angular/core';
|
|
680
|
+
import { FormControl } from '@angular/forms';
|
|
681
|
+
|
|
682
|
+
@Component({
|
|
683
|
+
selector: 'app-conditional-radio',
|
|
684
|
+
template: `
|
|
685
|
+
<app-radio-selection-input
|
|
686
|
+
[formControl]="accountTypeControl"
|
|
687
|
+
[data]="accountTypes"
|
|
688
|
+
label="Account Type"
|
|
689
|
+
placeholder="Select account type">
|
|
690
|
+
</app-radio-selection-input>
|
|
691
|
+
|
|
692
|
+
<app-radio-selection-input
|
|
693
|
+
[formControl]="planControl"
|
|
694
|
+
[data]="availablePlans"
|
|
695
|
+
label="Subscription Plan"
|
|
696
|
+
placeholder="Choose a plan"
|
|
697
|
+
[disabled]="!accountTypeControl.value">
|
|
698
|
+
</app-radio-selection-input>
|
|
699
|
+
|
|
700
|
+
<div class="plan-details" *ngIf="planControl.value">
|
|
701
|
+
<h4>Plan Details</h4>
|
|
702
|
+
<p>{{ getPlanDetails(planControl.value)?.description }}</p>
|
|
703
|
+
<p><strong>Price:</strong> ${{ getPlanDetails(planControl.value)?.price }}/month</p>
|
|
704
|
+
</div>
|
|
705
|
+
`
|
|
706
|
+
})
|
|
707
|
+
export class ConditionalRadioComponent {
|
|
708
|
+
accountTypeControl = new FormControl();
|
|
709
|
+
planControl = new FormControl();
|
|
710
|
+
|
|
711
|
+
accountTypes = [
|
|
712
|
+
{ value: 'individual', label: 'Individual', description: 'Personal use' },
|
|
713
|
+
{ value: 'business', label: 'Business', description: 'Commercial use' },
|
|
714
|
+
{ value: 'enterprise', label: 'Enterprise', description: 'Large organizations' }
|
|
715
|
+
];
|
|
716
|
+
|
|
717
|
+
availablePlans: any[] = [];
|
|
718
|
+
|
|
719
|
+
constructor() {
|
|
720
|
+
this.accountTypeControl.valueChanges.subscribe(accountType => {
|
|
721
|
+
this.updateAvailablePlans(accountType);
|
|
722
|
+
this.planControl.reset();
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
updateAvailablePlans(accountType: string) {
|
|
727
|
+
const planMap: { [key: string]: any[] } = {
|
|
728
|
+
'individual': [
|
|
729
|
+
{ value: 'basic', label: 'Basic', description: 'Essential features', price: 9.99 },
|
|
730
|
+
{ value: 'premium', label: 'Premium', description: 'Advanced features', price: 19.99 }
|
|
731
|
+
],
|
|
732
|
+
'business': [
|
|
733
|
+
{ value: 'starter', label: 'Starter', description: 'Business basics', price: 29.99 },
|
|
734
|
+
{ value: 'professional', label: 'Professional', description: 'Full business suite', price: 59.99 },
|
|
735
|
+
{ value: 'enterprise', label: 'Enterprise', description: 'Complete solution', price: 99.99 }
|
|
736
|
+
],
|
|
737
|
+
'enterprise': [
|
|
738
|
+
{ value: 'custom', label: 'Custom', description: 'Tailored solution', price: 0 }
|
|
739
|
+
]
|
|
740
|
+
};
|
|
741
|
+
|
|
742
|
+
this.availablePlans = planMap[accountType] || [];
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
getPlanDetails(planValue: string) {
|
|
746
|
+
return this.availablePlans.find(plan => plan.value === planValue);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
```
|
|
750
|
+
|
|
751
|
+
### Accessibility Enhancement
|
|
752
|
+
|
|
753
|
+
```typescript
|
|
754
|
+
import { Component } from '@angular/core';
|
|
755
|
+
|
|
756
|
+
@Component({
|
|
757
|
+
selector: 'app-accessible-radio',
|
|
758
|
+
template: `
|
|
759
|
+
<app-radio-selection-input
|
|
760
|
+
[formControl]="accessibilityControl"
|
|
761
|
+
[data]="accessibilityOptions"
|
|
762
|
+
label="Accessibility Preference"
|
|
763
|
+
placeholder="Choose accessibility option"
|
|
764
|
+
[ariaLabel]="'Accessibility settings for screen readers'"
|
|
765
|
+
[ariaDescribedBy]="'accessibility-description'">
|
|
766
|
+
</app-radio-selection-input>
|
|
767
|
+
|
|
768
|
+
<div id="accessibility-description" class="sr-only">
|
|
769
|
+
These options control accessibility features like screen reader support and keyboard navigation.
|
|
770
|
+
</div>
|
|
771
|
+
|
|
772
|
+
<div class="accessibility-info" *ngIf="accessibilityControl.value">
|
|
773
|
+
<h4>Accessibility Information</h4>
|
|
774
|
+
<p>{{ getAccessibilityInfo(accessibilityControl.value) }}</p>
|
|
775
|
+
</div>
|
|
776
|
+
`,
|
|
777
|
+
styles: [`
|
|
778
|
+
.sr-only {
|
|
779
|
+
position: absolute;
|
|
780
|
+
width: 1px;
|
|
781
|
+
height: 1px;
|
|
782
|
+
padding: 0;
|
|
783
|
+
margin: -1px;
|
|
784
|
+
overflow: hidden;
|
|
785
|
+
clip: rect(0, 0, 0, 0);
|
|
786
|
+
white-space: nowrap;
|
|
787
|
+
border: 0;
|
|
788
|
+
}
|
|
789
|
+
.accessibility-info {
|
|
790
|
+
margin-top: 1rem;
|
|
791
|
+
padding: 1rem;
|
|
792
|
+
background: #e3f2fd;
|
|
793
|
+
border: 1px solid #2196f3;
|
|
794
|
+
border-radius: 4px;
|
|
795
|
+
}
|
|
796
|
+
`]
|
|
797
|
+
})
|
|
798
|
+
export class AccessibleRadioComponent {
|
|
799
|
+
accessibilityControl = new FormControl();
|
|
800
|
+
|
|
801
|
+
accessibilityOptions = [
|
|
802
|
+
{
|
|
803
|
+
value: 'high-contrast',
|
|
804
|
+
label: 'High Contrast Mode',
|
|
805
|
+
description: 'Enhanced colors for better visibility',
|
|
806
|
+
info: 'Increases color contrast and uses thicker borders for better visibility.'
|
|
807
|
+
},
|
|
808
|
+
{
|
|
809
|
+
value: 'large-text',
|
|
810
|
+
label: 'Large Text',
|
|
811
|
+
description: 'Bigger fonts throughout the interface',
|
|
812
|
+
info: 'Increases font sizes for easier reading.'
|
|
813
|
+
},
|
|
814
|
+
{
|
|
815
|
+
value: 'reduced-motion',
|
|
816
|
+
label: 'Reduced Motion',
|
|
817
|
+
description: 'Minimizes animations and transitions',
|
|
818
|
+
info: 'Disables non-essential animations to reduce motion sensitivity.'
|
|
819
|
+
},
|
|
820
|
+
{
|
|
821
|
+
value: 'screen-reader',
|
|
822
|
+
label: 'Screen Reader Optimized',
|
|
823
|
+
description: 'Enhanced screen reader support',
|
|
824
|
+
info: 'Provides additional labels and descriptions for screen readers.'
|
|
825
|
+
}
|
|
826
|
+
];
|
|
827
|
+
|
|
828
|
+
getAccessibilityInfo(value: string) {
|
|
829
|
+
const option = this.accessibilityOptions.find(opt => opt.value === value);
|
|
830
|
+
return option?.info || '';
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
```
|
|
834
|
+
|
|
835
|
+
### Performance Optimization
|
|
836
|
+
|
|
837
|
+
```typescript
|
|
838
|
+
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
|
839
|
+
import { FormControl } from '@angular/forms';
|
|
840
|
+
|
|
841
|
+
@Component({
|
|
842
|
+
selector: 'app-optimized-radio',
|
|
843
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
844
|
+
template: `
|
|
845
|
+
<app-radio-selection-input
|
|
846
|
+
[formControl]="optimizedControl"
|
|
847
|
+
[data]="largeDataset"
|
|
848
|
+
label="Large Dataset Selection"
|
|
849
|
+
placeholder="Select from large dataset..."
|
|
850
|
+
[filterFunction]="customFilter">
|
|
851
|
+
</app-radio-selection-input>
|
|
852
|
+
`
|
|
853
|
+
})
|
|
854
|
+
export class OptimizedRadioComponent {
|
|
855
|
+
optimizedControl = new FormControl();
|
|
856
|
+
|
|
857
|
+
// Large dataset with efficient filtering
|
|
858
|
+
largeDataset = Array.from({ length: 1000 }, (_, i) => ({
|
|
859
|
+
id: i,
|
|
860
|
+
name: `Option ${i}`,
|
|
861
|
+
category: `Category ${Math.floor(i / 100)}`,
|
|
862
|
+
disabled: i % 10 === 0, // Every 10th option is disabled
|
|
863
|
+
selected: i === 0 // First option is pre-selected
|
|
864
|
+
}));
|
|
865
|
+
|
|
866
|
+
customFilter(data: any[], filterText: string): any[] {
|
|
867
|
+
if (!filterText) return data;
|
|
868
|
+
|
|
869
|
+
const lowerFilter = filterText.toLowerCase();
|
|
870
|
+
return data.filter(item =>
|
|
871
|
+
item.name.toLowerCase().includes(lowerFilter) ||
|
|
872
|
+
item.category.toLowerCase().includes(lowerFilter)
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
```
|
|
877
|
+
|
|
878
|
+
---
|
|
879
|
+
|
|
880
|
+
## Integration Examples
|
|
881
|
+
|
|
882
|
+
### With Other UI Components
|
|
883
|
+
|
|
884
|
+
```typescript
|
|
885
|
+
import { Component } from '@angular/core';
|
|
886
|
+
|
|
887
|
+
@Component({
|
|
888
|
+
selector: 'app-integrated-radio',
|
|
889
|
+
template: `
|
|
890
|
+
<app-display-card title="Shipping Method">
|
|
891
|
+
<app-radio-selection-input
|
|
892
|
+
[formControl]="shippingControl"
|
|
893
|
+
[data]="shippingMethods"
|
|
894
|
+
label="Shipping Method"
|
|
895
|
+
placeholder="Choose shipping option"
|
|
896
|
+
displayField="label">
|
|
897
|
+
</app-radio-selection-input>
|
|
898
|
+
|
|
899
|
+
<div *ngIf="shippingControl.value" class="shipping-details">
|
|
900
|
+
<h4>{{ shippingControl.value.label }}</h4>
|
|
901
|
+
<p>{{ shippingControl.value.description }}</p>
|
|
902
|
+
<p><strong>Cost:</strong> {{ shippingControl.value.cost | currency }}</p>
|
|
903
|
+
<p><strong>Estimated Delivery:</strong> {{ shippingControl.value.estimatedDays }} business days</p>
|
|
904
|
+
</div>
|
|
905
|
+
</app-display-card>
|
|
906
|
+
`
|
|
907
|
+
})
|
|
908
|
+
export class IntegratedRadioComponent {
|
|
909
|
+
shippingControl = new FormControl();
|
|
910
|
+
|
|
911
|
+
shippingMethods = [
|
|
912
|
+
{
|
|
913
|
+
value: 'standard',
|
|
914
|
+
label: 'Standard Shipping',
|
|
915
|
+
description: 'Regular delivery service',
|
|
916
|
+
cost: 5.99,
|
|
917
|
+
estimatedDays: '5-7'
|
|
918
|
+
},
|
|
919
|
+
{
|
|
920
|
+
value: 'express',
|
|
921
|
+
label: 'Express Shipping',
|
|
922
|
+
description: 'Fast delivery service',
|
|
923
|
+
cost: 12.99,
|
|
924
|
+
estimatedDays: '2-3'
|
|
925
|
+
},
|
|
926
|
+
{
|
|
927
|
+
value: 'overnight',
|
|
928
|
+
label: 'Overnight Shipping',
|
|
929
|
+
description: 'Next day delivery',
|
|
930
|
+
cost: 24.99,
|
|
931
|
+
estimatedDays: '1'
|
|
932
|
+
},
|
|
933
|
+
{
|
|
934
|
+
value: 'pickup',
|
|
935
|
+
label: 'Store Pickup',
|
|
936
|
+
description: 'Pick up at local store',
|
|
937
|
+
cost: 0,
|
|
938
|
+
estimatedDays: '1-2'
|
|
939
|
+
}
|
|
940
|
+
];
|
|
941
|
+
}
|
|
942
|
+
```
|
|
943
|
+
|
|
944
|
+
### With State Management
|
|
945
|
+
|
|
946
|
+
```typescript
|
|
947
|
+
import { Component } from '@angular/core';
|
|
948
|
+
import { Store } from '@ngrx/store';
|
|
949
|
+
|
|
950
|
+
@Component({
|
|
951
|
+
selector: 'app-state-radio',
|
|
952
|
+
template: `
|
|
953
|
+
<app-radio-selection-input
|
|
954
|
+
[formControl]="currencyControl"
|
|
955
|
+
[data]="currencies$ | async"
|
|
956
|
+
label="Preferred Currency"
|
|
957
|
+
placeholder="Select currency"
|
|
958
|
+
(selectionChange)="onCurrencySelected($event)">
|
|
959
|
+
</app-radio-selection-input>
|
|
960
|
+
`
|
|
961
|
+
})
|
|
962
|
+
export class StateRadioComponent {
|
|
963
|
+
currencyControl = new FormControl();
|
|
964
|
+
currencies$ = this.store.select(state => state.currencies);
|
|
965
|
+
|
|
966
|
+
constructor(private store: Store) {}
|
|
967
|
+
|
|
968
|
+
onCurrencySelected(currency: any) {
|
|
969
|
+
this.store.dispatch(selectCurrency({ currency }));
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
```
|
|
973
|
+
|
|
974
|
+
---
|
|
975
|
+
|
|
976
|
+
## Testing
|
|
977
|
+
|
|
978
|
+
### Unit Testing Example
|
|
979
|
+
|
|
980
|
+
```typescript
|
|
981
|
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
982
|
+
import { RadioSelectionInputComponent } from './radio-selection-input.component';
|
|
983
|
+
import { ReactiveFormsModule } from '@angular/forms';
|
|
984
|
+
|
|
985
|
+
describe('RadioSelectionInputComponent', () => {
|
|
986
|
+
let component: RadioSelectionInputComponent;
|
|
987
|
+
let fixture: ComponentFixture<RadioSelectionInputComponent>;
|
|
988
|
+
|
|
989
|
+
beforeEach(async () => {
|
|
990
|
+
await TestBed.configureTestingModule({
|
|
991
|
+
declarations: [ RadioSelectionInputComponent ],
|
|
992
|
+
imports: [ ReactiveFormsModule ]
|
|
993
|
+
}).compileComponents();
|
|
994
|
+
|
|
995
|
+
fixture = TestBed.createComponent(RadioSelectionInputComponent);
|
|
996
|
+
component = fixture.componentInstance;
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
it('should create', () => {
|
|
1000
|
+
expect(component).toBeTruthy();
|
|
1001
|
+
});
|
|
1002
|
+
|
|
1003
|
+
it('should display radio options from data input', () => {
|
|
1004
|
+
component.data = ['Option 1', 'Option 2', 'Option 3'];
|
|
1005
|
+
fixture.detectChanges();
|
|
1006
|
+
|
|
1007
|
+
const compiled = fixture.nativeElement;
|
|
1008
|
+
const radioInputs = compiled.querySelectorAll('input[type="radio"]');
|
|
1009
|
+
expect(radioInputs.length).toBe(3);
|
|
1010
|
+
});
|
|
1011
|
+
|
|
1012
|
+
it('should handle single selection (radio behavior)', () => {
|
|
1013
|
+
component.data = ['Option 1', 'Option 2', 'Option 3'];
|
|
1014
|
+
fixture.detectChanges();
|
|
1015
|
+
|
|
1016
|
+
const compiled = fixture.nativeElement;
|
|
1017
|
+
const radioInputs = compiled.querySelectorAll('input[type="radio"]');
|
|
1018
|
+
|
|
1019
|
+
// Select first option
|
|
1020
|
+
radioInputs[0].click();
|
|
1021
|
+
fixture.detectChanges();
|
|
1022
|
+
expect(component.selectedValue).toBe('Option 1');
|
|
1023
|
+
|
|
1024
|
+
// Select second option (should deselect first)
|
|
1025
|
+
radioInputs[1].click();
|
|
1026
|
+
fixture.detectChanges();
|
|
1027
|
+
expect(component.selectedValue).toBe('Option 2');
|
|
1028
|
+
expect(radioInputs[0].checked).toBe(false);
|
|
1029
|
+
expect(radioInputs[1].checked).toBe(true);
|
|
1030
|
+
});
|
|
1031
|
+
|
|
1032
|
+
it('should emit selection changes', () => {
|
|
1033
|
+
spyOn(component.selectionChange, 'emit');
|
|
1034
|
+
|
|
1035
|
+
component.onSelectionChange('Option 1');
|
|
1036
|
+
|
|
1037
|
+
expect(component.selectionChange.emit).toHaveBeenCalledWith('Option 1');
|
|
1038
|
+
});
|
|
1039
|
+
|
|
1040
|
+
it('should handle form control integration', () => {
|
|
1041
|
+
const formControl = new FormControl();
|
|
1042
|
+
component.formControl = formControl;
|
|
1043
|
+
|
|
1044
|
+
spyOn(formControl, 'setValue');
|
|
1045
|
+
component.writeValue('Test Value');
|
|
1046
|
+
|
|
1047
|
+
expect(formControl.setValue).toHaveBeenCalledWith('Test Value');
|
|
1048
|
+
});
|
|
1049
|
+
});
|
|
1050
|
+
```
|
|
1051
|
+
|
|
1052
|
+
---
|
|
1053
|
+
|
|
1054
|
+
## Troubleshooting
|
|
1055
|
+
|
|
1056
|
+
### Common Issues
|
|
1057
|
+
|
|
1058
|
+
1. **Form control not working**: Ensure ReactiveFormsModule is imported
|
|
1059
|
+
2. **Selection not updating**: Check that data format matches expected structure
|
|
1060
|
+
3. **Object display issues**: Set `displayField` property for object arrays
|
|
1061
|
+
4. **Multiple selections**: Radio buttons only support single selection by design
|
|
1062
|
+
5. **Styling issues**: Verify Material theme is properly configured
|
|
1063
|
+
6. **Validation not working**: Ensure validators are properly configured
|
|
1064
|
+
|
|
1065
|
+
### Debug Mode
|
|
1066
|
+
|
|
1067
|
+
```typescript
|
|
1068
|
+
@Component({
|
|
1069
|
+
template: `
|
|
1070
|
+
<div class="debug-info">
|
|
1071
|
+
Form Control Value: {{ formControl.value | json }}<br>
|
|
1072
|
+
Form Control Valid: {{ formControl.valid }}<br>
|
|
1073
|
+
Form Control Errors: {{ formControl.errors | json }}<br>
|
|
1074
|
+
Data Length: {{ data?.length || 0 }}<br>
|
|
1075
|
+
Selected Value: {{ selectedValue | json }}<br>
|
|
1076
|
+
Required: {{ required }}
|
|
1077
|
+
</div>
|
|
1078
|
+
|
|
1079
|
+
<app-radio-selection-input
|
|
1080
|
+
[formControl]="formControl"
|
|
1081
|
+
[data]="data"
|
|
1082
|
+
[required]="required"
|
|
1083
|
+
(selectionChange)="onSelectionChange($event)">
|
|
1084
|
+
</app-radio-selection-input>
|
|
1085
|
+
`
|
|
1086
|
+
})
|
|
1087
|
+
export class DebugRadioComponent {
|
|
1088
|
+
formControl = new FormControl();
|
|
1089
|
+
data: any[] = [];
|
|
1090
|
+
selectedValue: any = null;
|
|
1091
|
+
required = false;
|
|
1092
|
+
|
|
1093
|
+
constructor() {
|
|
1094
|
+
this.formControl.valueChanges.subscribe(value => {
|
|
1095
|
+
console.log('Radio selection value changed:', value);
|
|
1096
|
+
});
|
|
1097
|
+
|
|
1098
|
+
this.formControl.statusChanges.subscribe(status => {
|
|
1099
|
+
console.log('Form control status:', status);
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
onSelectionChange(selection: any) {
|
|
1104
|
+
this.selectedValue = selection;
|
|
1105
|
+
console.log('Selection changed:', selection);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
```
|
|
1109
|
+
|
|
1110
|
+
### Performance Monitoring
|
|
1111
|
+
|
|
1112
|
+
```typescript
|
|
1113
|
+
@Component({
|
|
1114
|
+
template: `
|
|
1115
|
+
<div class="performance-info">
|
|
1116
|
+
Data Size: {{ data?.length || 0 }} options<br>
|
|
1117
|
+
Render Time: {{ renderTime }}ms<br>
|
|
1118
|
+
Selection Operations: {{ selectionOperationCount }}
|
|
1119
|
+
</div>
|
|
1120
|
+
|
|
1121
|
+
<app-radio-selection-input
|
|
1122
|
+
[formControl]="formControl"
|
|
1123
|
+
[data]="data"
|
|
1124
|
+
(selectionChange)="onSelectionChange($event)">
|
|
1125
|
+
</app-radio-selection-input>
|
|
1126
|
+
`
|
|
1127
|
+
})
|
|
1128
|
+
export class PerformanceDebugComponent {
|
|
1129
|
+
formControl = new FormControl();
|
|
1130
|
+
data: any[] = [];
|
|
1131
|
+
renderTime = 0;
|
|
1132
|
+
selectionOperationCount = 0;
|
|
1133
|
+
|
|
1134
|
+
onSelectionChange(selection: any) {
|
|
1135
|
+
const start = performance.now();
|
|
1136
|
+
|
|
1137
|
+
// Perform selection operation
|
|
1138
|
+
this.processSelection(selection);
|
|
1139
|
+
|
|
1140
|
+
const end = performance.now();
|
|
1141
|
+
this.renderTime = end - start;
|
|
1142
|
+
this.selectionOperationCount++;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
private processSelection(selection: any) {
|
|
1146
|
+
// Your selection processing logic
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
```
|
|
1150
|
+
|
|
1151
|
+
---
|
|
1152
|
+
|
|
1153
|
+
## Accessibility Features
|
|
1154
|
+
|
|
1155
|
+
### ARIA Support
|
|
1156
|
+
|
|
1157
|
+
- Radio buttons include proper ARIA roles and labels
|
|
1158
|
+
- Group labeling through label input property
|
|
1159
|
+
- Keyboard navigation is fully supported (Tab, Arrow keys, Space, Enter)
|
|
1160
|
+
- Screen reader friendly with appropriate descriptions
|
|
1161
|
+
- Focus management for keyboard users
|
|
1162
|
+
- Proper radio button grouping
|
|
1163
|
+
|
|
1164
|
+
### Keyboard Navigation
|
|
1165
|
+
|
|
1166
|
+
| Key | Action |
|
|
1167
|
+
|-----|--------|
|
|
1168
|
+
| `Tab` | Navigate to radio group |
|
|
1169
|
+
| `Arrow Keys` | Navigate between radio options |
|
|
1170
|
+
| `Space` | Select radio option |
|
|
1171
|
+
| `Enter` | Select radio option |
|
|
1172
|
+
|
|
1173
|
+
### Best Practices
|
|
1174
|
+
|
|
1175
|
+
1. **Provide meaningful labels** for all radio options
|
|
1176
|
+
2. **Use descriptive text** for better accessibility
|
|
1177
|
+
3. **Consider grouping** related radio options
|
|
1178
|
+
4. **Test with screen readers** to ensure proper announcements
|
|
1179
|
+
5. **Maintain logical tab order** for keyboard navigation
|
|
1180
|
+
6. **Use consistent styling** for better visual accessibility
|