@libs-ui/components-inputs-input 0.2.356-9 → 0.2.357-1

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,53 +1,38 @@
1
1
  # @libs-ui/components-inputs-input
2
2
 
3
- > Universal input component hỗ trợ text, number, textarea với validation và customization đầy đủ
4
-
5
- **Version:** 0.2.355-14
3
+ > Universal input component hỗ trợ text, number, textarea và iframe-textarea với validation, formatting và customization đầy đủ.
6
4
 
7
5
  ## Giới thiệu
8
6
 
9
- `LibsUiComponentsInputsInputComponent` là một standalone Angular component đa năng cho việc nhập liệu. Component hỗ trợ nhiều loại dữ liệu (string, int, float, bigint), nhiều tag (input, textarea, iframe-textarea), khả năng tùy biến cao với validation, icons, templates,external controls.
7
+ `LibsUiComponentsInputsInputComponent` là Angular standalone component đa năng cho việc nhập liệu. Component hỗ trợ 4 kiểu dữ liệu (`string`, `int`, `float`, `bigint`), 3 loại tag (`input`, `textarea`, `iframe-textarea`), tự động format number theo locale (VI/EN), auto-resize textarea, cung cấp API điều khiển ngoài qua `FunctionsControl`. Tích hợp sẵn OnPush Change Detection Angular Signals.
10
8
 
11
- ### Tính năng
9
+ ## Tính năng
12
10
 
13
- - ✅ Hỗ trợ nhiều data types: string, int, float, bigint
14
- - ✅ Hỗ trợ nhiều tag types: input, textarea, iframe-textarea
15
- - ✅ Auto-resize cho textarea
16
- - ✅ Number formatting theo ngôn ngữ (VI/EN)
17
- - ✅ Min/Max value validation cho number
18
- - ✅ Character count display
19
- - ✅ Icon left/right với popover
20
- - ✅ Template slots (left/right bottom)
21
- - ✅ Up/Down buttons cho number input
22
- - ✅ Drag & drop file support
23
- - ✅ External control via FunctionsControl
24
- - ✅ OnPush Change Detection
25
- - ✅ Angular Signals
11
+ - ✅ Hỗ trợ 4 kiểu dữ liệu: `string`, `int`, `float`, `bigint`
12
+ - ✅ Hỗ trợ 3 loại tag: `input`, `textarea`, `iframe-textarea`
13
+ - ✅ Tự động format number theo ngôn ngữ (VI dùng `.` phân cách nghìn, EN dùng `,`)
14
+ - ✅ Auto-resize textarea theo nội dung (min/max height có thể cấu hình)
15
+ - ✅ Validation min/max value cho number input
16
+ - ✅ Hiển thị character count (`showCount`)
17
+ - ✅ Icon left/right kèm popover tooltip
18
+ - ✅ Template slot tùy chỉnh dưới input (trái/phải)
19
+ - ✅ Nút tăng/giảm (up/down) cho number input
20
+ - ✅ Drag & drop file vào input
21
+ - ✅ Điều khiển từ bên ngoài qua `FunctionsControl` (focus, blur, insert, reset, selectAll)
22
+ - ✅ `iframe-textarea`: lập CSS toàn cục, style riêng qua `iframeTextareaCustomStyle`
23
+ - ✅ OnPush Change Detection + Angular Signals
26
24
 
27
25
  ## Khi nào sử dụng
28
26
 
29
27
  - Cần input field cơ bản cho text hoặc number
30
- - Cần textarea với auto-resize
31
- - Cần format number theo locale (VI/EN)
28
+ - Cần textarea tự động co giãn theo nội dung
29
+ - Cần format number theo locale VI/EN
32
30
  - Cần validation min/max cho number input
33
- - Cần hiển thị character count
34
- - Cần custom icons với popover
35
- - Cần control input từ component cha
36
- - Cần drag & drop file vào input
37
-
38
- ## Important Notes
39
-
40
- ⚠️ **Data Type Handling**: Component tự động format number theo ngôn ngữ hiện tại (VI dùng dấu `.` cho thousands, EN dùng `,`).
41
-
42
- ⚠️ **Textarea Auto-resize**: Khi dùng `tagInput="textarea"`, component tự động điều chỉnh height dựa trên content.
43
-
44
- ⚠️ **Number Precision**:
45
-
46
- - `int`: max 16-17 digits
47
- - `float`: max 15 digits (font) + decimals
48
- - `bigint`: configurable via `maxLengthNumberCount` và `fixedFloat`
49
-
50
- ⚠️ **Value Model**: `value` là model (two-way binding), component tự động parse và emit theo `dataType`.
31
+ - Cần hiển thị số ký tự đã nhập
32
+ - Cần custom icon trái/phải với popover tooltip
33
+ - Cần điều khiển input từ component cha (focus, insert text, reset)
34
+ - Cần drag & drop file vào vùng input
35
+ - Cần cô lập editor content khỏi CSS toàn cục (dùng `iframe-textarea`)
51
36
 
52
37
  ## Cài đặt
53
38
 
@@ -67,17 +52,43 @@ import { LibsUiComponentsInputsInputComponent } from '@libs-ui/components-inputs
67
52
  export class YourComponent {}
68
53
  ```
69
54
 
70
- ## dụ
55
+ Import thêm types/interfaces khi cần:
56
+
57
+ ```typescript
58
+ import {
59
+ LibsUiComponentsInputsInputComponent,
60
+ IInputFunctionControlEvent,
61
+ IFocusAndBlurEvent,
62
+ IIframeTextareaCustomStyle,
63
+ TYPE_DATA_TYPE_INPUT,
64
+ TYPE_TAG_INPUT,
65
+ TYPE_INPUT,
66
+ TYPE_INPUT_RESIZE_MODE,
67
+ TYPE_MODE_INPUT,
68
+ } from '@libs-ui/components-inputs-input';
69
+ ```
70
+
71
+ ## Ví dụ sử dụng
71
72
 
72
73
  ### Basic Text Input
73
74
 
74
75
  ```html
