@libs-ui/pipes-convert-signal-to-object
v0.2.357-11
Published
> Pipe chuyển đổi cấu trúc Signals lồng nhau thành plain object/array để sẵn sàng gửi API hoặc tích hợp thư viện bên ngoài.
Downloads
5,465
Readme
@libs-ui/pipes-convert-signal-to-object
Pipe chuyển đổi cấu trúc Signals lồng nhau thành plain object/array để sẵn sàng gửi API hoặc tích hợp thư viện bên ngoài.
Giới thiệu
LibsUiPipesConvertSignalToObjectPipe nhận vào một cấu trúc signal lồng nhau (object hoặc array chứa các signal con) và đệ quy unwrap toàn bộ chúng thành plain object/array. Pipe này là cặp đôi ngược lại của @libs-ui/pipes-convert-object-to-signal, cho phép chuyển đổi hai chiều giữa dạng signal và plain object. Kết quả đầu ra là một plain object chuẩn, có thể serialize bằng JSON.stringify, gửi trực tiếp qua HTTP, hoặc truyền cho bất kỳ thư viện nào không hiểu Angular Signal.
Tính năng
- ✅ Unwrap nested signals đệ quy nhiều cấp (multi-level)
- ✅ Chuyển đổi Signal Object thành Plain Object
- ✅ Chuyển đổi Signal Array thành Plain Array
- ✅ Hỗ trợ Map và Set chứa signal
- ✅ Hỗ trợ primitive signal (string, number, boolean)
- ✅ Deep clone kết quả mặc định (bảo vệ signal gốc không bị mutate)
- ✅ Tùy chọn tắt deep clone để tối ưu hiệu năng
- ✅ Bỏ qua an toàn các đối tượng đặc biệt: Date, RegExp, DOM element, dayjs...
- ✅ Chống vòng lặp vô hạn với circular reference detection
Khi nào sử dụng
- Cần gửi state dạng signal lên API (HTTP POST/PUT) — các HTTP client yêu cầu plain object
- Cần serialize state sang
JSON.stringifyđể lưu vàolocalStorage/sessionStorage - Tích hợp với thư viện bên ngoài (chart, map, form lib...) chỉ nhận plain object
- Cần log hoặc debug giá trị thực bên trong một cấu trúc signal phức tạp
- Chuẩn bị payload trước khi submit form chứa các signal field
Cài đặt
npm install @libs-ui/pipes-convert-signal-to-objectImport
import { LibsUiPipesConvertSignalToObjectPipe } from '@libs-ui/pipes-convert-signal-to-object';
@Component({
standalone: true,
imports: [LibsUiPipesConvertSignalToObjectPipe],
// ...
})
export class UserFormComponent {}Ví dụ sử dụng
1. Unwrap signal object thành plain object (template)
import { Component, signal } from '@angular/core';
import { LibsUiPipesConvertSignalToObjectPipe } from '@libs-ui/pipes-convert-signal-to-object';
@Component({
standalone: true,
imports: [LibsUiPipesConvertSignalToObjectPipe],
template: `
@let user = (userSignal | LibsUiPipesConvertSignalToObjectPipe);
<p>Tên: {{ user.name }}</p>
<p>Email: {{ user.email }}</p>
<button (click)="handlerSave(user)">Lưu lên API</button>
`,
})
export class UserFormComponent {
readonly userSignal = signal({
name: signal('Nguyễn Văn A'),
email: signal('[email protected]'),
age: signal(28),
});
handlerSave(payload: { name: string; email: string; age: number }): void {
// payload đã là plain object, sẵn sàng gửi API
// this.http.post('/api/users', payload).subscribe(...)
}
}2. Unwrap signal array thành plain array
import { Component, signal } from '@angular/core';
import { LibsUiPipesConvertSignalToObjectPipe } from '@libs-ui/pipes-convert-signal-to-object';
@Component({
standalone: true,
imports: [LibsUiPipesConvertSignalToObjectPipe],
template: `
@let items = (itemsSignal | LibsUiPipesConvertSignalToObjectPipe);
<ul>
@for (item of items; track item) {
<li>{{ item }}</li>
}
</ul>
<p>JSON: {{ items | json }}</p>
`,
})
export class ItemListComponent {
readonly itemsSignal = signal([
signal('Táo'),
signal('Chuối'),
signal('Cam'),
]);
}3. Unwrap nested signal nhiều cấp
import { Component, signal } from '@angular/core';
import { LibsUiPipesConvertSignalToObjectPipe } from '@libs-ui/pipes-convert-signal-to-object';
@Component({
standalone: true,
imports: [LibsUiPipesConvertSignalToObjectPipe],
template: `
@let company = (companySignal | LibsUiPipesConvertSignalToObjectPipe);
<p>Công ty: {{ company.name }}</p>
<p>Giám đốc: {{ company.ceo.name }} ({{ company.ceo.age }} tuổi)</p>
<pre>{{ company | json }}</pre>
`,
})
export class CompanyDetailComponent {
readonly companySignal = signal({
name: signal('Tech Corp'),
ceo: signal({
name: signal('Trần Văn B'),
age: signal(45),
}),
});
// companySignal | LibsUiPipesConvertSignalToObjectPipe
// => { name: 'Tech Corp', ceo: { name: 'Trần Văn B', age: 45 } }
}4. Tắt deep clone để tăng hiệu năng
import { Component, signal } from '@angular/core';
import { LibsUiPipesConvertSignalToObjectPipe } from '@libs-ui/pipes-convert-signal-to-object';
@Component({
standalone: true,
imports: [LibsUiPipesConvertSignalToObjectPipe],
template: `
<!-- Truyền false để bỏ qua deep clone — nhanh hơn nhưng không sao chép -->
@let rawData = (dataSignal | LibsUiPipesConvertSignalToObjectPipe : false);
<p>{{ rawData.title }}</p>
`,
})
export class FastReadComponent {
readonly dataSignal = signal({
title: signal('Báo cáo tháng 6'),
count: signal(120),
});
}5. Sử dụng độc lập trong TypeScript (pipe.transform)
import { Component, inject, signal } from '@angular/core';
import { LibsUiPipesConvertSignalToObjectPipe } from '@libs-ui/pipes-convert-signal-to-object';
@Component({
standalone: true,
imports: [LibsUiPipesConvertSignalToObjectPipe],
providers: [LibsUiPipesConvertSignalToObjectPipe],
template: `<button (click)="handlerSubmit()">Gửi form</button>`,
})
export class OrderFormComponent {
private readonly convertPipe = inject(LibsUiPipesConvertSignalToObjectPipe);
readonly orderSignal = signal({
productId: signal('P001'),
quantity: signal(5),
note: signal('Giao buổi sáng'),
});
handlerSubmit(): void {
const payload = this.convertPipe.transform(this.orderSignal);
// payload = { productId: 'P001', quantity: 5, note: 'Giao buổi sáng' }
// this.orderApi.create(payload).subscribe(...)
}
}6. Lưu state vào localStorage
import { Component, inject, signal } from '@angular/core';
import { LibsUiPipesConvertSignalToObjectPipe } from '@libs-ui/pipes-convert-signal-to-object';
@Component({
standalone: true,
imports: [LibsUiPipesConvertSignalToObjectPipe],
providers: [LibsUiPipesConvertSignalToObjectPipe],
template: `<button (click)="handlerSaveFilter()">Lưu bộ lọc</button>`,
})
export class FilterBarComponent {
private readonly convertPipe = inject(LibsUiPipesConvertSignalToObjectPipe);
readonly filterSignal = signal({
keyword: signal('angular'),
status: signal('active'),
page: signal(1),
});
handlerSaveFilter(): void {
const plainFilter = this.convertPipe.transform(this.filterSignal);
// plainFilter = { keyword: 'angular', status: 'active', page: 1 }
localStorage.setItem('user-filter', JSON.stringify(plainFilter));
}
}Transform
| Tham số | Type | Bắt buộc | Default | Mô tả |
|---|---|---|---|---|
| data | any | Có | — | Giá trị signal hoặc cấu trúc object/array chứa nested signals cần unwrap |
| isCloneDeep | boolean | Không | true | true để deep clone kết quả (bảo vệ signal gốc); false để giữ reference gốc (nhanh hơn, cẩn thận mutate) |
Cú pháp template:
{{ dataSignal | LibsUiPipesConvertSignalToObjectPipe }}
{{ dataSignal | LibsUiPipesConvertSignalToObjectPipe : false }}
@let plain = (dataSignal | LibsUiPipesConvertSignalToObjectPipe);Cú pháp standalone (inject):
const plain = this.convertPipe.transform(this.dataSignal);
const plainNoClone = this.convertPipe.transform(this.dataSignal, false);Kiểu trả về: Plain object, plain array, hoặc primitive value sau khi đã unwrap toàn bộ signal con.
Hành vi chi tiết
Unwrap đệ quy so với gọi ()
// Gọi () chỉ unwrap 1 cấp — vẫn còn signal con bên trong
userSignal()
// => { name: WritableSignal<string>, age: WritableSignal<number> }
// Pipe unwrap toàn bộ tất cả cấp
userSignal | LibsUiPipesConvertSignalToObjectPipe
// => { name: 'Nguyễn Văn A', age: 28 }Quy tắc xử lý theo kiểu dữ liệu
| Kiểu input | Hành vi |
|---|---|
| Signal<primitive> | Unwrap thành giá trị nguyên thủy |
| Signal<object> | Unwrap object, tiếp tục đệ quy vào các property |
| Signal<array> | Unwrap array, tiếp tục đệ quy từng phần tử |
| Signal<Map> | Unwrap thành Map mới với values đã unwrap |
| Signal<Set> | Unwrap thành Set mới với values đã unwrap |
| null / undefined | Trả về nguyên như đầu vào, không throw |
| Date, RegExp, DOM element, dayjs | Giữ nguyên, không xử lý |
| Circular reference | Được phát hiện và xử lý an toàn, không bị stack overflow |
Deep clone (mặc định true)
// isCloneDeep = true (mặc định) — AN TOÀN
const plain = this.convertPipe.transform(this.dataSignal);
plain.name = 'Jane'; // Không ảnh hưởng đến this.dataSignal
// isCloneDeep = false — NHANH HƠN nhưng cần cẩn thận
const plainRef = this.convertPipe.transform(this.dataSignal, false);
plainRef.name = 'Jane'; // Có thể ảnh hưởng đến giá trị bên trong signal gốcTypes & Interfaces
import { LibsUiPipesConvertSignalToObjectPipe } from '@libs-ui/pipes-convert-signal-to-object';
// Pipe transform signature
// transform(data: any, isCloneDeep?: boolean): any
// Lưu ý: transform(data: any, isCloneDeep?: boolean): any KHÔNG phải generic method
// — không được viết transform<T>(...), TypeScript sẽ báo lỗi compile.
// Muốn có type rõ ràng, ép kiểu (cast) kết quả trả về:
const result = this.convertPipe.transform(this.userSignal) as { name: string; age: number };Lưu ý quan trọng
⚠️ Không serialize trực tiếp Signal: JSON.stringify(signal) sẽ không cho kết quả đúng vì signal là function object. Luôn dùng pipe này trước khi serialize.
⚠️ isCloneDeep = false và mutation: Khi tắt deep clone, việc thay đổi property trên plain object kết quả có thể làm thay đổi giá trị bên trong signal gốc không mong muốn. Chỉ dùng false khi chắc chắn không mutate kết quả.
⚠️ Đối tượng đặc biệt được giữ nguyên: Date, RegExp, dayjs, DOM element và các đối tượng framework sẽ được truyền nguyên vẹn mà không bị unwrap, đảm bảo không mất dữ liệu.
⚠️ Pipe chỉ unwrap — không làm reactive: Kết quả trả về là snapshot tại thời điểm pipe chạy. Nếu signal thay đổi sau đó, cần pipe chạy lại (Angular change detection sẽ tự kích hoạt trong template với OnPush khi signal thay đổi).
⚠️ Dùng với inject khi cần trong TypeScript: Để dùng ngoài template, khai báo trong providers của component và inject bằng inject(LibsUiPipesConvertSignalToObjectPipe).
