@libs-ui/components-inputs-add 0.2.356-8 → 0.2.357-0

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,56 +1,51 @@
1
1
  # @libs-ui/components-inputs-add
2
2
 
3
- > Component cho phép thêm/xóa nhiều input fields động với validation và customization đầy đủ
4
-
5
- **Version:** 0.2.355-14
3
+ > Component quản danh sách input fields động với khả năng thêm/xóa, validation đầy đủ và customization linh hoạt
6
4
 
7
5
  ## Giới thiệu
8
6
 
9
- `LibsUiComponentsInputsAddComponent` là một standalone Angular component để quản lý danh sách các input fields động. Component cho phép người dùng thêm/xóa fields, validate từng field riêng biệt, kiểm tra duplicate values.
7
+ `LibsUiComponentsInputsAddComponent` là một standalone Angular component cho phép người dùng quản lý danh sách các input fields động. Component tích hợp sẵn chế thêm/xóa items, validation từng field riêng biệt, kiểm tra giá trị duplicate, và hỗ trợ nhiều kiểu dữ liệu khác nhau (text, number, password...). Component sử dụng Angular Signals và OnPush Change Detection để đảm bảo hiệu năng tối ưu.
10
8
 
11
- ### Tính năng
9
+ ## Tính năng
12
10
 
13
- - Thêm/xóa input fields động
14
- - Validation cho từng field riêng biệt
15
- - Kiểm tra duplicate values
16
- - Min/Max items configuration
17
- - Custom button configuration
18
- - OnPush Change Detection
19
- - Angular Signals
20
- - Two-way binding với items array
11
+ - Thêm/xóa input fields động với nút Add và Remove
12
+ - Two-way binding với `model()` cho `items` array
13
+ - Validation tích hợp: required, minLength, maxLength, pattern, min/max value, duplicate
14
+ - Hỗ trợ nhiều kiểu dữ liệu: text, number (int/float), password, textarea
15
+ - Giới hạn số lượng items tối thiểu/tối đa (`minItems`, `maxItems`)
16
+ - Custom template trái/phải cho từng item (TemplateRef outlets)
17
+ - Hỗ trợ đơn vị tính bên trái và bên phải (units left/right)
18
+ - Label configuration tích hợp (title, required mark, description, toggle, popover)
19
+ - FunctionsControl API để validate và set error từ component cha
20
+ - Custom button "Add New" với đầy đủ styling options
21
+ - Debounce 250ms khi validate sau khi value thay đổi hoặc remove item
22
+ - Angular Signals + OnPush Change Detection
21
23
 
22
24
  ## Khi nào sử dụng
23
25
 
24
- - Cần nhập nhiều giá trị cùng loại (emails, phone numbers, URLs...)
25
- - Cho phép người dùng thêm/xóa fields động
26
- - Cần validation cho từng field riêng biệt
27
- - Quản lý danh sách inputs với min/max items
28
- - Cần kiểm tra duplicate values giữa các fields
29
-
30
- ## Important Notes
31
-
32
- ⚠️ **fieldNameBind Required**: Phải truyền fieldNameBind để xác định property name trong items array.
33
-
34
- ⚠️ **items Model**: items là model (two-way binding), component tự động cập nhật array khi add/remove.
35
-
36
- ⚠️ **Validation Timing**: Validation chạy sau 250ms debounce khi value change hoặc remove item.
37
-
38
- ⚠️ **ignoreValidEmptyField**: Khi true, chỉ cần ít nhất 1 field có giá trị là valid (không bắt buộc tất cả).
26
+ - Cần nhập nhiều giá trị cùng loại: emails, số điện thoại, URLs, tags
27
+ - Cho phép người dùng thêm/xóa fields động theo nhu cầu
28
+ - Cần validation cho từng field riêng biệt trong danh sách
29
+ - Quản lý danh sách inputs với ràng buộc số lượng min/max
30
+ - Cần kiểm tra giá trị trùng lặp (duplicate) giữa các fields
31
+ - Form có section nhập liệu lặp lại theo cấu trúc giống nhau
39
32
 
40
33
  ## Cài đặt
41
34
 
42
35
  ```bash
43
- # npm
44
36
  npm install @libs-ui/components-inputs-add
45
-
46
- # yarn
47
- yarn add @libs-ui/components-inputs-add
48
37
  ```
49
38
 
50
39
  ## Import
51
40
 
52
41
  ```typescript
53
- import { LibsUiComponentsInputsAddComponent, IInputAdd, IInputAddFunctionControlEvent } from '@libs-ui/components-inputs-add';
42
+ import {
43
+ LibsUiComponentsInputsAddComponent,
44
+ IInputAdd,
45
+ IInputAddFunctionControlEvent,
46
+ IEmitValueChange,
47
+ ITemplateRightLeftItem,
48
+ } from '@libs-ui/components-inputs-add';
54
49
 
55
50
  @Component({
56
51
  standalone: true,
@@ -60,261 +55,371 @@ import { LibsUiComponentsInputsAddComponent, IInputAdd, IInputAddFunctionControl
60
55
  export class YourComponent {}
61
56
  ```
62
57
 
63
- ## Ví dụ
58
+ ## Ví dụ sử dụng
64
59
 
65
- ### Basic
66
-
67
- ```html
68
- <libs_ui-components-inputs-add
69
- [fieldNameBind]="'email'"
70
- [(items)]="emailItems"
71
- [placeholder]="'Nhập email'"
72
- [labelConfig]="{
73
- labelLeft: 'Email addresses',
74
- required: true
75
- }" />
76
- ```
60
+ ### Cơ bản — Danh sách email
77
61
 
