@libs-ui/components-datetime-input 0.2.356-41 → 0.2.356-43

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,272 +1,424 @@
1
- # Datetime Input Component
1
+ # @libs-ui/components-datetime-input
2
2
 
3
- > **Version**: 0.2.355-15
3
+ > 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.
4
4
 
5
- Component nhập giờ phút với hỗ trợ single time và time range, tự động format và validation.
5
+ ## Giới thiệu
6
+
7
+ `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.
6
8
 
7
9
  ## Tính năng
8
10
 
9
11
  - ✅ **Single Time Input**: Nhập giờ phút đơn giản (HH:mm)
10
- - ✅ **Time Range Input**: Nhập khoảng thời gian (From - To)
11
- - ✅ **Auto Format**: Tự động format thành HH:mm khi blur
12
- - ✅ **Auto Focus**: Tự động chuyển focus giữa hours minutes
13
- - ✅ **Validation**: Hỗ trợ validation required compare time
14
- - ✅ **Real-time Emit**: Emit data real-time khi nhập
15
- - ✅ **Keyboard Support**: Hỗ trợ đầy đủ keyboard navigation
16
- - ✅ **FunctionsControl**: Điều khiển component từ bên ngoài
12
+ - ✅ **Time Range Input**: Nhập khoảng thời gian (From HH:mm - To HH:mm) với `[multiTime]="true"`
13
+ - ✅ **Auto Format**: Tự động format thành HH:mm khi blur (nhập "9" → hiển thị "09")
14
+ - ✅ **Auto Focus**: Tự động chuyển focus hours minutes → toHours → toMinutes khi nhập đủ ký tự
15
+ - ✅ **Backspace Navigation**: Nhấn Backspace ô trống để quay lại ô trước
16
+ - ✅ **Validation Required**: Cấu hình validation bắt buộc nhập giá trị
17
+ - ✅ **Validation Compare**: Kiểm tra giờ kết thúc phải lớn hơn giờ bắt đầu
18
+ - ✅ **Real-time Emit**: Emit data ngay khi nhập từng ký tự với `[isEmitRealTime]="true"`
19
+ - ✅ **FunctionsControl**: Điều khiển validate, set error, reset error từ component cha
20
+ - ✅ **Label tích hợp**: Hỗ trợ cấu hình label đầy đủ qua `[labelConfig]`
21
+ - ✅ **Disable / Readonly**: Hỗ trợ trạng thái disable và readonly
17
22
 
18
23
  ## Khi nào sử dụng
19
24
 
20
- - Khi cần nhập giờ phút đơn giản (HH:mm)
21
- - Khi cần nhập khoảng thời gian (From - To)
22
- - Khi cần validation cho time input (required, compare time)
23
- - Khi cần so sánh thời gian bắt đầu kết thúc
24
- - Khi cần format time input tự động
25
- - Khi cần emit data real-time trong quá trình nhập
26
-
27
- ## Important Notes
28
-
29
- ⚠️ `ISelectedMultiTime` sử dụng `WritableSignal` — phải tạo bằng `signal()` khi truyền vào `[selectedTime]`
30
-
31
- ⚠️ Mặc định `[ignoreAllowEqualTime]="true"` — From và To bằng nhau sẽ bị coi là lỗi khi dùng `isCheckErrorTimeEndGreaterTimeStart`
32
-
33
- ⚠️ Auto Focus tự động chuyển focus: hours → minutes → toHours → toMinutes khi nhập đủ ký tự
25
+ - Khi cần nhập giờ phút đơn giản (HH:mm) trong form
26
+ - Khi cần nhập khoảng thời gian (From - To) như giờ làm việc, giờ đặt lịch
27
+ - Khi cần validation bắt buộc hoặc so sánh thời gian bắt đầu/kết thúc
28
+ - Khi cần emit data real-time trong quá trình nhập để cập nhật UI phụ thuộc
29
+ - Khi cần điều khiển validate từ bên ngoài (submit form)
34
30
 
35
- ⚠️ Component tự động format thành HH:mm khi blur (vd: nhập "9" → hiển thị "09")
36
-
37
- ⚠️ Validation chỉ chạy khi không ở trạng thái disable hoặc readonly
38
-
39
- ## Installation
31
+ ## Cài đặt
40
32
 
41
33
  ```bash
