@libs-ui/interfaces-types 0.2.356-41 → 0.2.356-43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +617 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,3 +1,618 @@
|
|
|
1
|
-
# interfaces-types
|
|
1
|
+
# @libs-ui/interfaces-types
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> Bộ sưu tập tập trung các Interface và Type TypeScript dùng chung toàn hệ thống libs-ui.
|
|
4
|
+
|
|
5
|
+
## Giới thiệu
|
|
6
|
+
|
|
7
|
+
`@libs-ui/interfaces-types` là thư viện thuần TypeScript cung cấp các interface và type được tái sử dụng xuyên suốt hệ sinh thái `@libs-ui`. Lib bao gồm các định nghĩa cho HTTP response/upload, phân trang, file, sự kiện trình duyệt, ngày tháng (dayjs), cũng như các utility type nâng cao phục vụ Angular Signals như `ExtractInputs`, `PathOf`, `GetValueAtPath`, và `SignalOf`.
|
|
8
|
+
|
|
9
|
+
## Tính năng
|
|
10
|
+
|
|
11
|
+
- ✅ Định nghĩa chuẩn cho HTTP Response và phân trang (`IHttpResponse`, `IPaging`)
|
|
12
|
+
- ✅ Utility type trích xuất Input Signal từ Angular component (`ExtractInputs`)
|
|
13
|
+
- ✅ Type-safe path navigation cho object/signal lồng nhau (`PathOf`, `GetValueAtPath`)
|
|
14
|
+
- ✅ Chuyển đổi kiểu dữ liệu sang dạng Signal và ngược lại (`SignalOf`, `NonSignalOf`)
|
|
15
|
+
- ✅ Định nghĩa cho sự kiện trình duyệt mở rộng (`IEvent`, `IEventTarget`)
|
|
16
|
+
- ✅ Interface quản lý file upload và tiến trình (`IFile`, `IHttpProcessUpload`)
|
|
17
|
+
- ✅ Type ngày tháng với dayjs tích hợp (`GetDayjsConfig`, `GetDayjsReturn`)
|
|
18
|
+
- ✅ Interface validator và định dạng chuỗi (`IIsValidRequired`, `ITextFormatOptions`)
|
|
19
|
+
- ✅ Type tỉ lệ khung hình và toạ độ DOM (`TYPE_ASPECT_RATIO`, `IBoundingClientRect`)
|
|
20
|
+
|
|
21
|
+
## Khi nào sử dụng
|
|
22
|
+
|
|
23
|
+
- Khi cần định nghĩa kiểu cho response API trả về từ backend
|
|
24
|
+
- Khi truyền data vào Modal V2 qua `of<ExtractInputs<...>>()` để đảm bảo type-safe
|
|
25
|
+
- Khi xây dựng hàm generic truy cập giá trị theo đường dẫn chuỗi trên object/signal lồng nhau
|
|
26
|
+
- Khi cần chuyển đổi một interface sang dạng Signal-wrapped để dùng với Angular Signals
|
|
27
|
+
- Khi xây dựng form validator và cần type chuẩn cho message lỗi
|
|
28
|
+
- Khi xử lý file upload và cần theo dõi tiến trình phần trăm
|
|
29
|
+
|
|
30
|
+
## Cài đặt
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install @libs-ui/interfaces-types
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Import
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
// HTTP & Paging
|
|
40
|
+
import { IHttpResponse, IHttpResponseError, IPaging } from '@libs-ui/interfaces-types';
|
|
41
|
+
|
|
42
|
+
// File & Upload
|
|
43
|
+
import { IFile, TYPE_FILE, IHttpProcessUpload } from '@libs-ui/interfaces-types';
|
|
44
|
+
|
|
45
|
+
// Angular Signal Utilities
|
|
46
|
+
import { ExtractInputs } from '@libs-ui/interfaces-types';
|
|
47
|
+
import { PathOf, GetValueAtPath, GetReturnType, SignalOf, NonSignalOf } from '@libs-ui/interfaces-types';
|
|
48
|
+
|
|
49
|
+
// Event browser
|
|
50
|
+
import { IEvent, IEventTarget, IEventCustom } from '@libs-ui/interfaces-types';
|
|
51
|
+
|
|
52
|
+
// Date (dayjs)
|
|
53
|
+
import { GetDayjsConfig, GetDayjsReturn, NonNullableDate } from '@libs-ui/interfaces-types';
|
|
54
|
+
|
|
55
|
+
// DOM
|
|
56
|
+
import { IBoundingClientRect } from '@libs-ui/interfaces-types';
|
|
57
|
+
import { TYPE_ASPECT_RATIO, IAspectRatio } from '@libs-ui/interfaces-types';
|
|
58
|
+
|
|
59
|
+
// Validator
|
|
60
|
+
import { IIsValidRequired, IIsValidPattern, IIsValidLength, IMessageTranslate } from '@libs-ui/interfaces-types';
|
|
61
|
+
|
|
62
|
+
// String
|
|
63
|
+
import { IString, IStringCustom, ITextFormatOptions } from '@libs-ui/interfaces-types';
|
|
64
|
+
|
|
65
|
+
// Others
|
|
66
|
+
import { TYPE_OBJECT } from '@libs-ui/interfaces-types';
|
|
67
|
+
import { TYPE_LANGUAGE_SUPPORT } from '@libs-ui/interfaces-types';
|
|
68
|
+
import { TYPE_FUNCTION } from '@libs-ui/interfaces-types';
|
|
69
|
+
import { TYPE_IFRAME_MESSAGE } from '@libs-ui/interfaces-types';
|
|
70
|
+
import { TYPE_TEMPLATE_REF } from '@libs-ui/interfaces-types';
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Ví dụ sử dụng
|
|
74
|
+
|
|
75
|
+
### 1. IHttpResponse — Định nghĩa kiểu trả về từ API
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
import { IHttpResponse, IPaging } from '@libs-ui/interfaces-types';
|
|
79
|
+
import { HttpClient } from '@angular/common/http';
|
|
80
|
+
import { inject } from '@angular/core';
|
|
81
|
+
import { Observable } from 'rxjs';
|
|
82
|
+
|
|
83
|
+
interface IUser {
|
|
84
|
+
id: string;
|
|
85
|
+
name: string;
|
|
86
|
+
email: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Service gọi API danh sách user có phân trang
|
|
90
|
+
export class UserApiService {
|
|
91
|
+
private readonly http = inject(HttpClient);
|
|
92
|
+
|
|
93
|
+
getList(params: { page: number; per_page: number }): Observable<IHttpResponse<IUser[]>> {
|
|
94
|
+
return this.http.get<IHttpResponse<IUser[]>>('/api/users', { params });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Component nhận response
|
|
99
|
+
// component.ts
|
|
100
|
+
protected handlerLoadData(): void {
|
|
101
|
+
this.userApiService.getList({ page: 1, per_page: 20 })
|
|
102
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
103
|
+
.subscribe((res: IHttpResponse<IUser[]>) => {
|
|
104
|
+
if (res.code !== 200) return;
|
|
105
|
+
this.users.set(res.data ?? []);
|
|
106
|
+
this.paging.set(res.paging ?? {});
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### 2. ExtractInputs — Truyền data type-safe vào Modal V2
|
|
112
|
+
|
|
113
|
+
`ExtractInputs<T>` trích xuất tất cả `InputSignal` từ một component thành một plain object type, dùng khi gọi `of<ExtractInputs<...>>()` để truyền data vào `getDataComponentOutlet`.
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
import { ExtractInputs } from '@libs-ui/interfaces-types';
|
|
117
|
+
import { InputSignal, input } from '@angular/core';
|
|
118
|
+
import { of } from 'rxjs';
|
|
119
|
+
import { from } from 'rxjs';
|
|
120
|
+
import { LibsUiComponentsModalV2Component } from '@libs-ui/components-modal-v2';
|
|
121
|
+
|
|
122
|
+
// Định nghĩa interface khớp với component con
|
|
123
|
+
interface I_DataInComponentUserDetail {
|
|
124
|
+
userId: InputSignal<string>; // required
|
|
125
|
+
mode: InputSignal<'view' | 'edit'>; // required
|
|
126
|
+
title?: InputSignal<string>; // optional
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Component con implements interface
|
|
130
|
+
export class UserDetailModalComponent implements I_DataInComponentUserDetail {
|
|
131
|
+
readonly userId = input.required<string>();
|
|
132
|
+
readonly mode = input.required<'view' | 'edit'>();
|
|
133
|
+
readonly title = input<string>();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Component cha truyền data qua getDataComponentOutlet
|
|
137
|
+
this.libsUiDynamicComponentService.setInputs(this.modalComponentRef, {
|
|
138
|
+
bodyConfig: {
|
|
139
|
+
component: () => from(
|
|
140
|
+
import('./user-detail-modal/user-detail-modal.component')
|
|
141
|
+
.then(m => m.UserDetailModalComponent)
|
|
142
|
+
),
|
|
143
|
+
getDataComponentOutlet: () => of<ExtractInputs<I_DataInComponentUserDetail>>({
|
|
144
|
+
userId: 'user-123', // required — bắt buộc truyền
|
|
145
|
+
mode: 'view', // required — bắt buộc truyền
|
|
146
|
+
// title: 'Chi tiết', // optional — có thể bỏ qua
|
|
147
|
+
}),
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### 3. PathOf & GetValueAtPath — Truy cập an toàn theo đường dẫn chuỗi
|
|
153
|
+
|
|
154
|
+
`PathOf<T>` sinh ra union type của tất cả đường dẫn hợp lệ trong object `T`. `GetValueAtPath<T, P>` trả về type của giá trị tại đường dẫn `P`.
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
import { PathOf, GetValueAtPath } from '@libs-ui/interfaces-types';
|
|
158
|
+
import { WritableSignal, signal } from '@angular/core';
|
|
159
|
+
|
|
160
|
+
interface IOrder {
|
|
161
|
+
id: string;
|
|
162
|
+
customer: {
|
|
163
|
+
name: string;
|
|
164
|
+
address: {
|
|
165
|
+
city: string;
|
|
166
|
+
zip: string;
|
|
167
|
+
};
|
|
168
|
+
};
|
|
169
|
+
items: Array<{
|
|
170
|
+
productId: string;
|
|
171
|
+
quantity: number;
|
|
172
|
+
}>;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// PathOf<IOrder> tự động sinh ra:
|
|
176
|
+
// 'id' | 'customer' | 'customer.name' | 'customer.address' |
|
|
177
|
+
// 'customer.address.city' | 'customer.address.zip' |
|
|
178
|
+
// 'items' | 'items[0]' | 'items[0].productId' | 'items[0].quantity' | ...
|
|
179
|
+
type OrderPaths = PathOf<IOrder>;
|
|
180
|
+
|
|
181
|
+
// GetValueAtPath trả về type chính xác tại path
|
|
182
|
+
type CityType = GetValueAtPath<IOrder, 'customer.address.city'>; // string
|
|
183
|
+
type ItemQty = GetValueAtPath<IOrder, 'items[0].quantity'>; // number
|
|
184
|
+
|
|
185
|
+
// Dùng trong hàm generic type-safe
|
|
186
|
+
function getField<T, P extends PathOf<T>>(
|
|
187
|
+
obj: T,
|
|
188
|
+
path: P
|
|
189
|
+
): GetValueAtPath<T, P> {
|
|
190
|
+
// implementation dùng get() từ @libs-ui/utils
|
|
191
|
+
return (obj as any)[path as string];
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### 4. SignalOf — Chuyển đổi interface sang Signal-wrapped
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
import { SignalOf, NonSignalOf } from '@libs-ui/interfaces-types';
|
|
199
|
+
import { signal, WritableSignal } from '@angular/core';
|
|
200
|
+
import { convertObjectToSignal, convertSignalToObject } from '@libs-ui/utils';
|
|
201
|
+
|
|
202
|
+
interface IUserProfile {
|
|
203
|
+
name: string;
|
|
204
|
+
age: number;
|
|
205
|
+
address: {
|
|
206
|
+
city: string;
|
|
207
|
+
country: string;
|
|
208
|
+
};
|
|
209
|
+
tags: string[];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// SignalOf<IUserProfile> sẽ sinh ra:
|
|
213
|
+
// {
|
|
214
|
+
// name: WritableSignal<string>;
|
|
215
|
+
// age: WritableSignal<number>;
|
|
216
|
+
// address: WritableSignal<{ city: WritableSignal<string>; country: WritableSignal<string>; }>;
|
|
217
|
+
// tags: WritableSignal<WritableSignal<string>[]>;
|
|
218
|
+
// }
|
|
219
|
+
type SignaledProfile = SignalOf<IUserProfile>;
|
|
220
|
+
|
|
221
|
+
// Sử dụng với convertObjectToSignal từ @libs-ui/utils
|
|
222
|
+
const profileSignal = convertObjectToSignal<IUserProfile>({
|
|
223
|
+
name: 'Nguyễn Văn A',
|
|
224
|
+
age: 30,
|
|
225
|
+
address: { city: 'Hà Nội', country: 'Việt Nam' },
|
|
226
|
+
tags: ['admin', 'dev'],
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// Chuyển ngược lại về plain object để gửi API
|
|
230
|
+
const payload = convertSignalToObject(profileSignal);
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### 5. IFile & IHttpProcessUpload — Quản lý file upload
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
import { IFile, TYPE_FILE, IHttpProcessUpload } from '@libs-ui/interfaces-types';
|
|
237
|
+
import { signal } from '@angular/core';
|
|
238
|
+
|
|
239
|
+
// component.ts
|
|
240
|
+
protected uploadingFiles = signal<IFile[]>([]);
|
|
241
|
+
protected uploadProgress = signal<IHttpProcessUpload>({ loaded: 0, total: 0, percent: 0 });
|
|
242
|
+
|
|
243
|
+
protected handlerFileSelect(event: Event): void {
|
|
244
|
+
event.stopPropagation();
|
|
245
|
+
const input = event.target as HTMLInputElement;
|
|
246
|
+
if (!input.files?.length) return;
|
|
247
|
+
|
|
248
|
+
const newFiles: IFile[] = Array.from(input.files).map(file => ({
|
|
249
|
+
name: file.name,
|
|
250
|
+
file: file,
|
|
251
|
+
size: `${(file.size / 1024).toFixed(1)} KB`,
|
|
252
|
+
type: this.detectFileType(file.type),
|
|
253
|
+
isUploading: true,
|
|
254
|
+
percentUploading: 0,
|
|
255
|
+
}));
|
|
256
|
+
|
|
257
|
+
this.uploadingFiles.set(newFiles);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
private detectFileType(mimeType: string): TYPE_FILE {
|
|
261
|
+
if (mimeType.startsWith('image/')) return 'image';
|
|
262
|
+
if (mimeType.startsWith('video/')) return 'video';
|
|
263
|
+
if (mimeType.startsWith('audio/')) return 'audio';
|
|
264
|
+
return 'document';
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### 6. IEvent & IEventTarget — Xử lý sự kiện DOM mở rộng
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
import { IEvent } from '@libs-ui/interfaces-types';
|
|
272
|
+
|
|
273
|
+
// component.ts
|
|
274
|
+
protected handlerInputChange(event: IEvent): void {
|
|
275
|
+
event.stopPropagation();
|
|
276
|
+
const value = event.target.value as string;
|
|
277
|
+
this.searchTerm.set(value.trim());
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
protected handlerFileDrop(event: IEvent): void {
|
|
281
|
+
event.stopPropagation();
|
|
282
|
+
const files = event.dataTransfer?.files;
|
|
283
|
+
if (!files?.length) return;
|
|
284
|
+
this.processFiles(files);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
protected handlerKeyDown(event: IEvent): void {
|
|
288
|
+
event.stopPropagation();
|
|
289
|
+
if (event.keyCode === 13) {
|
|
290
|
+
this.handlerSubmit();
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (event.keyCode === 27) {
|
|
294
|
+
this.handlerClose();
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### 7. GetDayjsConfig & GetDayjsReturn — Type cho hàm getDayjs
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
import { GetDayjsConfig, GetDayjsReturn, NonNullableDate } from '@libs-ui/interfaces-types';
|
|
303
|
+
import { getDayjs } from '@libs-ui/utils';
|
|
304
|
+
|
|
305
|
+
// Định nghĩa hàm format ngày với type rõ ràng
|
|
306
|
+
function formatDateSafe(config: GetDayjsConfig): string {
|
|
307
|
+
const result = getDayjs(config);
|
|
308
|
+
if (!result) return '';
|
|
309
|
+
return result.format('DD/MM/YYYY HH:mm');
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Ví dụ sử dụng
|
|
313
|
+
const formatted1 = formatDateSafe({ date: '2024-06-15', formatOfDate: 'YYYY-MM-DD' });
|
|
314
|
+
// => '15/06/2024 00:00'
|
|
315
|
+
|
|
316
|
+
const formatted2 = formatDateSafe({
|
|
317
|
+
date: 1718409600000,
|
|
318
|
+
utc: false,
|
|
319
|
+
returnDayjsIfConfigDateNotExist: true,
|
|
320
|
+
});
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### 8. IPaging — Phân trang
|
|
324
|
+
|
|
325
|
+
```typescript
|
|
326
|
+
import { IPaging, IHttpResponse } from '@libs-ui/interfaces-types';
|
|
327
|
+
import { signal, computed } from '@angular/core';
|
|
328
|
+
|
|
329
|
+
// component.ts
|
|
330
|
+
protected paging = signal<IPaging>({});
|
|
331
|
+
|
|
332
|
+
protected totalPages = computed(() => this.paging().total_pages ?? 0);
|
|
333
|
+
protected currentPage = computed(() => this.paging().page ?? 1);
|
|
334
|
+
protected hasNextPage = computed(() => !!this.paging().next);
|
|
335
|
+
|
|
336
|
+
protected handlerPageChange(page: number): void {
|
|
337
|
+
this.loadData({ page, per_page: 20 });
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
private loadData(params: { page: number; per_page: number }): void {
|
|
341
|
+
this.apiService.getList(params)
|
|
342
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
343
|
+
.subscribe((res: IHttpResponse<unknown[], IPaging>) => {
|
|
344
|
+
if (res.code !== 200) return;
|
|
345
|
+
this.paging.set(res.paging ?? {});
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### 9. Validator interfaces — Cấu hình rule validate
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
import {
|
|
354
|
+
IIsValidRequired,
|
|
355
|
+
IIsValidPattern,
|
|
356
|
+
IIsValidLength,
|
|
357
|
+
IMessageTranslate,
|
|
358
|
+
} from '@libs-ui/interfaces-types';
|
|
359
|
+
|
|
360
|
+
// Cấu hình validator cho input email
|
|
361
|
+
const emailValidators = {
|
|
362
|
+
required: {
|
|
363
|
+
isRequired: true,
|
|
364
|
+
message: 'i18n_email_required',
|
|
365
|
+
} satisfies IIsValidRequired,
|
|
366
|
+
|
|
367
|
+
pattern: {
|
|
368
|
+
pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
|
|
369
|
+
message: 'i18n_email_invalid',
|
|
370
|
+
valuePatternShowError: true,
|
|
371
|
+
} satisfies IIsValidPattern,
|
|
372
|
+
|
|
373
|
+
maxLength: {
|
|
374
|
+
length: 100,
|
|
375
|
+
message: 'i18n_email_too_long',
|
|
376
|
+
interpolateParams: { max: 100 },
|
|
377
|
+
} satisfies IIsValidLength,
|
|
378
|
+
};
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### 10. TYPE_LANGUAGE_SUPPORT — Kiểu ngôn ngữ được hỗ trợ
|
|
382
|
+
|
|
383
|
+
```typescript
|
|
384
|
+
import { TYPE_LANGUAGE_SUPPORT } from '@libs-ui/interfaces-types';
|
|
385
|
+
import { signal } from '@angular/core';
|
|
386
|
+
|
|
387
|
+
// component.ts
|
|
388
|
+
protected currentLang = signal<TYPE_LANGUAGE_SUPPORT>('vi');
|
|
389
|
+
|
|
390
|
+
protected handlerSwitchLang(lang: TYPE_LANGUAGE_SUPPORT): void {
|
|
391
|
+
this.currentLang.set(lang);
|
|
392
|
+
this.translateService.use(lang);
|
|
393
|
+
}
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
### 11. IAspectRatio & TYPE_ASPECT_RATIO — Tỉ lệ khung hình
|
|
397
|
+
|
|
398
|
+
```typescript
|
|
399
|
+
import { IAspectRatio, TYPE_ASPECT_RATIO } from '@libs-ui/interfaces-types';
|
|
400
|
+
import { signal } from '@angular/core';
|
|
401
|
+
|
|
402
|
+
const aspectRatioOptions: IAspectRatio[] = [
|
|
403
|
+
{ key: 'Tự do', value: 0 },
|
|
404
|
+
{ key: '1:1', value: 1 },
|
|
405
|
+
{ key: '16:9', value: 16 / 9 },
|
|
406
|
+
{ key: '4:3', value: 4 / 3 },
|
|
407
|
+
];
|
|
408
|
+
|
|
409
|
+
// component.ts
|
|
410
|
+
protected selectedRatio = signal<TYPE_ASPECT_RATIO>('16:9');
|
|
411
|
+
|
|
412
|
+
protected handlerSelectRatio(ratio: TYPE_ASPECT_RATIO): void {
|
|
413
|
+
this.selectedRatio.set(ratio);
|
|
414
|
+
}
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
### 12. IBoundingClientRect — Toạ độ và kích thước DOM
|
|
418
|
+
|
|
419
|
+
```typescript
|
|
420
|
+
import { IBoundingClientRect } from '@libs-ui/interfaces-types';
|
|
421
|
+
import { ElementRef, inject } from '@angular/core';
|
|
422
|
+
import { signal } from '@angular/core';
|
|
423
|
+
|
|
424
|
+
// component.ts (dùng trong directive hoặc component tính toán vị trí)
|
|
425
|
+
private readonly elementRef = inject(ElementRef);
|
|
426
|
+
|
|
427
|
+
protected boundingRect = signal<IBoundingClientRect>({
|
|
428
|
+
top: 0, left: 0, width: 0, height: 0,
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
ngAfterViewInit(): void {
|
|
432
|
+
const rect = this.elementRef.nativeElement.getBoundingClientRect();
|
|
433
|
+
this.boundingRect.set({
|
|
434
|
+
top: rect.top,
|
|
435
|
+
left: rect.left,
|
|
436
|
+
width: rect.width,
|
|
437
|
+
height: rect.height,
|
|
438
|
+
right: rect.right,
|
|
439
|
+
bottom: rect.bottom,
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
### 13. TYPE_IFRAME_MESSAGE — Giao tiếp giữa các micro-frontend
|
|
445
|
+
|
|
446
|
+
```typescript
|
|
447
|
+
import { TYPE_IFRAME_MESSAGE } from '@libs-ui/interfaces-types';
|
|
448
|
+
|
|
449
|
+
type MessageType = 'AUTH_TOKEN' | 'PAGE_READY' | 'CLOSE_MODAL';
|
|
450
|
+
type MessagePayload = { token: string } | { route: string } | null;
|
|
451
|
+
|
|
452
|
+
type AppMessage = TYPE_IFRAME_MESSAGE<MessageType, MessagePayload>;
|
|
453
|
+
|
|
454
|
+
// Gửi message
|
|
455
|
+
const msg: AppMessage = { type: 'AUTH_TOKEN', response: { token: 'demo-token' } };
|
|
456
|
+
window.parent.postMessage(msg, '*');
|
|
457
|
+
|
|
458
|
+
// Nhận message
|
|
459
|
+
window.addEventListener('message', (event) => {
|
|
460
|
+
const data = event.data as AppMessage;
|
|
461
|
+
if (data.type === 'AUTH_TOKEN') {
|
|
462
|
+
// xử lý token
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
## Types & Interfaces
|
|
468
|
+
|
|
469
|
+
### HTTP
|
|
470
|
+
|
|
471
|
+
| Export | Loại | Mô tả |
|
|
472
|
+
|---|---|---|
|
|
473
|
+
| `IHttpResponse<T, P>` | type | Kiểu chuẩn cho HTTP response từ backend. `T` = data type, `P` = paging type |
|
|
474
|
+
| `IHttpResponseError` | interface | Lỗi HTTP mở rộng từ `Error`, có thêm `code` |
|
|
475
|
+
| `IPaging` | interface | Phân trang hỗ trợ cả cursor-based và offset-based |
|
|
476
|
+
| `IHttpProcessUpload` | interface | Tiến trình upload: `loaded`, `total`, `percent` |
|
|
477
|
+
|
|
478
|
+
```typescript
|
|
479
|
+
// IHttpResponse chi tiết
|
|
480
|
+
type IHttpResponse<T = any, P = IPaging> = {
|
|
481
|
+
code?: number; // HTTP status code
|
|
482
|
+
feCloneCode?: any; // Code override khi server trả 200 nhưng logic khác
|
|
483
|
+
message?: string; // Thông báo từ server
|
|
484
|
+
data?: T; // Dữ liệu chính
|
|
485
|
+
paging?: P; // Thông tin phân trang
|
|
486
|
+
[key: string]: unknown; // Các field mở rộng
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
// IPaging chi tiết
|
|
490
|
+
interface IPaging {
|
|
491
|
+
page?: number; // Trang hiện tại (offset-based)
|
|
492
|
+
per_page?: number; // Số item mỗi trang
|
|
493
|
+
total_items?: number; // Tổng số item
|
|
494
|
+
total_pages?: number; // Tổng số trang
|
|
495
|
+
before?: string; // Cursor trước (cursor-based)
|
|
496
|
+
after?: string; // Cursor sau (cursor-based)
|
|
497
|
+
previous?: string; // URL trang trước
|
|
498
|
+
next?: string; // URL trang sau
|
|
499
|
+
fake?: boolean; // Dữ liệu giả (dùng trong loading state)
|
|
500
|
+
}
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
### File
|
|
504
|
+
|
|
505
|
+
| Export | Loại | Mô tả |
|
|
506
|
+
|---|---|---|
|
|
507
|
+
| `IFile` | interface | Đối tượng file đầy đủ: id, name, url, type, upload progress |
|
|
508
|
+
| `TYPE_FILE` | type | `'document' \| 'image' \| 'video' \| 'audio'` |
|
|
509
|
+
|
|
510
|
+
```typescript
|
|
511
|
+
interface IFile {
|
|
512
|
+
id?: string;
|
|
513
|
+
name?: string;
|
|
514
|
+
file?: File; // File object gốc từ input[type=file]
|
|
515
|
+
size?: string; // Chuỗi kích thước (vd: '2.5 MB')
|
|
516
|
+
isUploading?: boolean;
|
|
517
|
+
percentUploading?: number; // 0-100
|
|
518
|
+
isUpdate?: boolean; // Đang cập nhật file cũ
|
|
519
|
+
url?: string; // URL sau khi upload xong
|
|
520
|
+
origin_url?: string; // URL gốc
|
|
521
|
+
mimetype?: string; // MIME type (vd: 'image/png')
|
|
522
|
+
type?: TYPE_FILE;
|
|
523
|
+
error?: string;
|
|
524
|
+
isAvatar?: boolean;
|
|
525
|
+
}
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
### Angular Signal Utilities
|
|
529
|
+
|
|
530
|
+
| Export | Loại | Mô tả |
|
|
531
|
+
|---|---|---|
|
|
532
|
+
| `ExtractInputs<T, R>` | type | Trích xuất `InputSignal` / `ModelSignal` từ component thành plain object type. `R=true` (mặc định) giữ nguyên required/optional |
|
|
533
|
+
| `PathOf<T, Depth>` | type | Sinh union type tất cả đường dẫn hợp lệ trong object `T`, hỗ trợ signal lồng nhau, depth mặc định 6 |
|
|
534
|
+
| `GetValueAtPath<T, P, R>` | type | Resolve type giá trị tại path `P`. `R=false` (mặc định) unwrap signal, `R=true` giữ nguyên signal |
|
|
535
|
+
| `GetReturnType<O, P, KS, D>` | type | Kiểu trả về của `get()` dựa trên `defaultValue D` — loại `null \| undefined` nếu `D` có giá trị |
|
|
536
|
+
| `SignalOf<T, P, F>` | type | Chuyển toàn bộ object sang dạng Signal-wrapped. `P=true` (mặc định) wrap cả primitive |
|
|
537
|
+
| `NonSignalOf<T>` | type | Unwrap toàn bộ Signal về plain type |
|
|
538
|
+
|
|
539
|
+
```typescript
|
|
540
|
+
// Ví dụ ExtractInputs
|
|
541
|
+
interface I_DataInComponentBadge {
|
|
542
|
+
label: InputSignal<string>; // required
|
|
543
|
+
variant: InputSignal<'primary' | 'secondary'>; // required
|
|
544
|
+
count?: InputSignal<number>; // optional (có ? vì InputSignal<number | undefined>)
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
type BadgeData = ExtractInputs<I_DataInComponentBadge>;
|
|
548
|
+
// => { label: string; variant: 'primary' | 'secondary'; count?: number }
|
|
549
|
+
|
|
550
|
+
// R=false: optional field có thể bỏ qua
|
|
551
|
+
type BadgeDataPartial = ExtractInputs<I_DataInComponentBadge, false>;
|
|
552
|
+
// => { label?: string; variant?: string; count?: number }
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
### Event Browser
|
|
556
|
+
|
|
557
|
+
| Export | Loại | Mô tả |
|
|
558
|
+
|---|---|---|
|
|
559
|
+
| `IEvent` | interface | Kết hợp `Event`, `MouseEvent`, `KeyboardEvent`, `ErrorEvent` và custom |
|
|
560
|
+
| `IEventTarget` | interface | `EventTarget` mở rộng với `value`, `files`, `style`, `innerHTML`,... |
|
|
561
|
+
| `IEventCustom` | interface | Event tuỳ chỉnh với `data`, `fakeEvent`, `inputType` |
|
|
562
|
+
|
|
563
|
+
### Date
|
|
564
|
+
|
|
565
|
+
| Export | Loại | Mô tả |
|
|
566
|
+
|---|---|---|
|
|
567
|
+
| `GetDayjsConfig` | type | Config truyền vào `getDayjs()`: `date`, `utc`, `formatOfDate`, `returnDayjsIfConfigDateNotExist`, `localeZone` |
|
|
568
|
+
| `GetDayjsReturn<C>` | type | Kiểu trả về điều kiện của `getDayjs()`: `dayjs.Dayjs` hoặc `undefined` tùy config |
|
|
569
|
+
| `NonNullableDate` | type | `dayjs.ConfigType` loại bỏ `null \| undefined` |
|
|
570
|
+
|
|
571
|
+
### DOM & Layout
|
|
572
|
+
|
|
573
|
+
| Export | Loại | Mô tả |
|
|
574
|
+
|---|---|---|
|
|
575
|
+
| `IBoundingClientRect` | interface | `top`, `left`, `width`, `height`, `right?`, `bottom?` |
|
|
576
|
+
| `IAspectRatio` | interface | `key: string`, `value: number` |
|
|
577
|
+
| `TYPE_ASPECT_RATIO` | type | `'free' \| '1:1' \| '2:3' \| '3:2' \| '3:4' \| '4:3' \| '9:16' \| '16:9'` |
|
|
578
|
+
|
|
579
|
+
### Validator
|
|
580
|
+
|
|
581
|
+
| Export | Loại | Mô tả |
|
|
582
|
+
|---|---|---|
|
|
583
|
+
| `IIsValidRequired` | interface | Validator required: `isRequired`, `message`, `interpolateParams` |
|
|
584
|
+
| `IIsValidPattern` | interface | Validator regex: `pattern`, `valuePatternShowError`, `message` |
|
|
585
|
+
| `IIsValidLength` | interface | Validator độ dài: `length`, `message`, `interpolateParams` |
|
|
586
|
+
| `IMessageTranslate` | interface | Base interface cho message lỗi: `message?`, `interpolateParams?` |
|
|
587
|
+
|
|
588
|
+
### String
|
|
589
|
+
|
|
590
|
+
| Export | Loại | Mô tả |
|
|
591
|
+
|---|---|---|
|
|
592
|
+
| `IString` | type | `String & string & IStringCustom` — chuỗi mở rộng |
|
|
593
|
+
| `IStringCustom` | interface | Thêm `replaceAt`, `replaceAll`, `occurrencesByCharacter`, `indexesOfCharacter`, `indexesByString` |
|
|
594
|
+
| `ITextFormatOptions` | interface | Options format text: `uppercaseOtherCharacter`, `lowercaseOtherCharacter`, `trim`, `removeMultipleSpace`, `removeEmoji`, `removeUnicode` |
|
|
595
|
+
|
|
596
|
+
### Khác
|
|
597
|
+
|
|
598
|
+
| Export | Loại | Mô tả |
|
|
599
|
+
|---|---|---|
|
|
600
|
+
| `TYPE_OBJECT` | type | `Record<any, any>` — object không định kiểu. Chỉ dùng khi có lý do, phải kèm comment `// CONVENTION-EXCEPT: TYPE_OBJECT — [lý do]` |
|
|
601
|
+
| `TYPE_LANGUAGE_SUPPORT` | type | `'vi' \| 'en'` — ngôn ngữ được hỗ trợ hiện tại |
|
|
602
|
+
| `TYPE_FUNCTION<T>` | type | `(data: { value: any; item?: any; otherData?: any }) => Observable<T>` |
|
|
603
|
+
| `TYPE_IFRAME_MESSAGE<T, U>` | type | `{ type: T; response: U }` — message giao tiếp giữa micro-frontend qua iframe |
|
|
604
|
+
| `TYPE_TEMPLATE_REF` | type | `any` — placeholder type cho `TemplateRef` chưa xác định |
|
|
605
|
+
|
|
606
|
+
## Lưu ý quan trọng
|
|
607
|
+
|
|
608
|
+
⚠️ **ExtractInputs yêu cầu Generic Type tường minh**: Khi truyền data qua `of()` vào `getDataComponentOutlet`, BẮT BUỘC khai báo `of<ExtractInputs<I_DataInComponent...>>({...})`. Dùng `of({...})` không có generic sẽ mất type-safety và TypeScript không báo lỗi thiếu field.
|
|
609
|
+
|
|
610
|
+
⚠️ **PathOf có Depth giới hạn mặc định là 6**: Với object lồng nhau sâu hơn 6 cấp, các path vượt quá sẽ trả về `never`. Có thể tăng `Depth` khi cần: `PathOf<T, 8>`.
|
|
611
|
+
|
|
612
|
+
⚠️ **SignalOf không wrap các kiểu đặc biệt thêm lần nữa**: `Dayjs`, `Date`, `File`, `Blob`, `HttpParams`, `FormData`, `Event`, `Node`, `Map`, `Set`, `RegExp` và các Promise/Function sẽ chỉ được wrap 1 cấp signal — không đệ quy vào bên trong.
|
|
613
|
+
|
|
614
|
+
⚠️ **TYPE_OBJECT cần comment lý do**: Theo coding convention, mọi nơi sử dụng `TYPE_OBJECT` phải kèm comment `// CONVENTION-EXCEPT: TYPE_OBJECT — [lý do cụ thể]`. Không dùng tuỳ tiện.
|
|
615
|
+
|
|
616
|
+
⚠️ **IHttpResponse.feCloneCode là trường đặc biệt**: Trường này chỉ được gán từ phía frontend khi muốn override logic sau khi server trả về HTTP 200 nhưng business code lại là giá trị khác. Không phải field từ backend.
|
|
617
|
+
|
|
618
|
+
⚠️ **GetValueAtPath với R=false/true khác nhau**: Tham số `R=false` (mặc định) sẽ unwrap toàn bộ `WritableSignal` về giá trị thuần. `R=true` giữ nguyên cấu trúc signal. Lựa chọn `R` phải nhất quán với mục đích sử dụng.
|