78
62
  ```typescript
79
- export class ExampleComponent {
63
+ import { Component, signal } from '@angular/core';
64
+ import { LibsUiComponentsInputsAddComponent, IInputAdd } from '@libs-ui/components-inputs-add';
65
+
66
+ @Component({
67
+ selector: 'app-email-list',
68
+ standalone: true,
69
+ imports: [LibsUiComponentsInputsAddComponent],
70
+ template: `
71
+ <libs_ui-components-inputs-add
72
+ [fieldNameBind]="'email'"
73
+ [(items)]="emailItems"
74
+ [placeholder]="'Nhập địa chỉ email'"
75
+ [labelConfig]="{ labelLeft: 'Danh sách email', required: true }"
76
+ />
77
+ `,
78
+ })
79
+ export class EmailListComponent {
80
80
  emailItems = signal<Array<IInputAdd>>([{ email: '' }]);
81
81
  }
82
82
  ```
83
83
 
84
- ### Min/Max Items
84
+ ### Giới hạn số lượng items
85
85
 
86
- ```html
87
- <libs_ui-components-inputs-add
88
- [fieldNameBind]="'phone'"
89
- [(items)]="phoneItems"
90
- [minItems]="2"
91
- [maxItems]="10"
92
- [placeholder]="'Nhập số điện thoại'" />
93
- ```
86
+ ```typescript
87
+ import { Component, signal } from '@angular/core';
88
+ import { LibsUiComponentsInputsAddComponent, IInputAdd } from '@libs-ui/components-inputs-add';
94
89
 
95
- ### With Validation & Duplicate Check
96
-
97
- ```html
98
- <libs_ui-components-inputs-add
99
- [fieldNameBind]="'url'"
100
- [(items)]="urlItems"
101
- [placeholder]="'https://example.com'"
102
- [validRequired]="{
103
- isRequired: true,
104
- message: 'URL is required'
105
- }"
106
- [validDuplicate]="{
107
- message: 'URL already exists'
108
- }"
109
- (outValueChange)="onValueChange($event)"
110
- (outFunctionsControl)="onFunctionsControl($event)" />
90
+ @Component({
91
+ selector: 'app-phone-list',
92
+ standalone: true,
93
+ imports: [LibsUiComponentsInputsAddComponent],
94
+ template: `
95
+ <libs_ui-components-inputs-add
96
+ [fieldNameBind]="'phone'"
97
+ [(items)]="phoneItems"
98
+ [minItems]="2"
99
+ [maxItems]="5"
100
+ [placeholder]="'Nhập số điện thoại'"
101
+ [labelConfig]="{ labelLeft: 'Số điện thoại (2–5 số)' }"
102
+ />
103
+ `,
104
+ })
105
+ export class PhoneListComponent {
106
+ phoneItems = signal<Array<IInputAdd>>([{ phone: '' }, { phone: '' }]);
107
+ }
111
108
  ```
112
109
 
110
+ ### Validation và kiểm tra duplicate
111
+
113
112
  ```typescript
114
- export class ExampleComponent {
113
+ import { Component, signal } from '@angular/core';
114
+ import {
115
+ LibsUiComponentsInputsAddComponent,
116
+ IInputAdd,
117
+ IInputAddFunctionControlEvent,
118
+ IEmitValueChange,
119
+ } from '@libs-ui/components-inputs-add';
120
+
121
+ @Component({
122
+ selector: 'app-url-list',
123
+ standalone: true,
124
+ imports: [LibsUiComponentsInputsAddComponent],
125
+ template: `
126
+ <libs_ui-components-inputs-add
127
+ [fieldNameBind]="'url'"
128
+ [(items)]="urlItems"
129
+ [placeholder]="'https://example.com'"
130
+ [labelConfig]="{ labelLeft: 'Danh sách URL', required: true }"
131
+ [validRequired]="{ message: 'URL là bắt buộc' }"
132
+ [validDuplicate]="{ message: 'URL đã tồn tại trong danh sách' }"
133
+ (outFunctionsControl)="handlerFunctionsControl($event)"
134
+ (outValueChange)="handlerValueChange($event)"
135
+ (outAddItem)="handlerAddItem($event)"
136
+ (outRemove)="handlerRemove($event)"
137
+ />
138
+ <button (click)="validateAll()">Kiểm tra hợp lệ</button>
139
+ `,
140
+ })
141
+ export class UrlListComponent {
115
142
  urlItems = signal<Array<IInputAdd>>([{ url: '' }]);
116
- functionControls = signal<IInputAddFunctionControlEvent | null>(null);
143
+ private functionControls: IInputAddFunctionControlEvent | null = null;
117
144
 
118
- onValueChange(event: IEmitValueChange) {
119
- console.log('Value changed:', event.value, event.item);
145
+ handlerFunctionsControl(controls: IInputAddFunctionControlEvent): void {
146
+ this.functionControls = controls;
120
147
  }
121
148
 
122
- async validateAll() {
123
- const isValid = await this.functionControls()?.checkIsValid();
124
- console.log('Is valid:', isValid);
149
+ handlerValueChange(event: IEmitValueChange): void {
150
+ // xử khi giá trị thay đổi
125
151
  }
152
+
153
+ handlerAddItem(item: IInputAdd): void {
154
+ // xử lý khi thêm item mới
155
+ }
156
+
157
+ handlerRemove(item: IInputAdd): void {
158
+ // xử lý khi xóa item
159
+ }
160
+
161
+ async validateAll(): Promise<void> {
162
+ const isValid = await this.functionControls?.checkIsValid();
163
+ // xử lý kết quả validation
164
+ }
165
+ }
166
+ ```
167
+
168
+ ### Nhập số có đơn vị tính
169
+
170
+ ```typescript
171
+ import { Component, signal } from '@angular/core';
172
+ import { LibsUiComponentsInputsAddComponent, IInputAdd } from '@libs-ui/components-inputs-add';
173
+
174
+ @Component({
175
+ selector: 'app-weight-list',
176
+ standalone: true,
177
+ imports: [LibsUiComponentsInputsAddComponent],
178
+ template: `
179
+ <libs_ui-components-inputs-add
180
+ [fieldNameBind]="'weight'"
181
+ [(items)]="weightItems"
182
+ [dataType]="'int'"
183
+ [placeholder]="'Nhập trọng lượng'"
184
+ [unitsRight]="[{ id: 'kg', label: 'Kg' }, { id: 'lb', label: 'Lbs' }]"
185
+ [keySelectedUnitRight]="'kg'"
186
+ [configUnitRight]="{ fieldKey: 'id', fieldLabel: 'label' }"
187
+ [labelConfig]="{ labelLeft: 'Trọng lượng', description: 'Nhập trọng lượng kèm đơn vị' }"
188
+ />
189
+ `,
190
+ })
191
+ export class WeightListComponent {
192
+ weightItems = signal<Array<IInputAdd>>([{ weight: '' }]);
126
193
  }
127
194
  ```
128
195
 
129
- ### Custom Button & Ignore Empty Field Validation
130
-
131
- ```html
132
- <libs_ui-components-inputs-add
133
- [fieldNameBind]="'tag'"
134
- [(items)]="tagItems"
135
- [maxItems]="8"
136
- [ignoreValidEmptyField]="true"
137
- [addItemButtonConfig]="{
138
- type: 'button-primary',
139
- label: 'Add Tag',
140
- classIconLeft: 'libs-ui-icon-add'
141
- }"
142
- [validRequired]="{
143
- isRequired: true,
144
- message: 'At least one tag is required'
145
- }" />
196
+ ### Custom button Add ignoreValidEmptyField
197
+
198
+ ```typescript
199
+ import { Component, signal } from '@angular/core';
200
+ import { LibsUiComponentsInputsAddComponent, IInputAdd } from '@libs-ui/components-inputs-add';
201
+
202
+ @Component({
203
+ selector: 'app-tag-list',
204
+ standalone: true,
205
+ imports: [LibsUiComponentsInputsAddComponent],
206
+ template: `
207
+ <libs_ui-components-inputs-add
208
+ [fieldNameBind]="'tag'"
209
+ [(items)]="tagItems"
210
+ [placeholder]="'Nhập tag'"
211
+ [maxItems]="8"
212
+ [ignoreValidEmptyField]="true"
213
+ [addItemButtonConfig]="{
214
+ type: 'button-link-primary',
215
+ label: 'Thêm tag',
216
+ classIconLeft: 'libs-ui-icon-add text-[10px] mr-[8px]'
217
+ }"
218
+ [labelConfig]="{ labelLeft: 'Tags', description: 'Ít nhất một tag là bắt buộc' }"
219
+ [validRequired]="{ message: 'Cần ít nhất một tag' }"
220
+ />
221
+ `,
222
+ })
223
+ export class TagListComponent {
224
+ tagItems = signal<Array<IInputAdd>>([{ tag: '' }]);
225
+ }
146
226
  ```
