@libs-ui/components-datetime-input
v0.2.357-9
Published
> Component nhập giờ phút (HH:mm) hỗ trợ single time và time range, tự động format, auto-focus và validation tích hợp.
Readme
@libs-ui/components-datetime-input
Component nhập giờ phút (HH:mm) hỗ trợ single time và time range, tự động format, auto-focus và validation tích hợp.
Giới thiệu
LibsUiComponentsDatetimeInputComponent là component nhập liệu thời gian dạng giờ:phút (HH:mm). Component hỗ trợ hai chế độ: nhập giờ đơn lẻ (single time) và nhập khoảng thời gian (time range với From - To). Giá trị tự động được format thành định dạng chuẩn khi blur và tự động chuyển focus giữa các ô input giúp tăng tốc độ nhập liệu.
Tính năng
- ✅ Single Time Input: Nhập giờ phút đơn giản (HH:mm)
- ✅ Time Range Input: Nhập khoảng thời gian (From HH:mm - To HH:mm) với
[multiTime]="true" - ✅ Auto Format: Tự động format thành HH:mm khi blur (nhập "9" → hiển thị "09")
- ✅ Auto Focus: Tự động chuyển focus hours → minutes → toHours → toMinutes khi nhập đủ ký tự
- ✅ Backspace Navigation: Nhấn Backspace ở ô trống để quay lại ô trước
- ✅ Validation Required: Cấu hình validation bắt buộc nhập giá trị
- ✅ Validation Compare: Kiểm tra giờ kết thúc phải lớn hơn giờ bắt đầu
- ✅ Real-time Emit: Emit data ngay khi nhập từng ký tự với
[isEmitRealTime]="true" - ✅ FunctionsControl: Điều khiển validate, set error, reset error từ component cha
- ✅ Label tích hợp: Hỗ trợ cấu hình label đầy đủ qua
[labelConfig] - ✅ Disable / Readonly: Hỗ trợ trạng thái disable và readonly
Khi nào sử dụng
- Khi cần nhập giờ phút đơn giản (HH:mm) trong form
- Khi cần nhập khoảng thời gian (From - To) như giờ làm việc, giờ đặt lịch
- Khi cần validation bắt buộc hoặc so sánh thời gian bắt đầu/kết thúc
- Khi cần emit data real-time trong quá trình nhập để cập nhật UI phụ thuộc
- Khi cần điều khiển validate từ bên ngoài (submit form)
Cài đặt
npm install @libs-ui/components-datetime-inputImport
import {
LibsUiComponentsDatetimeInputComponent,
ISelectedTimeInput,
ISelectedMultiTime,
IDateTimeInputFunctionControlEvent,
IValidCompare,
} from '@libs-ui/components-datetime-input';Ví dụ sử dụng
1. Single Time — Nhập giờ đơn
import { Component } from '@angular/core';
import {
LibsUiComponentsDatetimeInputComponent,
ISelectedTimeInput,
} from '@libs-ui/components-datetime-input';
@Component({
selector: 'app-single-time',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [LibsUiComponentsDatetimeInputComponent],
template: `
<libs_ui-components-datetime-input
[labelConfig]="{ labelLeft: 'Giờ bắt đầu', required: true }"
(outEmitSingleTime)="handlerSingleTimeSelected($event)"
/>
`,
})
export class SingleTimeComponent {
protected handlerSingleTimeSelected(event: ISelectedTimeInput): void {
event.stopPropagation?.();
console.log('Giờ đã chọn:', event);
// { hours: 9, minute: 30 }
}
}2. Time Range — Nhập khoảng thời gian
import { Component, ChangeDetectionStrategy } from '@angular/core';
import {
LibsUiComponentsDatetimeInputComponent,
ISelectedMultiTime,
} from '@libs-ui/components-datetime-input';
@Component({
selector: 'app-time-range',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [LibsUiComponentsDatetimeInputComponent],
template: `
<libs_ui-components-datetime-input
[multiTime]="true"
[labelConfig]="{ labelLeft: 'Khoảng giờ làm việc', required: true }"
(outEmitMultiTime)="handlerTimeRangeSelected($event)"
/>
`,
})
export class TimeRangeComponent {
protected handlerTimeRangeSelected(event: ISelectedMultiTime): void {
event.from?.(); // { hours: 8, minute: 0 }
event.to?.(); // { hours: 17, minute: 30 }
}
}3. Time Range với Giá Trị Mặc Định (Preset)
import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
import {
LibsUiComponentsDatetimeInputComponent,
ISelectedMultiTime,
} from '@libs-ui/components-datetime-input';
@Component({
selector: 'app-time-preset',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [LibsUiComponentsDatetimeInputComponent],
template: `
<libs_ui-components-datetime-input
[multiTime]="true"
[selectedTime]="selectedTime()"
[labelConfig]="{ labelLeft: 'Giờ ca làm việc' }"
(outEmitMultiTime)="handlerTimeRangeSelected($event)"
/>
`,
})
export class TimePresetComponent {
// QUAN TRỌNG: from và to PHẢI là WritableSignal
readonly selectedTime = signal<ISelectedMultiTime>({
from: signal({ hours: 8, minute: 0 }),
to: signal({ hours: 17, minute: 30 }),
});
protected handlerTimeRangeSelected(event: ISelectedMultiTime): void {
console.log('Khoảng giờ:', event.from?.(), event.to?.());
}
}4. Validation — Required và So Sánh Thời Gian
import { Component, ChangeDetectionStrategy } from '@angular/core';
import {
LibsUiComponentsDatetimeInputComponent,
ISelectedMultiTime,
} from '@libs-ui/components-datetime-input';
@Component({
selector: 'app-time-validation',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [LibsUiComponentsDatetimeInputComponent],
template: `
<libs_ui-components-datetime-input
[multiTime]="true"
[labelConfig]="{ labelLeft: 'Giờ làm thêm', required: true }"
[validRequired]="{ message: 'Vui lòng nhập giờ' }"
[validCompareTime]="{
message: 'Giờ kết thúc phải lớn hơn giờ bắt đầu',
isCheckErrorTimeEndGreaterTimeStart: true
}"
(outEmitMultiTime)="handlerTimeRangeSelected($event)"
(outEmitValid)="handlerValidChange($event)"
/>
`,
})
export class TimeValidationComponent {
protected handlerTimeRangeSelected(event: ISelectedMultiTime): void {
console.log('Time range:', event);
}
protected handlerValidChange(valid: { validRequired: boolean; validCompare: boolean }): void {
console.log('validRequired:', valid.validRequired);
console.log('validCompare:', valid.validCompare);
}
}5. FunctionsControl — Điều Khiển Từ Bên Ngoài
import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
import {
LibsUiComponentsDatetimeInputComponent,
ISelectedMultiTime,
IDateTimeInputFunctionControlEvent,
} from '@libs-ui/components-datetime-input';
@Component({
selector: 'app-functions-control',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [LibsUiComponentsDatetimeInputComponent],
template: `
<libs_ui-components-datetime-input
[multiTime]="true"
[labelConfig]="{ labelLeft: 'Giờ làm việc' }"
[validRequired]="{ message: 'Vui lòng nhập giờ' }"
(outFunctionsControl)="datetimeControls.set($event)"
(outEmitMultiTime)="handlerTimeRangeSelected($event)"
/>
<button (click)="handlerSubmit($event)">Lưu</button>
`,
})
export class FunctionsControlComponent {
protected readonly datetimeControls = signal<IDateTimeInputFunctionControlEvent | null>(null);
protected handlerTimeRangeSelected(event: ISelectedMultiTime): void {
console.log('Time range:', event);
}
protected async handlerSubmit(event: Event): Promise<void> {
event.stopPropagation();
const isValid = await this.datetimeControls()?.checkIsValid();
if (!isValid) {
return;
}
console.log('Form valid, submit...');
}
protected async handlerSetError(event: Event): Promise<void> {
event.stopPropagation();
await this.datetimeControls()?.setMessageError('Thời gian không hợp lệ với lịch làm việc');
}
protected async handlerResetError(event: Event): Promise<void> {
event.stopPropagation();
await this.datetimeControls()?.resetError();
}
}6. Disable / Readonly
import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
import {
LibsUiComponentsDatetimeInputComponent,
ISelectedMultiTime,
} from '@libs-ui/components-datetime-input';
@Component({
selector: 'app-disable-readonly',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [LibsUiComponentsDatetimeInputComponent],
template: `
<!-- Readonly — hiển thị giá trị, không cho nhập -->
<libs_ui-components-datetime-input
[multiTime]="true"
[readonly]="true"
[selectedTime]="workingHours()"
[labelConfig]="{ labelLeft: 'Giờ làm việc (chỉ xem)' }"
/>
<!-- Disable — xám toàn bộ -->
<libs_ui-components-datetime-input
[disable]="true"
[labelConfig]="{ labelLeft: 'Giờ nghỉ (không áp dụng)' }"
/>
`,
})
export class DisableReadonlyComponent {
readonly workingHours = signal<ISelectedMultiTime>({
from: signal({ hours: 8, minute: 0 }),
to: signal({ hours: 17, minute: 0 }),
});
}@Input()
| Input | Type | Default | Mô tả | Ví dụ |
|---|---|---|---|---|
| [classDatePickerInput] | string | undefined | CSS class bổ sung cho wrapper input | classDatePickerInput="my-time-input" |
| [classIncludeBetweenTime] | string \| undefined | undefined | CSS class cho ký tự "-" phân cách giữa From và To | classIncludeBetweenTime="mx-2" |
| [classIncludeInput] | string | 'libs-ui-font-h5r' | CSS class cho từng input hours/minutes | classIncludeInput="libs-ui-font-h4r" |
| [defaultHeight] | number | 28 | Chiều cao mặc định (px) của ô input | [defaultHeight]="36" |
| [disable] | boolean | false | Vô hiệu hóa toàn bộ input | [disable]="true" |
| [emitDataSingleDataWhenUseMultiTime] | boolean | false | Khi dùng multiTime, emit từng phần (from/to) riêng biệt dù chưa nhập đủ cả hai | [emitDataSingleDataWhenUseMultiTime]="true" |
| [fromAndToDateLabel] | { from?: string; to?: string } | undefined | Label cho phần From và To của time range | [fromAndToDateLabel]="{ from: 'Từ', to: 'Đến' }" |
| [ignoreAllowEqualTime] | boolean | true | Khi true, From = To bị coi là lỗi khi dùng isCheckErrorTimeEndGreaterTimeStart. Đặt false để cho phép bằng nhau | [ignoreAllowEqualTime]="false" |
| [ignoreBorder] | boolean | false | Ẩn border của wrapper input | [ignoreBorder]="true" |
| [ignoreCheckValidWhenInput] | boolean | false | Bỏ qua validate ngay khi nhập từ bàn phím; chỉ validate khi gọi checkIsValid() | [ignoreCheckValidWhenInput]="true" |
| [ignoreMultiIcon] | boolean | false | Ẩn icon đồng hồ giữa phần From và To | [ignoreMultiIcon]="true" |
| [ignoreShowValid] | boolean | false | Ẩn thông báo lỗi validation phía dưới input | [ignoreShowValid]="true" |
| [ignoreWithDefault] | boolean | false | Bỏ qua width mặc định của wrapper, tự do tùy chỉnh kích thước | [ignoreWithDefault]="true" |
| [isEmitRealTime] | boolean | false | Emit outEmitRealTime ngay khi người dùng nhập từng ký tự | [isEmitRealTime]="true" |
| [labelConfig] | ILabel \| undefined | undefined | Cấu hình label phía trên input (labelLeft, required, description, buttons...) | [labelConfig]="{ labelLeft: 'Giờ vào', required: true }" |
| [multiTime] | boolean | false | Bật chế độ nhập khoảng thời gian From - To | [multiTime]="true" |
| [readonly] | boolean | false | Chỉ hiển thị giá trị, không cho phép chỉnh sửa | [readonly]="true" |
| [resetInput] | boolean | true | Hiển thị nút xóa (X) khi có giá trị | [resetInput]="false" |
| [selectedTime] | ISelectedMultiTime \| undefined | undefined | Giá trị được set sẵn từ bên ngoài. from và to BẮT BUỘC là WritableSignal | [selectedTime]="selectedTime()" |
| [showBorderError] | boolean | false | Hiển thị border màu đỏ (error state) mà không cần có lỗi validation thực sự | [showBorderError]="true" |
| [validCompareTime] | IValidCompare \| undefined | undefined | Cấu hình validation so sánh From và To | [validCompareTime]="{ message: 'Giờ kết thúc phải sau giờ bắt đầu', isCheckErrorTimeEndGreaterTimeStart: true }" |
| [validRequired] | IMessageTranslate \| undefined | undefined | Cấu hình validation bắt buộc nhập giá trị | [validRequired]="{ message: 'Vui lòng nhập giờ' }" |
@Output()
| Output | Type | Mô tả | Handler TS | Binding HTML |
|---|---|---|---|---|
| (outClickButtonLabel) | IButton | Emit khi click một button được cấu hình trên label | handlerClickButtonLabel(e: IButton): void { e.stopPropagation?.(); } | (outClickButtonLabel)="handlerClickButtonLabel($event)" |
| (outEmitMultiTime) | ISelectedMultiTime | Emit khi giá trị time range thay đổi và hợp lệ (cả from và to đã nhập đủ) | handlerEmitMultiTime(e: ISelectedMultiTime): void { } | (outEmitMultiTime)="handlerEmitMultiTime($event)" |
| (outEmitRealTime) | ISelectedMultiTime | Emit real-time ngay khi nhập từng ký tự (cần bật [isEmitRealTime]="true") | handlerEmitRealTime(e: ISelectedMultiTime): void { } | (outEmitRealTime)="handlerEmitRealTime($event)" |
| (outEmitSingleTime) | ISelectedTimeInput | Emit khi giá trị single time thay đổi và hợp lệ | handlerEmitSingleTime(e: ISelectedTimeInput): void { } | (outEmitSingleTime)="handlerEmitSingleTime($event)" |
| (outEmitValid) | { validRequired: boolean; validCompare: boolean } | Emit trạng thái validation sau mỗi lần kiểm tra | handlerEmitValid(e: { validRequired: boolean; validCompare: boolean }): void { } | (outEmitValid)="handlerEmitValid($event)" |
| (outFunctionsControl) | IDateTimeInputFunctionControlEvent | Emit object chứa các hàm điều khiển component từ bên ngoài | handlerFunctionsControl(e: IDateTimeInputFunctionControlEvent): void { this.controls.set(e); } | (outFunctionsControl)="handlerFunctionsControl($event)" |
| (outLabelRightClick) | boolean | Emit true khi click vào phần label bên phải | handlerLabelRightClick(e: boolean): void { e.stopPropagation?.(); } | (outLabelRightClick)="handlerLabelRightClick($event)" |
| (outResetTime) | ISelectedMultiTime \| undefined | Emit undefined khi người dùng nhấn nút xóa (X) | handlerResetTime(e: ISelectedMultiTime \| undefined): void { } | (outResetTime)="handlerResetTime($event)" |
| (outSwitchEventLabel) | ISwitchEvent | Emit khi toggle switch trên label thay đổi | handlerSwitchEventLabel(e: ISwitchEvent): void { e.stopPropagation?.(); } | (outSwitchEventLabel)="handlerSwitchEventLabel($event)" |
FunctionsControl API
Nhận object IDateTimeInputFunctionControlEvent qua (outFunctionsControl) để điều khiển component từ bên ngoài.
| Method | Signature | Mô tả |
|---|---|---|
| checkIsValid | () => Promise<boolean> | Kích hoạt kiểm tra validation và trả về true nếu hợp lệ, false nếu có lỗi |
| resetError | (resetAll?: boolean) => Promise<void> | Xóa toàn bộ trạng thái lỗi hiện tại |
| setMessageError | (message: string \| undefined) => Promise<void> | Hiển thị message lỗi tùy chỉnh; truyền undefined để xóa lỗi |
// Cách sử dụng FunctionsControl
protected readonly controls = signal<IDateTimeInputFunctionControlEvent | null>(null);
protected async handlerSubmit(event: Event): Promise<void> {
event.stopPropagation();
const isValid = await this.controls()?.checkIsValid();
if (!isValid) {
return;
}
// submit logic...
}
protected async handlerSetError(event: Event): Promise<void> {
event.stopPropagation();
await this.controls()?.setMessageError('Trùng với ca làm việc khác');
}
protected async handlerResetError(event: Event): Promise<void> {
event.stopPropagation();
await this.controls()?.resetError();
}Types & Interfaces
import {
ISelectedTimeInput,
ISelectedMultiTime,
IValidCompare,
IDateTimeInputFunctionControlEvent,
} from '@libs-ui/components-datetime-input';ISelectedTimeInput
Dữ liệu của một mốc thời gian đơn (hours:minutes).
export interface ISelectedTimeInput {
hours?: string | number; // Giờ (0-23)
minute?: string | number; // Phút (0-59)
}ISelectedMultiTime
Dữ liệu khoảng thời gian (From - To). from và to bắt buộc là WritableSignal.
import { WritableSignal } from '@angular/core';
export interface ISelectedMultiTime {
from?: WritableSignal<ISelectedTimeInput>; // Thời gian bắt đầu
to?: WritableSignal<ISelectedTimeInput>; // Thời gian kết thúc
}
// Cách khởi tạo đúng
const myTime = signal<ISelectedMultiTime>({
from: signal({ hours: 8, minute: 0 }),
to: signal({ hours: 17, minute: 30 }),
});
// Cách đọc giá trị
myTime().from?.() // { hours: 8, minute: 0 }
myTime().to?.() // { hours: 17, minute: 30 }IValidCompare
Cấu hình validation so sánh giờ bắt đầu và kết thúc. Extends IMessageTranslate.
import { IMessageTranslate } from '@libs-ui/interfaces-types';
export interface IValidCompare extends IMessageTranslate {
isCheckErrorTimeEndGreaterTimeStart?: boolean; // true: báo lỗi nếu From >= To
isCheckErrorTimeDuplicate?: boolean; // true: báo lỗi nếu From === To
}
// IMessageTranslate
// interface IMessageTranslate {
// message?: string;
// interpolateParams?: object;
// }IDateTimeInputFunctionControlEvent
Object chứa các hàm điều khiển component từ bên ngoài.
export interface IDateTimeInputFunctionControlEvent {
checkIsValid: () => Promise<boolean>;
resetError: (resetAll?: boolean) => Promise<void>;
setMessageError: (message: string | undefined) => Promise<void>;
}Lưu ý quan trọng
⚠️ WritableSignal bắt buộc cho selectedTime: Các field from và to trong ISelectedMultiTime PHẢI được tạo bằng signal(). Truyền plain object sẽ gây lỗi runtime vì component gọi .() để unwrap.
// ĐÚNG
selectedTime = signal<ISelectedMultiTime>({
from: signal({ hours: 9, minute: 30 }),
to: signal({ hours: 17, minute: 0 }),
});
// SAI — sẽ lỗi runtime
selectedTime = signal<ISelectedMultiTime>({
from: { hours: 9, minute: 30 }, // plain object, không phải signal
to: { hours: 17, minute: 0 },
});⚠️ ignoreAllowEqualTime mặc định true: Khi dùng isCheckErrorTimeEndGreaterTimeStart: true, mặc định From bằng To cũng bị coi là lỗi. Để cho phép From = To, thêm [ignoreAllowEqualTime]="false".
⚠️ Backspace navigation cần nhấn 2 lần: Khi ô input đang trống và nhấn Backspace, lần 1 xử lý xóa giá trị (không có gì để xóa), lần 2 mới chuyển focus về ô trước. Đây là hành vi thiết kế có chủ đích.
⚠️ Validation không chạy khi disable hoặc readonly: checkIsValid() trả về true ngay lập tức khi component đang ở trạng thái [disable]="true" hoặc [readonly]="true".
⚠️ outEmitMultiTime chỉ emit khi đủ dữ liệu: Theo mặc định (emitDataSingleDataWhenUseMultiTime = false), outEmitMultiTime chỉ emit khi cả From và To đã nhập đủ giờ và phút. Dùng [emitDataSingleDataWhenUseMultiTime]="true" để emit từng phần.
