range-selection-input 15.0.2 → 15.0.3
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 +1128 -13
- package/fesm2022/range-selection-input.mjs +24 -25
- package/fesm2022/range-selection-input.mjs.map +1 -1
- package/package.json +12 -5
- package/range-selection-input-15.0.3.tgz +0 -0
- package/types/range-selection-input.d.ts +84 -0
- package/esm2022/lib/input-slider/range-selection-input.component.mjs +0 -144
- package/esm2022/lib/range-demo/range-demo.component.mjs +0 -71
- package/esm2022/lib/range-selection-input.module.mjs +0 -60
- package/esm2022/public-api.mjs +0 -7
- package/esm2022/range-selection-input.mjs +0 -5
- package/index.d.ts +0 -5
- package/lib/input-slider/range-selection-input.component.d.ts +0 -42
- package/lib/range-demo/range-demo.component.d.ts +0 -27
- package/lib/range-selection-input.module.d.ts +0 -16
- package/public-api.d.ts +0 -3
- package/range-selection-input-15.0.2.tgz +0 -0
package/README.md
CHANGED
|
@@ -1,24 +1,1139 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Range Selection Input Component
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## Overview
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
The `range-selection-input` library provides a Material Design slider component that allows users to select numeric values or ranges through an intuitive draggable interface. It supports both single value sliders and dual-thumb range selection, configurable min/max values, step divisions, tick marks, and display formatting. Built with Angular Material slider components and implementing `ControlValueAccessor` for seamless form integration.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
> Note: Don't forget to add `--project range-selection-input` or else it will be added to the default project in your `angular.json` file.
|
|
7
|
+
### Core Capabilities
|
|
9
8
|
|
|
10
|
-
|
|
9
|
+
#### 🎚️ Numeric Range Selection Interface
|
|
11
10
|
|
|
12
|
-
|
|
11
|
+
- **Single Value Selection**: Select one numeric value using a single thumb slider
|
|
12
|
+
- **Dual Range Selection**: Select a range using dual thumbs with minimum and maximum values
|
|
13
|
+
- **Configurable Range**: Set minimum, maximum, and step values
|
|
14
|
+
- **Tick Mark Support**: Optional tick marks for discrete value selection
|
|
15
|
+
- **Display Formatting**: Customizable value display with formatting functions
|
|
16
|
+
- **Form Control Integration**: Implements `ControlValueAccessor` for reactive forms
|
|
17
|
+
- **Validation Support**: Native Angular validation system compatibility
|
|
18
|
+
- **Material Design**: Built on Angular Material slider foundation
|
|
13
19
|
|
|
14
|
-
|
|
20
|
+
#### 🔧 Features
|
|
15
21
|
|
|
16
|
-
|
|
22
|
+
✅ **ControlValueAccessor Implementation** - Works with Angular forms
|
|
23
|
+
✅ **Material Design Integration** - Uses Angular Material components
|
|
24
|
+
✅ **Single & Range Selection** - Flexible selection modes
|
|
25
|
+
✅ **Configurable Steps** - Set increment/decrement values
|
|
26
|
+
✅ **Tick Marks** - Discrete value selection support
|
|
27
|
+
✅ **Value Formatting** - Custom display formatting
|
|
28
|
+
✅ **Form Validation** - Native validation integration
|
|
29
|
+
✅ **Disabled State** - Disable slider interaction
|
|
30
|
+
✅ **Display Functions** - Custom value presentation
|
|
17
31
|
|
|
18
|
-
|
|
32
|
+
### Key Benefits
|
|
19
33
|
|
|
20
|
-
|
|
34
|
+
| Feature | Description |
|
|
35
|
+
|---------|-------------|
|
|
36
|
+
| **Intuitive Selection** | Drag-based numeric value selection |
|
|
37
|
+
| **Range Support** | Select minimum and maximum values simultaneously |
|
|
38
|
+
| **Precise Control** | Configurable step values for precise selection |
|
|
39
|
+
| **Visual Feedback** | Tick marks and value display |
|
|
40
|
+
| **Form Integration** | Seamless Angular form control integration |
|
|
41
|
+
| **Flexible Formatting** | Custom value display formatting |
|
|
21
42
|
|
|
22
|
-
|
|
43
|
+
---
|
|
23
44
|
|
|
24
|
-
|
|
45
|
+
## Summary
|
|
46
|
+
|
|
47
|
+
The `range-selection-input` library provides an intuitive numeric selection component with support for both single values and ranges, making it ideal for settings like price ranges, quantity selections, or any scenario requiring precise numeric input through a visual interface.
|
|
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 { RangeSelectionInputModule } from 'range-selection-input';
|
|
60
|
+
|
|
61
|
+
@NgModule({
|
|
62
|
+
imports: [
|
|
63
|
+
RangeSelectionInputModule
|
|
64
|
+
]
|
|
65
|
+
})
|
|
66
|
+
export class AppModule { }
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
#### 2. No Module Configuration Required
|
|
70
|
+
|
|
71
|
+
The `RangeSelectionInputModule` does not require global configuration. Components can be used immediately after module import.
|
|
72
|
+
|
|
73
|
+
### Quick Examples
|
|
74
|
+
|
|
75
|
+
#### Example 1: Basic Single Value Slider
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
import { Component } from '@angular/core';
|
|
79
|
+
import { FormControl } from '@angular/forms';
|
|
80
|
+
|
|
81
|
+
@Component({
|
|
82
|
+
selector: 'app-basic-range',
|
|
83
|
+
template: `
|
|
84
|
+
<app-range-selection-input
|
|
85
|
+
[formControl]="volumeControl"
|
|
86
|
+
[min]="0"
|
|
87
|
+
[max]="100"
|
|
88
|
+
[step]="1"
|
|
89
|
+
label="Volume Level"
|
|
90
|
+
[showTicks]="true">
|
|
91
|
+
</app-range-selection-input>
|
|
92
|
+
|
|
93
|
+
<div>Volume: {{ volumeControl.value }}</div>
|
|
94
|
+
`
|
|
95
|
+
})
|
|
96
|
+
export class BasicRangeComponent {
|
|
97
|
+
volumeControl = new FormControl(50);
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
#### Example 2: Dual Range Slider (Price Range)
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
import { Component } from '@angular/core';
|
|
105
|
+
import { FormControl } from '@angular/forms';
|
|
106
|
+
|
|
107
|
+
@Component({
|
|
108
|
+
selector: 'app-price-range',
|
|
109
|
+
template: `
|
|
110
|
+
<app-range-selection-input
|
|
111
|
+
[formControl]="priceControl"
|
|
112
|
+
[min]="0"
|
|
113
|
+
[max]="1000"
|
|
114
|
+
[step]="10"
|
|
115
|
+
[showTicks]="true"
|
|
116
|
+
label="Price Range ($)"
|
|
117
|
+
[displayFunction]="formatPrice">
|
|
118
|
+
</app-range-selection-input>
|
|
119
|
+
|
|
120
|
+
<div class="price-display">
|
|
121
|
+
<div *ngIf="priceControl.value">
|
|
122
|
+
<strong>Selected Range:</strong>
|
|
123
|
+
{{ formatPrice(priceControl.value.min) }} - {{ formatPrice(priceControl.value.max) }}
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
`,
|
|
127
|
+
styles: [`
|
|
128
|
+
.price-display {
|
|
129
|
+
margin-top: 1rem;
|
|
130
|
+
padding: 1rem;
|
|
131
|
+
background: #f5f5f5;
|
|
132
|
+
border-radius: 4px;
|
|
133
|
+
font-size: 1.1rem;
|
|
134
|
+
}
|
|
135
|
+
`]
|
|
136
|
+
})
|
|
137
|
+
export class PriceRangeComponent {
|
|
138
|
+
priceControl = new FormControl({ min: 100, max: 500 });
|
|
139
|
+
|
|
140
|
+
formatPrice(value: number): string {
|
|
141
|
+
return `$${value.toFixed(0)}`;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
#### Example 3: Quantity Selection with Validation
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
import { Component } from '@angular/core';
|
|
150
|
+
import { FormControl, Validators } from '@angular/forms';
|
|
151
|
+
|
|
152
|
+
@Component({
|
|
153
|
+
selector: 'app-quantity-selector',
|
|
154
|
+
template: `
|
|
155
|
+
<app-range-selection-input
|
|
156
|
+
[formControl]="quantityControl"
|
|
157
|
+
[min]="1"
|
|
158
|
+
[max]="10"
|
|
159
|
+
[step]="1"
|
|
160
|
+
[showTicks]="true"
|
|
161
|
+
label="Select Quantity"
|
|
162
|
+
[required]="true">
|
|
163
|
+
</app-range-selection-input>
|
|
164
|
+
|
|
165
|
+
<div class="errors" *ngIf="quantityControl.errors && quantityControl.touched">
|
|
166
|
+
<div *ngIf="quantityControl.hasError('required')">
|
|
167
|
+
Quantity selection is required
|
|
168
|
+
</div>
|
|
169
|
+
<div *ngIf="quantityControl.hasError('min')">
|
|
170
|
+
Minimum quantity is 1
|
|
171
|
+
</div>
|
|
172
|
+
<div *ngIf="quantityControl.hasError('max')">
|
|
173
|
+
Maximum quantity is 10
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
<div class="quantity-info">
|
|
178
|
+
Quantity: {{ quantityControl.value }}
|
|
179
|
+
<span *ngIf="quantityControl.value">
|
|
180
|
+
(Total: ${{ (quantityControl.value * unitPrice).toFixed(2) }})
|
|
181
|
+
</span>
|
|
182
|
+
</div>
|
|
183
|
+
`,
|
|
184
|
+
styles: [`
|
|
185
|
+
.errors {
|
|
186
|
+
color: #f44336;
|
|
187
|
+
font-size: 0.875rem;
|
|
188
|
+
margin-top: 0.5rem;
|
|
189
|
+
}
|
|
190
|
+
.quantity-info {
|
|
191
|
+
margin-top: 1rem;
|
|
192
|
+
padding: 0.75rem;
|
|
193
|
+
background: #e8f5e8;
|
|
194
|
+
border: 1px solid #4caf50;
|
|
195
|
+
border-radius: 4px;
|
|
196
|
+
color: #2e7d32;
|
|
197
|
+
}
|
|
198
|
+
`]
|
|
199
|
+
})
|
|
200
|
+
export class QuantitySelectorComponent {
|
|
201
|
+
quantityControl = new FormControl(1, [
|
|
202
|
+
Validators.required,
|
|
203
|
+
Validators.min(1),
|
|
204
|
+
Validators.max(10)
|
|
205
|
+
]);
|
|
206
|
+
|
|
207
|
+
unitPrice = 29.99;
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
#### Example 4: Percentage Selection
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
import { Component } from '@angular/core';
|
|
215
|
+
import { FormControl } from '@angular/forms';
|
|
216
|
+
|
|
217
|
+
@Component({
|
|
218
|
+
selector: 'app-percentage-selector',
|
|
219
|
+
template: `
|
|
220
|
+
<app-range-selection-input
|
|
221
|
+
[formControl]="opacityControl"
|
|
222
|
+
[min]="0"
|
|
223
|
+
[max]="100"
|
|
224
|
+
[step]="5"
|
|
225
|
+
[showTicks]="true"
|
|
226
|
+
label="Opacity Level"
|
|
227
|
+
[displayFunction]="formatPercentage">
|
|
228
|
+
</app-range-selection-input>
|
|
229
|
+
|
|
230
|
+
<div class="preview-container">
|
|
231
|
+
<div
|
|
232
|
+
class="preview-box"
|
|
233
|
+
[style.opacity]="(opacityControl.value || 50) / 100">
|
|
234
|
+
<p>Preview Box</p>
|
|
235
|
+
<p>Opacity: {{ formatPercentage(opacityControl.value || 50) }}</p>
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
`,
|
|
239
|
+
styles: [`
|
|
240
|
+
.preview-container {
|
|
241
|
+
margin-top: 2rem;
|
|
242
|
+
display: flex;
|
|
243
|
+
justify-content: center;
|
|
244
|
+
}
|
|
245
|
+
.preview-box {
|
|
246
|
+
width: 200px;
|
|
247
|
+
height: 100px;
|
|
248
|
+
background: linear-gradient(45deg, #2196f3, #21cbf3);
|
|
249
|
+
border-radius: 8px;
|
|
250
|
+
display: flex;
|
|
251
|
+
flex-direction: column;
|
|
252
|
+
align-items: center;
|
|
253
|
+
justify-content: center;
|
|
254
|
+
color: white;
|
|
255
|
+
font-weight: bold;
|
|
256
|
+
text-shadow: 0 1px 2px rgba(0,0,0,0.5);
|
|
257
|
+
transition: opacity 0.3s ease;
|
|
258
|
+
}
|
|
259
|
+
.preview-box p {
|
|
260
|
+
margin: 0.25rem 0;
|
|
261
|
+
}
|
|
262
|
+
`]
|
|
263
|
+
})
|
|
264
|
+
export class PercentageSelectorComponent {
|
|
265
|
+
opacityControl = new FormControl(75);
|
|
266
|
+
|
|
267
|
+
formatPercentage(value: number): string {
|
|
268
|
+
return `${value}%`;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
#### Example 5: Temperature Range Selection
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
import { Component } from '@angular/core';
|
|
277
|
+
import { FormControl } from '@angular/forms';
|
|
278
|
+
|
|
279
|
+
@Component({
|
|
280
|
+
selector: 'app-temperature-range',
|
|
281
|
+
template: `
|
|
282
|
+
<app-range-selection-input
|
|
283
|
+
[formControl]="temperatureControl"
|
|
284
|
+
[min]="-10"
|
|
285
|
+
[max]="40"
|
|
286
|
+
[step]="1"
|
|
287
|
+
[showTicks]="true"
|
|
288
|
+
label="Temperature Range (°C)"
|
|
289
|
+
[displayFunction]="formatTemperature">
|
|
290
|
+
</app-range-selection-input>
|
|
291
|
+
|
|
292
|
+
<div class="temperature-info" [attr.data-range]="getTemperatureRange()">
|
|
293
|
+
<h4>Temperature Range</h4>
|
|
294
|
+
<p>{{ formatTemperature(temperatureControl.value.min) }} to {{ formatTemperature(temperatureControl.value.max) }}</p>
|
|
295
|
+
<div class="range-description">
|
|
296
|
+
{{ getTemperatureDescription() }}
|
|
297
|
+
</div>
|
|
298
|
+
</div>
|
|
299
|
+
`,
|
|
300
|
+
styles: [`
|
|
301
|
+
.temperature-info {
|
|
302
|
+
margin-top: 1rem;
|
|
303
|
+
padding: 1rem;
|
|
304
|
+
border-radius: 8px;
|
|
305
|
+
text-align: center;
|
|
306
|
+
transition: all 0.3s ease;
|
|
307
|
+
}
|
|
308
|
+
.temperature-info[data-range="cold"] {
|
|
309
|
+
background: linear-gradient(135deg, #e3f2fd, #bbdefb);
|
|
310
|
+
border: 2px solid #2196f3;
|
|
311
|
+
color: #1565c0;
|
|
312
|
+
}
|
|
313
|
+
.temperature-info[data-range="moderate"] {
|
|
314
|
+
background: linear-gradient(135deg, #fff3e0, #ffcc02);
|
|
315
|
+
border: 2px solid #ff9800;
|
|
316
|
+
color: #ef6c00;
|
|
317
|
+
}
|
|
318
|
+
.temperature-info[data-range="warm"] {
|
|
319
|
+
background: linear-gradient(135deg, #ffebee, #ef5350);
|
|
320
|
+
border: 2px solid #f44336;
|
|
321
|
+
color: #c62828;
|
|
322
|
+
}
|
|
323
|
+
.range-description {
|
|
324
|
+
margin-top: 0.5rem;
|
|
325
|
+
font-style: italic;
|
|
326
|
+
opacity: 0.8;
|
|
327
|
+
}
|
|
328
|
+
`]
|
|
329
|
+
})
|
|
330
|
+
export class TemperatureRangeComponent {
|
|
331
|
+
temperatureControl = new FormControl({ min: 15, max: 25 });
|
|
332
|
+
|
|
333
|
+
formatTemperature(value: number): string {
|
|
334
|
+
if (value < 0) {
|
|
335
|
+
return `${Math.abs(value)}°C below freezing`;
|
|
336
|
+
}
|
|
337
|
+
return `${value}°C`;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
getTemperatureRange(): string {
|
|
341
|
+
const avg = (this.temperatureControl.value.min + this.temperatureControl.value.max) / 2;
|
|
342
|
+
if (avg < 10) return 'cold';
|
|
343
|
+
if (avg < 25) return 'moderate';
|
|
344
|
+
return 'warm';
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
getTemperatureDescription(): string {
|
|
348
|
+
const range = this.getTemperatureRange();
|
|
349
|
+
const descriptions = {
|
|
350
|
+
cold: 'Cool weather - jacket recommended',
|
|
351
|
+
moderate: 'Pleasant temperature',
|
|
352
|
+
warm: 'Warm weather - stay hydrated'
|
|
353
|
+
};
|
|
354
|
+
return descriptions[range];
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
#### Example 6: Form Integration with Dynamic Range
|
|
360
|
+
|
|
361
|
+
```typescript
|
|
362
|
+
import { Component } from '@angular/core';
|
|
363
|
+
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
|
364
|
+
|
|
365
|
+
@Component({
|
|
366
|
+
selector: 'app-dynamic-range-form',
|
|
367
|
+
template: `
|
|
368
|
+
<form [formGroup]="dynamicForm">
|
|
369
|
+
<app-range-selection-input
|
|
370
|
+
formControlName="age"
|
|
371
|
+
[min]="18"
|
|
372
|
+
[max]="100"
|
|
373
|
+
[step]="1"
|
|
374
|
+
[showTicks]="true"
|
|
375
|
+
label="Age"
|
|
376
|
+
placeholder="Select age"
|
|
377
|
+
[required]="true">
|
|
378
|
+
</app-range-selection-input>
|
|
379
|
+
|
|
380
|
+
<app-range-selection-input
|
|
381
|
+
formControlName="budget"
|
|
382
|
+
[min]="budgetRange.min"
|
|
383
|
+
[max]="budgetRange.max"
|
|
384
|
+
[step]="budgetRange.step"
|
|
385
|
+
[showTicks]="true"
|
|
386
|
+
label="Budget Range ($)"
|
|
387
|
+
[displayFunction]="formatCurrency">
|
|
388
|
+
</app-range-selection-input>
|
|
389
|
+
|
|
390
|
+
<app-range-selection-input
|
|
391
|
+
formControlName="rating"
|
|
392
|
+
[min]="0"
|
|
393
|
+
[max]="5"
|
|
394
|
+
[step]="0.5"
|
|
395
|
+
[showTicks]="true"
|
|
396
|
+
label="Rating"
|
|
397
|
+
[displayFunction]="formatStars">
|
|
398
|
+
</app-range-selection-input>
|
|
399
|
+
</form>
|
|
400
|
+
|
|
401
|
+
<div class="form-status">
|
|
402
|
+
<div>Form Valid: {{ dynamicForm.valid }}</div>
|
|
403
|
+
<div>Age: {{ dynamicForm.get('age')?.value }}</div>
|
|
404
|
+
<div>Budget: {{ formatCurrency(dynamicForm.get('budget')?.value?.min) }} - {{ formatCurrency(dynamicForm.get('budget')?.value?.max) }}</div>
|
|
405
|
+
<div>Rating: {{ formatStars(dynamicForm.get('rating')?.value) }}</div>
|
|
406
|
+
</div>
|
|
407
|
+
|
|
408
|
+
<div class="controls">
|
|
409
|
+
<button (click)="setDefaults()">Set Defaults</button>
|
|
410
|
+
<button (click)="resetForm()">Reset</button>
|
|
411
|
+
<button (click)="submitForm()" [disabled]="dynamicForm.invalid">Submit</button>
|
|
412
|
+
</div>
|
|
413
|
+
`,
|
|
414
|
+
styles: [`
|
|
415
|
+
form {
|
|
416
|
+
display: flex;
|
|
417
|
+
flex-direction: column;
|
|
418
|
+
gap: 2rem;
|
|
419
|
+
max-width: 400px;
|
|
420
|
+
}
|
|
421
|
+
.form-status {
|
|
422
|
+
margin-top: 1rem;
|
|
423
|
+
padding: 1rem;
|
|
424
|
+
background: #f5f5f5;
|
|
425
|
+
border-radius: 4px;
|
|
426
|
+
font-family: monospace;
|
|
427
|
+
font-size: 0.9rem;
|
|
428
|
+
}
|
|
429
|
+
.controls {
|
|
430
|
+
margin-top: 1rem;
|
|
431
|
+
display: flex;
|
|
432
|
+
gap: 0.5rem;
|
|
433
|
+
}
|
|
434
|
+
button {
|
|
435
|
+
padding: 0.5rem 1rem;
|
|
436
|
+
border: 1px solid #ccc;
|
|
437
|
+
border-radius: 4px;
|
|
438
|
+
background: white;
|
|
439
|
+
cursor: pointer;
|
|
440
|
+
}
|
|
441
|
+
button:disabled {
|
|
442
|
+
opacity: 0.5;
|
|
443
|
+
cursor: not-allowed;
|
|
444
|
+
}
|
|
445
|
+
`]
|
|
446
|
+
})
|
|
447
|
+
export class DynamicRangeFormComponent {
|
|
448
|
+
dynamicForm: FormGroup;
|
|
449
|
+
budgetRange = { min: 1000, max: 10000, step: 100 };
|
|
450
|
+
|
|
451
|
+
constructor(private fb: FormBuilder) {
|
|
452
|
+
this.dynamicForm = this.fb.group({
|
|
453
|
+
age: [25, [Validators.required, Validators.min(18), Validators.max(100)]],
|
|
454
|
+
budget: [{ min: 3000, max: 7000 }, [Validators.required]],
|
|
455
|
+
rating: [3.5, [Validators.required, Validators.min(0), Validators.max(5)]]
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
formatCurrency(value: number): string {
|
|
460
|
+
if (typeof value === 'object' && value !== null) {
|
|
461
|
+
return `$${value.min?.toLocaleString()} - $${value.max?.toLocaleString()}`;
|
|
462
|
+
}
|
|
463
|
+
return `$${value.toLocaleString()}`;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
formatStars(value: number): string {
|
|
467
|
+
const fullStars = Math.floor(value);
|
|
468
|
+
const hasHalfStar = value % 1 >= 0.5;
|
|
469
|
+
const emptyStars = 5 - fullStars - (hasHalfStar ? 1 : 0);
|
|
470
|
+
|
|
471
|
+
return '★'.repeat(fullStars) +
|
|
472
|
+
(hasHalfStar ? '☆' : '') +
|
|
473
|
+
'☆'.repeat(emptyStars) +
|
|
474
|
+
` (${value})`;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
setDefaults() {
|
|
478
|
+
this.dynamicForm.patchValue({
|
|
479
|
+
age: 30,
|
|
480
|
+
budget: { min: 2500, max: 8000 },
|
|
481
|
+
rating: 4.0
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
resetForm() {
|
|
486
|
+
this.dynamicForm.reset();
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
submitForm() {
|
|
490
|
+
if (this.dynamicForm.valid) {
|
|
491
|
+
console.log('Form submitted:', this.dynamicForm.value);
|
|
492
|
+
alert('Form submitted successfully!');
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
---
|
|
499
|
+
|
|
500
|
+
## Component API
|
|
501
|
+
|
|
502
|
+
### Inputs
|
|
503
|
+
|
|
504
|
+
| Input | Type | Description | Default |
|
|
505
|
+
| :--- | :--- | :--- | :--- |
|
|
506
|
+
| `min` | `number` | Minimum value for the slider | `0` |
|
|
507
|
+
| `max` | `number` | Maximum value for the slider | `100` |
|
|
508
|
+
| `step` | `number` | Step increment/decrement value | `1` |
|
|
509
|
+
| `showTicks` | `boolean` | Whether to show tick marks | `false` |
|
|
510
|
+
| `label` | `string` | Label text for the slider | `undefined` |
|
|
511
|
+
| `placeholder` | `string` | Placeholder text for the slider | `undefined` |
|
|
512
|
+
| `displayFunction` | `(value: number) => string` | Function to format display values | `undefined` |
|
|
513
|
+
| `required` | `boolean` | Whether selection is required | `false` |
|
|
514
|
+
| `disabled` | `boolean` | Whether the slider is disabled | `false` |
|
|
515
|
+
|
|
516
|
+
### Outputs
|
|
517
|
+
|
|
518
|
+
| Output | Type | Description |
|
|
519
|
+
|--------|------|-------------|
|
|
520
|
+
| `valueChange` | `EventEmitter<number \| { min: number; max: number }>` | Emits when slider value changes |
|
|
521
|
+
|
|
522
|
+
---
|
|
523
|
+
|
|
524
|
+
## Form Integration (ControlValueAccessor)
|
|
525
|
+
|
|
526
|
+
The component implements Angular's `ControlValueAccessor` interface for seamless form integration.
|
|
527
|
+
|
|
528
|
+
### ControlValueAccessor Implementation
|
|
529
|
+
|
|
530
|
+
```typescript
|
|
531
|
+
// writeValue(value: number | { min: number; max: number }): void
|
|
532
|
+
// Sets the slider value(s)
|
|
533
|
+
writeValue(value: number | { min: number; max: number }): void {
|
|
534
|
+
this.selectedValue = value;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// registerOnChange(fn: any): void
|
|
538
|
+
// Registers a callback for value changes
|
|
539
|
+
registerOnChange(fn: any): void {
|
|
540
|
+
this.onChange = fn;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// registerOnTouched(fn: any): void
|
|
544
|
+
// Registers a callback for touch events
|
|
545
|
+
registerOnTouched(fn: any): void {
|
|
546
|
+
this.onTouch = fn;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// setDisabledState(isDisabled: boolean): void
|
|
550
|
+
// Sets disabled state
|
|
551
|
+
setDisabledState?(isDisabled: boolean): void {
|
|
552
|
+
this.disabled = isDisabled;
|
|
553
|
+
}
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
### Form Integration Examples
|
|
557
|
+
|
|
558
|
+
#### Reactive Forms
|
|
559
|
+
|
|
560
|
+
```typescript
|
|
561
|
+
import { Component } from '@angular/core';
|
|
562
|
+
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
|
563
|
+
|
|
564
|
+
@Component({
|
|
565
|
+
selector: 'app-reactive-range-form',
|
|
566
|
+
template: `
|
|
567
|
+
<form [formGroup]="rangeForm">
|
|
568
|
+
<app-range-selection-input
|
|
569
|
+
formControlName="singleValue"
|
|
570
|
+
[min]="0"
|
|
571
|
+
[max]="100"
|
|
572
|
+
[step]="5"
|
|
573
|
+
[showTicks]="true"
|
|
574
|
+
label="Single Value"
|
|
575
|
+
[required]="true">
|
|
576
|
+
</app-range-selection-input>
|
|
577
|
+
|
|
578
|
+
<app-range-selection-input
|
|
579
|
+
formControlName="rangeValue"
|
|
580
|
+
[min]="0"
|
|
581
|
+
[max]="1000"
|
|
582
|
+
[step]="25"
|
|
583
|
+
[showTicks]="true"
|
|
584
|
+
label="Range Value"
|
|
585
|
+
[required]="true">
|
|
586
|
+
</app-range-selection-input>
|
|
587
|
+
</form>
|
|
588
|
+
`
|
|
589
|
+
})
|
|
590
|
+
export class ReactiveRangeFormComponent {
|
|
591
|
+
rangeForm = new FormGroup({
|
|
592
|
+
singleValue: new FormControl(50, [Validators.required, Validators.min(0), Validators.max(100)]),
|
|
593
|
+
rangeValue: new FormControl({ min: 100, max: 500 }, [Validators.required])
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
#### Template-Driven Forms
|
|
599
|
+
|
|
600
|
+
```typescript
|
|
601
|
+
import { Component } from '@angular/core';
|
|
602
|
+
|
|
603
|
+
@Component({
|
|
604
|
+
selector: 'app-template-range-form',
|
|
605
|
+
template: `
|
|
606
|
+
<app-range-selection-input
|
|
607
|
+
[(ngModel)]="selectedValue"
|
|
608
|
+
name="templateRange"
|
|
609
|
+
[min]="0"
|
|
610
|
+
[max]="100"
|
|
611
|
+
[step]="10"
|
|
612
|
+
[showTicks]="true"
|
|
613
|
+
label="Template Range Selection"
|
|
614
|
+
required>
|
|
615
|
+
</app-range-selection-input>
|
|
616
|
+
|
|
617
|
+
<div *ngIf="selectedValue">
|
|
618
|
+
Selected: {{ selectedValue }}
|
|
619
|
+
</div>
|
|
620
|
+
`
|
|
621
|
+
})
|
|
622
|
+
export class TemplateRangeFormComponent {
|
|
623
|
+
selectedValue = 25;
|
|
624
|
+
}
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
---
|
|
628
|
+
|
|
629
|
+
## Module Configuration
|
|
630
|
+
|
|
631
|
+
### RangeSelectionInputModule
|
|
632
|
+
|
|
633
|
+
**No Global Configuration Required**
|
|
634
|
+
|
|
635
|
+
The `RangeSelectionInputModule` does not provide a `forRoot()` method or global configuration options. All configuration is done at the component level through input properties.
|
|
636
|
+
|
|
637
|
+
#### Dependencies
|
|
638
|
+
|
|
639
|
+
- **@angular/core**: Core Angular functionality
|
|
640
|
+
- **@angular/forms**: Form control integration (FormsModule, ReactiveFormsModule)
|
|
641
|
+
- **@angular/material**: Material Design components (MatSliderModule, MatFormFieldModule, etc.)
|
|
642
|
+
|
|
643
|
+
---
|
|
644
|
+
|
|
645
|
+
## Advanced Usage Patterns
|
|
646
|
+
|
|
647
|
+
### Custom Display Formatting
|
|
648
|
+
|
|
649
|
+
```typescript
|
|
650
|
+
import { Component } from '@angular/core';
|
|
651
|
+
import { FormControl } from '@angular/forms';
|
|
652
|
+
|
|
653
|
+
@Component({
|
|
654
|
+
selector: 'app-custom-formatting',
|
|
655
|
+
template: `
|
|
656
|
+
<app-range-selection-input
|
|
657
|
+
[formControl]="progressControl"
|
|
658
|
+
[min]="0"
|
|
659
|
+
[max]="100"
|
|
660
|
+
[step]="5"
|
|
661
|
+
[showTicks]="true"
|
|
662
|
+
label="Project Progress"
|
|
663
|
+
[displayFunction]="formatProgress">
|
|
664
|
+
</app-range-selection-input>
|
|
665
|
+
|
|
666
|
+
<div class="progress-visual">
|
|
667
|
+
<div class="progress-bar">
|
|
668
|
+
<div
|
|
669
|
+
class="progress-fill"
|
|
670
|
+
[style.width.%]="progressControl.value || 0">
|
|
671
|
+
</div>
|
|
672
|
+
</div>
|
|
673
|
+
<span class="progress-text">{{ formatProgress(progressControl.value || 0) }}</span>
|
|
674
|
+
</div>
|
|
675
|
+
`,
|
|
676
|
+
styles: [`
|
|
677
|
+
.progress-visual {
|
|
678
|
+
margin-top: 2rem;
|
|
679
|
+
}
|
|
680
|
+
.progress-bar {
|
|
681
|
+
width: 100%;
|
|
682
|
+
height: 20px;
|
|
683
|
+
background: #e0e0e0;
|
|
684
|
+
border-radius: 10px;
|
|
685
|
+
overflow: hidden;
|
|
686
|
+
margin-bottom: 0.5rem;
|
|
687
|
+
}
|
|
688
|
+
.progress-fill {
|
|
689
|
+
height: 100%;
|
|
690
|
+
background: linear-gradient(90deg, #4caf50, #8bc34a);
|
|
691
|
+
transition: width 0.3s ease;
|
|
692
|
+
}
|
|
693
|
+
.progress-text {
|
|
694
|
+
font-weight: bold;
|
|
695
|
+
color: #333;
|
|
696
|
+
}
|
|
697
|
+
`]
|
|
698
|
+
})
|
|
699
|
+
export class CustomFormattingComponent {
|
|
700
|
+
progressControl = new FormControl(25);
|
|
701
|
+
|
|
702
|
+
formatProgress(value: number): string {
|
|
703
|
+
const percentage = Math.round(value);
|
|
704
|
+
const status = percentage < 25 ? 'Just Started' :
|
|
705
|
+
percentage < 50 ? 'Early Progress' :
|
|
706
|
+
percentage < 75 ? 'Halfway There' :
|
|
707
|
+
percentage < 90 ? 'Almost Done' : 'Complete';
|
|
708
|
+
return `${percentage}% - ${status}`;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
### Dynamic Range Updates
|
|
714
|
+
|
|
715
|
+
```typescript
|
|
716
|
+
import { Component } from '@angular/core';
|
|
717
|
+
import { FormControl } from '@angular/forms';
|
|
718
|
+
|
|
719
|
+
@Component({
|
|
720
|
+
selector: 'app-dynamic-range',
|
|
721
|
+
template: `
|
|
722
|
+
<app-range-selection-input
|
|
723
|
+
[formControl]="difficultyControl"
|
|
724
|
+
[min]="difficultyRange.min"
|
|
725
|
+
[max]="difficultyRange.max"
|
|
726
|
+
[step]="difficultyRange.step"
|
|
727
|
+
[showTicks]="true"
|
|
728
|
+
label="Difficulty Level"
|
|
729
|
+
[displayFunction]="formatDifficulty">
|
|
730
|
+
</app-range-selection-input>
|
|
731
|
+
|
|
732
|
+
<div class="difficulty-info">
|
|
733
|
+
<h4>Difficulty: {{ formatDifficulty(difficultyControl.value || difficultyRange.min) }}</h4>
|
|
734
|
+
<p>{{ getDifficultyDescription() }}</p>
|
|
735
|
+
</div>
|
|
736
|
+
|
|
737
|
+
<div class="range-controls">
|
|
738
|
+
<label>
|
|
739
|
+
Quick Presets:
|
|
740
|
+
<select (change)="setPreset($event.target.value)">
|
|
741
|
+
<option value="">Choose preset</option>
|
|
742
|
+
<option value="easy">Easy (1-3)</option>
|
|
743
|
+
<option value="medium">Medium (4-6)</option>
|
|
744
|
+
<option value="hard">Hard (7-9)</option>
|
|
745
|
+
</select>
|
|
746
|
+
</label>
|
|
747
|
+
</div>
|
|
748
|
+
`,
|
|
749
|
+
styles: [`
|
|
750
|
+
.difficulty-info {
|
|
751
|
+
margin-top: 1rem;
|
|
752
|
+
padding: 1rem;
|
|
753
|
+
border-radius: 8px;
|
|
754
|
+
text-align: center;
|
|
755
|
+
}
|
|
756
|
+
.difficulty-info[data-level="easy"] {
|
|
757
|
+
background: #e8f5e8;
|
|
758
|
+
border: 2px solid #4caf50;
|
|
759
|
+
color: #2e7d32;
|
|
760
|
+
}
|
|
761
|
+
.difficulty-info[data-level="medium"] {
|
|
762
|
+
background: #fff3e0;
|
|
763
|
+
border: 2px solid #ff9800;
|
|
764
|
+
color: #ef6c00;
|
|
765
|
+
}
|
|
766
|
+
.difficulty-info[data-level="hard"] {
|
|
767
|
+
background: #ffebee;
|
|
768
|
+
border: 2px solid #f44336;
|
|
769
|
+
color: #c62828;
|
|
770
|
+
}
|
|
771
|
+
.range-controls {
|
|
772
|
+
margin-top: 1rem;
|
|
773
|
+
}
|
|
774
|
+
.range-controls select {
|
|
775
|
+
margin-left: 0.5rem;
|
|
776
|
+
padding: 0.25rem;
|
|
777
|
+
}
|
|
778
|
+
`]
|
|
779
|
+
})
|
|
780
|
+
export class DynamicRangeComponent {
|
|
781
|
+
difficultyControl = new FormControl(5);
|
|
782
|
+
difficultyRange = { min: 1, max: 10, step: 1 };
|
|
783
|
+
|
|
784
|
+
formatDifficulty(value: number): string {
|
|
785
|
+
if (value <= 3) return `Easy (${value})`;
|
|
786
|
+
if (value <= 6) return `Medium (${value})`;
|
|
787
|
+
return `Hard (${value})`;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
getDifficultyDescription(): string {
|
|
791
|
+
const value = this.difficultyControl.value || this.difficultyRange.min;
|
|
792
|
+
if (value <= 3) return 'Simple and straightforward tasks';
|
|
793
|
+
if (value <= 6) return 'Moderate complexity with some challenges';
|
|
794
|
+
return 'Complex tasks requiring significant effort';
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
setPreset(preset: string) {
|
|
798
|
+
const presets = {
|
|
799
|
+
easy: { min: 1, max: 3 },
|
|
800
|
+
medium: { min: 4, max: 6 },
|
|
801
|
+
hard: { min: 7, max: 9 }
|
|
802
|
+
};
|
|
803
|
+
|
|
804
|
+
if (preset && presets[preset as keyof typeof presets]) {
|
|
805
|
+
const range = presets[preset as keyof typeof presets];
|
|
806
|
+
this.difficultyControl.setValue(range.min);
|
|
807
|
+
this.difficultyRange = { ...this.difficultyRange, ...range };
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
```
|
|
812
|
+
|
|
813
|
+
### Performance Optimization
|
|
814
|
+
|
|
815
|
+
```typescript
|
|
816
|
+
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
|
817
|
+
import { FormControl } from '@angular/forms';
|
|
818
|
+
|
|
819
|
+
@Component({
|
|
820
|
+
selector: 'app-optimized-range',
|
|
821
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
822
|
+
template: `
|
|
823
|
+
<app-range-selection-input
|
|
824
|
+
[formControl]="optimizedControl"
|
|
825
|
+
[min]="0"
|
|
826
|
+
[max]="1000000"
|
|
827
|
+
[step]="1000"
|
|
828
|
+
[showTicks]="true"
|
|
829
|
+
label="Large Range Selection"
|
|
830
|
+
[displayFunction]="formatLargeNumber">
|
|
831
|
+
</app-range-selection-input>
|
|
832
|
+
`
|
|
833
|
+
})
|
|
834
|
+
export class OptimizedRangeComponent {
|
|
835
|
+
optimizedControl = new FormControl(500000);
|
|
836
|
+
|
|
837
|
+
formatLargeNumber(value: number): string {
|
|
838
|
+
if (value >= 1000000) {
|
|
839
|
+
return `${(value / 1000000).toFixed(1)}M`;
|
|
840
|
+
} else if (value >= 1000) {
|
|
841
|
+
return `${(value / 1000).toFixed(0)}K`;
|
|
842
|
+
}
|
|
843
|
+
return value.toString();
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
```
|
|
847
|
+
|
|
848
|
+
---
|
|
849
|
+
|
|
850
|
+
## Integration Examples
|
|
851
|
+
|
|
852
|
+
### With Other UI Components
|
|
853
|
+
|
|
854
|
+
```typescript
|
|
855
|
+
import { Component } from '@angular/core';
|
|
856
|
+
|
|
857
|
+
@Component({
|
|
858
|
+
selector: 'app-integrated-range',
|
|
859
|
+
template: `
|
|
860
|
+
<app-display-card title="Price Configuration">
|
|
861
|
+
<app-range-selection-input
|
|
862
|
+
[formControl]="priceControl"
|
|
863
|
+
[min]="0"
|
|
864
|
+
[max]="1000"
|
|
865
|
+
[step]="10"
|
|
866
|
+
[showTicks]="true"
|
|
867
|
+
label="Price Range"
|
|
868
|
+
[displayFunction]="formatPrice">
|
|
869
|
+
</app-range-selection-input>
|
|
870
|
+
|
|
871
|
+
<div *ngIf="priceControl.value" class="price-summary">
|
|
872
|
+
<h4>Price Configuration</h4>
|
|
873
|
+
<p><strong>Selected Range:</strong> {{ formatPrice(priceControl.value.min) }} - {{ formatPrice(priceControl.value.max) }}</p>
|
|
874
|
+
<p><strong>Average Price:</strong> {{ formatPrice((priceControl.value.min + priceControl.value.max) / 2) }}</p>
|
|
875
|
+
<p><strong>Price Spread:</strong> {{ formatPrice(priceControl.value.max - priceControl.value.min) }}</p>
|
|
876
|
+
</div>
|
|
877
|
+
</app-display-card>
|
|
878
|
+
`
|
|
879
|
+
})
|
|
880
|
+
export class IntegratedRangeComponent {
|
|
881
|
+
priceControl = new FormControl({ min: 100, max: 500 });
|
|
882
|
+
|
|
883
|
+
formatPrice(value: number): string {
|
|
884
|
+
if (typeof value === 'object' && value !== null) {
|
|
885
|
+
return `$${value.min?.toFixed(0)} - $${value.max?.toFixed(0)}`;
|
|
886
|
+
}
|
|
887
|
+
return `$${value.toFixed(0)}`;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
```
|
|
891
|
+
|
|
892
|
+
### With State Management
|
|
893
|
+
|
|
894
|
+
```typescript
|
|
895
|
+
import { Component } from '@angular/core';
|
|
896
|
+
import { Store } from '@ngrx/store';
|
|
897
|
+
|
|
898
|
+
@Component({
|
|
899
|
+
selector: 'app-state-range',
|
|
900
|
+
template: `
|
|
901
|
+
<app-range-selection-input
|
|
902
|
+
[formControl]="volumeControl"
|
|
903
|
+
[data]="volumeOptions$ | async"
|
|
904
|
+
label="Audio Volume"
|
|
905
|
+
placeholder="Adjust volume"
|
|
906
|
+
(valueChange)="onVolumeChange($event)">
|
|
907
|
+
</app-range-selection-input>
|
|
908
|
+
`
|
|
909
|
+
})
|
|
910
|
+
export class StateRangeComponent {
|
|
911
|
+
volumeControl = new FormControl();
|
|
912
|
+
volumeOptions$ = this.store.select(state => state.volumeOptions);
|
|
913
|
+
|
|
914
|
+
constructor(private store: Store) {}
|
|
915
|
+
|
|
916
|
+
onVolumeChange(volume: number) {
|
|
917
|
+
this.store.dispatch(adjustVolume({ volume }));
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
```
|
|
921
|
+
|
|
922
|
+
---
|
|
923
|
+
|
|
924
|
+
## Testing
|
|
925
|
+
|
|
926
|
+
### Unit Testing Example
|
|
927
|
+
|
|
928
|
+
```typescript
|
|
929
|
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
930
|
+
import { RangeSelectionInputComponent } from './range-selection-input.component';
|
|
931
|
+
import { ReactiveFormsModule } from '@angular/forms';
|
|
932
|
+
|
|
933
|
+
describe('RangeSelectionInputComponent', () => {
|
|
934
|
+
let component: RangeSelectionInputComponent;
|
|
935
|
+
let fixture: ComponentFixture<RangeSelectionInputComponent>;
|
|
936
|
+
|
|
937
|
+
beforeEach(async () => {
|
|
938
|
+
await TestBed.configureTestingModule({
|
|
939
|
+
declarations: [ RangeSelectionInputComponent ],
|
|
940
|
+
imports: [ ReactiveFormsModule ]
|
|
941
|
+
}).compileComponents();
|
|
942
|
+
|
|
943
|
+
fixture = TestBed.createComponent(RangeSelectionInputComponent);
|
|
944
|
+
component = fixture.componentInstance;
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
it('should create', () => {
|
|
948
|
+
expect(component).toBeTruthy();
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
it('should handle single value selection', () => {
|
|
952
|
+
component.min = 0;
|
|
953
|
+
component.max = 100;
|
|
954
|
+
component.step = 1;
|
|
955
|
+
fixture.detectChanges();
|
|
956
|
+
|
|
957
|
+
component.writeValue(50);
|
|
958
|
+
expect(component.selectedValue).toBe(50);
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
it('should handle range value selection', () => {
|
|
962
|
+
component.min = 0;
|
|
963
|
+
component.max = 100;
|
|
964
|
+
component.step = 1;
|
|
965
|
+
fixture.detectChanges();
|
|
966
|
+
|
|
967
|
+
component.writeValue({ min: 25, max: 75 });
|
|
968
|
+
expect(component.selectedValue).toEqual({ min: 25, max: 75 });
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
it('should emit value changes', () => {
|
|
972
|
+
spyOn(component.valueChange, 'emit');
|
|
973
|
+
|
|
974
|
+
component.onValueChange(50);
|
|
975
|
+
|
|
976
|
+
expect(component.valueChange.emit).toHaveBeenCalledWith(50);
|
|
977
|
+
});
|
|
978
|
+
|
|
979
|
+
it('should handle form control integration', () => {
|
|
980
|
+
const formControl = new FormControl();
|
|
981
|
+
component.formControl = formControl;
|
|
982
|
+
|
|
983
|
+
spyOn(formControl, 'setValue');
|
|
984
|
+
component.writeValue(75);
|
|
985
|
+
|
|
986
|
+
expect(formControl.setValue).toHaveBeenCalledWith(75);
|
|
987
|
+
});
|
|
988
|
+
|
|
989
|
+
it('should apply custom display formatting', () => {
|
|
990
|
+
component.displayFunction = (value: number) => `${value}%`;
|
|
991
|
+
fixture.detectChanges();
|
|
992
|
+
|
|
993
|
+
const formattedValue = component.displayFunction(50);
|
|
994
|
+
expect(formattedValue).toBe('50%');
|
|
995
|
+
});
|
|
996
|
+
});
|
|
997
|
+
```
|
|
998
|
+
|
|
999
|
+
---
|
|
1000
|
+
|
|
1001
|
+
## Troubleshooting
|
|
1002
|
+
|
|
1003
|
+
### Common Issues
|
|
1004
|
+
|
|
1005
|
+
1. **Form control not working**: Ensure ReactiveFormsModule is imported
|
|
1006
|
+
2. **Slider not responding**: Check min/max/step values are properly configured
|
|
1007
|
+
3. **Display formatting not working**: Verify displayFunction is properly defined
|
|
1008
|
+
4. **Range selection not working**: Ensure dual-thumb mode is properly configured
|
|
1009
|
+
5. **Styling issues**: Verify Material theme is properly configured
|
|
1010
|
+
|
|
1011
|
+
### Debug Mode
|
|
1012
|
+
|
|
1013
|
+
```typescript
|
|
1014
|
+
@Component({
|
|
1015
|
+
template: `
|
|
1016
|
+
<div class="debug-info">
|
|
1017
|
+
Form Control Value: {{ formControl.value | json }}<br>
|
|
1018
|
+
Form Control Valid: {{ formControl.valid }}<br>
|
|
1019
|
+
Min: {{ min }}<br>
|
|
1020
|
+
Max: {{ max }}<br>
|
|
1021
|
+
Step: {{ step }}<br>
|
|
1022
|
+
Show Ticks: {{ showTicks }}<br>
|
|
1023
|
+
Selected Value: {{ selectedValue | json }}
|
|
1024
|
+
</div>
|
|
1025
|
+
|
|
1026
|
+
<app-range-selection-input
|
|
1027
|
+
[formControl]="formControl"
|
|
1028
|
+
[min]="min"
|
|
1029
|
+
[max]="max"
|
|
1030
|
+
[step]="step"
|
|
1031
|
+
[showTicks]="showTicks"
|
|
1032
|
+
[displayFunction]="displayFunction"
|
|
1033
|
+
(valueChange)="onValueChange($event)">
|
|
1034
|
+
</app-range-selection-input>
|
|
1035
|
+
`
|
|
1036
|
+
})
|
|
1037
|
+
export class DebugRangeComponent {
|
|
1038
|
+
formControl = new FormControl();
|
|
1039
|
+
min = 0;
|
|
1040
|
+
max = 100;
|
|
1041
|
+
step = 1;
|
|
1042
|
+
showTicks = false;
|
|
1043
|
+
selectedValue: any = null;
|
|
1044
|
+
|
|
1045
|
+
displayFunction = (value: number) => `Value: ${value}`;
|
|
1046
|
+
|
|
1047
|
+
constructor() {
|
|
1048
|
+
this.formControl.valueChanges.subscribe(value => {
|
|
1049
|
+
console.log('Range value changed:', value);
|
|
1050
|
+
});
|
|
1051
|
+
|
|
1052
|
+
this.formControl.statusChanges.subscribe(status => {
|
|
1053
|
+
console.log('Form control status:', status);
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
onValueChange(value: any) {
|
|
1058
|
+
this.selectedValue = value;
|
|
1059
|
+
console.log('Value changed:', value);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
```
|
|
1063
|
+
|
|
1064
|
+
### Performance Monitoring
|
|
1065
|
+
|
|
1066
|
+
```typescript
|
|
1067
|
+
@Component({
|
|
1068
|
+
template: `
|
|
1069
|
+
<div class="performance-info">
|
|
1070
|
+
Range Size: {{ max - min }}<br>
|
|
1071
|
+
Step Count: {{ (max - min) / step }}<br>
|
|
1072
|
+
Update Time: {{ updateTime }}ms<br>
|
|
1073
|
+
Change Operations: {{ changeOperationCount }}
|
|
1074
|
+
</div>
|
|
1075
|
+
|
|
1076
|
+
<app-range-selection-input
|
|
1077
|
+
[formControl]="formControl"
|
|
1078
|
+
[min]="min"
|
|
1079
|
+
[max]="max"
|
|
1080
|
+
[step]="step"
|
|
1081
|
+
(valueChange)="onValueChange($event)">
|
|
1082
|
+
</app-range-selection-input>
|
|
1083
|
+
`
|
|
1084
|
+
})
|
|
1085
|
+
export class PerformanceDebugComponent {
|
|
1086
|
+
formControl = new FormControl();
|
|
1087
|
+
min = 0;
|
|
1088
|
+
max = 1000;
|
|
1089
|
+
step = 1;
|
|
1090
|
+
updateTime = 0;
|
|
1091
|
+
changeOperationCount = 0;
|
|
1092
|
+
|
|
1093
|
+
onValueChange(value: any) {
|
|
1094
|
+
const start = performance.now();
|
|
1095
|
+
|
|
1096
|
+
// Perform value update operation
|
|
1097
|
+
this.processValueUpdate(value);
|
|
1098
|
+
|
|
1099
|
+
const end = performance.now();
|
|
1100
|
+
this.updateTime = end - start;
|
|
1101
|
+
this.changeOperationCount++;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
private processValueUpdate(value: any) {
|
|
1105
|
+
// Your value processing logic
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
```
|
|
1109
|
+
|
|
1110
|
+
---
|
|
1111
|
+
|
|
1112
|
+
## Accessibility Features
|
|
1113
|
+
|
|
1114
|
+
### ARIA Support
|
|
1115
|
+
|
|
1116
|
+
- Sliders include proper ARIA labels and roles
|
|
1117
|
+
- Keyboard navigation is fully supported (Arrow keys, Page Up/Down, Home/End)
|
|
1118
|
+
- Screen reader friendly with appropriate descriptions
|
|
1119
|
+
- Focus management for keyboard users
|
|
1120
|
+
- Value announcements for screen readers
|
|
1121
|
+
|
|
1122
|
+
### Keyboard Navigation
|
|
1123
|
+
|
|
1124
|
+
| Key | Action |
|
|
1125
|
+
|-----|--------|
|
|
1126
|
+
| `Arrow Keys` | Adjust slider value by step |
|
|
1127
|
+
| `Page Up/Down` | Adjust by larger increments |
|
|
1128
|
+
| `Home` | Set to minimum value |
|
|
1129
|
+
| `End` | Set to maximum value |
|
|
1130
|
+
| `Space` | Toggle selection (if applicable) |
|
|
1131
|
+
|
|
1132
|
+
### Best Practices
|
|
1133
|
+
|
|
1134
|
+
1. **Provide meaningful labels** for slider accessibility
|
|
1135
|
+
2. **Set appropriate min/max values** for context
|
|
1136
|
+
3. **Use sensible step values** for precision
|
|
1137
|
+
4. **Test with screen readers** to ensure proper announcements
|
|
1138
|
+
5. **Consider keyboard users** for navigation
|
|
1139
|
+
6. **Use consistent styling** for better visual accessibility
|