147
227
 
148
- ## API
149
-
150
- ### libs_ui-components-inputs-add
151
-
152
- #### Inputs
153
-
154
- | Property | Type | Default | Description |
155
- | ---------------------------------- | -------------------------------- | ----------------------------------------- | ----------------------------------------- |
156
- | `[acceptNegativeValue]` | `boolean` | `false` | Cho phép giá trị âm (với dataType number) |
157
- | `[addItemButtonConfig]` | `IButton` | `undefined` | Cấu hình button "Add New" |
158
- | `[autoAddZeroLessThan10InTypeInt]` | `boolean` | `false` | Tự động thêm số 0 nếu < 10 (kiểu int) |
159
- | `[autoRemoveEmoji]` | `boolean` | `false` | Tự động loại bỏ emoji khỏi input |
160
- | `[backgroundNone]` | `boolean` | `false` | Sử dụng background trong suốt |
161
- | `[borderError]` | `boolean` | `false` | Hiển thị border màu đỏ khi lỗi |
162
- | `[classContainerBottomInput]` | `string` | `undefined` | Class CSS cho container dưới input |
163
- | `[classContainerInput]` | `string` | `undefined` | Class CSS cho container bao quanh input |
164
- | `[classInclude]` | `string` | `undefined` | Class CSS cho container chính |
165
- | `[classIncludeInput]` | `string` | `undefined` | Class CSS cho mỗi input field |
166
- | `[classMessageErrorInclude]` | `string` | `undefined` | Class CSS cho message lỗi |
167
- | `[configItemAddToItems]` | `IInputAdd` | `undefined` | Template config khi add item mới |
168
- | `[configUnitLeft]` | `IInputValidUnitConfig` | `{ fieldKey: "id", fieldLabel: "label" }` | Cấu hình hiển thị unit bên trái |
169
- | `[configUnitRight]` | `IInputValidUnitConfig` | `{ fieldKey: "id", fieldLabel: "label" }` | Cấu hình hiển thị unit bên phải |
170
- | `[dataType]` | `TYPE_DATA_TYPE_INPUT` | `undefined` | Loại dữ liệu: text, number, email... |
171
- | `[defaultHeight]` | `number` | `undefined` | Chiều cao mặc định |
172
- | `[disable]` | `boolean` | `false` | Disable tất cả inputs |
173
- | `[emitEmptyInDataTypeNumber]` | `boolean` | `false` | Emit giá trị rỗng khi dataType number |
174
- | `[fieldNameBind]` | `string` | `required` | Tên property trong items array |
175
- | `[fixedFloat]` | `number` | `undefined` | Số lượng số lẻ sau dấu phẩy |
176
- | `[focusTimeOut]` | `number` | `undefined` | Thời gian delay focus |
177
- | `[formInputSpacing]` | `number` | `8` | Khoảng cách giữa các input (px) |
178
- | `[functionValid]` | `TYPE_FUNCTION_INPUT_VALID` | `undefined` | Hàm validate tùy chỉnh |
179
- | `[ignoreAddItem]` | `boolean` | `false` | Ẩn button "Add New" |
180
- | `[ignoreRemove]` | `boolean` | `false` | Ẩn button remove |
181
- | `[ignoreShowError]` | `boolean` | `false` | Ẩn hiển thị lỗi |
182
- | `[ignoreStopPropagationEvent]` | `boolean` | `false` | Không chặn sự kiện lan truyền |
183
- | `[ignoreUnitRightClassReadOnly]` | `boolean` | `false` | Bỏ class readonly cho unit phải |
184
- | `[ignoreValidEmptyField]` | `boolean` | `false` | Chỉ cần ít nhất 1 field giá trị |
185
- | `[ignoreWidthInput100]` | `boolean` | `false` | Bỏ width 100% cho input |
186
- | `[isBaselineStyle]` | `boolean` | `false` | Căn chỉnh baseline items |
187
- | `[items]` | `Array<IInputAdd>` | `required` | Array items (model binding) |
188
- | `[keepPlaceholderOnly]` | `boolean` | `false` | Chỉ giữ placeholder |
189
- | `[keySelectedUnitLeft]` | `any` | `undefined` | Key unit trái đang chọn |
190
- | `[keySelectedUnitRight]` | `any` | `undefined` | Key unit phải đang chọn |
191
- | `[labelConfig]` | `ILabel` | `undefined` | Cấu hình label cho component |
192
- | `[leftTemplateItems]` | `ITemplateRightLeftItem` | `undefined` | Template item bên trái |
193
- | `[maxItems]` | `number` | `5` | Số lượng items tối đa |
194
- | `[maxLength]` | `number` | `undefined` | Độ dài tối đa cho input |
195
- | `[maxLengthNumberCount]` | `number` | `undefined` | Max length cho number count |
196
- | `[maxValueNumber]` | `number` | `undefined` | Giá trị số tối đa |
197
- | `[minItems]` | `number` | `1` | Số lượng items tối thiểu |
198
- | `[minValueNumber]` | `number` | `undefined` | Giá trị số tối thiểu |
199
- | `[noBorder]` | `boolean` | `false` | Không hiển thị border |
200
- | `[onlyAcceptNegativeValue]` | `boolean` | `false` | Chỉ chấp nhận số âm |
201
- | `[paddingRightCustomSpecific]` | `number` | `undefined` | Custom padding phải |
202
- | `[placeholder]` | `string` | `undefined` | Placeholder cho inputs |
203
- | `[positionMessageErrorStartInput]` | `boolean` | `false` | Hiển thị lỗi bắt đầu từ input |
204
- | `[readonly]` | `boolean` | `false` | Readonly tất cả inputs |
205
- | `[resetAutoCompletePassword]` | `boolean` | `false` | Reset autocomplete password |
206
- | `[resize]` | `'auto' \| 'none'` | `undefined` | Khả năng resize |
207
- | `[rightTemplateItems]` | `ITemplateRightLeftItem` | `undefined` | Template item bên phải |
208
- | `[showCount]` | `boolean` | `false` | Hiển thị đếm tự |
209
- | `[tagInput]` | `TYPE_TAG_INPUT` | `undefined` | Tag input (textarea, input...) |
210
- | `[templateLeftBottomInput]` | `TemplateRef<TYPE_TEMPLATE_REF>` | `undefined` | Template dưới trái input |
211
- | `[templateLeftOutlet]` | `TemplateRef<TYPE_TEMPLATE_REF>` | `undefined` | Template outlet bên trái |
212
- | `[templateRightBottomInput]` | `TemplateRef<TYPE_TEMPLATE_REF>` | `undefined` | Template dưới phải input |
213
- | `[templateRightOutlet]` | `TemplateRef<TYPE_TEMPLATE_REF>` | `undefined` | Template outlet bên phải |
214
- | `[typeInput]` | `TYPE_INPUT` | `undefined` | Loại input (text, password...) |
215
- | `[unitsLeft]` | `Array<any>` | `undefined` | Danh sách units bên trái |
216
- | `[unitsRight]` | `Array<any>` | `undefined` | Danh sách units bên phải |
217
- | `[useColorModeExist]` | `boolean` | `false` | Sử dụng color mode sẵn |
218
- | `[validDuplicate]` | `IMessageTranslate` | `undefined` | Message khi giá trị duplicate |
219
- | `[validMaxValue]` | `IMessageTranslate` | `undefined` | Validate giá trị tối đa |
220
- | `[validMaxLength]` | `IMessageTranslate` | `undefined` | Validate độ dài tối đa |
221
- | `[validMinLength]` | `IValidLength` | `undefined` | Validate độ dài tối thiểu |
222
- | `[validMinValue]` | `IMessageTranslate` | `undefined` | Validate giá trị tối thiểu |
223
- | `[validPattern]` | `Array<IValidPattern>` | `undefined` | Validate theo pattern regex |
224
- | `[validRequired]` | `IValidRequired` | `undefined` | Validation required |
225
- | `[valuePatternShowError]` | `boolean` | `false` | Hiển thị lỗi pattern |
226
- | `[valueUpDownNumber]` | `number` | `undefined` | Bước nhảy tăng giảm số |
227
-
228
- #### Outputs
229
-
230
- | Property | Type | Description |
231
- | ----------------------- | ------------------------------- | -------------------------------- |
232
- | `(outAddItem)` | `IInputAdd` | Emit khi thêm item mới |
233
- | `(outClickButtonLabel)` | `IButton` | Emit khi click button trên label |
234
- | `(outFunctionsControl)` | `IInputAddFunctionControlEvent` | Emit function controls |
235
- | `(outLabelLeftClick)` | `MouseEvent` | Emit khi click label trái |
236
- | `(outLabelRightClick)` | `boolean` | Emit khi click label phải |
237
- | `(outRemove)` | `IInputAdd` | Emit khi xóa item |
238
- | `(outSwitchEventLabel)` | `ISwitchEvent` | Emit sự kiện switch trên label |
239
- | `(outValueChange)` | `IEmitValueChange` | Emit khi giá trị thay đổi |
240
-
241
- #### FunctionsControl Methods
242
-
243
- | Method | Description |
244
- | -------------------------- | --------------------------------------------------------- |
245
- | `checkIsValid()` | Kiểm tra validation tất cả items, trả về Promise<boolean> |
246
- | `setMessageError(message)` | Set error message cho tất cả items |
228
+ ## @Input()
229
+
230
+ | Input | Type | Default | Mô tả | Ví dụ |
231
+ |---|---|---|---|---|
232
+ | `[fieldNameBind]` | `string` | **required** | Tên property trong mỗi item dùng để bind giá trị | `[fieldNameBind]="'email'"` |
233
+ | `[(items)]` | `Array<IInputAdd>` | **required** | Mảng items — two-way binding qua model() | `[(items)]="emailItems"` |
234
+ | `[acceptNegativeValue]` | `boolean` | `false` | Cho phép nhập giá trị âm (dùng với dataType number) | `[acceptNegativeValue]="true"` |
235
+ | `[addItemButtonConfig]` | `IButton` | `undefined` | Cấu hình nút "Thêm mới" | `[addItemButtonConfig]="{ type: 'button-primary', label: 'Add' }"` |
236
+ | `[autoAddZeroLessThan10InTypeInt]` | `boolean` | `false` | Tự động thêm số 0 trước khi số < 10 (với kiểu int) | `[autoAddZeroLessThan10InTypeInt]="true"` |
237
+ | `[autoRemoveEmoji]` | `boolean` | `false` | Tự động loại bỏ emoji khỏi giá trị input | `[autoRemoveEmoji]="true"` |
238
+ | `[backgroundNone]` | `boolean` | `false` | Sử dụng nền trong suốt cho input | `[backgroundNone]="true"` |
239
+ | `[borderError]` | `boolean` | `false` | Hiển thị border màu đỏ khi có lỗi | `[borderError]="true"` |
240
+ | `[classContainerBottomInput]` | `string` | `undefined` | Class CSS cho vùng container phía dưới input | `[classContainerBottomInput]="'mt-2'"` |
241
+ | `[classContainerInput]` | `string` | `undefined` | Class CSS cho container bao ngoài mỗi input | `[classContainerInput]="'w-full'"` |
242
+ | `[classInclude]` | `string` | `undefined` | Class CSS bổ sung cho container chính của component | `[classInclude]="'mb-4'"` |
243
+ | `[classIncludeInput]` | `string` | `undefined` | Class CSS bổ sung cho từng input field | `[classIncludeInput]="'flex-1'"` |
244
+ | `[classMessageErrorInclude]` | `string` | `undefined` | Class CSS bổ sung cho message lỗi | `[classMessageErrorInclude]="'text-xs'"` |
245
+ | `[configItemAddToItems]` | `IInputAdd` | `undefined` | Template dữ liệu mặc định khi thêm item mới | `[configItemAddToItems]="{ unit: 'kg' }"` |
246
+ | `[configUnitLeft]` | `IInputValidUnitConfig` | `{ fieldKey: 'id', fieldLabel: 'label' }` | Cấu hình mapping field cho danh sách unit trái | `[configUnitLeft]="{ fieldKey: 'value', fieldLabel: 'name' }"` |
247
+ | `[configUnitRight]` | `IInputValidUnitConfig` | `{ fieldKey: 'id', fieldLabel: 'label' }` | Cấu hình mapping field cho danh sách unit phải | `[configUnitRight]="{ fieldKey: 'value', fieldLabel: 'name' }"` |
248
+ | `[dataType]` | `TYPE_DATA_TYPE_INPUT` | `undefined` | Kiểu dữ liệu input: text, int, float, email... | `[dataType]="'int'"` |
249
+ | `[defaultHeight]` | `number` | `undefined` | Chiều cao mặc định (px) của input | `[defaultHeight]="40"` |
250
+ | `[disable]` | `boolean` | `false` | hiệu hóa toàn bộ inputs trong danh sách | `[disable]="true"` |
251
+ | `[emitEmptyInDataTypeNumber]` | `boolean` | `false` | Emit giá trị rỗng khi dataType là number và field trống | `[emitEmptyInDataTypeNumber]="true"` |
252
+ | `[fixedFloat]` | `number` | `undefined` | Số chữ số thập phân cố định (dataType float) | `[fixedFloat]="2"` |
253
+ | `[focusTimeOut]` | `number` | `undefined` | Thời gian delay (ms) trước khi focus input | `[focusTimeOut]="100"` |
254
+ | `[formInputSpacing]` | `number` | `8` | Khoảng cách (px) giữa các dòng input | `[formInputSpacing]="12"` |
255
+ | `[functionValid]` | `TYPE_FUNCTION_INPUT_VALID` | `undefined` | Hàm validate tùy chỉnh bổ sung | `[functionValid]="customValidFn"` |
256
+ | `[ignoreAddItem]` | `boolean` | `false` | Ẩn nút "Thêm mới" | `[ignoreAddItem]="true"` |
257
+ | `[ignoreRemove]` | `boolean` | `false` | Ẩn nút xóa cho tất cả items | `[ignoreRemove]="true"` |
258
+ | `[ignoreShowError]` | `boolean` | `false` | Ẩn hiển thị thông báo lỗi validation | `[ignoreShowError]="true"` |
259
+ | `[ignoreStopPropagationEvent]` | `boolean` | `false` | Không chặn sự kiện lan truyền (stopPropagation) | `[ignoreStopPropagationEvent]="true"` |
260
+ | `[ignoreUnitRightClassReadOnly]` | `boolean` | `false` | Không áp class readonly cho unit bên phải | `[ignoreUnitRightClassReadOnly]="true"` |
261
+ | `[ignoreValidEmptyField]` | `boolean` | `false` | Coi valid nếu ít nhất 1 field có giá trị (không bắt buộc tất cả) | `[ignoreValidEmptyField]="true"` |
262
+ | `[ignoreWidthInput100]` | `boolean` | `false` | Không áp dụng width 100% cho input | `[ignoreWidthInput100]="true"` |
263
+ | `[isBaselineStyle]` | `boolean` | `false` | Căn chỉnh items theo baseline (dùng khi input có chiều cao khác nhau) | `[isBaselineStyle]="true"` |
264
+ | `[keepPlaceholderOnly]` | `boolean` | `false` | Chỉ giữ lại placeholder, ẩn các nội dung khác khi trống | `[keepPlaceholderOnly]="true"` |
265
+ | `[keySelectedUnitLeft]` | `any` | `undefined` | Giá trị key của unit bên trái đang được chọn | `[keySelectedUnitLeft]="'kg'"` |
266
+ | `[keySelectedUnitRight]` | `any` | `undefined` | Giá trị key của unit bên phải đang được chọn | `[keySelectedUnitRight]="'usd'"` |
267
+ | `[labelConfig]` | `ILabel` | `undefined` | Cấu hình toàn bộ label (tiêu đề, required, description, toggle...) | `[labelConfig]="{ labelLeft: 'Email', required: true }"` |
268
+ | `[leftTemplateItems]` | `ITemplateRightLeftItem` | *(default từ define)* | Cấu hình các loại template hiển thị bên trái mỗi item | `[leftTemplateItems]="customLeftTemplate"` |
269
+ | `[maxItems]` | `number` | `5` | Số lượng items tối đa được phép thêm | `[maxItems]="10"` |
270
+ | `[maxLength]` | `number` | `undefined` | Độ dài tối đa ký tự được nhập | `[maxLength]="100"` |
271
+ | `[maxLengthNumberCount]` | `number` | `undefined` | Độ dài tối đa cho kiểu number count | `[maxLengthNumberCount]="6"` |
272
+ | `[maxValueNumber]` | `number` | `undefined` | Giá trị số tối đa được phép nhập | `[maxValueNumber]="999"` |
273
+ | `[minItems]` | `number` | `1` | Số lượng items tối thiểu, không thể xóa nếu đạt mức này | `[minItems]="2"` |
274
+ | `[minValueNumber]` | `number` | `undefined` | Giá trị số tối thiểu được phép nhập | `[minValueNumber]="0"` |
275
+ | `[noBorder]` | `boolean` | `false` | Ẩn border của input | `[noBorder]="true"` |
276
+ | `[onlyAcceptNegativeValue]` | `boolean` | `false` | Chỉ chấp nhận giá trị âm | `[onlyAcceptNegativeValue]="true"` |
277
+ | `[paddingRightCustomSpecific]` | `number` | `undefined` | Giá trị padding-right tùy chỉnh cụ thể (fix lỗi count display) | `[paddingRightCustomSpecific]="40"` |
278
+ | `[placeholder]` | `string` | `undefined` | Placeholder mặc định áp dụng cho tất cả inputs | `[placeholder]="'Nhập giá trị'"` |
279
+ | `[positionMessageErrorStartInput]` | `boolean` | `false` | Hiển thị message lỗi bắt đầu từ vị trí input (bỏ qua template trái) | `[positionMessageErrorStartInput]="true"` |
280
+ | `[readonly]` | `boolean` | `false` | Đặt toàn bộ inputs thành chỉ đọc | `[readonly]="true"` |
281
+ | `[resetAutoCompletePassword]` | `boolean` | `false` | Reset autocomplete cho trường password | `[resetAutoCompletePassword]="true"` |
282
+ | `[resize]` | `'auto' \| 'none'` | `undefined` | Kiểu resize cho textarea | `[resize]="'auto'"` |
283
+ | `[rightTemplateItems]` | `ITemplateRightLeftItem` | *(default: remove + template)* | Cấu hình các loại template hiển thị bên phải mỗi item | `[rightTemplateItems]="customRightTemplate"` |
284
+ | `[showCount]` | `boolean` | `false` | Hiển thị bộ đếm ký tự | `[showCount]="true"` |
285
+ | `[tagInput]` | `TYPE_TAG_INPUT` | `undefined` | Tag HTML của input (input hoặc textarea) | `[tagInput]="'textarea'"` |
286
+ | `[templateLeftBottomInput]` | `TemplateRef<TYPE_TEMPLATE_REF>` | `undefined` | Template hiển thị phía dưới bên trái input | `[templateLeftBottomInput]="myTpl"` |
287
+ | `[templateLeftOutlet]` | `TemplateRef<TYPE_TEMPLATE_REF>` | `undefined` | Template outlet toàn cục hiển thị bên trái input | `[templateLeftOutlet]="leftTpl"` |
288
+ | `[templateRightBottomInput]` | `TemplateRef<TYPE_TEMPLATE_REF>` | `undefined` | Template hiển thị phía dưới bên phải input | `[templateRightBottomInput]="myTpl"` |
289
+ | `[templateRightOutlet]` | `TemplateRef<TYPE_TEMPLATE_REF>` | `undefined` | Template outlet toàn cục hiển thị bên phải input | `[templateRightOutlet]="rightTpl"` |
290
+ | `[typeInput]` | `TYPE_INPUT` | `undefined` | Kiểu input HTML: text, password, email... | `[typeInput]="'password'"` |
291
+ | `[unitsLeft]` | `Array<any>` | `undefined` | Danh sách đơn vị tính hiển thị bên trái input | `[unitsLeft]="[{ id: 'vnd', label: 'VND' }]"` |
292
+ | `[unitsRight]` | `Array<any>` | `undefined` | Danh sách đơn vị tính hiển thị bên phải input | `[unitsRight]="[{ id: 'kg', label: 'Kg' }]"` |
293
+ | `[useColorModeExist]` | `boolean` | `false` | Sử dụng color mode có sẵn | `[useColorModeExist]="true"` |
294
+ | `[validDuplicate]` | `IMessageTranslate` | `undefined` | Thông báo lỗi khi phát hiện giá trị trùng lặp | `[validDuplicate]="{ message: 'Giá trị đã tồn tại' }"` |
295
+ | `[validMaxLength]` | `IMessageTranslate` | `undefined` | Validate thông báo khi vượt độ dài tối đa | `[validMaxLength]="{ message: 'Tối đa 50 ký tự' }"` |
296
+ | `[validMaxValue]` | `IMessageTranslate` | `undefined` | Validate thông báo khi vượt giá trị số tối đa | `[validMaxValue]="{ message: 'Không được vượt quá 100' }"` |
297
+ | `[validMinLength]` | `IValidLength` | `undefined` | Validate độ dài tối thiểu | `[validMinLength]="{ length: 3, message: 'Tối thiểu 3 ký tự' }"` |
298
+ | `[validMinValue]` | `IMessageTranslate` | `undefined` | Validate và thông báo khi nhỏ hơn giá trị số tối thiểu | `[validMinValue]="{ message: 'Phải >= 0' }"` |
299
+ | `[validPattern]` | `Array<IValidPattern>` | `undefined` | Validate theo danh sách regex pattern | `[validPattern]="[{ pattern: /^[a-z]+$/, message: 'Chỉ chữ thường' }]"` |
300
+ | `[validRequired]` | `IValidRequired` | `undefined` | Validate bắt buộc nhập | `[validRequired]="{ message: 'Trường này bắt buộc' }"` |
301
+ | `[valuePatternShowError]` | `boolean` | `false` | Hiển thị lỗi pattern (khi pattern trả về false) | `[valuePatternShowError]="true"` |
302
+ | `[valueUpDownNumber]` | `number` | `undefined` | Bước nhảy tăng/giảm khi nhấn mũi tên trên số | `[valueUpDownNumber]="5"` |
303
+
304
+ ## @Output()
305
+
306
+ | Output | Type | Mô tả | Handler TS | Binding HTML |
307
+ |---|---|---|---|---|
308
+ | `(outAddItem)` | `IInputAdd` | Emit khi người dùng nhấn nút thêm item mới | `handlerAddItem(item: IInputAdd): void { item; }` | `(outAddItem)="handlerAddItem($event)"` |
309
+ | `(outClickButtonLabel)` | `IButton` | Emit khi click button trong khu vực label | `handlerClickButtonLabel(button: IButton): void { button; }` | `(outClickButtonLabel)="handlerClickButtonLabel($event)"` |
310
+ | `(outFunctionsControl)` | `IInputAddFunctionControlEvent` | Emit API functions để component cha thực hiện validate hoặc set error | `handlerFunctionsControl(controls: IInputAddFunctionControlEvent): void { this.controls = controls; }` | `(outFunctionsControl)="handlerFunctionsControl($event)"` |
311
+ | `(outLabelLeftClick)` | `MouseEvent` | Emit khi click vào label bên trái | `handlerLabelLeftClick(event: MouseEvent): void { event.stopPropagation(); }` | `(outLabelLeftClick)="handlerLabelLeftClick($event)"` |
312
+ | `(outLabelRightClick)` | `boolean` | Emit khi click vào label bên phải | `handlerLabelRightClick(value: boolean): void { value; }` | `(outLabelRightClick)="handlerLabelRightClick($event)"` |
313
+ | `(outRemove)` | `IInputAdd` | Emit item vừa bị xóa khi người dùng nhấn nút remove | `handlerRemove(item: IInputAdd): void { item; }` | `(outRemove)="handlerRemove($event)"` |
314
+ | `(outSwitchEventLabel)` | `ISwitchEvent` | Emit sự kiện switch/toggle trong khu vực label | `handlerSwitchLabel(event: ISwitchEvent): void { event; }` | `(outSwitchEventLabel)="handlerSwitchLabel($event)"` |
315
+ | `(outValueChange)` | `IEmitValueChange` | Emit mỗi khi giá trị của bất kỳ input nào trong danh sách thay đổi | `handlerValueChange(event: IEmitValueChange): void { event.value; event.item; }` | `(outValueChange)="handlerValueChange($event)"` |
316
+
317
+ ## FunctionsControl
318
+
319
+ Component expose API thông qua `(outFunctionsControl)` để component cha thể gọi validate hoặc set error từ bên ngoài.
320
+
321
+ ```typescript
322
+ // Nhận và lưu functions control
323
+ private functionControls: IInputAddFunctionControlEvent | null = null;
324
+
325
+ handlerFunctionsControl(controls: IInputAddFunctionControlEvent): void {
326
+ this.functionControls = controls;
327
+ }
328
+
329
+ // Gọi validate tất cả items
330
+ async validateAll(): Promise<void> {
331
+ const isValid = await this.functionControls?.checkIsValid();
332
+ // isValid = true nếu tất cả inputs hợp lệ
333
+ }
334
+
335
+ // Đặt thông báo lỗi cho tất cả items
336
+ async setAllErrors(message: string): Promise<void> {
337
+ await this.functionControls?.setMessageError(message);
338
+ }
339
+ ```
340
+
341
+ | Method | Signature | Mô tả |
342
+ |---|---|---|
343
+ | `checkIsValid()` | `() => Promise<boolean>` | Kiểm tra validation tất cả items, trả về `true` nếu hợp lệ. Bỏ qua items có `disable` hoặc `readonly`. |
344
+ | `setMessageError(message)` | `(message: string) => Promise<void>` | Đặt thông báo lỗi tùy chỉnh cho tất cả items trong danh sách. |
247
345
 