75
76
  <libs_ui-components-inputs-input
76
77
  [(value)]="textValue"
77
- [placeholder]="'Enter text'"
78
- (outChange)="onTextChange($event)" />
78
+ [placeholder]="'Nhập nội dung'"
79
+ (outChange)="handlerTextChange($event)" />
80
+ ```
81
+
82
+ ```typescript
83
+ protected textValue = signal('');
84
+
85
+ handlerTextChange(value: string): void {
86
+ // value đã được emit
87
+ }
79
88
  ```
80
89
 
90
+ ---
91
+
81
92
  ### Number Input với Min/Max
82
93
 
83
94
  ```html
@@ -86,179 +97,451 @@ export class YourComponent {}
86
97
  [(value)]="numberValue"
87
98
  [minValueNumber]="0"
88
99
  [maxValueNumber]="100"
89
- [placeholder]="'0-100'" />
100
+ [placeholder]="'Nhập số 0-100'"
101
+ (outChange)="handlerNumberChange($event)" />
102
+ ```
103
+
104
+ ```typescript
105
+ protected numberValue = signal<number>(0);
106
+
107
+ handlerNumberChange(value: number): void {
108
+ // value là number đã được parse
109
+ }
110
+ ```
111
+
112
+ ---
113
+
114
+ ### Float Input với Fixed Decimal và Giá trị âm
115
+
116
+ ```html
117
+ <libs_ui-components-inputs-input
118
+ [dataType]="'float'"
119
+ [(value)]="floatValue"
120
+ [fixedFloat]="2"
121
+ [acceptNegativeValue]="true"
122
+ [placeholder]="'Nhập số thực (vd: -99.99)'"
123
+ (outChange)="handlerFloatChange($event)" />
124
+ ```
125
+
126
+ ```typescript
127
+ protected floatValue = signal<number>(0);
128
+
129
+ handlerFloatChange(value: number): void {
130
+ // value là số thực, tối đa 2 chữ số thập phân
131
+ }
90
132
  ```
91
133
 
92
- ### Textarea với Auto-resize
134
+ ---
135
+
136
+ ### Bigint Input với Precision tùy chỉnh
137
+
138
+ ```html
139
+ <libs_ui-components-inputs-input
140
+ [dataType]="'bigint'"
141
+ [(value)]="bigintValue"
142
+ [maxLengthNumberCount]="19"
143
+ [fixedFloat]="4"
144
+ [placeholder]="'Nhập giá trị lớn'"
145
+ (outChange)="handlerBigintChange($event)" />
146
+ ```
147
+
148
+ ```typescript
149
+ protected bigintValue = signal('');
150
+
151
+ handlerBigintChange(value: string): void {
152
+ // bigint trả về string thay vì number
153
+ }
154
+ ```
155
+
156
+ ---
157
+
158
+ ### Textarea với Auto-resize và Character Count
93
159
 
94
160
  ```html
95
161
  <libs_ui-components-inputs-input
96
162
  [tagInput]="'textarea'"
97
163
  [(value)]="textareaValue"
98
- [minHeightTextArea]="100"
164
+ [defaultHeight]="80"
165
+ [minHeightTextArea]="80"
99
166
  [maxHeightTextArea]="300"
167
+ [maxLength]="500"
100
168
  [showCount]="true"
101
- [maxLength]="500" />
169
+ [placeholder]="'Nhập mô tả...'"
170
+ (outChange)="handlerTextareaChange($event)"
171
+ (outHeightAreaChange)="handlerHeightChange($event)" />
172
+ ```
173
+
174
+ ```typescript
175
+ protected textareaValue = signal('');
176
+
177
+ handlerTextareaChange(value: string): void {
178
+ // value là chuỗi text
179
+ }
180
+
181
+ handlerHeightChange(data: { isChange: boolean; height: number }): void {
182
+ // data.height là chiều cao mới của textarea
183
+ }
102
184
  ```
103
185
 
104
- ### Float Input với Fixed Decimal
186
+ ---
187
+
188
+ ### Password Input
105
189
 
106
190
  ```html
107
191
  <libs_ui-components-inputs-input
108
- [dataType]="'float'"
109
- [(value)]="floatValue"
110
- [fixedFloat]="2"
111
- [acceptNegativeValue]="true" />
192
+ [typeInput]="'password'"
193
+ [(value)]="passwordValue"
194
+ [resetAutoCompletePassword]="true"
195
+ [placeholder]="'Nhập mật khẩu'"
196
+ (outChange)="handlerPasswordChange($event)" />
112
197
  ```
113
198
 
114
- ### Input với Icons
199
+ ```typescript
200
+ protected passwordValue = signal('');
201
+
202
+ handlerPasswordChange(value: string): void {
203
+ // xử lý password
204
+ }
205
+ ```
206
+
207
+ ---
208
+
209
+ ### Input với Icon Left/Right và Popover
115
210
 
116
211
  ```html
117
212
  <libs_ui-components-inputs-input
118
213
  [(value)]="searchValue"
119
214
  [iconLeftClass]="'libs-ui-icon-search'"
120
215
  [iconRightClass]="'libs-ui-icon-close'"
