@libs-ui/pipes-convert-object-to-signal
v0.2.357-7
Published
> Pipe chuyển đổi object/array thành cấu trúc Signals lồng nhau để tận dụng fine-grained reactivity của Angular.
Readme
@libs-ui/pipes-convert-object-to-signal
Pipe chuyển đổi object/array thành cấu trúc Signals lồng nhau để tận dụng fine-grained reactivity của Angular.
Giới thiệu
LibsUiPipesConvertObjectToSignalPipe chuyển đổi một plain object hoặc array thành cấu trúc nested WritableSignal. Mỗi property của object trở thành một WritableSignal riêng biệt, cho phép Angular chỉ re-render đúng phần UI phụ thuộc vào property đó thay vì toàn bộ component. Pipe ủy quyền xử lý cho hàm tiện ích convertObjectToSignal từ @libs-ui/utils.
Tính năng
- ✅ Chuyển object thành nested
WritableSignal— mỗi property là một signal độc lập - ✅ Chuyển array thành
WritableSignal<WritableSignal<T>[]>— mỗi phần tử là một signal - ✅ Hỗ trợ nested objects đa cấp (multi-level)
- ✅ Deep clone mặc định — an toàn, không mutate data gốc
- ✅ Tùy chọn shallow (no-clone) để tăng performance khi cần thiết
- ✅ Fine-grained reactivity — tối ưu hiệu năng cho form/table phức tạp
- ✅ Trả về nguyên nếu input là
nullhoặcundefined
Khi nào sử dụng
- Form nhiều fields: Thay đổi một field không trigger re-render các field khác — mỗi
<input>bind vào signal riêng. - Data table lớn: Chỉ re-render cell thay đổi thay vì cả row khi cập nhật một giá trị.
- Two-way binding tối ưu: Bind từng property đến component con mà không cần spread cả object.
- Performance-critical UI: Giảm tối đa số lần re-render trong component phức tạp có nhiều phần tử phụ thuộc cùng một nguồn dữ liệu.
Cài đặt
npm install @libs-ui/pipes-convert-object-to-signalImport
import { LibsUiPipesConvertObjectToSignalPipe } from '@libs-ui/pipes-convert-object-to-signal';
@Component({
standalone: true,
imports: [LibsUiPipesConvertObjectToSignalPipe],
// ...
})
export class MyComponent {}Khi cần dùng trong class TypeScript (inject trực tiếp):
import { LibsUiPipesConvertObjectToSignalPipe } from '@libs-ui/pipes-convert-object-to-signal';
@Component({
standalone: true,
imports: [LibsUiPipesConvertObjectToSignalPipe],
providers: [LibsUiPipesConvertObjectToSignalPipe],
})
export class MyComponent {
private readonly convertPipe = inject(LibsUiPipesConvertObjectToSignalPipe);
constructor() {
this.userSig = this.convertPipe.transform({ name: 'John', age: 30 });
}
}Ví dụ sử dụng
1. Object thành Nested Signals (Template)
@let user = ({ name: 'John', email: '[email protected]', age: 30 } | LibsUiPipesConvertObjectToSignalPipe);
<input [value]="user().name()" (input)="user().name.set($any($event.target).value)" />
<input [value]="user().email()" (input)="user().email.set($any($event.target).value)" />
<input type="number" [value]="user().age()" (input)="user().age.set(+$any($event.target).value)" />
<p>Name: {{ user().name() }}</p>
<p>Email: {{ user().email() }}</p>
<p>Age: {{ user().age() }}</p>// Đọc signal: user().name()
// Set signal: user().name.set('Jane')
// Thay đổi name KHÔNG trigger re-render cho email và age2. Array thành Signal của Signals
@let items = (['apple', 'banana', 'orange'] | LibsUiPipesConvertObjectToSignalPipe);
<ul>
@for (item of items(); track item) {
<li>
<input [value]="item()" (input)="item.set($any($event.target).value)" />
<span>{{ item() }}</span>
</li>
}
</ul>// items() → WritableSignal<WritableSignal<string>[]>
// items()[0]() → 'apple'
// items()[0].set('grape') → chỉ re-render item[0]3. Nested Object (Multi-level)
@let company = ({
name: 'Tech Corp',
ceo: { name: 'John', age: 45 }
} | LibsUiPipesConvertObjectToSignalPipe);
<input [value]="company().name()" (input)="company().name.set($any($event.target).value)" />
<input [value]="company().ceo().name()" (input)="company().ceo().name.set($any($event.target).value)" />
<input type="number" [value]="company().ceo().age()" (input)="company().ceo().age.set(+$any($event.target).value)" />
<p>Company: {{ company().name() }}</p>
<p>CEO: {{ company().ceo().name() }}, tuổi {{ company().ceo().age() }}</p>4. Shallow (No-clone) — giữ nguyên reference
@let user = (userData | LibsUiPipesConvertObjectToSignalPipe : false);
<p>Name: {{ user().name() }}</p>readonly userData = { name: 'John', age: 30 };
// isCloneDeep = false: nhanh hơn, nhưng mutate userData cũng ảnh hưởng signal
// isCloneDeep = true (default): an toàn, data được deep clone trước khi convert5. Sử dụng trong TypeScript (inject pipe)
import { LibsUiPipesConvertObjectToSignalPipe } from '@libs-ui/pipes-convert-object-to-signal';
@Component({
standalone: true,
imports: [LibsUiPipesConvertObjectToSignalPipe],
providers: [LibsUiPipesConvertObjectToSignalPipe],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UserFormComponent {
private readonly convertPipe = inject(LibsUiPipesConvertObjectToSignalPipe);
protected userSig = this.convertPipe.transform({ name: 'John', email: '[email protected]' });
protected handlerSaveName(event: Event): void {
event.stopPropagation();
const value = (event.target as HTMLInputElement).value;
this.userSig().name.set(value);
}
}Transform
| Tham số | Type | Bắt buộc | Default | Mô tả | Ví dụ |
|---|---|---|---|---|---|
| data | any | Có | — | Object hoặc array cần chuyển đổi thành nested Signals. Trả về nguyên nếu là null/undefined. | { name: 'John' } |
| isCloneDeep | boolean \| undefined | Không | true | true: deep clone data trước khi convert (an toàn). false: giữ nguyên reference (nhanh hơn, cẩn thận mutate). | false |
Cú pháp template:
{{ data | LibsUiPipesConvertObjectToSignalPipe }}
{{ data | LibsUiPipesConvertObjectToSignalPipe : false }}Cú pháp standalone (TypeScript):
const signalObj = pipe.transform(data);
const signalObj = pipe.transform(data, false);Kiểu dữ liệu đầu ra
// Pipe transform signature
transform<T>(data: T, isCloneDeep?: boolean): T
// Object → Nested WritableSignals
{ name: 'John', age: 30 }
=>
WritableSignal<{
name: WritableSignal<string>,
age: WritableSignal<number>
}>
// Array → WritableSignal<WritableSignal<T>[]>
['apple', 'banana']
=>
WritableSignal<[
WritableSignal<string>,
WritableSignal<string>
]>Bảng hành vi theo kiểu đầu vào:
| Kiểu dữ liệu | Kết quả |
|---|---|
| null / undefined | Trả về nguyên (không convert) |
| Primitive (string, number, boolean) | Wrapped trong WritableSignal<T> |
| Plain object | WritableSignal<{ [key]: WritableSignal<V> }> — mỗi property là signal |
| Array | WritableSignal<WritableSignal<T>[]> — mỗi phần tử là signal |
| Nested object | Đệ quy — mọi cấp độ đều được convert |
| Promise / Observable | Trả về nguyên (async objects không convert) |
| Map / Set | Được xử lý đặc biệt theo logic trong convertObjectToSignal |
Lưu ý quan trọng
⚠️ Fine-grained Reactivity: Khác với signal({ name, email }) — khi dùng pipe này, thay đổi name không trigger re-render phần template dùng email. Đây là lợi thế chính để tối ưu hiệu năng.
⚠️ isCloneDeep = false: Khi tắt deep clone, mutate object gốc (data nguồn) cũng ảnh hưởng trực tiếp đến các signal. Chỉ dùng khi chắc chắn không có side effect từ code khác.
⚠️ @let trong template: Dùng @let để khai báo biến từ pipe trong template (Angular 18+). Với Angular < 18, cần dùng as trong *ngIf hoặc chuyển sang inject pipe trong TS.
⚠️ track trong @for với Array Signals: Khi dùng pipe với array, BẮT BUỘC track item (track signal reference bất biến) — KHÔNG dùng track $index.
<!-- ✅ ĐÚNG — track signal reference -->
@for (item of items(); track item) { ... }
<!-- ❌ SAI — BLOCKER -->
@for (item of items(); track $index) { ... }⚠️ Không dùng trong computed(): Pipe gọi convertObjectToSignal là stateful. Không nên wrap kết quả pipe trong computed() vì mỗi lần computed chạy lại sẽ tạo signal mới, mất state đã set trước đó.
So sánh: Object Signal vs Nested Signals
// ❌ Object là một signal duy nhất — không hiệu quả
protected user = signal({ name: 'John', email: '[email protected]' });
// Khi thay đổi name:
this.user.set({ ...this.user(), name: 'Jane' });
// => Cả name VÀ email đều trigger re-render!
// ✅ Nested Signals — fine-grained
// Trong template:
// @let user = (userData | LibsUiPipesConvertObjectToSignalPipe)
// Hoặc trong TS:
protected userSig = this.convertPipe.transform({ name: 'John', email: '[email protected]' });
// Khi thay đổi name:
this.userSig().name.set('Jane');
// => Chỉ phần template dùng user().name() re-render, email() KHÔNG re-renderDemo
npx nx serve core-uiTruy cập: http://localhost:4500/pipes/convert-object-to-signal