248
346
  ## Types & Interfaces
249
347
 
250
348
  ```typescript
349
+ import {
350
+ IInputAdd,
351
+ IInputAddFunctionControlEvent,
352
+ IEmitValueChange,
353
+ ITemplateRightLeftItem,
354
+ } from '@libs-ui/components-inputs-add';
355
+ ```
356
+
357
+ ```typescript
358
+ // Interface cho mỗi item trong danh sách
359
+ // Mở rộng Record<string, any> để hỗ trợ dynamic field theo fieldNameBind
251
360
  interface IInputAdd extends Record<string, any> {
252
- uniqKey?: string;
253
- disable?: boolean;
254
- readonly?: boolean;
255
- ignoreRemove?: boolean;
256
- labelConfig?: ILabel;
257
- placeholder?: string;
258
- maxValueNumber?: number;
259
- minValueNumber?: number;
260
- maxLength?: number;
261
-
262
- unitsLeft?: Array<any>;
263
- configUnitLeft?: IInputValidUnitConfig;
264
- keySelectedUnitLeft?: any;
265
-
266
- unitsRight?: Array<any>;
267
- configUnitRight?: IInputValidUnitConfig;
268
- keySelectedUnitRight?: any;
269
-
270
- templateLeftOutlet?: TemplateRef<TYPE_TEMPLATE_REF>;
271
- templateRightOutlet?: TemplateRef<TYPE_TEMPLATE_REF>;
272
- functionControl?: IInputValidFunctionControlEvent;
273
-
274
- [key: string]: any;
361
+ uniqKey?: string; // key nội bộ, tự động tạo
362
+ disable?: boolean; // disable item riêng lẻ
363
+ readonly?: boolean; // readonly item riêng lẻ
364
+ ignoreRemove?: boolean; // ẩn nút xóa cho item này
365
+ placeholder?: string; // placeholder riêng cho item
366
+ maxValueNumber?: number; // giá trị số tối đa riêng
367
+ minValueNumber?: number; // giá trị số tối thiểu riêng
368
+ maxLength?: number; // độ dài tối đa riêng
369
+ unitsLeft?: Array<any>; // units trái riêng cho item
370
+ configUnitLeft?: IInputValidUnitConfig; // config unit trái riêng
371
+ keySelectedUnitLeft?: any; // unit trái đang chọn riêng
372
+ unitsRight?: Array<any>; // units phải riêng cho item
373
+ configUnitRight?: IInputValidUnitConfig; // config unit phải riêng
374
+ keySelectedUnitRight?: any; // unit phải đang chọn riêng
375
+ templateLeftOutlet?: TemplateRef<TYPE_TEMPLATE_REF>; // template trái riêng
376
+ templateRightOutlet?: TemplateRef<TYPE_TEMPLATE_REF>; // template phải riêng
377
+ functionControl?: IInputValidFunctionControlEvent; // internal, tự quản lý
378
+ [key: string]: any; // dynamic properties
275
379
  }
276
380
 
381
+ // API functions để validate và set error từ component cha
277
382
  interface IInputAddFunctionControlEvent {
278
383
  checkIsValid: () => Promise<boolean>;
279
384
  setMessageError: (message: string) => Promise<void>;
280
385
  }
281
386
 
387
+ // Event emit khi giá trị thay đổi
282
388
  interface IEmitValueChange {
283
- value: string | number;
284
- item: IInputAdd;
389
+ value: string | number; // giá trị mới
390
+ item: IInputAdd; // item bị thay đổi
391
+ }
392
+
393
+ // Cấu hình template trái/phải của danh sách items
394
+ interface ITemplateRightLeftItem {
395
+ classInclude?: string;
396
+ items: Array<{
397
+ type: 'remove' | 'template'; // 'remove': nút xóa, 'template': custom template
398
+ classInclude?: string;
399
+ }>;
285
400
  }
286
401
  ```
