@libs-ui/components-inputs-add
v0.2.357-4
Published
> Component quản lý danh sách input fields động với khả năng thêm/xóa, validation đầy đủ và customization linh hoạt
Downloads
6,363
Readme
@libs-ui/components-inputs-add
Component quản lý danh sách input fields động với khả năng thêm/xóa, validation đầy đủ và customization linh hoạt
Giới thiệu
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 cơ 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.
Tính năng
- Thêm/xóa input fields động với nút Add và Remove
- Two-way binding với
model()choitemsarray - Validation tích hợp: required, minLength, maxLength, pattern, min/max value, duplicate
- Hỗ trợ nhiều kiểu dữ liệu: text, number (int/float), password, textarea
- Giới hạn số lượng items tối thiểu/tối đa (
minItems,maxItems) - Custom template trái/phải cho từng item (TemplateRef outlets)
- Hỗ trợ đơn vị tính bên trái và bên phải (units left/right)
- Label configuration tích hợp (title, required mark, description, toggle, popover)
- FunctionsControl API để validate và set error từ component cha
- Custom button "Add New" với đầy đủ styling options
- Debounce 250ms khi validate sau khi value thay đổi hoặc remove item
- Angular Signals + OnPush Change Detection
Khi nào sử dụng
- Cần nhập nhiều giá trị cùng loại: emails, số điện thoại, URLs, tags
- Cho phép người dùng thêm/xóa fields động theo nhu cầu
- Cần validation cho từng field riêng biệt trong danh sách
- Quản lý danh sách inputs với ràng buộc số lượng min/max
- Cần kiểm tra giá trị trùng lặp (duplicate) giữa các fields
- Form có section nhập liệu lặp lại theo cấu trúc giống nhau
Cài đặt
npm install @libs-ui/components-inputs-addImport
import {
LibsUiComponentsInputsAddComponent,
IInputAdd,
IInputAddFunctionControlEvent,
IEmitValueChange,
ITemplateRightLeftItem,
} from '@libs-ui/components-inputs-add';
@Component({
standalone: true,
imports: [LibsUiComponentsInputsAddComponent],
// ...
})
export class YourComponent {}Ví dụ sử dụng
Cơ bản — Danh sách email
import { Component, signal } from '@angular/core';
import { LibsUiComponentsInputsAddComponent, IInputAdd } from '@libs-ui/components-inputs-add';
@Component({
selector: 'app-email-list',
standalone: true,
imports: [LibsUiComponentsInputsAddComponent],
template: `
<libs_ui-components-inputs-add
[fieldNameBind]="'email'"
[(items)]="emailItems"
[placeholder]="'Nhập địa chỉ email'"
[labelConfig]="{ labelLeft: 'Danh sách email', required: true }"
/>
`,
})
export class EmailListComponent {
emailItems = signal<Array<IInputAdd>>([{ email: '' }]);
}Giới hạn số lượng items
import { Component, signal } from '@angular/core';
import { LibsUiComponentsInputsAddComponent, IInputAdd } from '@libs-ui/components-inputs-add';
@Component({
selector: 'app-phone-list',
standalone: true,
imports: [LibsUiComponentsInputsAddComponent],
template: `
<libs_ui-components-inputs-add
[fieldNameBind]="'phone'"
[(items)]="phoneItems"
[minItems]="2"
[maxItems]="5"
[placeholder]="'Nhập số điện thoại'"
[labelConfig]="{ labelLeft: 'Số điện thoại (2–5 số)' }"
/>
`,
})
export class PhoneListComponent {
phoneItems = signal<Array<IInputAdd>>([{ phone: '' }, { phone: '' }]);
}Validation và kiểm tra duplicate
import { Component, signal } from '@angular/core';
import {
LibsUiComponentsInputsAddComponent,
IInputAdd,
IInputAddFunctionControlEvent,
IEmitValueChange,
} from '@libs-ui/components-inputs-add';
@Component({
selector: 'app-url-list',
standalone: true,
imports: [LibsUiComponentsInputsAddComponent],
template: `
<libs_ui-components-inputs-add
[fieldNameBind]="'url'"
[(items)]="urlItems"
[placeholder]="'https://example.com'"
[labelConfig]="{ labelLeft: 'Danh sách URL', required: true }"
[validRequired]="{ message: 'URL là bắt buộc' }"
[validDuplicate]="{ message: 'URL đã tồn tại trong danh sách' }"
(outFunctionsControl)="handlerFunctionsControl($event)"
(outValueChange)="handlerValueChange($event)"
(outAddItem)="handlerAddItem($event)"
(outRemove)="handlerRemove($event)"
/>
<button (click)="validateAll()">Kiểm tra hợp lệ</button>
`,
})
export class UrlListComponent {
urlItems = signal<Array<IInputAdd>>([{ url: '' }]);
private functionControls: IInputAddFunctionControlEvent | null = null;
handlerFunctionsControl(controls: IInputAddFunctionControlEvent): void {
this.functionControls = controls;
}
handlerValueChange(event: IEmitValueChange): void {
// xử lý khi giá trị thay đổi
}
handlerAddItem(item: IInputAdd): void {
// xử lý khi thêm item mới
}
handlerRemove(item: IInputAdd): void {
// xử lý khi xóa item
}
async validateAll(): Promise<void> {
const isValid = await this.functionControls?.checkIsValid();
// xử lý kết quả validation
}
}Nhập số có đơn vị tính
import { Component, signal } from '@angular/core';
import { LibsUiComponentsInputsAddComponent, IInputAdd } from '@libs-ui/components-inputs-add';
@Component({
selector: 'app-weight-list',
standalone: true,
imports: [LibsUiComponentsInputsAddComponent],
template: `
<libs_ui-components-inputs-add
[fieldNameBind]="'weight'"
[(items)]="weightItems"
[dataType]="'int'"
[placeholder]="'Nhập trọng lượng'"
[unitsRight]="[{ id: 'kg', label: 'Kg' }, { id: 'lb', label: 'Lbs' }]"
[keySelectedUnitRight]="'kg'"
[configUnitRight]="{ fieldKey: 'id', fieldLabel: 'label' }"
[labelConfig]="{ labelLeft: 'Trọng lượng', description: 'Nhập trọng lượng kèm đơn vị' }"
/>
`,
})
export class WeightListComponent {
weightItems = signal<Array<IInputAdd>>([{ weight: '' }]);
}Custom button Add và ignoreValidEmptyField
import { Component, signal } from '@angular/core';
import { LibsUiComponentsInputsAddComponent, IInputAdd } from '@libs-ui/components-inputs-add';
@Component({
selector: 'app-tag-list',
standalone: true,
imports: [LibsUiComponentsInputsAddComponent],
template: `
<libs_ui-components-inputs-add
[fieldNameBind]="'tag'"
[(items)]="tagItems"
[placeholder]="'Nhập tag'"
[maxItems]="8"
[ignoreValidEmptyField]="true"
[addItemButtonConfig]="{
type: 'button-link-primary',
label: 'Thêm tag',
classIconLeft: 'libs-ui-icon-add text-[10px] mr-[8px]'
}"
[labelConfig]="{ labelLeft: 'Tags', description: 'Ít nhất một tag là bắt buộc' }"
[validRequired]="{ message: 'Cần ít nhất một tag' }"
/>
`,
})
export class TagListComponent {
tagItems = signal<Array<IInputAdd>>([{ tag: '' }]);
}@Input()
| Input | Type | Default | Mô tả | Ví dụ |
|---|---|---|---|---|
| [fieldNameBind] | string | required | Tên property trong mỗi item dùng để bind giá trị | [fieldNameBind]="'email'" |
| [(items)] | Array<IInputAdd> | required | Mảng items — two-way binding qua model() | [(items)]="emailItems" |
| [acceptNegativeValue] | boolean | false | Cho phép nhập giá trị âm (dùng với dataType number) | [acceptNegativeValue]="true" |
| [addItemButtonConfig] | IButton | undefined | Cấu hình nút "Thêm mới" | [addItemButtonConfig]="{ type: 'button-primary', label: 'Add' }" |
| [autoAddZeroLessThan10InTypeInt] | boolean | false | Tự động thêm số 0 trước khi số < 10 (với kiểu int) | [autoAddZeroLessThan10InTypeInt]="true" |
| [autoRemoveEmoji] | boolean | false | Tự động loại bỏ emoji khỏi giá trị input | [autoRemoveEmoji]="true" |
| [backgroundNone] | boolean | false | Sử dụng nền trong suốt cho input | [backgroundNone]="true" |
| [borderError] | boolean | false | Hiển thị border màu đỏ khi có lỗi | [borderError]="true" |
| [classContainerBottomInput] | string | undefined | Class CSS cho vùng container phía dưới input | [classContainerBottomInput]="'mt-2'" |
| [classContainerInput] | string | undefined | Class CSS cho container bao ngoài mỗi input | [classContainerInput]="'w-full'" |
| [classInclude] | string | undefined | Class CSS bổ sung cho container chính của component | [classInclude]="'mb-4'" |
| [classIncludeInput] | string | undefined | Class CSS bổ sung cho từng input field | [classIncludeInput]="'flex-1'" |
| [classMessageErrorInclude] | string | undefined | Class CSS bổ sung cho message lỗi | [classMessageErrorInclude]="'text-xs'" |
| [configItemAddToItems] | IInputAdd | undefined | Template dữ liệu mặc định khi thêm item mới | [configItemAddToItems]="{ unit: 'kg' }" |
| [configUnitLeft] | IInputValidUnitConfig | { fieldKey: 'id', fieldLabel: 'label' } | Cấu hình mapping field cho danh sách unit trái | [configUnitLeft]="{ fieldKey: 'value', fieldLabel: 'name' }" |
| [configUnitRight] | IInputValidUnitConfig | { fieldKey: 'id', fieldLabel: 'label' } | Cấu hình mapping field cho danh sách unit phải | [configUnitRight]="{ fieldKey: 'value', fieldLabel: 'name' }" |
| [dataType] | TYPE_DATA_TYPE_INPUT | undefined | Kiểu dữ liệu input: text, int, float, email... | [dataType]="'int'" |
| [defaultHeight] | number | undefined | Chiều cao mặc định (px) của input | [defaultHeight]="40" |
| [disable] | boolean | false | Vô hiệu hóa toàn bộ inputs trong danh sách | [disable]="true" |
| [emitEmptyInDataTypeNumber] | boolean | false | Emit giá trị rỗng khi dataType là number và field trống | [emitEmptyInDataTypeNumber]="true" |
| [fixedFloat] | number | undefined | Số chữ số thập phân cố định (dataType float) | [fixedFloat]="2" |
| [focusTimeOut] | number | undefined | Thời gian delay (ms) trước khi focus input | [focusTimeOut]="100" |
| [formInputSpacing] | number | 8 | Khoảng cách (px) giữa các dòng input | [formInputSpacing]="12" |
| [functionValid] | TYPE_FUNCTION_INPUT_VALID | undefined | Hàm validate tùy chỉnh bổ sung | [functionValid]="customValidFn" |
| [ignoreAddItem] | boolean | false | Ẩn nút "Thêm mới" | [ignoreAddItem]="true" |
| [ignoreRemove] | boolean | false | Ẩn nút xóa cho tất cả items | [ignoreRemove]="true" |
| [ignoreShowError] | boolean | false | Ẩn hiển thị thông báo lỗi validation | [ignoreShowError]="true" |
| [ignoreStopPropagationEvent] | boolean | false | Không chặn sự kiện lan truyền (stopPropagation) | [ignoreStopPropagationEvent]="true" |
| [ignoreUnitRightClassReadOnly] | boolean | false | Không áp class readonly cho unit bên phải | [ignoreUnitRightClassReadOnly]="true" |
| [ignoreValidEmptyField] | boolean | false | Coi là valid nếu ít nhất 1 field có giá trị (không bắt buộc tất cả) | [ignoreValidEmptyField]="true" |
| [ignoreWidthInput100] | boolean | false | Không áp dụng width 100% cho input | [ignoreWidthInput100]="true" |
| [isBaselineStyle] | boolean | false | Căn chỉnh items theo baseline (dùng khi input có chiều cao khác nhau) | [isBaselineStyle]="true" |
| [keepPlaceholderOnly] | boolean | false | Chỉ giữ lại placeholder, ẩn các nội dung khác khi trống | [keepPlaceholderOnly]="true" |
| [keySelectedUnitLeft] | any | undefined | Giá trị key của unit bên trái đang được chọn | [keySelectedUnitLeft]="'kg'" |
| [keySelectedUnitRight] | any | undefined | Giá trị key của unit bên phải đang được chọn | [keySelectedUnitRight]="'usd'" |
| [labelConfig] | ILabel | undefined | Cấu hình toàn bộ label (tiêu đề, required, description, toggle...) | [labelConfig]="{ labelLeft: 'Email', required: true }" |
| [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" |
| [maxItems] | number | 5 | Số lượng items tối đa được phép thêm | [maxItems]="10" |
| [maxLength] | number | undefined | Độ dài tối đa ký tự được nhập | [maxLength]="100" |
| [maxLengthNumberCount] | number | undefined | Độ dài tối đa cho kiểu number count | [maxLengthNumberCount]="6" |
| [maxValueNumber] | number | undefined | Giá trị số tối đa được phép nhập | [maxValueNumber]="999" |
| [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" |
| [minValueNumber] | number | undefined | Giá trị số tối thiểu được phép nhập | [minValueNumber]="0" |
| [noBorder] | boolean | false | Ẩn border của input | [noBorder]="true" |
| [onlyAcceptNegativeValue] | boolean | false | Chỉ chấp nhận giá trị âm | [onlyAcceptNegativeValue]="true" |
| [paddingRightCustomSpecific] | number | undefined | Giá trị padding-right tùy chỉnh cụ thể (fix lỗi count display) | [paddingRightCustomSpecific]="40" |
| [placeholder] | string | undefined | Placeholder mặc định áp dụng cho tất cả inputs | [placeholder]="'Nhập giá trị'" |
| [positionMessageErrorStartInput] | boolean | false | Hiển thị message lỗi bắt đầu từ vị trí input (bỏ qua template trái) | [positionMessageErrorStartInput]="true" |
| [readonly] | boolean | false | Đặt toàn bộ inputs thành chỉ đọc | [readonly]="true" |
| [resetAutoCompletePassword] | boolean | false | Reset autocomplete cho trường password | [resetAutoCompletePassword]="true" |
| [resize] | 'auto' \| 'none' | undefined | Kiểu resize cho textarea | [resize]="'auto'" |
| [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" |
| [showCount] | boolean | false | Hiển thị bộ đếm ký tự | [showCount]="true" |
| [tagInput] | TYPE_TAG_INPUT | undefined | Tag HTML của input (input hoặc textarea) | [tagInput]="'textarea'" |
| [templateLeftBottomInput] | TemplateRef<TYPE_TEMPLATE_REF> | undefined | Template hiển thị phía dưới bên trái input | [templateLeftBottomInput]="myTpl" |
| [templateLeftOutlet] | TemplateRef<TYPE_TEMPLATE_REF> | undefined | Template outlet toàn cục hiển thị bên trái input | [templateLeftOutlet]="leftTpl" |
| [templateRightBottomInput] | TemplateRef<TYPE_TEMPLATE_REF> | undefined | Template hiển thị phía dưới bên phải input | [templateRightBottomInput]="myTpl" |
| [templateRightOutlet] | TemplateRef<TYPE_TEMPLATE_REF> | undefined | Template outlet toàn cục hiển thị bên phải input | [templateRightOutlet]="rightTpl" |
| [typeInput] | TYPE_INPUT | undefined | Kiểu input HTML: text, password, email... | [typeInput]="'password'" |
| [unitsLeft] | Array<any> | undefined | Danh sách đơn vị tính hiển thị bên trái input | [unitsLeft]="[{ id: 'vnd', label: 'VND' }]" |
| [unitsRight] | Array<any> | undefined | Danh sách đơn vị tính hiển thị bên phải input | [unitsRight]="[{ id: 'kg', label: 'Kg' }]" |
| [useColorModeExist] | boolean | false | Sử dụng color mode có sẵn | [useColorModeExist]="true" |
| [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' }" |
| [validMaxLength] | IMessageTranslate | undefined | Validate và thông báo khi vượt độ dài tối đa | [validMaxLength]="{ message: 'Tối đa 50 ký tự' }" |
| [validMaxValue] | IMessageTranslate | undefined | Validate và thông báo khi vượt giá trị số tối đa | [validMaxValue]="{ message: 'Không được vượt quá 100' }" |
| [validMinLength] | IValidLength | undefined | Validate độ dài tối thiểu | [validMinLength]="{ length: 3, message: 'Tối thiểu 3 ký tự' }" |
| [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' }" |
| [validPattern] | Array<IValidPattern> | undefined | Validate theo danh sách regex pattern | [validPattern]="[{ pattern: /^[a-z]+$/, message: 'Chỉ chữ thường' }]" |
| [validRequired] | IValidRequired | undefined | Validate bắt buộc nhập | [validRequired]="{ message: 'Trường này bắt buộc' }" |
| [valuePatternShowError] | boolean | false | Hiển thị lỗi pattern (khi pattern trả về false) | [valuePatternShowError]="true" |
| [valueUpDownNumber] | number | undefined | Bước nhảy tăng/giảm khi nhấn mũi tên trên số | [valueUpDownNumber]="5" |
@Output()
| Output | Type | Mô tả | Handler TS | Binding HTML |
|---|---|---|---|---|
| (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)" |
| (outClickButtonLabel) | IButton | Emit khi click button trong khu vực label | handlerClickButtonLabel(button: IButton): void { button; } | (outClickButtonLabel)="handlerClickButtonLabel($event)" |
| (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)" |
| (outLabelLeftClick) | MouseEvent | Emit khi click vào label bên trái | handlerLabelLeftClick(event: MouseEvent): void { event.stopPropagation(); } | (outLabelLeftClick)="handlerLabelLeftClick($event)" |
| (outLabelRightClick) | boolean | Emit khi click vào label bên phải | handlerLabelRightClick(value: boolean): void { value; } | (outLabelRightClick)="handlerLabelRightClick($event)" |
| (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)" |
| (outSwitchEventLabel) | ISwitchEvent | Emit sự kiện switch/toggle trong khu vực label | handlerSwitchLabel(event: ISwitchEvent): void { event; } | (outSwitchEventLabel)="handlerSwitchLabel($event)" |
| (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)" |
FunctionsControl
Component expose API thông qua (outFunctionsControl) để component cha có thể gọi validate hoặc set error từ bên ngoài.
// Nhận và lưu functions control
private functionControls: IInputAddFunctionControlEvent | null = null;
handlerFunctionsControl(controls: IInputAddFunctionControlEvent): void {
this.functionControls = controls;
}
// Gọi validate tất cả items
async validateAll(): Promise<void> {
const isValid = await this.functionControls?.checkIsValid();
// isValid = true nếu tất cả inputs hợp lệ
}
// Đặt thông báo lỗi cho tất cả items
async setAllErrors(message: string): Promise<void> {
await this.functionControls?.setMessageError(message);
}| Method | Signature | Mô tả |
|---|---|---|
| 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. |
| 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. |
Types & Interfaces
import {
IInputAdd,
IInputAddFunctionControlEvent,
IEmitValueChange,
ITemplateRightLeftItem,
} from '@libs-ui/components-inputs-add';// Interface cho mỗi item trong danh sách
// Mở rộng Record<string, any> để hỗ trợ dynamic field theo fieldNameBind
interface IInputAdd extends Record<string, any> {
uniqKey?: string; // key nội bộ, tự động tạo
disable?: boolean; // disable item riêng lẻ
readonly?: boolean; // readonly item riêng lẻ
ignoreRemove?: boolean; // ẩn nút xóa cho item này
placeholder?: string; // placeholder riêng cho item
maxValueNumber?: number; // giá trị số tối đa riêng
minValueNumber?: number; // giá trị số tối thiểu riêng
maxLength?: number; // độ dài tối đa riêng
unitsLeft?: Array<any>; // units trái riêng cho item
configUnitLeft?: IInputValidUnitConfig; // config unit trái riêng
keySelectedUnitLeft?: any; // unit trái đang chọn riêng
unitsRight?: Array<any>; // units phải riêng cho item
configUnitRight?: IInputValidUnitConfig; // config unit phải riêng
keySelectedUnitRight?: any; // unit phải đang chọn riêng
templateLeftOutlet?: TemplateRef<TYPE_TEMPLATE_REF>; // template trái riêng
templateRightOutlet?: TemplateRef<TYPE_TEMPLATE_REF>; // template phải riêng
functionControl?: IInputValidFunctionControlEvent; // internal, tự quản lý
[key: string]: any; // dynamic properties
}
// API functions để validate và set error từ component cha
interface IInputAddFunctionControlEvent {
checkIsValid: () => Promise<boolean>;
setMessageError: (message: string) => Promise<void>;
}
// Event emit khi giá trị thay đổi
interface IEmitValueChange {
value: string | number; // giá trị mới
item: IInputAdd; // item bị thay đổi
}
// Cấu hình template trái/phải của danh sách items
interface ITemplateRightLeftItem {
classInclude?: string;
items: Array<{
type: 'remove' | 'template'; // 'remove': nút xóa, 'template': custom template
classInclude?: string;
}>;
}Lưu ý quan trọng
⚠️ 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.
⚠️ 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.
⚠️ 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.
⚠️ 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.
⚠️ 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ị".
⚠️ 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.
⚠️ 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.
Demo
npx nx serve core-uiTruy cập: http://localhost:4500/inputs/add
