@libs-ui/pipes-call-function-in-template
v0.2.357-11
Published
> Pipe đa năng giúp gọi function xử lý logic ngay trong Angular template, hỗ trợ truyền tham số và trả về Observable (async).
Readme
@libs-ui/pipes-call-function-in-template
Pipe đa năng giúp gọi function xử lý logic ngay trong Angular template, hỗ trợ truyền tham số và trả về Observable (async).
Giới thiệu
LibsUiPipesCallFunctionInTemplatePipe cho phép gọi một arrow function tùy ý từ component ngay bên trong template HTML của Angular. Thay vì tạo nhiều Pipe riêng biệt cho từng nghiệp vụ, pipe này đóng vai trò là "bridge" truyền value, item, otherData vào function, nhận lại Observable<T> và kết hợp với async pipe để hiển thị kết quả.
Pipe tích hợp sẵn logic chuẩn hóa giá trị đầu ra: tự động xử lý null, undefined, chuỗi rỗng, số 0 và non-primitive (object, mảng) — không cần viết thêm guard trong function.
Tính năng
- ✅ Gọi bất kỳ arrow function nào trực tiếp trong template mà không cần tạo Pipe riêng
- ✅ Hỗ trợ truyền 2 tham số phụ:
itemvàotherData - ✅ Trả về
Observable<T>— kết hợp vớiasyncpipe hoặc@if (...; as val) - ✅ Tích hợp
switchMap— tự hủy Observable cũ khi giá trị đầu vào thay đổi - ✅ Tự động chuẩn hóa giá trị rỗng:
null/undefined/''/NaN→'__'(mặc định) - ✅ Tùy chỉnh giá trị mặc định cho
0và empty qua tham sốdefaultValueEmpty - ✅ Non-primitive (object, mảng, Date) được giữ nguyên reference
- ✅ Boolean được trả về nguyên dạng
boolean, không bị stringify - ✅ Standalone pipe, Angular 16+
Khi nào sử dụng
- Cần xử lý/biến đổi dữ liệu phức tạp để hiển thị nhưng không muốn tạo riêng một Pipe chỉ dùng 1 lần
- Cần truyền thêm ngữ cảnh (
item,otherData) vào hàm xử lý — ví dụ: tính tổng tiền từ giá × số lượng − giảm giá - Logic transform có thể tái sử dụng giữa nhiều hàng trong
@forhoặc nhiều chỗ trong cùng component - Cần gọi một hàm async (mock API, lookup map) và hiển thị kết quả trong template
Cài đặt
npm install @libs-ui/pipes-call-function-in-templateImport
import { LibsUiPipesCallFunctionInTemplatePipe } from '@libs-ui/pipes-call-function-in-template';
@Component({
standalone: true,
imports: [LibsUiPipesCallFunctionInTemplatePipe],
// ...
})
export class MyComponent {}Ví dụ sử dụng
1. Transform chuỗi đơn giản
import { Component } from '@angular/core';
import { LibsUiPipesCallFunctionInTemplatePipe } from '@libs-ui/pipes-call-function-in-template';
import { Observable, of } from 'rxjs';
import { TYPE_FUNCTION } from '@libs-ui/interfaces-types';
@Component({
standalone: true,
imports: [LibsUiPipesCallFunctionInTemplatePipe],
template: `
<span>{{ 'hello world' | LibsUiPipesCallFunctionInTemplatePipe : transformUppercase | async }}</span>
`,
})
export class MyComponent {
transformUppercase: TYPE_FUNCTION<string> = (data: { value: string }): Observable<string> => {
return of((data.value || '').toUpperCase());
};
}Kết quả hiển thị: HELLO WORLD
2. Truyền tham số phụ item và otherData
import { Component } from '@angular/core';
import { LibsUiPipesCallFunctionInTemplatePipe } from '@libs-ui/pipes-call-function-in-template';
import { Observable, of } from 'rxjs';
import { TYPE_FUNCTION } from '@libs-ui/interfaces-types';
@Component({
standalone: true,
imports: [LibsUiPipesCallFunctionInTemplatePipe],
template: `
<!-- value=100, item=5 (số lượng), otherData=20 (giảm giá) -->
<span>{{ 100 | LibsUiPipesCallFunctionInTemplatePipe : calculateTotal : 5 : 20 | async }}</span>
`,
})
export class MyComponent {
calculateTotal: TYPE_FUNCTION<string> = (data: {
value: number;
item?: number;
otherData?: number;
}): Observable<string> => {
const qty = data.item || 0;
const discount = data.otherData || 0;
const total = data.value * qty - discount;
return of(`${total.toFixed(0)} đ`);
};
}Kết quả hiển thị: 480 đ (100 × 5 − 20)
3. Dùng trong @for để render từng hàng
import { Component, signal } from '@angular/core';
import { LibsUiPipesCallFunctionInTemplatePipe } from '@libs-ui/pipes-call-function-in-template';
import { Observable, of } from 'rxjs';
import { TYPE_FUNCTION } from '@libs-ui/interfaces-types';
type T_product = { id: string; name: string; status: string };
@Component({
standalone: true,
imports: [LibsUiPipesCallFunctionInTemplatePipe],
template: `
@for (product of products(); track product.id) {
<div>
{{ product.name }}
— {{ product.status | LibsUiPipesCallFunctionInTemplatePipe : formatStatus | async }}
</div>
}
`,
})
export class MyComponent {
protected products = signal<T_product[]>([
{ id: '1', name: 'Sản phẩm A', status: 'active' },
{ id: '2', name: 'Sản phẩm B', status: 'inactive' },
]);
formatStatus: TYPE_FUNCTION<string> = (data: { value: string }): Observable<string> => {
const map: Record<string, string> = {
active: 'Đang kinh doanh',
inactive: 'Ngừng kinh doanh',
};
return of(map[data.value] ?? data.value);
};
}4. Tùy chỉnh giá trị mặc định cho null và 0
<!-- null → 'Chưa có dữ liệu' thay vì '__' mặc định -->
{{ null | LibsUiPipesCallFunctionInTemplatePipe : undefined : undefined : undefined : { valueIsEmpty: 'Chưa có dữ liệu' } | async }}
<!-- 0 → 'Miễn phí' thay vì '0' mặc định -->
{{ 0 | LibsUiPipesCallFunctionInTemplatePipe : undefined : undefined : undefined : { valueIs0: 'Miễn phí' } | async }}
<!-- Không cần function, chỉ cần normalize giá trị hiển thị -->
{{ price() | LibsUiPipesCallFunctionInTemplatePipe : undefined : undefined : undefined : { valueIs0: 'Miễn phí', valueIsEmpty: 'Chưa cập nhật' } | async }}5. Gọi hàm async (mock API, lookup bất đồng bộ)
import { Component } from '@angular/core';
import { LibsUiPipesCallFunctionInTemplatePipe } from '@libs-ui/pipes-call-function-in-template';
import { Observable, of, delay } from 'rxjs';
import { TYPE_FUNCTION } from '@libs-ui/interfaces-types';
@Component({
standalone: true,
imports: [LibsUiPipesCallFunctionInTemplatePipe],
template: `
@if ('active' | LibsUiPipesCallFunctionInTemplatePipe : fetchStatusLabel | async; as label) {
<span class="status-badge">{{ label }}</span>
} @else {
<span>Đang tải...</span>
}
`,
})
export class MyComponent {
fetchStatusLabel: TYPE_FUNCTION<string> = (data: { value: string }): Observable<string> => {
const statusMap: Record<string, string> = {
active: 'Đang hoạt động',
inactive: 'Ngừng hoạt động',
pending: 'Đang chờ duyệt',
};
return of(statusMap[data.value] || 'Không xác định').pipe(delay(500));
};
}6. Sử dụng standalone (gọi transform trực tiếp trong TypeScript)
import { TestBed } from '@angular/core/testing';
import { firstValueFrom, of } from 'rxjs';
import { LibsUiPipesCallFunctionInTemplatePipe } from '@libs-ui/pipes-call-function-in-template';
const pipe = TestBed.runInInjectionContext(
() => new LibsUiPipesCallFunctionInTemplatePipe()
);
// Không truyền function — chỉ normalize giá trị
const result = await firstValueFrom(pipe.transform('hello'));
// result === 'hello'
const empty = await firstValueFrom(pipe.transform(null));
// empty === '__' (CHARACTER_DATA_EMPTY)
// Truyền function transform
const upper = await firstValueFrom(
pipe.transform('world', (data) => of(data.value.toUpperCase()))
);
// upper === 'WORLD'Transform
| Tham số | Type | Bắt buộc | Default | Mô tả |
|---|---|---|---|---|
| value | any | ✅ | — | Giá trị đầu vào cần xử lý hoặc hiển thị |
| functionCall | TYPE_FUNCTION | ❌ | undefined | Arrow function nhận { value, item, otherData } và trả về Observable<T>. Nếu không truyền, pipe chỉ chuẩn hóa value |
| item | any | ❌ | undefined | Tham số phụ thứ nhất — ví dụ: số lượng, row index, config object |
| otherData | any | ❌ | undefined | Tham số phụ thứ hai — ví dụ: giảm giá, currency, extra config |
| defaultValueEmpty | { valueIs0?: any; valueIsEmpty?: any } | ❌ | undefined | Ghi đè giá trị mặc định khi output là 0 hoặc falsy/null/undefined |
Cú pháp trong template:
{{ value | LibsUiPipesCallFunctionInTemplatePipe : functionCall : item : otherData : defaultValueEmpty | async }}Kết quả trả về — quy tắc chuẩn hóa:
| Giá trị output | Kết quả |
|---|---|
| null / undefined / '' / NaN | defaultValueEmpty.valueIsEmpty nếu có, hoặc '__' |
| 0 (số nguyên, float) | defaultValueEmpty.valueIs0 nếu có, hoặc '0' |
| Số khác 0 | Chuỗi stringify: '42', '-1', '3.14' |
| boolean | Giữ nguyên true / false (không stringify) |
| Non-primitive (object, mảng, Date, function) | Giữ nguyên reference |
Types & Interfaces
import { TYPE_FUNCTION } from '@libs-ui/interfaces-types';/**
* Type cho function xử lý logic trong pipe.
* Nhận object chứa value, item, otherData — trả về Observable<T>.
*/
export type TYPE_FUNCTION<T = any> = (
data: { value: any; item?: any; otherData?: any }
) => Observable<T>;Ví dụ khai báo có type cụ thể:
import { Observable, of } from 'rxjs';
import { TYPE_FUNCTION } from '@libs-ui/interfaces-types';
// Typed rõ ràng — T = string
formatCurrency: TYPE_FUNCTION<string> = (data: {
value: number;
item?: string; // currency code
}): Observable<string> => {
const currency = data.item ?? 'VND';
return of(`${data.value.toLocaleString()} ${currency}`);
};Lưu ý quan trọng
⚠️ Bắt buộc dùng async pipe: Pipe này luôn trả về Observable. Bắt buộc kết hợp với | async trong template hoặc dùng @if (... | async; as val) để unwrap giá trị. Thiếu async sẽ hiển thị object Observable thay vì giá trị thực.
⚠️ Function phải là arrow function (class property): functionCall phải là class property kiểu arrow function, không được là method thông thường. Method thông thường sẽ mất this context khi được truyền qua pipe.
// ✅ ĐÚNG — class property arrow function
formatLabel: TYPE_FUNCTION<string> = (data) => of(this.translate(data.value));
// ❌ SAI — method thông thường, mất `this`
formatLabel(data: { value: string }): Observable<string> {
return of(this.translate(data.value)); // this là undefined khi pipe gọi
}⚠️ SwitchMap hủy Observable cũ: Pipe dùng switchMap nội bộ. Khi value thay đổi, Observable từ lần gọi trước bị hủy ngay lập tức. Đây là hành vi đúng cho hầu hết use case, nhưng cần chú ý nếu function trả về Observable thực hiện side-effect.
⚠️ bigint 0n không kích hoạt valueIs0: defaultValueEmpty.valueIs0 chỉ áp dụng cho value === 0 (số JS tiêu chuẩn). 0n (BigInt) được xử lý như falsy primitive và rơi vào nhánh valueIsEmpty.
⚠️ Symbol gây TypeError: Truyền Symbol vào pipe (dù là value hay kết quả trả về từ function) sẽ ném TypeError khi pipe cố stringify. Không dùng Symbol với pipe này.