121
- [popoverContentIconLeft]="'Search for items'"
122
- (outIconLeft)="onSearchClick()"
123
- (outIconRight)="onClearClick()" />
216
+ [popoverContentIconLeft]="'Tìm kiếm'"
217
+ [popoverContentIconRight]="'Xóa nội dung'"
218
+ [placeholder]="'Tìm kiếm...'"
219
+ (outChange)="handlerSearchChange($event)"
220
+ (outIconLeft)="handlerIconLeftClick($event)"
221
+ (outIconRight)="handlerIconRightClick($event)" />
124
222
  ```
125
223
 
126
- ### External Control
224
+ ```typescript
225
+ protected searchValue = signal('');
226
+
227
+ handlerSearchChange(value: string): void {
228
+ // xử lý tìm kiếm
229
+ }
230
+
231
+ handlerIconLeftClick(eventName: string): void {
232
+ // eventName = 'click'
233
+ }
234
+
235
+ handlerIconRightClick(eventName: string): void {
236
+ this.searchValue.set('');
237
+ }
238
+ ```
239
+
240
+ ---
241
+
242
+ ### External Control qua FunctionsControl
127
243
 
128
244
  ```html
129
245
  <libs_ui-components-inputs-input
130
246
  [(value)]="controlledValue"
131
- (outFunctionsControl)="onFunctionsControl($event)" />
247
+ [placeholder]="'Điều khiển từ bên ngoài'"
248
+ (outFunctionsControl)="handlerFunctionsControl($event)" />
132
249
 
133
- <button (click)="focusInput()">Focus</button>
134
- <button (click)="insertText('Hello')">Insert Text</button>
250
+ <button (click)="handlerFocusInput()">Focus</button>
251
+ <button (click)="handlerInsertText()">Insert "Hello"</button>
252
+ <button (click)="handlerResetInput()">Reset</button>
253
+ <button (click)="handlerSelectAll()">Select All</button>
135
254
  ```
136
255
 
137
256
  ```typescript
138
- functionControl = signal<IInputFunctionControlEvent | null>(null);
257
+ import { signal } from '@angular/core';
258
+ import { IInputFunctionControlEvent } from '@libs-ui/components-inputs-input';
139
259
 