42
34
  npm install @libs-ui/components-datetime-input
43
35
  ```
44
36
 
45
- ## Usage
37
+ ## Import
38
+
39
+ ```typescript
40
+ import {
41
+ LibsUiComponentsDatetimeInputComponent,
42
+ ISelectedTimeInput,
43
+ ISelectedMultiTime,
44
+ IDateTimeInputFunctionControlEvent,
45
+ IValidCompare,
46
+ } from '@libs-ui/components-datetime-input';
47
+ ```
48
+
49
+ ## Ví dụ sử dụng
46
50
 
47
- ### Basic Single Time
51
+ ### 1. Single Time — Nhập giờ đơn
48
52
 
49
53
  ```typescript
50
54
  import { Component } from '@angular/core';
51
- import { LibsUiComponentsDatetimeInputComponent, ISelectedTimeInput } from '@libs-ui/components-datetime-input';
55
+ import {
56
+ LibsUiComponentsDatetimeInputComponent,
57
+ ISelectedTimeInput,
58
+ } from '@libs-ui/components-datetime-input';
52
59
 
53
60
  @Component({
54
- selector: 'app-example',
61
+ selector: 'app-single-time',
55
62
  standalone: true,
63
+ changeDetection: ChangeDetectionStrategy.OnPush,
56
64
  imports: [LibsUiComponentsDatetimeInputComponent],
57
65
  template: `
58
66
  <libs_ui-components-datetime-input
59
- [labelConfig]="{ labelLeft: 'Chọn giờ' }"
60
- (outEmitSingleTime)="onTimeSelected($event)" />
67
+ [labelConfig]="{ labelLeft: 'Giờ bắt đầu', required: true }"
68
+ (outEmitSingleTime)="handlerSingleTimeSelected($event)"
69
+ />
61
70
  `,
62
71
  })
