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 CHANGED
@@ -1,24 +1,1180 @@
1
- # RadioSelectionInput
1
+ # Radio Selection Input Component
2
2
 
3
- This library was generated with [Angular CLI](https://github.com/angular/angular-cli) version 15.2.0.
3
+ ## Overview
4
4
 
5
- ## Code scaffolding
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
- Run `ng generate component component-name --project radio-selection-input` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module --project radio-selection-input`.
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
- ## Build
9
+ #### 🔘 Radio Button Selection Interface
11
10
 
12
- Run `ng build radio-selection-input` to build the project. The build artifacts will be stored in the `dist/` directory.
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
- ## Publishing
20
+ #### 🔧 Features
15
21
 
16
- After building your library with `ng build radio-selection-input`, go to the dist folder `cd dist/radio-selection-input` and run `npm publish`.
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
- ## Running unit tests
32
+ ### Key Benefits
19
33
 
20
- Run `ng test radio-selection-input` to execute the unit tests via [Karma](https://karma-runner.github.io).
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
- ## Further help
43
+ ---
23
44
 
24
- To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.
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