@libs-ui/components-inputs-calculator
v0.2.357-4
Published
> Component Calculator nâng cao cho phép xây dựng biểu thức toán học dạng token, kết hợp số, toán tử và trường dữ liệu động.
Readme
@libs-ui/components-inputs-calculator
Component Calculator nâng cao cho phép xây dựng biểu thức toán học dạng token, kết hợp số, toán tử và trường dữ liệu động.
Giới thiệu
LibsUiComponentsInputsCalculatorComponent là một Angular standalone component dùng để soạn thảo công thức tính toán theo dạng token trực quan. Người dùng có thể nhập số, chọn toán tử (+, -, *, /) và chèn các trường dữ liệu động từ hệ thống (fields) vào biểu thức. Component hỗ trợ đầy đủ thao tác bàn phím, xóa thông minh theo vị trí cursor, validation và cung cấp interface điều khiển từ bên ngoài qua outFunctionControl.
Tính năng
- ✅ Token-based Input: Hiển thị biểu thức dưới dạng các khối token trực quan (giá trị, toán tử, field).
- ✅ Dynamic Fields: Tích hợp Popover + List để chọn trường dữ liệu động từ hệ thống.
- ✅ Keyboard Shortcuts: Hỗ trợ phím số, phím toán tử (+, -, *, /) và Enter để xác nhận từng token.
- ✅ Smart Editing: Backspace xóa token phía trước, Delete xóa token phía sau so với vị trí cursor.
- ✅ Dynamic Input Width: Tự động co giãn độ rộng ô nhập theo nội dung đang gõ.
- ✅ FunctionControl API: Cung cấp
checkIsValid()vàgetData()để điều khiển từ component cha. - ✅ Validation: Hỗ trợ hiển thị lỗi khi biểu thức trống qua
validRequired. - ✅ OnPush + Signals: Tối ưu hiệu năng với Angular Signals và
ChangeDetectionStrategy.OnPush.
Khi nào sử dụng
- Cần cho phép người dùng xây dựng công thức tính toán tùy chỉnh (formula editor).
- Cấu hình biểu thức tính lương, doanh thu, KPI hoặc các chỉ số nghiệp vụ.
- Kết hợp các trường dữ liệu từ hệ thống (fields) vào biểu thức toán học.
- Yêu cầu giao diện nhập liệu toán học an toàn, ngăn chặn nhập text tự do không hợp lệ.
- Cần validator và lấy dữ liệu biểu thức từ bên ngoài component qua FunctionControl.
Cài đặt
npm install @libs-ui/components-inputs-calculatorImport
import {
LibsUiComponentsInputsCalculatorComponent,
IInputCalculatorConfigField,
IInputCalculatorExpression,
IInputCalculatorFunctionControlEvent,
IInputCalculatorValid,
IInputCalculatorField,
} from '@libs-ui/components-inputs-calculator';
@Component({
standalone: true,
imports: [LibsUiComponentsInputsCalculatorComponent],
// ...
})
export class YourComponent {}Ví dụ sử dụng
Ví dụ 1 — Cơ bản (chỉ cần configField)
// your.component.ts
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import {
LibsUiComponentsInputsCalculatorComponent,
IInputCalculatorConfigField,
IInputCalculatorExpression,
} from '@libs-ui/components-inputs-calculator';
import { returnListObject } from '@libs-ui/services-http-request';
import { escapeHtml } from '@libs-ui/utils';
@Component({
selector: 'app-your',
templateUrl: './your.component.html',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [LibsUiComponentsInputsCalculatorComponent],
})
export class YourComponent {
private readonly fields = [
{ key: 'revenue', name: 'Doanh thu', icon: 'libs-ui-icon-coin' },
{ key: 'cost', name: 'Chi phí', icon: 'libs-ui-icon-calculation' },
{ key: 'quantity', name: 'Số lượng', icon: 'libs-ui-icon-archive' },
];
readonly calculatorConfig: IInputCalculatorConfigField = {
label: 'Chọn trường dữ liệu',
placeholderInputList: 'Tìm kiếm tham số...',
listConfig: {
type: 'group',
httpRequestData: signal({
objectInstance: returnListObject(this.fields),
argumentsValue: [],
functionName: 'list',
}),
configTemplateGroup: signal({
fieldKey: 'key',
getLabelItem: (item) => escapeHtml(item.name),
singleSelectItem: true,
}),
},
getFieldSelected: (key: string) => this.fields.find((f) => f.key === key),
};
protected handlerSaveExpressions(event: Event): void {
event.stopPropagation();
// xử lý dữ liệu biểu thức
}
protected handlerChangeExpressions(expressions: IInputCalculatorExpression[]): void {
console.log('Biểu thức đã lưu:', expressions);
}
}<!-- your.component.html -->
<libs_ui-components-inputs-calculator
[configField]="calculatorConfig"
(outChangeExpressions)="handlerChangeExpressions($event)" />Ví dụ 2 — Khởi tạo với biểu thức có sẵn
// your.component.ts
readonly initialExpressions: IInputCalculatorExpression[] = [
{ type: 'field', data: 'revenue' },
{ type: 'operation', data: '-' },
{ type: 'field', data: 'cost' },
];<!-- your.component.html -->
<libs_ui-components-inputs-calculator
[configField]="calculatorConfig"
[expressions]="initialExpressions"
[placeholder]="'Nhập công thức của bạn...'"
(outChangeExpressions)="handlerChangeExpressions($event)" />Ví dụ 3 — Điều khiển từ bên ngoài qua FunctionControl
// your.component.ts
import { signal } from '@angular/core';
import {
IInputCalculatorFunctionControlEvent,
IInputCalculatorExpression,
} from '@libs-ui/components-inputs-calculator';
readonly calculatorControls = signal<IInputCalculatorFunctionControlEvent | null>(null);
protected handlerFunctionControl(event: IInputCalculatorFunctionControlEvent): void {
event.stopPropagation?.();
this.calculatorControls.set(event);
}
protected async handlerValidate(event: Event): Promise<void> {
event.stopPropagation();
const isValid = await this.calculatorControls()?.checkIsValid();
console.log('Biểu thức hợp lệ:', isValid);
}
protected handlerGetData(event: Event): void {
event.stopPropagation();
const data: IInputCalculatorExpression[] | undefined = this.calculatorControls()?.getData();
console.log('Dữ liệu hiện tại:', data);
}<!-- your.component.html -->
<libs_ui-components-inputs-calculator
[configField]="calculatorConfig"
[ignoreButtonSave]="true"
(outFunctionControl)="calculatorControls.set($event)" />
<button (click)="handlerValidate($event)">Kiểm tra hợp lệ</button>
<button (click)="handlerGetData($event)">Lấy dữ liệu</button>Ví dụ 4 — Validation bắt buộc nhập
// your.component.ts
readonly requiredValidation = { message: 'i18n_expression_required' };<!-- your.component.html -->
<libs_ui-components-inputs-calculator
[configField]="calculatorConfig"
[validRequired]="requiredValidation"
(outChangeExpressions)="handlerChangeExpressions($event)" />Ví dụ 5 — Ẩn nút Save, tùy chỉnh class
<libs_ui-components-inputs-calculator
[configField]="calculatorConfig"
[ignoreButtonSave]="true"
[classInclude]="'shadow-md'"
[classContainerInput]="'min-h-[200px]'"
[classContainerButton]="'mt-4'"
(outChangeExpressions)="handlerChangeExpressions($event)" />@Input()
| Input | Type | Default | Mô tả | Ví dụ |
|---|---|---|---|---|
| [classContainerButton] | string | '' | Class CSS bổ sung cho container chứa các nút bấm (bàn phím số và toán tử). | [classContainerButton]="'mt-4'" |
| [classContainerInput] | string | '' | Class CSS bổ sung cho vùng hiển thị và nhập biểu thức. | [classContainerInput]="'min-h-[200px]'" |
| [classInclude] | string | '' | Class CSS bổ sung cho wrapper ngoài cùng của toàn bộ component. | [classInclude]="'shadow-md rounded-xl'" |
| [configField] | IInputCalculatorConfigField | undefined | Cấu hình cho việc hiển thị và chọn các trường dữ liệu động. Bao gồm listConfig và hàm getFieldSelected. | [configField]="calculatorConfig" |
| [expressions] | IInputCalculatorExpression[] | [] | Danh sách biểu thức khởi tạo ban đầu. Component sẽ clone sâu array này khi ngOnInit. | [expressions]="initialExpressions" |
| [ignoreButtonSave] | boolean | false | Khi true, ẩn nút Save mặc định ở cuối component. Dùng khi muốn lưu theo cách khác qua FunctionControl. | [ignoreButtonSave]="true" |
| [labelButtonSave] | string | 'i18n_save_calculation' | Nhãn hiển thị trên nút Save. Hỗ trợ i18n key. | [labelButtonSave]="'Lưu công thức'" |
| [placeholder] | string | 'i18n_enter_params' | Văn bản placeholder hiển thị khi biểu thức còn trống. Hỗ trợ i18n key. | [placeholder]="'Nhập công thức...'" |
| [validRequired] | IInputCalculatorValid | undefined | Cấu hình validation: hiển thị thông báo lỗi khi biểu thức rỗng. | [validRequired]="{ message: 'i18n_required' }" |
| [zIndex] | number | 10 | Z-index cho Popover hiển thị danh sách fields khi chọn trường dữ liệu. | [zIndex]="100" |
@Output()
| Output | Type | Mô tả | Handler TS | Binding HTML |
|---|---|---|---|---|
| (outChangeExpressions) | IInputCalculatorExpression[] | Phát ra danh sách biểu thức hiện tại khi người dùng nhấn nút Save. Chỉ phát khi có thay đổi so với lần Save trước. | handlerChangeExpressions(expressions: IInputCalculatorExpression[]): void { event.stopPropagation(); this.savedExpressions.set(expressions); } | (outChangeExpressions)="handlerChangeExpressions($event)" |
| (outFunctionControl) | IInputCalculatorFunctionControlEvent | Phát ra ngay khi component khởi tạo (ngOnInit), cung cấp interface để điều khiển component từ bên ngoài. | handlerFunctionControl(control: IInputCalculatorFunctionControlEvent): void { this.calculatorControls.set(control); } | (outFunctionControl)="calculatorControls.set($event)" |
FunctionControl Methods
Nhận về qua output (outFunctionControl). Lưu vào signal rồi gọi khi cần:
readonly calculatorControls = signal<IInputCalculatorFunctionControlEvent | null>(null);| Method | Signature | Mô tả |
|---|---|---|
| checkIsValid() | () => Promise<boolean> | Kiểm tra tính hợp lệ của biểu thức. Trả về false nếu biểu thức rỗng và validRequired được cấu hình. Đồng thời hiển thị thông báo lỗi trên UI. |
| getData() | () => IInputCalculatorExpression[] | Lấy bản sao sâu (deep clone) của danh sách biểu thức hiện tại mà không cần nhấn nút Save. |
Types & Interfaces
import {
IInputCalculatorExpression,
IInputCalculatorConfigField,
IInputCalculatorFunctionControlEvent,
IInputCalculatorValid,
IInputCalculatorField,
} from '@libs-ui/components-inputs-calculator';/**
* Một phần tử trong biểu thức toán học.
* - type 'value': giá trị số hoặc text được nhập trực tiếp (VD: "100", "2.5")
* - type 'field': tham chiếu đến một trường dữ liệu động từ hệ thống (VD: key "revenue")
* - type 'operation': toán tử toán học ('+', '-', '*', '/', '(', ')')
*/
interface IInputCalculatorExpression {
type: 'value' | 'field' | 'operation';
data: string | number;
}
/**
* Cấu hình cho việc chọn và hiển thị các trường dữ liệu động.
*/
interface IInputCalculatorConfigField {
/** Nhãn nút chọn field, hiển thị ở dưới vùng nhập. Mặc định: 'i18n_select_field' */
label?: string;
/** Placeholder cho ô tìm kiếm trong danh sách field */
placeholderInputList?: string;
/** Cấu hình danh sách hiển thị từ @libs-ui/components-list */
listConfig: IListConfigItem;
/** Callback lấy thông tin hiển thị (name, icon) của field theo key */
getFieldSelected: (field: string) => IInputCalculatorField | undefined;
}
/**
* Thông tin hiển thị của một field trong biểu thức.
*/
interface IInputCalculatorField {
key?: string;
name?: string;
icon?: string;
}
/**
* Interface điều khiển component từ bên ngoài (nhận qua outFunctionControl).
*/
interface IInputCalculatorFunctionControlEvent {
checkIsValid: () => Promise<boolean>;
getData: () => Array<IInputCalculatorExpression>;
}
/**
* Cấu hình validation cho biểu thức.
*/
interface IInputCalculatorValid {
message?: string;
}Lưu ý quan trọng
⚠️ configField là cấu hình cốt lõi: Mặc dù không bắt buộc về mặt TypeScript, component sẽ không hiển thị đúng nút chọn field và không resolve được tên/icon của field trong biểu thức nếu thiếu configField. Luôn truyền vào khi dùng trong production.
⚠️ outFunctionControl phát ngay khi ngOnInit: Output này được emit ngay trong ngOnInit, không phải do tương tác người dùng. Cần lưu vào signal hoặc biến class ngay khi nhận. Không dùng async pipe hay subscribe muộn.
⚠️ outChangeExpressions chỉ phát khi nhấn nút Save: Component không phát liên tục khi người dùng chỉnh sửa. Nếu muốn lấy dữ liệu tức thời mà không cần nhấn Save, dùng getData() từ FunctionControl. Để ẩn nút Save và tự quản lý, đặt [ignoreButtonSave]="true".
⚠️ Xóa token bằng bàn phím: Khi ô nhập đang trống và focus, Backspace xóa token ngay phía trước vị trí cursor, Delete xóa token ngay phía sau. Đây là hành vi có chủ đích để tối ưu trải nghiệm chỉnh sửa.
⚠️ Toán tử qua bàn phím: Gõ +, -, *, / trong ô nhập sẽ tự động được chuyển thành token toán tử (type: 'operation') thay vì nhập text. Component dùng keyup event để bắt và xử lý.
⚠️ Deep clone expressions: Component thực hiện cloneDeep trên expressions input khi khởi tạo. Thay đổi array gốc bên ngoài sau khi component đã init sẽ không có tác dụng — cần re-render component nếu muốn reset.
Demo
npx nx serve core-uiTruy cập: http://localhost:4500/components/inputs/calculator
