@libs-ui/components-inputs-range-slider
v0.2.357-11
Published
> Component thanh trượt (range slider) cho phép người dùng chọn một giá trị số trong khoảng Min/Max bằng cách kéo, hỗ trợ hai chế độ hiển thị và nhãn giá trị động thông minh.
Readme
@libs-ui/components-inputs-range-slider
Component thanh trượt (range slider) cho phép người dùng chọn một giá trị số trong khoảng Min/Max bằng cách kéo, hỗ trợ hai chế độ hiển thị và nhãn giá trị động thông minh.
Giới thiệu
@libs-ui/components-inputs-range-slider là một Angular Standalone Component cung cấp thanh trượt (range input) đầy đủ tính năng. Component tự động hiển thị nhãn giá trị hiện tại theo vị trí con trỏ kéo và thông minh ẩn/hiện nhãn Min/Max khi bị chồng lấp. Tiến trình (progress) được điều khiển qua CSS Variables giúp hiệu năng rendering cao và dễ tuỳ chỉnh giao diện.
Tính năng
- ✅ Hai chế độ:
slider(có nhãn Min/Max/Value) vàaudio(tối giản, dùng cho media player) - ✅ Nhãn giá trị hiện tại di chuyển theo vị trí kéo, luôn nằm đúng giữa thumb
- ✅ Tự động ẩn nhãn Min/Max khi bị chồng lấp bởi nhãn giá trị hiện tại
- ✅ Two-way binding qua
model()signal với cú pháp[(value)] - ✅ Output
outChangeđược debounce 250ms, tránh phát quá nhiều sự kiện khi đang kéo - ✅ Hỗ trợ bước nhảy
steptuỳ chỉnh - ✅ Hỗ trợ đơn vị đo
unithiển thị sau giá trị (%, px, đ, ...) - ✅ Định dạng số theo ngôn ngữ qua
formatNumber - ✅ Trạng thái
disable— ngăn thay đổi giá trị và giữ nguyên giá trị cũ - ✅ Class bổ sung qua
classIncludekhông ghi đè class gốc container
Khi nào sử dụng
- Điều chỉnh âm lượng, độ sáng hoặc các thông số kỹ thuật dạng số
- Chọn khoảng giá trong bộ lọc tìm kiếm (price range)
- Thiết lập cấu hình có giới hạn Min/Max rõ ràng
- Cần interface trực quan, tốn ít diện tích hơn dạng Number Input
- Điều khiển tiến trình phát nhạc/video (dùng mode
audio)
Cài đặt
npm install @libs-ui/components-inputs-range-sliderImport
import { LibsUiComponentsInputsRangeSliderComponent } from '@libs-ui/components-inputs-range-slider';
@Component({
standalone: true,
imports: [LibsUiComponentsInputsRangeSliderComponent],
// ...
})
export class YourComponent {}Ví dụ sử dụng
Cơ bản — slider có nhãn đơn vị
// component.ts
import { Component, signal } from '@angular/core';
import { LibsUiComponentsInputsRangeSliderComponent } from '@libs-ui/components-inputs-range-slider';
@Component({
standalone: true,
imports: [LibsUiComponentsInputsRangeSliderComponent],
templateUrl: './volume.component.html',
})
export class VolumeComponent {
protected volume = signal<number>(50);
protected handlerVolumeChange(value: number): void {
// outChange đã được debounce 250ms
console.log('Volume changed:', value);
}
}<!-- volume.component.html -->
<libs_ui-components-inputs-range_slider
[(value)]="volume"
[min]="0"
[max]="100"
[unit]="'%'"
(outChange)="handlerVolumeChange($event)">
</libs_ui-components-inputs-range_slider>
<p>Âm lượng: {{ volume() }}%</p>Chế độ audio — tối giản cho media player
// audio-player.component.ts
import { Component, signal } from '@angular/core';
import { LibsUiComponentsInputsRangeSliderComponent } from '@libs-ui/components-inputs-range-slider';
@Component({
standalone: true,
imports: [LibsUiComponentsInputsRangeSliderComponent],
templateUrl: './audio-player.component.html',
})
export class AudioPlayerComponent {
protected currentTime = signal<number>(0);
protected duration = 180; // giây
protected handlerSeek(value: number): void {
// Seek đến vị trí mới trong audio
this.audioService.seekTo(value);
}
}<!-- audio-player.component.html -->
<libs_ui-components-inputs-range_slider
[(value)]="currentTime"
[mode]="'audio'"
[min]="0"
[max]="duration"
[step]="1"
(outChange)="handlerSeek($event)">
</libs_ui-components-inputs-range_slider>Bước nhảy (Step) — chỉ cho phép giá trị rời rạc
// config-panel.component.ts
import { Component, signal } from '@angular/core';
import { LibsUiComponentsInputsRangeSliderComponent } from '@libs-ui/components-inputs-range-slider';
@Component({
standalone: true,
imports: [LibsUiComponentsInputsRangeSliderComponent],
templateUrl: './config-panel.component.html',
})
export class ConfigPanelComponent {
protected pageSize = signal<number>(20);
protected handlerPageSizeChange(value: number): void {
this.loadData(value);
}
private loadData(size: number): void {
// reload với pageSize mới
}
}<!-- config-panel.component.html -->
<libs_ui-components-inputs-range_slider
[(value)]="pageSize"
[min]="10"
[max]="100"
[step]="10"
[unit]="'rows'"
(outChange)="handlerPageSizeChange($event)">
</libs_ui-components-inputs-range_slider>
<p>Hiển thị {{ pageSize() }} hàng mỗi trang</p>Định dạng số — hiển thị số lớn dễ đọc
// price-filter.component.ts
import { Component, signal } from '@angular/core';
import { LibsUiComponentsInputsRangeSliderComponent } from '@libs-ui/components-inputs-range-slider';
@Component({
standalone: true,
imports: [LibsUiComponentsInputsRangeSliderComponent],
templateUrl: './price-filter.component.html',
})
export class PriceFilterComponent {
protected budget = signal<number>(5000000);
protected handlerBudgetChange(value: number): void {
this.filterProducts(value);
}
private filterProducts(maxPrice: number): void {
// filter sản phẩm theo giá
}
}<!-- price-filter.component.html -->
<libs_ui-components-inputs-range_slider
[(value)]="budget"
[min]="0"
[max]="10000000"
[formatNumber]="true"
[unit]="'đ'"
(outChange)="handlerBudgetChange($event)">
</libs_ui-components-inputs-range_slider>
<!-- maxDisplay hiển thị "10,0" thay vì 10000000 -->Ẩn nhãn giá trị — chỉ thanh kéo đơn thuần
<libs_ui-components-inputs-range_slider
[(value)]="brightness"
[min]="0"
[max]="100"
[hideProgressingValue]="true"
[unit]="'%'">
</libs_ui-components-inputs-range_slider>Disabled — vô hiệu hoá tương tác
<libs_ui-components-inputs-range_slider
[(value)]="readonlyValue"
[min]="0"
[max]="100"
[disable]="true">
</libs_ui-components-inputs-range_slider>@Input()
| Input | Type | Default | Mô tả | Ví dụ |
|---|---|---|---|---|
| value | number (model) | 0 | Giá trị hiện tại — dùng two-way binding [(value)]. Là Angular model() signal. | [(value)]="volume" |
| mode | 'audio' \| 'slider' | 'slider' | Chế độ hiển thị: slider có thanh nhãn Min/Value/Max; audio chỉ hiển thị thanh kéo tối giản (không nhãn, không padding top). | [mode]="'audio'" |
| min | number | 1 | Giá trị tối thiểu của khoảng. | [min]="0" |
| max | number | 100 | Giá trị tối đa của khoảng. | [max]="200" |
| step | number | 0 | Bước nhảy khi kéo. 0 là kéo liên tục mượt mà. | [step]="10" |
| unit | string | undefined | Đơn vị đo hiển thị sau mỗi nhãn giá trị (Min, Value, Max). | [unit]="'%'" |
| disable | boolean | false | Vô hiệu hoá tương tác. Khi bằng true, người dùng không kéo được; giá trị được giữ nguyên dù có thao tác chuột. | [disable]="true" |
| formatNumber | boolean | false | Bật định dạng số cho nhãn Max (maxDisplay). Ví dụ: 10000 → 10,0. Sử dụng viewDataNumberByLanguage từ @libs-ui/utils. | [formatNumber]="true" |
| hideProgressingValue | boolean | undefined | Ẩn nhãn giá trị hiện tại di chuyển theo thumb. Chỉ còn nhãn Min và Max cố định. | [hideProgressingValue]="true" |
| classInclude | string | undefined | Class CSS bổ sung cho div container ngoài cùng. Class libs-ui-range-slider-container luôn được giữ nguyên, giá trị truyền vào được nối thêm sau. | [classInclude]="'mt-4 w-64'" |
@Output()
| Output | Type | Mô tả | Handler TS | Binding HTML |
|---|---|---|---|---|
| (outChange) | number | Phát ra giá trị mới sau khi người dùng kéo. Được debounce 250ms — không phát liên tục khi đang kéo. Với mode audio, sự kiện bị skip lần emit đầu tiên (khi khởi tạo). | handlerChange(value: number): void { value; } | (outChange)="handlerChange($event)" |
Logic ngầm quan trọng
Tính toán vị trí nhãn giá trị (Tooltip Position)
Nhãn giá trị hiện tại được định vị tuyệt đối bên dưới thanh kéo. Vị trí (left) được tính dựa trên tỷ lệ % của giá trị trong khoảng [min, max], nhân với chiều rộng container, rồi trừ đi nửa chiều rộng của chính nhãn để căn giữa. Có xử lý thêm để nhãn không tràn ra ngoài phạm vi container.
Xử lý chồng lấp nhãn (Overlap Detection)
Sử dụng getBoundingClientRect() để so sánh vị trí thực tế của nhãn giá trị hiện tại với nhãn Min và Max. Khi nhãn giá trị chạm đến vùng Min hoặc Max, nhãn tương ứng sẽ bị ẩn đi (qua signal visibilityMin / visibilityMax). Khi value = 0, nhãn Min tự động ẩn.
CSS Variables Progress
Phần màu tiến trình (màu xanh đã kéo) không dùng JavaScript để cập nhật DOM trực tiếp mà thông qua CSS custom property --libs-ui-slider-value được set theo phần trăm. Hiệu ứng hover/focus đổi màu thanh trượt nhạt hơn thông qua --libs-ui-slider-border-box-shadow.
Debounce Output
outChange được pipe qua debounceTime(250). Khi người dùng kéo liên tục, chỉ sau khi dừng kéo 250ms thì giá trị mới được emit, tránh gọi API hoặc tính toán nặng liên tục trong khi kéo. Với mode audio, skip 1 lần emit đầu (khi component init) để tránh trigger seek không cần thiết.
Lưu ý quan trọng
⚠️ Two-way binding bắt buộc: value là Angular model() signal — phải dùng [(value)] để đọc giá trị từ bên ngoài và nhận cập nhật ngược lại từ component. Nếu chỉ dùng [value] (một chiều), giá trị trong template ngoài sẽ không tự cập nhật.
⚠️ Debounce 250ms: outChange không phát theo thời gian thực khi đang kéo. Nếu cần phản hồi tức thì (ví dụ: preview màu sắc), hãy đọc trực tiếp signal [(value)] thay vì lắng nghe outChange.
⚠️ mode audio không có nhãn: Khi mode="audio", component không render phần nhãn Min/Value/Max và có chiều cao nhỏ hơn (22px thay vì 44px với padding top). Các input unit, hideProgressingValue, formatNumber không có tác dụng trực quan trong mode này.
⚠️ min mặc định là 1: Giá trị mặc định của min là 1, không phải 0. Nếu cần khoảng bắt đầu từ 0, phải truyền rõ [min]="0".
⚠️ disable giữ nguyên giá trị: Khi disable=true, component chủ động override giá trị input về giá trị cũ ngay trong handler — người dùng không thể thay đổi bằng cách kéo dù DOM input không bị disabled attribute.
⚠️ classInclude không ghi đè: Class libs-ui-range-slider-container luôn được giữ lại trên container dù classInclude được truyền vào hay không. Các class truyền vào được nối thêm sau.