287
402
 
288
- ## Công nghệ
403
+ ## Lưu ý quan trọng
289
404
 
290
- | Technology | Version | Purpose |
291
- | --------------- | ------- | ---------------- |
292
- | Angular | 18+ | Framework |
293
- | Angular Signals | - | State management |
294
- | TailwindCSS | 3.x | Styling |
295
- | OnPush | - | Change Detection |
405
+ ⚠️ **fieldNameBind bắt buộc**: Phải truyền `[fieldNameBind]` để component biết property nào trong `IInputAdd` chứa giá trị. Ví dụ `[fieldNameBind]="'email'"` thì mỗi item phải có field `email`.
296
406
 
297
- ## Demo
407
+ ⚠️ **items là two-way binding**: `items` sử dụng `model()` (Angular signal two-way binding). Dùng cú pháp `[(items)]="myItems"` để nhận cập nhật khi add/remove. Nếu chỉ truyền `[items]` một chiều, array trong component cha sẽ không được cập nhật.
298
408
 
299
- ```bash
300
- npx nx serve core-ui
301
- ```
409
+ ⚠️ **Khởi tạo items**: Nếu truyền vào `items` là mảng rỗng `[]`, component tự động tạo một item mặc định `{ [fieldNameBind]: '' }` trong `ngOnInit`.
302
410
 