63
- export class ExampleComponent {
64
- onTimeSelected(event: ISelectedTimeInput) {
65
- console.log('Time:', event); // { hours: 9, minute: 30 }
72
+ export class SingleTimeComponent {
73
+ protected handlerSingleTimeSelected(event: ISelectedTimeInput): void {
74
+ event.stopPropagation?.();
75
+ console.log('Giờ đã chọn:', event);
76
+ // { hours: 9, minute: 30 }
66
77
  }
67
78
  }
68
79
  ```
69
80
 
70
- ### Time Range
81
+ ### 2. Time Range — Nhập khoảng thời gian
71
82
 
72
83
  ```typescript
73
- import { Component } from '@angular/core';
74
- import { LibsUiComponentsDatetimeInputComponent, ISelectedMultiTime } from '@libs-ui/components-datetime-input';
84
+ import { Component, ChangeDetectionStrategy } from '@angular/core';
85
+ import {
86
+ LibsUiComponentsDatetimeInputComponent,
87
+ ISelectedMultiTime,
88
+ } from '@libs-ui/components-datetime-input';
75
89
 
76
90
  @Component({
77
- selector: 'app-example',
91
+ selector: 'app-time-range',
78
92
  standalone: true,
93
+ changeDetection: ChangeDetectionStrategy.OnPush,
79
94
  imports: [LibsUiComponentsDatetimeInputComponent],
80
95
  template: `
81
96
  <libs_ui-components-datetime-input
82
97
  [multiTime]="true"
83
- [labelConfig]="{ labelLeft: 'Khoảng giờ' }"
84
- (outEmitMultiTime)="onTimeRangeSelected($event)" />
98
+ [labelConfig]="{ labelLeft: 'Khoảng giờ làm việc', required: true }"
99
+ (outEmitMultiTime)="handlerTimeRangeSelected($event)"
100
+ />
85
101
  `,
86
102
  })
87
- export class ExampleComponent {
88
- onTimeRangeSelected(event: ISelectedMultiTime) {
89
- console.log('From:', event.from?.());
90
- console.log('To:', event.to?.());
103
+ export class TimeRangeComponent {
104
+ protected handlerTimeRangeSelected(event: ISelectedMultiTime): void {
105
+ event.from?.(); // { hours: 8, minute: 0 }
106
+ event.to?.(); // { hours: 17, minute: 30 }
91
107
  }
92
108
  }
93
109
  ```
94
110
 
95
- ### With Preset Value
111
+ ### 3. Time Range với Giá Trị Mặc Định (Preset)
96
112
 
97
113
  ```typescript
98
- import { Component, signal } from '@angular/core';
99
- import { ISelectedMultiTime } from '@libs-ui/components-datetime-input';
114
+ import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
115
+ import {
116
+ LibsUiComponentsDatetimeInputComponent,
117
+ ISelectedMultiTime,
118
+ } from '@libs-ui/components-datetime-input';
100
119
 
101
120
  @Component({
102
- // ...
121
+ selector: 'app-time-preset',
122
+ standalone: true,
123
+ changeDetection: ChangeDetectionStrategy.OnPush,
124
+ imports: [LibsUiComponentsDatetimeInputComponent],
103
125
  template: `
104
126
  <libs_ui-components-datetime-input
105
127
  [multiTime]="true"
106
128
  [selectedTime]="selectedTime()"
107
- [labelConfig]="{ labelLeft: 'Giờ làm việc' }"
108
- (outEmitMultiTime)="onTimeRangeSelected($event)" />
129
+ [labelConfig]="{ labelLeft: 'Giờ ca làm việc' }"
130
+ (outEmitMultiTime)="handlerTimeRangeSelected($event)"
131
+ />
109
132
  `,
110
133
  })
111
- export class ExampleComponent {
112
- // ⚠️ from và to PHẢI là WritableSignal
134
+ export class TimePresetComponent {
135
+ // QUAN TRỌNG: from và to PHẢI là WritableSignal
113
136
  readonly selectedTime = signal<ISelectedMultiTime>({
114
- from: signal({ hours: 9, minute: 30 }),
115
- to: signal({ hours: 17, minute: 0 }),
137
+ from: signal({ hours: 8, minute: 0 }),
138
+ to: signal({ hours: 17, minute: 30 }),
116
139
  });
117
140
 
118
- onTimeRangeSelected(event: ISelectedMultiTime) {
119
- console.log('Time range:', event);
141
+ protected handlerTimeRangeSelected(event: ISelectedMultiTime): void {
142
+ console.log('Khoảng giờ:', event.from?.(), event.to?.());
120
143
  }
121
144
  }
122
145
  ```
123
146
 
124
- ### With Validation
147
+ ### 4. Validation — Required và So Sánh Thời Gian
125
148
 
126
149
  ```typescript
127
- import { Component } from '@angular/core';
150
+ import { Component, ChangeDetectionStrategy } from '@angular/core';
151
+ import {
152
+ LibsUiComponentsDatetimeInputComponent,
153
+ ISelectedMultiTime,
154
+ } from '@libs-ui/components-datetime-input';
128
155
 
129
156
  @Component({
130
- // ...
157
+ selector: 'app-time-validation',
158
+ standalone: true,
159
+ changeDetection: ChangeDetectionStrategy.OnPush,
160
+ imports: [LibsUiComponentsDatetimeInputComponent],
131
161
  template: `
132
162
  <libs_ui-components-datetime-input
133
163
  [multiTime]="true"
164
+ [labelConfig]="{ labelLeft: 'Giờ làm thêm', required: true }"
134
165
  [validRequired]="{ message: 'Vui lòng nhập giờ' }"
135
166
  [validCompareTime]="{
136
167
  message: 'Giờ kết thúc phải lớn hơn giờ bắt đầu',
137
- isCheckErrorTimeEndGreaterTimeStart: true,
168
+ isCheckErrorTimeEndGreaterTimeStart: true
138
169
  }"
139
- (outEmitMultiTime)="onTimeRangeSelected($event)"
140
- (outEmitValid)="onValidChange($event)" />
170
+ (outEmitMultiTime)="handlerTimeRangeSelected($event)"
171
+ (outEmitValid)="handlerValidChange($event)"
172
+ />
141
173
  `,
142
174
  })
143
- export class ExampleComponent {
144
- onTimeRangeSelected(event: ISelectedMultiTime) {
175
+ export class TimeValidationComponent {
176
+ protected handlerTimeRangeSelected(event: ISelectedMultiTime): void {
145
177
  console.log('Time range:', event);
146
178
  }
147
179
 
148
- onValidChange(valid: { validRequired: boolean; validCompare: boolean }) {
149
- console.log('Validation:', valid);
180
+ protected handlerValidChange(valid: { validRequired: boolean; validCompare: boolean }): void {
181
+ console.log('validRequired:', valid.validRequired);
182
+ console.log('validCompare:', valid.validCompare);
150
183
  }
151
184
  }
152
185
  ```
153
186
 
154
- ### FunctionsControl
187
+ ### 5. FunctionsControl — Điều Khiển Từ Bên Ngoài
155
188
 
156
189
  ```typescript
157
- import { Component, signal } from '@angular/core';
158
- import { IDateTimeInputFunctionControlEvent } from '@libs-ui/components-datetime-input';
190
+ import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
191
+ import {
192
+ LibsUiComponentsDatetimeInputComponent,
193
+ ISelectedMultiTime,
194
+ IDateTimeInputFunctionControlEvent,
195
+ } from '@libs-ui/components-datetime-input';
159
196
 
160
197
  @Component({
161
- // ...
198
+ selector: 'app-functions-control',
199
+ standalone: true,
200
+ changeDetection: ChangeDetectionStrategy.OnPush,
201
+ imports: [LibsUiComponentsDatetimeInputComponent],
162
202
  template: `
163
203
  <libs_ui-components-datetime-input
164
204
  [multiTime]="true"
205
+ [labelConfig]="{ labelLeft: 'Giờ làm việc' }"
165
206
  [validRequired]="{ message: 'Vui lòng nhập giờ' }"
166
- (outFunctionsControl)="controls.set($event)"
207
+ (outFunctionsControl)="datetimeControls.set($event)"
208
+ (outEmitMultiTime)="handlerTimeRangeSelected($event)"
167
209
  />
210
+ <button (click)="handlerSubmit($event)">Lưu</button>
168
211
  `,
169
212
  })
170
- export class ExampleComponent {
171
- controls = signal<IDateTimeInputFunctionControlEvent | null>(null);
213
+ export class FunctionsControlComponent {
214
+ protected readonly datetimeControls = signal<IDateTimeInputFunctionControlEvent | null>(null);
215
+
216
+ protected handlerTimeRangeSelected(event: ISelectedMultiTime): void {
217
+ console.log('Time range:', event);
218
+ }
172
219
 
173
- async validate() {
174
- const isValid = await this.controls()?.checkIsValid();
175
- console.log('Valid:', isValid);
220
+ protected async handlerSubmit(event: Event): Promise<void> {
221
+ event.stopPropagation();
222
+ const isValid = await this.datetimeControls()?.checkIsValid();
223
+ if (!isValid) {
224
+ return;
225
+ }
226
+ console.log('Form valid, submit...');
176
227
  }
177
228
 
178
- async setError() {
179
- await this.controls()?.setMessageError('Thời gian không hợp lệ');
229
+ protected async handlerSetError(event: Event): Promise<void> {
230
+ event.stopPropagation();
231
+ await this.datetimeControls()?.setMessageError('Thời gian không hợp lệ với lịch làm việc');
180
232
  }
181
233
 
182
- async resetError() {
183
- await this.controls()?.resetError();
234
+ protected async handlerResetError(event: Event): Promise<void> {
235
+ event.stopPropagation();
236
+ await this.datetimeControls()?.resetError();
184
237
  }
185
238
  }
186
239
  ```
187
240
 
188
- ## API
189
-
190
- ### Inputs
191
-
192
- | Property | Type | Default | Description |
193
- | ---------------------------------------- | -------------------------------- | ----------------- | ------------------------------------------------------------------------------- |
194
- | `[classDatePickerInput]` | `string` | `undefined` | CSS class cho date picker input |
195
- | `[classIncludeBetweenTime]` | `string` | `undefined` | CSS class cho phần between time (dấu -) |
196
- | `[classIncludeInput]` | `string` | `libs-ui-font-h5r`| CSS class cho input element |
197
- | `[defaultHeight]` | `number` | `28` | Chiều cao mặc định của input (px) |
198
- | `[disable]` | `boolean` | `false` | Disable input |
199
- | `[emitDataSingleDataWhenUseMultiTime]` | `boolean` | `false` | Emit data riêng từng phần khi dùng multiTime |
200
- | `[fromAndToDateLabel]` | `{ from?: string; to?: string }` | `undefined` | Label cho From và To |
201
- | `[ignoreAllowEqualTime]` | `boolean` | `true` | Khi true, From = To bị coi là lỗi (với isCheckErrorTimeEndGreaterTimeStart) |
202
- | `[ignoreBorder]` | `boolean` | `false` | Ẩn border của input |
203
- | `[ignoreCheckValidWhenInput]` | `boolean` | `false` | Bỏ qua validate khi nhập từ bàn phím |
204
- | `[ignoreMultiIcon]` | `boolean` | `false` | Ẩn icon giữa 2 time range |
205
- | `[ignoreShowValid]` | `boolean` | `false` | Ẩn hiển thị lỗi validation |
206
- | `[ignoreWithDefault]` | `boolean` | `false` | Bỏ qua giá trị default |
207
- | `[isEmitRealTime]` | `boolean` | `false` | Emit data real-time khi nhập (mỗi keystroke) |
208
- | `[labelConfig]` | `ILabel` | `undefined` | Cấu hình label (labelLeft, required, ...) |
209
- | `[multiTime]` | `boolean` | `false` | Chế độ chọn time range (From - To) |
210
- | `[readonly]` | `boolean` | `false` | Readonly mode |
211
- | `[resetInput]` | `boolean` | `true` | Cho phép reset input |
212
- | `[selectedTime]` | `ISelectedMultiTime` | `undefined` | Giá trị time được set sẵn (sử dụng WritableSignal) |
213
- | `[showBorderError]` | `boolean` | `false` | Hiển thị border error state |
214
- | `[validCompareTime]` | `IValidCompare` | `undefined` | Cấu hình validation so sánh time |
215
- | `[validRequired]` | `IMessageTranslate` | `undefined` | Cấu hình validation required |
216
-
217
- ### Outputs
218
-
219
- | Property | Type | Description |
220
- | ----------------------- | --------------------------------------------------- | ------------------------------------------------------ |
221
- | `(outClickButtonLabel)` | `IButton` | Event khi click button trên label |
222
- | `(outEmitMultiTime)` | `ISelectedMultiTime` | Event khi chọn time range (cả from và to) |
223
- | `(outEmitRealTime)` | `ISelectedMultiTime` | Event emit real-time khi nhập (mỗi keystroke) |
224
- | `(outEmitSingleTime)` | `ISelectedTimeInput` | Event khi chọn single time |
225
- | `(outEmitValid)` | `{ validRequired: boolean; validCompare: boolean }` | Event trạng thái validation |
226
- | `(outFunctionsControl)` | `IDateTimeInputFunctionControlEvent` | Emit functions để điều khiển component từ bên ngoài |
227
- | `(outLabelRightClick)` | `boolean` | Event khi click label bên phải |
228
- | `(outResetTime)` | `ISelectedMultiTime \| undefined` | Event khi reset time |
229
- | `(outSwitchEventLabel)` | `ISwitchEvent` | Event khi switch trên label thay đổi |
230
-
231
- ### FunctionsControl Methods
232
-
233
- | Method | Description |
234
- | ----------------------------- | -------------------------------------------------- |
235
- | `checkIsValid()` | Kiểm tra validation trả về `Promise<boolean>` |
236
- | `resetError(resetAll?)` | Reset tất cả trạng thái lỗi |
237
- | `setMessageError(message)` | Đặt message lỗi tùy chỉnh (undefined để xóa) |
238
-
239
- ## Types
241
+ ### 6. Disable / Readonly
242
+
243
+ ```typescript
244
+ import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
245
+ import {
246
+ LibsUiComponentsDatetimeInputComponent,
247
+ ISelectedMultiTime,
248
+ } from '@libs-ui/components-datetime-input';
249
+
250
+ @Component({
251
+ selector: 'app-disable-readonly',
252
+ standalone: true,
253
+ changeDetection: ChangeDetectionStrategy.OnPush,
254
+ imports: [LibsUiComponentsDatetimeInputComponent],
255
+ template: `
256
+ <!-- Readonly hiển thị giá trị, không cho nhập -->
257
+ <libs_ui-components-datetime-input
258
+ [multiTime]="true"
259
+ [readonly]="true"
260
+ [selectedTime]="workingHours()"
261
+ [labelConfig]="{ labelLeft: 'Giờ làm việc (chỉ xem)' }"
262
+ />
263
+
264
+ <!-- Disable xám toàn bộ -->
265
+ <libs_ui-components-datetime-input
266
+ [disable]="true"
267
+ [labelConfig]="{ labelLeft: 'Giờ nghỉ (không áp dụng)' }"
268
+ />
269
+ `,
270
+ })
271
+ export class DisableReadonlyComponent {
272
+ readonly workingHours = signal<ISelectedMultiTime>({
273
+ from: signal({ hours: 8, minute: 0 }),
274
+ to: signal({ hours: 17, minute: 0 }),
275
+ });
276
+ }
277
+ ```
278
+
279
+ ## @Input()
280
+
281
+ | Input | Type | Default | tả | Ví dụ |
282
+ |---|---|---|---|---|
283
+ | `[classDatePickerInput]` | `string` | `undefined` | CSS class bổ sung cho wrapper input | `classDatePickerInput="my-time-input"` |
284
+ | `[classIncludeBetweenTime]` | `string \| undefined` | `undefined` | CSS class cho ký tự "-" phân cách giữa From và To | `classIncludeBetweenTime="mx-2"` |
285
+ | `[classIncludeInput]` | `string` | `'libs-ui-font-h5r'` | CSS class cho từng input hours/minutes | `classIncludeInput="libs-ui-font-h4r"` |
286
+ | `[defaultHeight]` | `number` | `28` | Chiều cao mặc định (px) của ô input | `[defaultHeight]="36"` |
287
+ | `[disable]` | `boolean` | `false` | Vô hiệu hóa toàn bộ input | `[disable]="true"` |
288
+ | `[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"` |
289
+ | `[fromAndToDateLabel]` | `{ from?: string; to?: string }` | `undefined` | Label cho phần From và To của time range | `[fromAndToDateLabel]="{ from: 'Từ', to: 'Đến' }"` |
290
+ | `[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"` |
291
+ | `[ignoreBorder]` | `boolean` | `false` | Ẩn border của wrapper input | `[ignoreBorder]="true"` |
292
+ | `[ignoreCheckValidWhenInput]` | `boolean` | `false` | Bỏ qua validate ngay khi nhập từ bàn phím; chỉ validate khi gọi `checkIsValid()` | `[ignoreCheckValidWhenInput]="true"` |
293
+ | `[ignoreMultiIcon]` | `boolean` | `false` | Ẩn icon đồng hồ giữa phần From và To | `[ignoreMultiIcon]="true"` |
294
+ | `[ignoreShowValid]` | `boolean` | `false` | Ẩn thông báo lỗi validation phía dưới input | `[ignoreShowValid]="true"` |
295
+ | `[ignoreWithDefault]` | `boolean` | `false` | Bỏ qua width mặc định của wrapper, tự do tùy chỉnh kích thước | `[ignoreWithDefault]="true"` |
296
+ | `[isEmitRealTime]` | `boolean` | `false` | Emit `outEmitRealTime` ngay khi người dùng nhập từng ký tự | `[isEmitRealTime]="true"` |
297
+ | `[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 }"` |
298
+ | `[multiTime]` | `boolean` | `false` | Bật chế độ nhập khoảng thời gian From - To | `[multiTime]="true"` |
299
+ | `[readonly]` | `boolean` | `false` | Chỉ hiển thị giá trị, không cho phép chỉnh sửa | `[readonly]="true"` |
300
+ | `[resetInput]` | `boolean` | `true` | Hiển thị nút xóa (X) khi có giá trị | `[resetInput]="false"` |
301
+ | `[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()"` |
302
+ | `[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"` |
303
+ | `[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 }"` |
304
+ | `[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ờ' }"` |
305
+
306
+ ## @Output()
307
+
308
+ | Output | Type | Mô tả | Handler TS | Binding HTML |
309
+ |---|---|---|---|---|
310
+ | `(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)"` |
311
+ | `(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)"` |
312
+ | `(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)"` |
313
+ | `(outEmitSingleTime)` | `ISelectedTimeInput` | Emit khi giá trị single time thay đổi và hợp lệ | `handlerEmitSingleTime(e: ISelectedTimeInput): void { }` | `(outEmitSingleTime)="handlerEmitSingleTime($event)"` |
314
+ | `(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)"` |
315
+ | `(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)"` |
316
+ | `(outLabelRightClick)` | `boolean` | Emit `true` khi click vào phần label bên phải | `handlerLabelRightClick(e: boolean): void { e.stopPropagation?.(); }` | `(outLabelRightClick)="handlerLabelRightClick($event)"` |
317
+ | `(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)"` |
318
+ | `(outSwitchEventLabel)` | `ISwitchEvent` | Emit khi toggle switch trên label thay đổi | `handlerSwitchEventLabel(e: ISwitchEvent): void { e.stopPropagation?.(); }` | `(outSwitchEventLabel)="handlerSwitchEventLabel($event)"` |
319
+
320
+ ## FunctionsControl API
321
+
322
+ Nhận object `IDateTimeInputFunctionControlEvent` qua `(outFunctionsControl)` để điều khiển component từ bên ngoài.
323
+
324
+ | Method | Signature | Mô tả |
325
+ |---|---|---|
326
+ | `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 |
327
+ | `resetError` | `(resetAll?: boolean) => Promise<void>` | Xóa toàn bộ trạng thái lỗi hiện tại |
328
+ | `setMessageError` | `(message: string \| undefined) => Promise<void>` | Hiển thị message lỗi tùy chỉnh; truyền `undefined` để xóa lỗi |
329
+
330
+ ```typescript
331
+ // Cách sử dụng FunctionsControl
332
+ protected readonly controls = signal<IDateTimeInputFunctionControlEvent | null>(null);
333
+
334
+ protected async handlerSubmit(event: Event): Promise<void> {
335
+ event.stopPropagation();
336
+ const isValid = await this.controls()?.checkIsValid();
337
+ if (!isValid) {
338
+ return;
339
+ }
340
+ // submit logic...
341
+ }
342
+
343
+ protected async handlerSetError(event: Event): Promise<void> {
344
+ event.stopPropagation();
345
+ await this.controls()?.setMessageError('Trùng với ca làm việc khác');
346
+ }
347
+
348
+ protected async handlerResetError(event: Event): Promise<void> {
349
+ event.stopPropagation();
350
+ await this.controls()?.resetError();
351
+ }
352
+ ```
353
+
354
+ ## Types & Interfaces
355
+
356
+ ```typescript
357
+ import {
358
+ ISelectedTimeInput,
359
+ ISelectedMultiTime,
360
+ IValidCompare,
361
+ IDateTimeInputFunctionControlEvent,
362
+ } from '@libs-ui/components-datetime-input';
363
+ ```
240
364
 
241
365
  ### ISelectedTimeInput
242
366
 
367
+ Dữ liệu của một mốc thời gian đơn (hours:minutes).
368
+
243
369
  ```typescript
244
370
  export interface ISelectedTimeInput {
245
- hours?: string | number;
246
- minute?: string | number;
371
+ hours?: string | number; // Giờ (0-23)
372
+ minute?: string | number; // Phút (0-59)
247
373
  }
248
374
  ```
249
375
 
250
376
  ### ISelectedMultiTime
251
377
 
378
+ Dữ liệu khoảng thời gian (From - To). `from` và `to` bắt buộc là `WritableSignal`.
379
+
252
380
  ```typescript
381
+ import { WritableSignal } from '@angular/core';
382
+
253
383
  export interface ISelectedMultiTime {
254
- from?: WritableSignal<ISelectedTimeInput>;
255
- to?: WritableSignal<ISelectedTimeInput>;
384
+ from?: WritableSignal<ISelectedTimeInput>; // Thời gian bắt đầu
385
+ to?: WritableSignal<ISelectedTimeInput>; // Thời gian kết thúc
256
386
  }
387
+
388
+ // Cách khởi tạo đúng
389
+ const myTime = signal<ISelectedMultiTime>({
390
+ from: signal({ hours: 8, minute: 0 }),
391
+ to: signal({ hours: 17, minute: 30 }),
392
+ });
393
+
394
+ // Cách đọc giá trị
395
+ myTime().from?.() // { hours: 8, minute: 0 }
396
+ myTime().to?.() // { hours: 17, minute: 30 }
257
397
  ```
258
398
 
259
399
  ### IValidCompare
260
400
 
401
+ Cấu hình validation so sánh giờ bắt đầu và kết thúc. Extends `IMessageTranslate`.
402
+
261
403
  ```typescript
404
+ import { IMessageTranslate } from '@libs-ui/interfaces-types';
405
+
262
406
  export interface IValidCompare extends IMessageTranslate {
263
- isCheckErrorTimeEndGreaterTimeStart?: boolean;
264
- isCheckErrorTimeDuplicate?: boolean;
407
+ isCheckErrorTimeEndGreaterTimeStart?: boolean; // true: báo lỗi nếu From >= To
408
+ isCheckErrorTimeDuplicate?: boolean; // true: báo lỗi nếu From === To
265
409
  }
410
+
411
+ // IMessageTranslate
412
+ // interface IMessageTranslate {
413
+ // message?: string;
414
+ // interpolateParams?: object;
415
+ // }
266
416
  ```
267
417
 
268
418
  ### IDateTimeInputFunctionControlEvent
269
419
 
420
+ Object chứa các hàm điều khiển component từ bên ngoài.
421
+
270
422
  ```typescript
271
423
  export interface IDateTimeInputFunctionControlEvent {
272
424
  checkIsValid: () => Promise<boolean>;
@@ -275,30 +427,36 @@ export interface IDateTimeInputFunctionControlEvent {
275
427
  }
276
428
  ```
277
429
 
278
- ## Hidden Logic
430
+ ## Lưu ý quan trọng
279
431
 
280
- ### Signal Auto-Unwrapping trong selectedTime
432
+ ⚠️ **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.
281
433
 
282
- Khi truyền `[selectedTime]`, component sử dụng `effect()` để watch thay đổi. Giá trị `from`/`to` là `WritableSignal`, component sẽ gọi `.()` để unwrap. Nếu truyền plain object thay vì signal sẽ gây lỗi.
434
+ ```typescript
435
+ // ĐÚNG
436
+ selectedTime = signal<ISelectedMultiTime>({
437
+ from: signal({ hours: 9, minute: 30 }),
438
+ to: signal({ hours: 17, minute: 0 }),
439
+ });
440
+
441
+ // SAI — sẽ lỗi runtime
442
+ selectedTime = signal<ISelectedMultiTime>({
443
+ from: { hours: 9, minute: 30 }, // plain object, không phải signal
444
+ to: { hours: 17, minute: 0 },
445
+ });
446
+ ```
283
447
 
284
- ### Backspace Navigation
448
+ ⚠️ **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"`.
285
449
 
286
- Khi nhấn Backspace ô trống, component tự động focus về ô trước đó (toMinutes toHours fromMinutes fromHours). Cần nhấn Backspace 2 lần: lần 1 xóa giá trị, lần 2 chuyển focus.
450
+ ⚠️ **Backspace navigation cần nhấn 2 lần**: Khi ô input đang trống nhấn Backspace, lần 1 xử xóa giá trị (không để xóa), lần 2 mới chuyển focus về ô trước. Đây hành vi thiết kế có chủ đích.
287
451
 
288
- ### Input Dependencies ignoreCheckValidWhenInput
452
+ ⚠️ **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"`.
289
453
 
290
- Khi `[ignoreCheckValidWhenInput]="true"`, validation sẽ KHÔNG chạy ngay khi nhập từ bàn phím, chỉ chạy khi gọi `checkIsValid()` từ FunctionsControl.
454
+ ⚠️ **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.
291
455
 
292
456
  ## Demo
293
457
 
294
- - **Local**: [http://localhost:4500/datetime/input](http://localhost:4500/datetime/input)
295
-
296
- ## Test
297
-
298
458
  ```bash
299
- nx test components-datetime-input
459
+ npx nx serve core-ui
300
460
  ```
301
461
 
302
- ## License
303
-
304
- MIT
462
+ Truy cập: http://localhost:4500/components/datetime/input