140
- onFunctionsControl(control: IInputFunctionControlEvent) {
260
+ protected controlledValue = signal('');
261
+ private functionControl = signal<IInputFunctionControlEvent | null>(null);
262
+
263
+ handlerFunctionsControl(control: IInputFunctionControlEvent): void {
141
264
  this.functionControl.set(control);
142
265
  }
143
266
 
144
- async focusInput() {
267
+ async handlerFocusInput(): Promise<void> {
145
268
  await this.functionControl()?.focus();
146
269
  }
147
270
 
148
- async insertText(text: string) {
149
- await this.functionControl()?.insertContent(text);
271
+ async handlerInsertText(): Promise<void> {
272
+ await this.functionControl()?.insertContent('Hello');
273
+ }
274
+
275
+ async handlerResetInput(): Promise<void> {
276
+ await this.functionControl()?.resetValue();
277
+ }
278
+
279
+ async handlerSelectAll(): Promise<void> {
280
+ await this.functionControl()?.selectAllContent();
150
281
  }
151
282
  ```
152
283
 
153
- ## API
154
-
155
- ### Inputs (Most Common)
156
-
157
- | Property | Type | Default | Description |
158
- | --------------------------------------------- | -------------------------------- | -------------------------- | -------------------------------------------- |
159
- | Property | Type | Default | Description |
160
- | -------------------------------------------- | -------------------------------- | -------------------------- | -------------------------------------------- |
161
- | `[acceptNegativeValue]` | `boolean` | `false` | Cho phép giá trị âm |
162
- | `[acceptOnlyClickIcon]` | `boolean` | `false` | Chỉ cho phép click vào icon |
163
- | `[autoAddZeroLessThan10InTypeInt]` | `boolean` | `false` | Tự động thêm số 0 nếu < 10 (int) |
164
- | `[autoRemoveEmoji]` | `boolean` | `false` | Tự động loại bỏ emoji |
165
- | `[backgroundNone]` | `boolean` | `false` | Trong suốt background |
166
- | `[blurTimeOut]` | `number` | `32` | Delay time cho blur event |
167
- | `[borderError]` | `boolean` | `false` | Hiển thị border error |
168
- | `[classContainerBottomInput]` | `string` | `' '` | Class cho container dưới input |
169
- | `[classContainerInput]` | `string` | `'w-full'` | Class cho container input |
170
- | `[classInclude]` | `string` | `' w-full '` | Class bao ngoài component |
171
- | `[dataType]` | `TYPE_DATA_TYPE_INPUT` | `'string'` | Data type: string, int, float, bigint |
172
- | `[defaultHeight]` | `number` | `32` | Chiều cao mặc định |
173
- | `[disable]` | `boolean` | `false` | Disable mode |
174
- | `[emitEmptyInDataTypeNumber]` | `boolean` | `false` | Emit empty string khi xoá hết number |
175
- | `[fixedFloat]` | `number` | `undefined` | Số chữ số thập phân |
176
- | `[focusInput]` | `boolean` | `false` | Auto focus input |
177
- | `[focusTimeOut]` | `number` | `600` | Delay time cho focus method |
178
- | `[iconLeftClass]` | `string` | `''` | Class cho icon trái |
179
- | `[iconRightClass]` | `string` | `undefined` | Class cho icon phải |
180
- | `[iframeTextareaCustomStyle]` | `IIframeTextareaCustomStyle` | `undefined` | Custom style cho iframe textarea |
181
- | `[ignoreBlockInputMaxValue]` | `boolean` | `undefined` | Không chặn nhập quá max value |
182
- | `[ignoreStopPropagationEvent]` | `boolean` | `undefined` | Không chặn propagation event |
183
- | `[ignoreWidthInput100]` | `boolean` | `undefined` | Không set width 100% |
184
- | `[keepPlaceholderOnly]` | `boolean` | `false` | Chỉ giữ placeholder (ẩn value) |
185
- | `[keepZeroInTypeInt]` | `boolean` | `false` | Giữ số 0 ở đầu (int) |
186
- | `[maxHeightTextArea]` | `number` | `250` | Max height cho textarea |
187
- | `[maxLength]` | `number` | `undefined` | Max character length |
188
- | `[maxLengthNumberCount]` | `number` | `undefined` | Max length cho phần số (bigint) |
189
- | `[minHeightTextArea]` | `number` | `undefined` | Min height cho textarea |
190
- | `[minValueNumber]` | `number` | `undefined` | Min value cho number input |
191
- | `[maxValueNumber]` | `number` | `undefined` | Max value cho number input |
192
- | `[noBorder]` | `boolean` | `false` | Ẩn border |
193
- | `[onlyAcceptNegativeValue]` | `boolean` | `false` | Chỉ chấp nhận số âm |
194
- | `[placeholder]` | `string` | `' '` | Placeholder text |
195
- | `[popoverContentIconLeft]` | `string` | `''` | Popover content icon trái |
196
- | `[popoverContentIconRight]` | `string` | `''` | Popover content icon phải |
197
- | `[readonly]` | `boolean` | `false` | Readonly mode |
198
- | `[resetAutoCompletePassword]` | `boolean` | `false` | Reset autocomplete cho password |
199
- | `[resize]` | `TYPE_INPUT_RESIZE_MODE` | `'vertical'` | Resize mode |
200
- | `[selectAllTimeOut]` | `number` | `600` | Delay time cho selectAll |
201
- | `[setIconRightColorSameColorDisableReadOnly]` | `boolean` | `false` | Icon phải cùng màu disable text |
202
- | `[showCount]` | `boolean` | `false` | Hiển thị character count |
203
- | `[tabInsertContentTagInput]` | `boolean` | `false` | Cho phép dùng Tab để insert |
204
- | `[tagInput]` | `TYPE_TAG_INPUT` | `'input'` | Tag type: input, textarea, iframe-textarea |
205
- | `[templateLeftBottomInput]` | `TemplateRef<TYPE_TEMPLATE_REF>` | `undefined` | Template custom dưới trái |
206
- | `[templateRightBottomInput]` | `TemplateRef<TYPE_TEMPLATE_REF>` | `undefined` | Template custom dưới phải |
207
- | `[textAreaEnterNotNewLine]` | `boolean` | `false` | Enter không xuống dòng trong textarea |
208
- | `[typeInput]` | `TYPE_INPUT` | `'text'` | Input type: text, number, password |
209
- | `[useColorModeExist]` | `boolean` | `false` | Sử dụng màu sắc theo mode của hệ thống |
210
- | `[value]` | `string \| number` | `''` | Input value (model binding) |
211
- | `[valueUpDownNumber]` | `number` | `undefined` | Bước nhảy cho nút tăng giảm |
212
- | `[zIndexPopoverContent]` | `number` | `10` | Z-index cho popover content |
213
-
214
- **Note**: Component có 50+ inputs. Xem demo tại `/inputs/input` để biết chi tiết.
215
-
216
- ### Outputs
217
-
218
- | Property | Type | Description |
219
- | -------------------------------- | ------------------------------------------ | --------------------------------------------- |
220
- | Property | Type | Description |
221
- | -------------------------------- | ------------------------------------------ | -------------------------------------------- |
222
- | `(outChange)` | `any` | Emit khi value thay đổi (two-way binding) |
223
- | `(outChangeValueByButtonUpDown)` | `void` | Emit khi click button up/down |
224
- | `(outEnterEvent)` | `IEvent` | Emit khi nhấn Enter |
225
- | `(outFileDrop)` | `File` | Emit khi drop single file |
226
- | `(outFilesDrop)` | `Array<File>` | Emit khi drop multiple files |
227
- | `(outFocusAndBlurEvent)` | `IFocusAndBlurEvent` | Emit khi focus/blur |
228
- | `(outFunctionsControl)` | `IInputFunctionControlEvent` | Emit function controls instance |
229
- | `(outHeightAreaChange)` | `{ isChange: boolean; height: number; }` | Emit khi height textarea thay đổi auto-resize |
230
- | `(outIconLeft)` | `string` | Emit khi click icon left |
231
- | `(outIconRight)` | `string` | Emit khi click icon right |
232
- | `(outInputEvent)` | `IEvent` | Emit pure input event (native) |
233
-
234
- ### FunctionsControl Methods
235
-
236
- | Method | Description |
237
- | ---------------------------- | ------------------------- |
238
- | `focus(emitEvent?: boolean)` | Focus vào input |
239
- | `blur(emitEvent?: boolean)` | Blur khỏi input |
240
- | `insertContent(data)` | Insert content vào cursor |
241
- | `resetValue()` | Reset value về empty |
242
- | `getElementValue()` | Lấy value từ element |
243
- | `selectAllContent()` | Select all content |
244
-
245
- ## Types
284
+ ---
285
+
286
+ ### IFrame Textarea với Custom Style
287
+
288
+ ```html
289
+ <libs_ui-components-inputs-input
290
+ [tagInput]="'iframe-textarea'"
291
+ [(value)]="iframeValue"
292
+ [iframeTextareaCustomStyle]="iframeStyle"
293
+ [defaultHeight]="120"
294
+ [maxHeightTextArea]="400"
295
+ [placeholder]="'Nhập nội dung (cô lập CSS)'"
296
+ (outChange)="handlerIframeChange($event)"
297
+ (outFunctionsControl)="handlerIframeFunctionsControl($event)" />
298
+ ```
246
299
 
247
300
  ```typescript
248
- type TYPE_INPUT = 'text' | 'number' | 'password';
249
- type TYPE_DATA_TYPE_INPUT = 'string' | 'int' | 'float' | 'bigint';
301
+ import { IIframeTextareaCustomStyle, IInputFunctionControlEvent } from '@libs-ui/components-inputs-input';
302
+
303
+ protected iframeValue = signal('');
304
+ protected iframeStyle: IIframeTextareaCustomStyle = {
305
+ fontSize: '14px',
306
+ lineHeight: '1.6',
307
+ padding: '8px 12px',
308
+ color: '#333333',
309
+ backgroundColor: '#ffffff',
310
+ borderColor: '#e2e8f0',
311
+ borderRadius: '6px',
312
+ };
313
+
314
+ handlerIframeChange(value: string): void {
315
+ // value là nội dung textarea bên trong iframe
316
+ }
317
+
318
+ handlerIframeFunctionsControl(control: IInputFunctionControlEvent): void {
319
+ // lưu control để điều khiển từ bên ngoài
320
+ }
321
+ ```
322
+
323
+ ---
324
+
325
+ ### Number Input với Nút Up/Down
326
+
327
+ ```html
328
+ <libs_ui-components-inputs-input
329
+ [dataType]="'int'"
330
+ [(value)]="stepValue"
331
+ [minValueNumber]="0"
332
+ [maxValueNumber]="10"
333
+ [valueUpDownNumber]="1"
334
+ [placeholder]="'0'"
335
+ (outChange)="handlerStepChange($event)"
336
+ (outChangeValueByButtonUpDown)="handlerUpDownClick()" />
337
+ ```
338
+
339
+ ```typescript
340
+ protected stepValue = signal<number>(0);
341
+
342
+ handlerStepChange(value: number): void {
343
+ // value thay đổi
344
+ }
345
+
346
+ handlerUpDownClick(): void {
347
+ // được gọi khi click nút tăng/giảm
348
+ }
349
+ ```
350
+
351
+ ---
352
+
353
+ ### Textarea không xuống dòng khi Enter
354
+
355
+ ```html
356
+ <libs_ui-components-inputs-input
357
+ [tagInput]="'textarea'"
358
+ [(value)]="singleLineValue"
359
+ [textAreaEnterNotNewLine]="true"
360
+ [placeholder]="'Enter để submit, Shift+Enter xuống dòng'"
361
+ (outChange)="handlerChange($event)"
362
+ (outEnterEvent)="handlerEnter($event)" />
363
+ ```
364
+
365
+ ```typescript
366
+ import { IEvent } from '@libs-ui/interfaces-types';
367
+
368
+ protected singleLineValue = signal('');
369
+
370
+ handlerChange(value: string): void {
371
+ // xử lý thay đổi
372
+ }
373
+
374
+ handlerEnter(event: IEvent): void {
375
+ event.stopPropagation();
376
+ // xử lý submit khi nhấn Enter
377
+ }
378
+ ```
379
+
380
+ ---
381
+
382
+ ### Drag & Drop File vào Input
383
+
384
+ ```html
385
+ <libs_ui-components-inputs-input
386
+ [(value)]="fileDropValue"
387
+ [placeholder]="'Kéo file vào đây hoặc nhập text'"
388
+ (outChange)="handlerChange($event)"
389
+ (outFilesDrop)="handlerFilesDrop($event)"
390
+ (outFileDrop)="handlerFileDrop($event)" />
391
+ ```
392
+
393
+ ```typescript
394
+ protected fileDropValue = signal('');
395
+
396
+ handlerChange(value: string): void {
397
+ // xử lý text thay đổi
398
+ }
399
+
400
+ handlerFilesDrop(files: File[]): void {
401
+ // xử lý nhiều file drop
402
+ files.forEach(file => console.log(file.name));
403
+ }
404
+
405
+ handlerFileDrop(file: File): void {
406
+ // xử lý từng file riêng lẻ
407
+ }
408
+ ```
409
+
410
+ ## @Input()
411
+
412
+ | Input | Type | Default | Mô tả | Ví dụ |
413
+ |---|---|---|---|---|
414
+ | `[acceptNegativeValue]` | `boolean` | `false` | Cho phép nhập giá trị âm | `[acceptNegativeValue]="true"` |
415
+ | `[acceptOnlyClickIcon]` | `boolean` | `false` | Cho phép click icon kể cả khi disable/readonly | `[acceptOnlyClickIcon]="true"` |
416
+ | `[autoAddZeroLessThan10InTypeInt]` | `boolean` | `false` | Tự động thêm số 0 trước nếu giá trị < 10 (dataType=int) | `[autoAddZeroLessThan10InTypeInt]="true"` |
417
+ | `[autoRemoveEmoji]` | `boolean` | `false` | Tự động loại bỏ emoji khỏi chuỗi nhập | `[autoRemoveEmoji]="true"` |
418
+ | `[backgroundNone]` | `boolean` | `false` | Làm trong suốt background của input | `[backgroundNone]="true"` |
419
+ | `[blurTimeOut]` | `number` | `32` | Delay (ms) trước khi blur được thực thi | `[blurTimeOut]="50"` |
420
+ | `[borderError]` | `boolean` | `false` | Hiển thị border màu đỏ báo lỗi | `[borderError]="hasError()"` |
421
+ | `[classContainerBottomInput]` | `string` | `' '` | CSS class cho container bên dưới input | `[classContainerBottomInput]="'flex gap-2'"` |
422
+ | `[classContainerInput]` | `string` | `'w-full'` | CSS class cho container bọc input | `[classContainerInput]="'w-64'"` |
423
+ | `[classInclude]` | `string` | `' w-full '` | CSS class bổ sung cho element input | `[classInclude]="'text-right'"` |
424
+ | `[dataType]` | `TYPE_DATA_TYPE_INPUT` | `'string'` | Kiểu dữ liệu: `'string'` \| `'int'` \| `'float'` \| `'bigint'` | `[dataType]="'int'"` |
425
+ | `[defaultHeight]` | `number` | `32` | Chiều cao mặc định (px) | `[defaultHeight]="40"` |
426
+ | `[disable]` | `boolean` | `false` | Vô hiệu hóa input | `[disable]="isLoading()"` |
427
+ | `[emitEmptyInDataTypeNumber]` | `boolean` | `false` | Emit `undefined` khi xóa hết giá trị number | `[emitEmptyInDataTypeNumber]="true"` |
428
+ | `[(fixedFloat)]` | `number` | `undefined` | Số chữ số thập phân tối đa (dùng với float/bigint) | `[(fixedFloat)]="2"` |
429
+ | `[focusInput]` | `boolean` | `false` | Tự động focus input khi render | `[focusInput]="true"` |
430
+ | `[focusTimeOut]` | `number` | `600` | Delay (ms) trước khi focus được thực thi | `[focusTimeOut]="300"` |
431
+ | `[iconLeftClass]` | `string` | `''` | CSS class cho icon bên trái | `[iconLeftClass]="'libs-ui-icon-search'"` |
432
+ | `[iconRightClass]` | `string` | `undefined` | CSS class cho icon bên phải | `[iconRightClass]="'libs-ui-icon-close'"` |
433
+ | `[iframeTextareaCustomStyle]` | `IIframeTextareaCustomStyle` | `undefined` | Custom style cho nội dung bên trong iframe-textarea | `[iframeTextareaCustomStyle]="myStyle"` |
434
+ | `[ignoreBlockInputMaxValue]` | `boolean` | `undefined` | Không chặn nhập khi vượt quá maxValueNumber | `[ignoreBlockInputMaxValue]="true"` |
435
+ | `[ignoreStopPropagationEvent]` | `boolean` | `undefined` | Không gọi stopPropagation trên sự kiện native | `[ignoreStopPropagationEvent]="true"` |
436
+ | `[ignoreWidthInput100]` | `boolean` | `undefined` | Không áp dụng `w-full` cho element input | `[ignoreWidthInput100]="true"` |
437
+ | `[keepPlaceholderOnly]` | `boolean` | `false` | Chỉ hiển thị placeholder, ẩn giá trị thực | `[keepPlaceholderOnly]="true"` |
438
+ | `[keepZeroInTypeInt]` | `boolean` | `false` | Giữ số 0 ở đầu khi nhập (dataType=int) | `[keepZeroInTypeInt]="true"` |
439
+ | `[(maxLength)]` | `number` | `undefined` | Số ký tự tối đa được nhập | `[(maxLength)]="100"` |
440
+ | `[(maxLengthNumberCount)]` | `number` | `undefined` | Số chữ số tối đa phần nguyên (dataType=bigint) | `[(maxLengthNumberCount)]="19"` |
441
+ | `[maxHeightTextArea]` | `number` | `250` | Chiều cao tối đa textarea (px) | `[maxHeightTextArea]="400"` |
442
+ | `[(maxValueNumber)]` | `number` | `undefined` | Giá trị tối đa cho number input | `[(maxValueNumber)]="100"` |
443
+ | `[minHeightTextArea]` | `number` | `undefined` | Chiều cao tối thiểu textarea (px) | `[minHeightTextArea]="80"` |
444
+ | `[(minValueNumber)]` | `number` | `undefined` | Giá trị tối thiểu cho number input | `[(minValueNumber)]="0"` |
445
+ | `[modeInput]` | `TYPE_MODE_INPUT` | `'text'` | inputmode cho mobile keyboard: `'none'` \| `'text'` \| `'tel'` \| `'url'` \| `'email'` \| `'numeric'` \| `'decimal'` | `[modeInput]="'numeric'"` |
446
+ | `[noBorder]` | `boolean` | `false` | Ẩn border của input | `[noBorder]="true"` |
447
+ | `[onlyAcceptNegativeValue]` | `boolean` | `false` | Chỉ chấp nhận số âm (tự động set acceptNegativeValue=true) | `[onlyAcceptNegativeValue]="true"` |
448
+ | `[placeholder]` | `string` | `' '` | Placeholder text (hỗ trợ i18n key) | `[placeholder]="'i18n_enter_value'"` |
449
+ | `[popoverContentIconLeft]` | `string` | `''` | Nội dung popover khi hover icon trái | `[popoverContentIconLeft]="'Tìm kiếm'"` |
450
+ | `[popoverContentIconRight]` | `string` | `''` | Nội dung popover khi hover icon phải | `[popoverContentIconRight]="'Xóa'"` |
451
+ | `[readonly]` | `boolean` | `false` | Chỉ đọc, không cho phép chỉnh sửa | `[readonly]="isViewing()"` |
452
+ | `[resetAutoCompletePassword]` | `boolean` | `false` | Tắt autocomplete của browser (dùng cho password) | `[resetAutoCompletePassword]="true"` |
453
+ | `[resize]` | `TYPE_INPUT_RESIZE_MODE` | `'vertical'` | Chế độ resize textarea: `'auto'` \| `'vertical'` \| `'horizontal'` \| `'none'` | `[resize]="'none'"` |
454
+ | `[selectAllTimeOut]` | `number` | `600` | Delay (ms) trước khi selectAll được thực thi | `[selectAllTimeOut]="300"` |
455
+ | `[setIconRightColorSameColorDisableReadOnly]` | `boolean` | `false` | Icon phải có màu giống màu text disable/readonly | `[setIconRightColorSameColorDisableReadOnly]="true"` |
456
+ | `[showCount]` | `boolean` | `false` | Hiển thị số ký tự đã nhập (dataType=string) | `[showCount]="true"` |
457
+ | `[tabInsertContentTagInput]` | `boolean` | `false` | Cho phép dùng phím Tab để chèn ký tự tab vào input | `[tabInsertContentTagInput]="true"` |
458
+ | `[tagInput]` | `TYPE_TAG_INPUT` | `'input'` | Loại element: `'input'` \| `'textarea'` \| `'iframe-textarea'` | `[tagInput]="'textarea'"` |
459
+ | `[templateLeftBottomInput]` | `TemplateRef` | `undefined` | Template tùy chỉnh ở góc dưới trái | `[templateLeftBottomInput]="myLeftTpl"` |
460
+ | `[templateRightBottomInput]` | `TemplateRef` | `undefined` | Template tùy chỉnh ở góc dưới phải | `[templateRightBottomInput]="myRightTpl"` |
461
+ | `[textAreaEnterNotNewLine]` | `boolean` | `false` | Enter không xuống dòng trong textarea (dùng Shift+Enter để xuống dòng) | `[textAreaEnterNotNewLine]="true"` |
462
+ | `[typeInput]` | `TYPE_INPUT` | `'text'` | HTML input type: `'text'` \| `'number'` \| `'password'` | `[typeInput]="'password'"` |
463
+ | `[useColorModeExist]` | `boolean` | `false` | Áp dụng màu text theo color mode của hệ thống | `[useColorModeExist]="true"` |
464
+ | `[(value)]` | `string \| number` | `''` | Giá trị input (two-way binding qua model) | `[(value)]="myValue"` |
465
+ | `[valueUpDownNumber]` | `number` | `undefined` | Bước nhảy mỗi lần nhấn nút tăng/giảm | `[valueUpDownNumber]="5"` |
466
+ | `[zIndexPopoverContent]` | `number` | `10` | Z-index của popover content | `[zIndexPopoverContent]="100"` |
467
+
468
+ ## @Output()
469
+
470
+ | Output | Type | Mô tả | Handler TS | Binding HTML |
471
+ |---|---|---|---|---|
472
+ | `(outChange)` | `string \| number \| undefined` | Emit khi value thay đổi, đã được parse theo `dataType` | `handlerChange(value: string \| number): void { /* xử lý */ }` | `(outChange)="handlerChange($event)"` |
473
+ | `(outChangeValueByButtonUpDown)` | `void` | Emit khi người dùng click nút tăng hoặc giảm | `handlerUpDownClick(): void { /* xử lý */ }` | `(outChangeValueByButtonUpDown)="handlerUpDownClick()"` |
474
+ | `(outEnterEvent)` | `IEvent` | Emit khi nhấn phím Enter (trong textarea nếu `textAreaEnterNotNewLine=true` thì chỉ Enter không Shift) | `handlerEnter(event: IEvent): void { event.stopPropagation(); /* xử lý submit */ }` | `(outEnterEvent)="handlerEnter($event)"` |
475
+ | `(outFileDrop)` | `File` | Emit từng file riêng lẻ khi drop hoặc paste file vào input | `handlerFileDrop(file: File): void { event.stopPropagation(); /* xử lý file */ }` | `(outFileDrop)="handlerFileDrop($event)"` |
476
+ | `(outFilesDrop)` | `Array<File>` | Emit toàn bộ danh sách file khi drop hoặc paste vào input | `handlerFilesDrop(files: File[]): void { event.stopPropagation(); /* xử lý danh sách */ }` | `(outFilesDrop)="handlerFilesDrop($event)"` |
477
+ | `(outFocusAndBlurEvent)` | `IFocusAndBlurEvent` | Emit khi input được focus hoặc blur, kèm `name: 'focus' \| 'blur'` | `handlerFocusBlur(e: IFocusAndBlurEvent): void { e.event.stopPropagation(); /* xử lý */ }` | `(outFocusAndBlurEvent)="handlerFocusBlur($event)"` |
478
+ | `(outFunctionsControl)` | `IInputFunctionControlEvent` | Emit API điều khiển: focus, blur, insert, reset, selectAll | `handlerFunctionsControl(ctrl: IInputFunctionControlEvent): void { this.ctrl.set(ctrl); }` | `(outFunctionsControl)="handlerFunctionsControl($event)"` |
479
+ | `(outHeightAreaChange)` | `{ isChange: boolean; height: number }` | Emit khi chiều cao textarea thay đổi do auto-resize | `handlerHeightChange(data: { isChange: boolean; height: number }): void { /* xử lý */ }` | `(outHeightAreaChange)="handlerHeightChange($event)"` |
480
+ | `(outIconLeft)` | `string` | Emit khi click icon trái, giá trị là tên event (`'click'`) | `handlerIconLeft(eventName: string): void { eventName; /* 'click' */ }` | `(outIconLeft)="handlerIconLeft($event)"` |
481
+ | `(outIconRight)` | `string` | Emit khi click icon phải, giá trị là tên event (`'click'`) | `handlerIconRight(eventName: string): void { eventName; /* 'click' */ }` | `(outIconRight)="handlerIconRight($event)"` |
482
+ | `(outInputEvent)` | `IEvent` | Emit native input event gốc (mỗi lần DOM input event xảy ra) | `handlerInputEvent(e: IEvent): void { e.stopPropagation(); /* xử lý native event */ }` | `(outInputEvent)="handlerInputEvent($event)"` |
483
+
484
+ ## FunctionsControl API
485
+
486
+ Nhận instance `IInputFunctionControlEvent` qua output `(outFunctionsControl)`. Tất cả methods là async.
487
+
488
+ | Method | Signature | Mô tả |
489
+ |---|---|---|
490
+ | `focus` | `(emitEvent?: boolean) => Promise<void>` | Focus vào input. Nếu `emitEvent=true` thì emit `outFocusAndBlurEvent` |
491
+ | `blur` | `(emitEvent?: boolean) => Promise<void>` | Blur khỏi input. Nếu `emitEvent=true` thì emit `outFocusAndBlurEvent` |
492
+ | `insertContent` | `(data: string \| number) => Promise<void>` | Chèn nội dung vào vị trí con trỏ hiện tại |
493
+ | `resetValue` | `() => Promise<void>` | Reset value về chuỗi rỗng và emit `outChange` |
494
+ | `getElementValue` | `() => Promise<string> \| undefined` | Lấy giá trị raw từ DOM element |
495
+ | `checkAndDisableUpDownButton` | `(value: number) => Promise<void>` | Kiểm tra và cập nhật trạng thái disable của nút tăng/giảm |
496
+ | `selectAllContent` | `() => Promise<void>` | Chọn toàn bộ nội dung trong input |
497
+
498
+ ## Types & Interfaces
499
+
500
+ ```typescript
501
+ import {
502
+ TYPE_INPUT,
503
+ TYPE_DATA_TYPE_INPUT,
504
+ TYPE_TAG_INPUT,
505
+ TYPE_MODE_INPUT,
506
+ TYPE_INPUT_RESIZE_MODE,
507
+ IInputFunctionControlEvent,
508
+ IFocusAndBlurEvent,
509
+ IIframeTextareaCustomStyle,
510
+ } from '@libs-ui/components-inputs-input';
511
+
512
+ // Loại HTML element
250
513
  type TYPE_TAG_INPUT = 'input' | 'textarea' | 'iframe-textarea';
251
- type TYPE_INPUT_RESIZE_MODE = 'auto' | 'vertical' | 'horizontal' | 'none';
252
514
 
515
+ // Kiểu dữ liệu
516
+ type TYPE_DATA_TYPE_INPUT = 'string' | 'int' | 'float' | 'bigint';
517
+
518
+ // HTML input type attribute
519
+ type TYPE_INPUT = 'text' | 'number' | 'password';
520
+
521
+ // inputmode attribute (mobile keyboard)
522
+ type TYPE_MODE_INPUT = 'none' | 'text' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal';
523
+
524
+ // Chế độ resize textarea
525
+ type TYPE_INPUT_RESIZE_MODE = 'auto' | 'vertical' | 'horizontal' | 'none' | undefined;
526
+
527
+ // API điều khiển từ component cha
253
528
  interface IInputFunctionControlEvent {
254
529
  focus: (emitEvent?: boolean) => Promise<void>;
255
530
  blur: (emitEvent?: boolean) => Promise<void>;
256
531
  insertContent: (data: string | number) => Promise<void>;
257
532
  resetValue: () => Promise<void>;
258
- getElementValue: () => Promise<any> | undefined;
533
+ getElementValue: () => Promise<string> | undefined;
534
+ checkAndDisableUpDownButton: (value: number) => Promise<void>;
259
535
  selectAllContent: () => Promise<void>;
260
536
  }
261
537
 
538
+ // Event focus/blur
539
+ interface IFocusAndBlurEvent {
540
+ name: 'focus' | 'blur';
541
+ event: Event;
542
+ }
543
+
544
+ // Custom style cho iframe-textarea
262
545
  interface IIframeTextareaCustomStyle {
263
546
  borderRadius?: string;
264
547
  borderColor?: string;
@@ -271,6 +554,22 @@ interface IIframeTextareaCustomStyle {
271
554
  }
272
555
  ```
273
556
 
557
+ ## Lưu ý quan trọng
558
+
559
+ ⚠️ **Number Formatting theo Locale**: Component tự động format number theo ngôn ngữ hiện tại. Tiếng Việt (VI) dùng dấu `.` phân cách nghìn và `,` phân cách thập phân. Tiếng Anh (EN) dùng `,` phân cách nghìn và `.` phân cách thập phân.
560
+
561
+ ⚠️ **Giới hạn độ chính xác số**: `int` tối đa 16-17 chữ số, `float` tối đa 15 chữ số phần nguyên + phần thập phân, `bigint` cấu hình được qua `maxLengthNumberCount` (default 19) và `fixedFloat` (default 4).
562
+
563
+ ⚠️ **value là model (two-way binding)**: Dùng `[(value)]="mySignal"` để binding hai chiều. Output `(outChange)` emit giá trị đã được parse theo `dataType` — với `bigint` trả về `string`, các loại khác trả về `number` hoặc `string`.
564
+
565
+ ⚠️ **iframe-textarea CSS isolation**: Khi dùng `tagInput="iframe-textarea"`, content được render bên trong `<iframe>` — CSS của trang cha không áp dụng vào bên trong. Dùng `[iframeTextareaCustomStyle]` để tùy chỉnh giao diện bên trong iframe (fontSize, padding, color, borderColor...).
566
+
567
+ ⚠️ **FunctionsControl timing**: Output `(outFunctionsControl)` emit sau khi DOM element được khởi tạo xong. Lưu instance vào signal để dùng sau: `private ctrl = signal<IInputFunctionControlEvent | null>(null)`.
568
+
569
+ ⚠️ **textAreaEnterNotNewLine với Shift+Enter**: Khi `[textAreaEnterNotNewLine]="true"`, nhấn `Enter` không xuống dòng và emit `outEnterEvent`. Dùng `Shift+Enter` để xuống dòng bình thường.
570
+
571
+ ⚠️ **emitEmptyInDataTypeNumber**: Mặc định khi xóa hết giá trị number, component emit `0`. Bật `[emitEmptyInDataTypeNumber]="true"` để emit `undefined` thay vì `0`.
572
+
274
573
  ## Demo
275
574
 
276
575
  ```bash
@@ -278,7 +577,3 @@ npx nx serve core-ui
278
577
  ```
279
578
 
280
579
  Truy cập: `http://localhost:4500/inputs/input`
281
-
282
- ## License
283
-
284
- MIT