303
- Truy cập: `http://localhost:4500/inputs/add`
411
+ ⚠️ **Debounce validation**: Validation tự động chạy sau 250ms debounce khi value thay đổi hoặc khi remove item. Không cần gọi `checkIsValid()` thủ công sau mỗi lần nhập.
304
412
 
305
- ## Unit Tests
413
+ ⚠️ **ignoreValidEmptyField**: Khi bật `[ignoreValidEmptyField]="true"`, nếu ít nhất 1 field trong danh sách có giá trị thì toàn bộ fields trống đều được coi là valid. Dùng cho trường hợp "nhập ít nhất một giá trị".
306
414
 
307
- ```bash
308
- # Chạy tests
309
- npx nx test components-inputs-add
415
+ ⚠️ **minItems và remove**: Nút remove sẽ bị ẩn tự động khi số lượng items hiện tại bằng giá trị `minItems`. Mặc định `minItems = 1` nên luôn giữ ít nhất 1 item.
310
416
 
311
- # Coverage
312
- npx nx test components-inputs-add --coverage
417
+ ⚠️ **Per-item config**: Mỗi `IInputAdd` item có thể override nhiều thuộc tính như `placeholder`, `maxLength`, `disable`, `readonly`, `ignoreRemove`, `unitsLeft/Right` riêng — ưu tiên hơn config toàn cục.
313
418
 
314
- # Watch mode
315
- npx nx test components-inputs-add --watch
316
- ```
419
+ ## Demo
317
420
 
318
- ## License
421
+ ```bash
422
+ npx nx serve core-ui
423
+ ```
319
424
 
320
- MIT
425
+ Truy cập: `http://localhost:4500/inputs/add`