@libs-ui/components-skeleton 0.2.356-42 → 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
CHANGED
|
@@ -1,39 +1,38 @@
|
|
|
1
1
|
# @libs-ui/components-skeleton
|
|
2
2
|
|
|
3
|
-
> Component hiển thị hiệu ứng loading
|
|
3
|
+
> Component hiển thị hiệu ứng loading skeleton với shimmer animation, hỗ trợ cấu hình layout linh hoạt theo dạng rows/cols.
|
|
4
4
|
|
|
5
5
|
## Giới thiệu
|
|
6
6
|
|
|
7
|
-
`LibsUiComponentsSkeletonComponent` là
|
|
7
|
+
`LibsUiComponentsSkeletonComponent` là standalone Angular component giúp tạo khung xương (skeleton) loading với hiệu ứng shimmer mượt mà. Component tự động tính toán số lượng item cần render dựa trên chiều cao container cha, đồng thời hỗ trợ cấu hình layout phức tạp theo dạng rows và cols lồng nhau để tái hiện cấu trúc nội dung thực tế trước khi dữ liệu được tải về.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
## Tính năng
|
|
10
10
|
|
|
11
|
-
- ✅ Hiệu ứng
|
|
12
|
-
- ✅
|
|
13
|
-
- ✅ Hỗ trợ cấu hình
|
|
14
|
-
- ✅ Custom
|
|
15
|
-
- ✅
|
|
11
|
+
- ✅ Hiệu ứng shimmer loading mượt mà (CSS animation)
|
|
12
|
+
- ✅ Tự động tính toán số lượng items dựa trên chiều cao container cha
|
|
13
|
+
- ✅ Hỗ trợ cấu hình layout phức tạp: rows và cols lồng nhau nhiều cấp
|
|
14
|
+
- ✅ Custom class cho từng cấp: container, row, col, item
|
|
15
|
+
- ✅ Hỗ trợ repeat ở mỗi cấp (container / row / col)
|
|
16
|
+
- ✅ Standalone component, tối ưu hiệu năng với `ChangeDetectionStrategy.OnPush`
|
|
16
17
|
|
|
17
18
|
## Khi nào sử dụng
|
|
18
19
|
|
|
19
|
-
- Khi đang tải dữ liệu từ API và muốn hiển thị cấu trúc
|
|
20
|
-
- Thay thế
|
|
21
|
-
-
|
|
20
|
+
- Khi đang tải dữ liệu từ API và muốn hiển thị trước cấu trúc layout (perceived performance).
|
|
21
|
+
- Thay thế loading spinner truyền thống để cải thiện trải nghiệm người dùng.
|
|
22
|
+
- Trong danh sách items: comments, posts, products, notifications.
|
|
23
|
+
- Trong màn hình chi tiết: user profile, order detail, article.
|
|
24
|
+
- Trong bảng dữ liệu: table rows chưa load xong.
|
|
22
25
|
|
|
23
26
|
## Cài đặt
|
|
24
27
|
|
|
25
28
|
```bash
|
|
26
|
-
# npm
|
|
27
29
|
npm install @libs-ui/components-skeleton
|
|
28
|
-
|
|
29
|
-
# yarn
|
|
30
|
-
yarn add @libs-ui/components-skeleton
|
|
31
30
|
```
|
|
32
31
|
|
|
33
32
|
## Import
|
|
34
33
|
|
|
35
34
|
```typescript
|
|
36
|
-
import { LibsUiComponentsSkeletonComponent } from '@libs-ui/components-skeleton';
|
|
35
|
+
import { LibsUiComponentsSkeletonComponent, ISkeletonConfig } from '@libs-ui/components-skeleton';
|
|
37
36
|
|
|
38
37
|
@Component({
|
|
39
38
|
standalone: true,
|
|
@@ -43,38 +42,49 @@ import { LibsUiComponentsSkeletonComponent } from '@libs-ui/components-skeleton'
|
|
|
43
42
|
export class YourComponent {}
|
|
44
43
|
```
|
|
45
44
|
|
|
46
|
-
## Ví dụ
|
|
45
|
+
## Ví dụ sử dụng
|
|
47
46
|
|
|
48
|
-
### Basic
|
|
47
|
+
### 1. Basic — Tự động fill container
|
|
49
48
|
|
|
50
|
-
Component
|
|
49
|
+
Component tự động tính toán số lượng dòng skeleton để lấp đầy container. Chỉ cần đặt trong một thẻ có chiều cao cụ thể.
|
|
51
50
|
|
|
52
51
|
```html
|
|
53
|
-
|
|
52
|
+
<!-- Template -->
|
|
53
|
+
<div class="h-[300px] w-full">
|
|
54
54
|
<libs_ui-components-skeleton />
|
|
55
55
|
</div>
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
-
###
|
|
58
|
+
### 2. Avatar & Lines (Horizontal)
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
Mô phỏng layout có avatar tròn và hai dòng nội dung bên cạnh — phù hợp cho danh sách người dùng, notifications.
|
|
61
61
|
|
|
62
62
|
```typescript
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
// component.ts
|
|
64
|
+
import { ISkeletonConfig } from '@libs-ui/components-skeleton';
|
|
65
|
+
|
|
66
|
+
readonly avatarLinesConfig: ISkeletonConfig = {
|
|
67
|
+
heightContainer: 118,
|
|
68
|
+
classRows: 'bg-[#fff] p-[16px] rounded-[8px] !flex-row',
|
|
69
|
+
styleMarginBottom: 16,
|
|
65
70
|
rows: [
|
|
66
71
|
{
|
|
67
|
-
|
|
72
|
+
classRowLast: '!w-[40px] !h-[40px] mr-[12px] shrink-0',
|
|
73
|
+
item: { classIncludeItem: '!rounded-[40px]' },
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
classColsLast: 'h-full flex-col ',
|
|
68
77
|
cols: [
|
|
69
|
-
// Avatar Column
|
|
70
78
|
{
|
|
71
|
-
|
|
79
|
+
classColLast: '!h-full mb-[8px]',
|
|
80
|
+
item: { classIncludeItem: '!rounded-[4px]' },
|
|
72
81
|
},
|
|
73
|
-
// Text Column
|
|
74
82
|
{
|
|
75
|
-
|
|
76
|
-
item: {
|
|
77
|
-
|
|
83
|
+
classColLast: '!h-auto',
|
|
84
|
+
item: {
|
|
85
|
+
classInclude: 'w-full !h-[20px]',
|
|
86
|
+
classIncludeItem: '!rounded-[4px]',
|
|
87
|
+
},
|
|
78
88
|
},
|
|
79
89
|
],
|
|
80
90
|
},
|
|
@@ -83,63 +93,307 @@ const cardConfig: ISkeletonConfig = {
|
|
|
83
93
|
```
|
|
84
94
|
|
|
85
95
|
```html
|
|
86
|
-
|
|
96
|
+
<!-- Template -->
|
|
97
|
+
<div class="h-[300px]">
|
|
98
|
+
<libs_ui-components-skeleton [config]="avatarLinesConfig" />
|
|
99
|
+
</div>
|
|
87
100
|
```
|
|
88
101
|
|
|
89
|
-
|
|
102
|
+
### 3. Card với Thumbnail nhỏ
|
|
90
103
|
|
|
91
|
-
|
|
104
|
+
Layout card có tiêu đề lớn phía trên và thumbnail kèm nội dung bên dưới — phù hợp cho danh sách bài viết, sản phẩm.
|
|
92
105
|
|
|
93
|
-
|
|
106
|
+
```typescript
|
|
107
|
+
// component.ts
|
|
108
|
+
import { ISkeletonConfig } from '@libs-ui/components-skeleton';
|
|
94
109
|
|
|
95
|
-
|
|
110
|
+
readonly cardThumbConfig: ISkeletonConfig = {
|
|
111
|
+
heightContainer: 158,
|
|
112
|
+
classRows: 'bg-[#fff] p-[16px] rounded-[8px]',
|
|
113
|
+
styleMarginBottom: 16,
|
|
114
|
+
rows: [
|
|
115
|
+
{
|
|
116
|
+
classRowLast: '!h-auto',
|
|
117
|
+
item: {
|
|
118
|
+
classInclude: 'w-full !h-[34px] mb-[8px]',
|
|
119
|
+
classIncludeItem: '!rounded-[4px]',
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
classColsLast: 'h-full ',
|
|
124
|
+
cols: [
|
|
125
|
+
{
|
|
126
|
+
classColLast: '!w-[80px] mr-[12px] shrink-0',
|
|
127
|
+
item: { classIncludeItem: '!rounded-[4px]' },
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
item: { classIncludeItem: '!rounded-[4px]' },
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
};
|
|
136
|
+
```
|
|
96
137
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
138
|
+
```html
|
|
139
|
+
<!-- Template -->
|
|
140
|
+
<div class="h-[400px]">
|
|
141
|
+
<libs_ui-components-skeleton [config]="cardThumbConfig" />
|
|
142
|
+
</div>
|
|
143
|
+
```
|
|
100
144
|
|
|
101
|
-
###
|
|
145
|
+
### 4. Header & Content (phức hợp)
|
|
102
146
|
|
|
103
|
-
|
|
147
|
+
Layout phức hợp: header gồm avatar tròn + tiêu đề, bên dưới là vùng nội dung nhiều dòng — phù hợp cho detail page.
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
// component.ts
|
|
151
|
+
import { ISkeletonConfig } from '@libs-ui/components-skeleton';
|
|
152
|
+
|
|
153
|
+
readonly headerContentConfig: ISkeletonConfig = {
|
|
154
|
+
heightContainer: 202,
|
|
155
|
+
classRows: 'bg-[#fff] p-[16px] rounded-[8px]',
|
|
156
|
+
styleMarginBottom: 16,
|
|
157
|
+
rows: [
|
|
158
|
+
{
|
|
159
|
+
classRowLast: '!h-auto ',
|
|
160
|
+
cols: [
|
|
161
|
+
{
|
|
162
|
+
classColLast: '!w-[40px] !h-[40px] mr-[12px] shrink-0',
|
|
163
|
+
item: { classIncludeItem: '!rounded-[40px]' },
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
classColLast: '!h-auto',
|
|
167
|
+
item: {
|
|
168
|
+
classInclude: 'w-full !h-[40px] mb-[8px]',
|
|
169
|
+
classIncludeItem: '!rounded-[4px]',
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
],
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
classColsLast: 'h-full flex-col ',
|
|
176
|
+
cols: [
|
|
177
|
+
{
|
|
178
|
+
classColLast: '!h-full mb-[8px]',
|
|
179
|
+
item: { classIncludeItem: '!rounded-[4px]' },
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
classColLast: '!h-auto',
|
|
183
|
+
item: {
|
|
184
|
+
classInclude: 'w-full !h-[20px]',
|
|
185
|
+
classIncludeItem: '!rounded-[4px]',
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
};
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
```html
|
|
195
|
+
<!-- Template -->
|
|
196
|
+
<div class="h-[500px]">
|
|
197
|
+
<libs_ui-components-skeleton [config]="headerContentConfig" />
|
|
198
|
+
</div>
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### 5. Sectioned Content (Table header + Body)
|
|
202
|
+
|
|
203
|
+
Layout chia hai vùng: header cố định phía trên (không có shimmer) và vùng nội dung bên dưới — phù hợp cho table loading.
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
// component.ts
|
|
207
|
+
import { ISkeletonConfig } from '@libs-ui/components-skeleton';
|
|
208
|
+
|
|
209
|
+
readonly sectionedConfig: ISkeletonConfig = {
|
|
210
|
+
rows: [
|
|
211
|
+
{
|
|
212
|
+
classRowLast: '!h-[40px] bg-[#fff]',
|
|
213
|
+
item: { classIncludeItem: '!h-[40px] !rounded-none opacity-[0]' },
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
classRowLast: 'h-full flex-col',
|
|
217
|
+
item: { classIncludeItem: '!rounded-none' },
|
|
218
|
+
},
|
|
219
|
+
],
|
|
220
|
+
};
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
```html
|
|
224
|
+
<!-- Template -->
|
|
225
|
+
<div class="h-[400px]">
|
|
226
|
+
<libs_ui-components-skeleton [config]="sectionedConfig" />
|
|
227
|
+
</div>
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### 6. Small Icon & Line
|
|
231
|
+
|
|
232
|
+
Layout tối giản: icon nhỏ và một dòng nội dung bên cạnh — phù hợp cho menu items, breadcrumb.
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
// component.ts
|
|
236
|
+
import { ISkeletonConfig } from '@libs-ui/components-skeleton';
|
|
237
|
+
|
|
238
|
+
readonly iconLineConfig: ISkeletonConfig = {
|
|
239
|
+
heightContainer: 116,
|
|
240
|
+
classRows: 'bg-[#fff] rounded-[8px] !flex-row',
|
|
241
|
+
styleMarginBottom: 16,
|
|
242
|
+
rows: [
|
|
243
|
+
{
|
|
244
|
+
classRowLast: '!w-[32px] !h-[32px] mr-[12px] shrink-0',
|
|
245
|
+
item: { classIncludeItem: '!rounded-[32px]' },
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
classColsLast: 'h-full flex-col ',
|
|
249
|
+
cols: [
|
|
250
|
+
{
|
|
251
|
+
classColLast: '!h-full',
|
|
252
|
+
item: { classIncludeItem: '!rounded-[4px]' },
|
|
253
|
+
},
|
|
254
|
+
],
|
|
255
|
+
},
|
|
256
|
+
],
|
|
257
|
+
};
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
```html
|
|
261
|
+
<!-- Template -->
|
|
262
|
+
<div class="h-[250px]">
|
|
263
|
+
<libs_ui-components-skeleton [config]="iconLineConfig" />
|
|
264
|
+
</div>
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
## @Input()
|
|
268
|
+
|
|
269
|
+
| Input | Type | Default | Mô tả | Ví dụ |
|
|
270
|
+
|---|---|---|---|---|
|
|
271
|
+
| `[config]` | `ISkeletonConfig` | `{}` | Object cấu hình toàn bộ layout skeleton: số lần lặp, chiều cao, rows/cols lồng nhau và class tuỳ chỉnh cho từng cấp | `[config]="avatarLinesConfig"` |
|
|
272
|
+
|
|
273
|
+
## Types & Interfaces
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
import { ISkeletonConfig } from '@libs-ui/components-skeleton';
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### ISkeletonConfig
|
|
104
280
|
|
|
105
281
|
```typescript
|
|
106
282
|
export interface ISkeletonConfig {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
283
|
+
/**
|
|
284
|
+
* Số lần lặp lại toàn bộ layout.
|
|
285
|
+
* Khi không set, component tự tính dựa trên chiều cao container / heightContainer.
|
|
286
|
+
*/
|
|
287
|
+
repeat?: number;
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Chiều cao cố định (px) cho mỗi block lặp lại.
|
|
291
|
+
* Khi không set, component đọc offsetHeight của container thực.
|
|
292
|
+
*/
|
|
293
|
+
heightContainer?: number;
|
|
294
|
+
|
|
295
|
+
/** Class áp dụng lên wrapper container bao ngoài tất cả rows. */
|
|
296
|
+
classRows?: string;
|
|
297
|
+
|
|
298
|
+
/** Khoảng cách margin-bottom (px) giữa các block lặp (trừ block cuối). */
|
|
299
|
+
styleMarginBottom?: number;
|
|
300
|
+
|
|
111
301
|
rows?: Array<{
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
302
|
+
/** Số lần lặp lại row này. */
|
|
303
|
+
repeat?: number;
|
|
304
|
+
|
|
305
|
+
/** Class cho row (áp dụng cho tất cả row trừ row cuối cùng). */
|
|
306
|
+
classRow?: string;
|
|
307
|
+
|
|
308
|
+
/** Class dành riêng cho row cuối cùng trong vòng lặp repeat. */
|
|
309
|
+
classRowLast?: string;
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Config skeleton item trực tiếp trong row (không qua cols).
|
|
313
|
+
* Dùng khi row chỉ có 1 skeleton block duy nhất.
|
|
314
|
+
*/
|
|
315
|
+
item?: ISkeletonItem;
|
|
316
|
+
|
|
317
|
+
/** Class cho container bao ngoài tất cả cols (áp dụng cho tất cả trừ cuối). */
|
|
318
|
+
classCols?: string;
|
|
319
|
+
|
|
320
|
+
/** Class cho container cols dành riêng cho row cuối. */
|
|
321
|
+
classColsLast?: string;
|
|
322
|
+
|
|
118
323
|
cols?: Array<{
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
324
|
+
/** Số lần lặp lại col này. */
|
|
325
|
+
repeat?: number;
|
|
326
|
+
|
|
327
|
+
/** Class cho col (áp dụng cho tất cả col trừ col cuối cùng). */
|
|
328
|
+
classCol?: string;
|
|
329
|
+
|
|
330
|
+
/** Class dành riêng cho col cuối cùng trong vòng lặp repeat. */
|
|
331
|
+
classColLast?: string;
|
|
332
|
+
|
|
333
|
+
/** Config skeleton item bên trong col. */
|
|
334
|
+
item?: ISkeletonItem;
|
|
123
335
|
}>;
|
|
124
336
|
}>;
|
|
125
337
|
}
|
|
126
338
|
```
|
|
127
339
|
|
|
128
|
-
|
|
340
|
+
### ISkeletonItem
|
|
129
341
|
|
|
130
342
|
```typescript
|
|
131
343
|
interface ISkeletonItem {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
344
|
+
/**
|
|
345
|
+
* Class áp dụng lên thẻ div skeleton trực tiếp (có class .skeleton và animation).
|
|
346
|
+
* Dùng để override kích thước, border-radius của skeleton block.
|
|
347
|
+
* Ví dụ: '!rounded-[40px]' cho avatar tròn, '!rounded-[4px]' cho text line.
|
|
348
|
+
*/
|
|
349
|
+
classIncludeItem?: string;
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Class áp dụng lên wrapper bên ngoài bao các skeleton divs.
|
|
353
|
+
* Dùng để kiểm soát chiều cao wrapper khi skeleton item có height cố định.
|
|
354
|
+
* Ví dụ: 'w-full !h-[20px]' để giới hạn chiều cao vùng chứa.
|
|
355
|
+
*/
|
|
356
|
+
classInclude?: string;
|
|
357
|
+
|
|
358
|
+
/** (Legacy) Class không khuyến nghị dùng trong code mới. */
|
|
359
|
+
class?: string;
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Khi true, tự động thêm margin-bottom 16px cho tất cả item trừ item cuối cùng.
|
|
363
|
+
* Tiện dùng khi repeat > 1 và không muốn config styleMarginBottom ở cấp ngoài.
|
|
364
|
+
*/
|
|
365
|
+
styleDefault?: boolean;
|
|
136
366
|
}
|
|
137
367
|
```
|
|
138
368
|
|
|
139
|
-
##
|
|
369
|
+
## Logic ẩn quan trọng
|
|
370
|
+
|
|
371
|
+
### Auto-calculation trong ngAfterViewInit
|
|
372
|
+
|
|
373
|
+
Component thực hiện tính toán tự động sau khi view khởi tạo (`ngAfterViewInit` + `setTimeout`):
|
|
374
|
+
|
|
375
|
+
1. **Khi `config.rows` rỗng hoặc không có**: Tự tạo 1 row mặc định với item rỗng.
|
|
376
|
+
2. **Khi `heightContainer` không được set**: Đọc `offsetHeight` thực tế của container DOM để làm chiều cao mỗi block.
|
|
377
|
+
3. **Khi `repeat` không được set**: Tính tự động bằng công thức:
|
|
378
|
+
```
|
|
379
|
+
repeat = Math.ceil(offsetHeight / (heightContainer + styleMarginBottom))
|
|
380
|
+
```
|
|
381
|
+
Kết quả: skeleton tự lấp đầy container cha mà không cần set số dòng thủ công.
|
|
382
|
+
4. **Khi `repeat` được set**: `heightContainer` được chia đều theo `repeat` để các block chia đều chiều cao container.
|
|
383
|
+
|
|
384
|
+
### Cơ chế classRowLast / classColLast
|
|
385
|
+
|
|
386
|
+
Khi một row hoặc col có `repeat > 1`, phần tử cuối cùng trong vòng lặp sẽ áp dụng class `classRowLast` (hoặc `classColLast`) thay vì `classRow` (hoặc `classCol`). Điều này cho phép tuỳ chỉnh style riêng cho phần tử cuối, ví dụ: bỏ margin-bottom ở item cuối cùng.
|
|
387
|
+
|
|
388
|
+
## Lưu ý quan trọng
|
|
389
|
+
|
|
390
|
+
⚠️ **Container phải có chiều cao xác định**: Component đọc `offsetHeight` của container cha để tính toán. Nếu container không có `height` hoặc `min-height` cụ thể, `offsetHeight = 0` và skeleton sẽ không render đúng. Luôn bọc trong thẻ có chiều cao rõ ràng (`h-[300px]`, `min-h-[200px]`, hoặc `h-full` trong container đã có height).
|
|
391
|
+
|
|
392
|
+
⚠️ **setTimeout trong ngAfterViewInit**: Việc tính toán được bọc trong `setTimeout` (0ms) để đảm bảo DOM đã render xong và `offsetHeight` trả về giá trị chính xác. Điều này là bình thường và có chủ ý.
|
|
393
|
+
|
|
394
|
+
⚠️ **classRowLast vs classRow**: Khi `repeat = 1` (mặc định), phần tử duy nhất được coi là "cả first lẫn last", tức là `classRowLast` sẽ được áp dụng. Thiết kế này có nghĩa là với row không repeat, bạn chỉ cần dùng `classRowLast` để set class cho row đó.
|
|
140
395
|
|
|
141
|
-
|
|
142
|
-
Mặc định skeleton sẽ fill size của container cha (`w-full h-full`).
|
|
396
|
+
⚠️ **Input `config` có transform**: Input `config` được khai báo với transform `(value) => value || {}`. Truyền `undefined` hoặc `null` cho `[config]` sẽ được tự động chuyển thành `{}` (config mặc định), component sẽ không crash.
|
|
143
397
|
|
|
144
398
|
## Demo
|
|
145
399
|
|
|
@@ -147,4 +401,4 @@ Mặc định skeleton sẽ fill size của container cha (`w-full h-full`).
|
|
|
147
401
|
npx nx serve core-ui
|
|
148
402
|
```
|
|
149
403
|
|
|
150
|
-
Truy cập:
|
|
404
|
+
Truy cập: http://localhost:4500/skeleton
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { NgTemplateOutlet } from '@angular/common';
|
|
2
2
|
import * as i0 from '@angular/core';
|
|
3
|
-
import { signal, input, viewChild,
|
|
3
|
+
import { signal, input, viewChild, Component, ChangeDetectionStrategy } from '@angular/core';
|
|
4
4
|
|
|
5
5
|
class LibsUiComponentsSkeletonComponent {
|
|
6
6
|
configRender = signal({});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"libs-ui-components-skeleton.mjs","sources":["../../../../../libs-ui/components/skeleton/src/skeleton.component.ts","../../../../../libs-ui/components/skeleton/src/skeleton.component.html","../../../../../libs-ui/components/skeleton/src/libs-ui-components-skeleton.ts"],"sourcesContent":["import { NgTemplateOutlet } from '@angular/common';\nimport { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, input, signal, viewChild } from '@angular/core';\nimport { ISkeletonConfig } from './interfaces/skeleton-item.interface';\n\n@Component({\n // eslint-disable-next-line @angular-eslint/component-selector\n selector: 'libs_ui-components-skeleton',\n templateUrl: 'skeleton.component.html',\n styleUrls: ['./skeleton.component.scss'],\n standalone: true,\n imports: [NgTemplateOutlet],\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class LibsUiComponentsSkeletonComponent implements AfterViewInit {\n protected configRender = signal<ISkeletonConfig>({});\n\n readonly config = input<ISkeletonConfig, ISkeletonConfig | undefined>({}, { transform: (value) => value || {} });\n\n readonly skeletonRef = viewChild.required<ElementRef>('skeletonRef');\n\n ngAfterViewInit(): void {\n setTimeout(() => {\n this.configRender.update((config) => {\n config = this.config();\n let offsetHeight = this.skeletonRef().nativeElement.offsetHeight;\n if (!config.rows?.length) {\n config.rows = [{ item: {} }];\n }\n if (!config.heightContainer) {\n config.heightContainer = offsetHeight - (config.styleMarginBottom || 0);\n }\n if (config.repeat) {\n offsetHeight = offsetHeight || config.heightContainer || config.styleMarginBottom || 0;\n config.heightContainer = offsetHeight / config.repeat - (config.styleMarginBottom || 0);\n }\n config.repeat = Math.ceil(offsetHeight / ((config.heightContainer || 0) + (config.styleMarginBottom || 0)));\n\n return { ...config };\n });\n });\n }\n}\n","<div\n #skeletonRef\n class=\"w-full h-full\">\n @if (configRender(); as config) {\n @for (configRepeat of [].constructor(config.repeat || 1); track $index; let lastItem = $last) {\n <div\n [style.marginBottom.px]=\"config.styleMarginBottom && !lastItem ? config.styleMarginBottom : 0\"\n [style.height]=\"config.heightContainer ? config.heightContainer + 'px' : '100%'\"\n [class]=\"(config.classRows || '') + ' flex flex-col'\">\n @for (row of config.rows; track $index) {\n @for (rowRepeat of [].constructor(row.repeat || 1); track $index; let firstRow = $first; let lastRow = $last) {\n <div [class]=\"((lastRow || (firstRow && lastRow) ? row.classRowLast : row.classRow) || '') + ' flex flex-col w-full h-full'\">\n @if (row.item) {\n <ng-container *ngTemplateOutlet=\"templateSkeleton; context: { config: row.item }\" />\n }\n <div [class]=\"((lastRow || (firstRow && lastRow) ? row.classColsLast : row.classCols) || '') + 'flex w-full'\">\n @for (col of row.cols; track $index) {\n @for (colRepeat of [].constructor(col.repeat || 1); track $index; let firstCol = $first; let lastCol = $last) {\n <div [class]=\"((lastCol || (firstCol && lastCol) ? col.classColLast : col.classCol) || '') + ' flex w-full'\">\n @if (col.item) {\n <ng-container *ngTemplateOutlet=\"templateSkeleton; context: { config: col.item }\" />\n }\n </div>\n }\n }\n </div>\n </div>\n }\n }\n </div>\n }\n }\n</div>\n\n<ng-template\n #templateSkeleton\n let-config=\"config\">\n <div [class]=\"config?.classInclude || 'w-full h-full'\">\n @for (_ of [].constructor(config.repeat || 1); track $index; let last = $last) {\n <div\n class=\"w-full h-full skeleton rounded-[8px] {{ config?.classIncludeItem || '' }}\"\n [class.mb-[16px]]=\"config?.styleDefault && !last\"></div>\n }\n </div>\n</ng-template>\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;;MAaa,iCAAiC,CAAA;AAClC,IAAA,YAAY,GAAG,MAAM,CAAkB,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"libs-ui-components-skeleton.mjs","sources":["../../../../../libs-ui/components/skeleton/src/skeleton.component.ts","../../../../../libs-ui/components/skeleton/src/skeleton.component.html","../../../../../libs-ui/components/skeleton/src/libs-ui-components-skeleton.ts"],"sourcesContent":["import { NgTemplateOutlet } from '@angular/common';\nimport { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, input, signal, viewChild } from '@angular/core';\nimport { ISkeletonConfig } from './interfaces/skeleton-item.interface';\n\n@Component({\n // eslint-disable-next-line @angular-eslint/component-selector\n selector: 'libs_ui-components-skeleton',\n templateUrl: 'skeleton.component.html',\n styleUrls: ['./skeleton.component.scss'],\n standalone: true,\n imports: [NgTemplateOutlet],\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class LibsUiComponentsSkeletonComponent implements AfterViewInit {\n protected configRender = signal<ISkeletonConfig>({});\n\n readonly config = input<ISkeletonConfig, ISkeletonConfig | undefined>({}, { transform: (value) => value || {} });\n\n readonly skeletonRef = viewChild.required<ElementRef>('skeletonRef');\n\n ngAfterViewInit(): void {\n setTimeout(() => {\n this.configRender.update((config) => {\n config = this.config();\n let offsetHeight = this.skeletonRef().nativeElement.offsetHeight;\n if (!config.rows?.length) {\n config.rows = [{ item: {} }];\n }\n if (!config.heightContainer) {\n config.heightContainer = offsetHeight - (config.styleMarginBottom || 0);\n }\n if (config.repeat) {\n offsetHeight = offsetHeight || config.heightContainer || config.styleMarginBottom || 0;\n config.heightContainer = offsetHeight / config.repeat - (config.styleMarginBottom || 0);\n }\n config.repeat = Math.ceil(offsetHeight / ((config.heightContainer || 0) + (config.styleMarginBottom || 0)));\n\n return { ...config };\n });\n });\n }\n}\n","<div\n #skeletonRef\n class=\"w-full h-full\">\n @if (configRender(); as config) {\n @for (configRepeat of [].constructor(config.repeat || 1); track $index; let lastItem = $last) {\n <div\n [style.marginBottom.px]=\"config.styleMarginBottom && !lastItem ? config.styleMarginBottom : 0\"\n [style.height]=\"config.heightContainer ? config.heightContainer + 'px' : '100%'\"\n [class]=\"(config.classRows || '') + ' flex flex-col'\">\n @for (row of config.rows; track $index) {\n @for (rowRepeat of [].constructor(row.repeat || 1); track $index; let firstRow = $first; let lastRow = $last) {\n <div [class]=\"((lastRow || (firstRow && lastRow) ? row.classRowLast : row.classRow) || '') + ' flex flex-col w-full h-full'\">\n @if (row.item) {\n <ng-container *ngTemplateOutlet=\"templateSkeleton; context: { config: row.item }\" />\n }\n <div [class]=\"((lastRow || (firstRow && lastRow) ? row.classColsLast : row.classCols) || '') + 'flex w-full'\">\n @for (col of row.cols; track $index) {\n @for (colRepeat of [].constructor(col.repeat || 1); track $index; let firstCol = $first; let lastCol = $last) {\n <div [class]=\"((lastCol || (firstCol && lastCol) ? col.classColLast : col.classCol) || '') + ' flex w-full'\">\n @if (col.item) {\n <ng-container *ngTemplateOutlet=\"templateSkeleton; context: { config: col.item }\" />\n }\n </div>\n }\n }\n </div>\n </div>\n }\n }\n </div>\n }\n }\n</div>\n\n<ng-template\n #templateSkeleton\n let-config=\"config\">\n <div [class]=\"config?.classInclude || 'w-full h-full'\">\n @for (_ of [].constructor(config.repeat || 1); track $index; let last = $last) {\n <div\n class=\"w-full h-full skeleton rounded-[8px] {{ config?.classIncludeItem || '' }}\"\n [class.mb-[16px]]=\"config?.styleDefault && !last\"></div>\n }\n </div>\n</ng-template>\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
|