@libs-ui/components-skeleton 0.2.357-1 → 0.2.357-2
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
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
- ✅ Hiệu ứng shimmer loading mượt mà (CSS animation)
|
|
12
12
|
- ✅ Tự động tính toán số lượng items dựa trên chiều cao container cha
|
|
13
|
+
- ✅ Tự động tính toán lại khi cửa sổ thay đổi kích thước (responsive, debounce 200ms)
|
|
13
14
|
- ✅ Hỗ trợ cấu hình layout phức tạp: rows và cols lồng nhau nhiều cấp
|
|
14
15
|
- ✅ Custom class cho từng cấp: container, row, col, item
|
|
15
16
|
- ✅ Hỗ trợ repeat ở mỗi cấp (container / row / col)
|
|
@@ -370,7 +371,7 @@ interface ISkeletonItem {
|
|
|
370
371
|
|
|
371
372
|
### Auto-calculation trong ngAfterViewInit
|
|
372
373
|
|
|
373
|
-
Component thực hiện tính toán tự động sau khi view khởi tạo (`ngAfterViewInit` + `setTimeout`):
|
|
374
|
+
Component thực hiện tính toán tự động sau khi view khởi tạo (`ngAfterViewInit` + `setTimeout`) và **tự chạy lại khi cửa sổ resize** (lắng nghe `window:resize`, debounce 200ms, tự cleanup bằng `takeUntilDestroyed`):
|
|
374
375
|
|
|
375
376
|
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
377
|
2. **Khi `heightContainer` không được set**: Đọc `offsetHeight` thực tế của container DOM để làm chiều cao mỗi block.
|
|
@@ -391,6 +392,8 @@ Khi một row hoặc col có `repeat > 1`, phần tử cuối cùng trong vòng
|
|
|
391
392
|
|
|
392
393
|
⚠️ **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
|
|
|
395
|
+
⚠️ **Tự tính lại khi resize**: Component lắng nghe `window:resize` (debounce 200ms) để tính lại số dòng skeleton theo chiều cao container mới — giữ skeleton lấp đầy đúng khi layout co giãn. Subscription tự cleanup qua `takeUntilDestroyed`, không cần xử lý thủ công.
|
|
396
|
+
|
|
394
397
|
⚠️ **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 đó.
|
|
395
398
|
|
|
396
399
|
⚠️ **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.
|
|
@@ -1,28 +1,35 @@
|
|
|
1
1
|
import { NgTemplateOutlet } from '@angular/common';
|
|
2
|
-
import { ChangeDetectionStrategy, Component, input, signal, viewChild } from '@angular/core';
|
|
2
|
+
import { ChangeDetectionStrategy, Component, DestroyRef, inject, input, signal, viewChild } from '@angular/core';
|
|
3
|
+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
4
|
+
import { debounceTime, fromEvent, merge, timer } from 'rxjs';
|
|
3
5
|
import * as i0 from "@angular/core";
|
|
4
6
|
export class LibsUiComponentsSkeletonComponent {
|
|
5
7
|
configRender = signal({});
|
|
6
8
|
config = input({}, { transform: (value) => value || {} });
|
|
7
9
|
skeletonRef = viewChild.required('skeletonRef');
|
|
10
|
+
destroyRef = inject(DestroyRef);
|
|
8
11
|
ngAfterViewInit() {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
12
|
+
// Tính 1 lần sau 100ms (chờ layout ổn định để đọc offsetHeight đúng) + tính lại mỗi khi resize.
|
|
13
|
+
merge(timer(100), fromEvent(window, 'resize').pipe(debounceTime(200)))
|
|
14
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
15
|
+
.subscribe(() => this.calculator());
|
|
16
|
+
}
|
|
17
|
+
calculator() {
|
|
18
|
+
this.configRender.update((config) => {
|
|
19
|
+
config = this.config();
|
|
20
|
+
let offsetHeight = this.skeletonRef().nativeElement.offsetHeight;
|
|
21
|
+
if (!config.rows?.length) {
|
|
22
|
+
config.rows = [{ item: {} }];
|
|
23
|
+
}
|
|
24
|
+
if (!config.heightContainer) {
|
|
25
|
+
config.heightContainer = offsetHeight - (config.styleMarginBottom || 0);
|
|
26
|
+
}
|
|
27
|
+
if (config.repeat) {
|
|
28
|
+
offsetHeight = offsetHeight || config.heightContainer || config.styleMarginBottom || 0;
|
|
29
|
+
config.heightContainer = offsetHeight / config.repeat - (config.styleMarginBottom || 0);
|
|
30
|
+
}
|
|
31
|
+
config.repeat = Math.ceil(offsetHeight / ((config.heightContainer || 0) + (config.styleMarginBottom || 0)));
|
|
32
|
+
return { ...config };
|
|
26
33
|
});
|
|
27
34
|
}
|
|
28
35
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LibsUiComponentsSkeletonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
@@ -32,4 +39,4 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
32
39
|
type: Component,
|
|
33
40
|
args: [{ selector: 'libs_ui-components-skeleton', standalone: true, imports: [NgTemplateOutlet], changeDetection: ChangeDetectionStrategy.OnPush, template: "<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", styles: ["@keyframes mo-skeleton-loading{0%{background-position:100% 50%}to{background-position:0 50%}}.skeleton{animation:mo-skeleton-loading 1.4s ease infinite;background:linear-gradient(90deg,#bebebe33 25%,#8181813d 37%,#bebebe33 63%);background-size:400% 100%}\n"] }]
|
|
34
41
|
}] });
|
|
35
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
42
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2tlbGV0b24uY29tcG9uZW50LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vLi4vbGlicy11aS9jb21wb25lbnRzL3NrZWxldG9uL3NyYy9za2VsZXRvbi5jb21wb25lbnQudHMiLCIuLi8uLi8uLi8uLi8uLi9saWJzLXVpL2NvbXBvbmVudHMvc2tlbGV0b24vc3JjL3NrZWxldG9uLmNvbXBvbmVudC5odG1sIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxnQkFBZ0IsRUFBRSxNQUFNLGlCQUFpQixDQUFDO0FBQ25ELE9BQU8sRUFBaUIsdUJBQXVCLEVBQUUsU0FBUyxFQUFFLFVBQVUsRUFBYyxNQUFNLEVBQUUsS0FBSyxFQUFFLE1BQU0sRUFBRSxTQUFTLEVBQUUsTUFBTSxlQUFlLENBQUM7QUFDNUksT0FBTyxFQUFFLGtCQUFrQixFQUFFLE1BQU0sNEJBQTRCLENBQUM7QUFDaEUsT0FBTyxFQUFFLFlBQVksRUFBRSxTQUFTLEVBQUUsS0FBSyxFQUFFLEtBQUssRUFBRSxNQUFNLE1BQU0sQ0FBQzs7QUFZN0QsTUFBTSxPQUFPLGlDQUFpQztJQUNsQyxZQUFZLEdBQUcsTUFBTSxDQUFrQixFQUFFLENBQUMsQ0FBQztJQUU1QyxNQUFNLEdBQUcsS0FBSyxDQUErQyxFQUFFLEVBQUUsRUFBRSxTQUFTLEVBQUUsQ0FBQyxLQUFLLEVBQUUsRUFBRSxDQUFDLEtBQUssSUFBSSxFQUFFLEVBQUUsQ0FBQyxDQUFDO0lBRXhHLFdBQVcsR0FBRyxTQUFTLENBQUMsUUFBUSxDQUFhLGFBQWEsQ0FBQyxDQUFDO0lBRXBELFVBQVUsR0FBRyxNQUFNLENBQUMsVUFBVSxDQUFDLENBQUM7SUFFakQsZUFBZTtRQUNiLGdHQUFnRztRQUNoRyxLQUFLLENBQUMsS0FBSyxDQUFDLEdBQUcsQ0FBQyxFQUFFLFNBQVMsQ0FBQyxNQUFNLEVBQUUsUUFBUSxDQUFDLENBQUMsSUFBSSxDQUFDLFlBQVksQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDO2FBQ25FLElBQUksQ0FBQyxrQkFBa0IsQ0FBQyxJQUFJLENBQUMsVUFBVSxDQUFDLENBQUM7YUFDekMsU0FBUyxDQUFDLEdBQUcsRUFBRSxDQUFDLElBQUksQ0FBQyxVQUFVLEVBQUUsQ0FBQyxDQUFDO0lBQ3hDLENBQUM7SUFFTyxVQUFVO1FBQ2hCLElBQUksQ0FBQyxZQUFZLENBQUMsTUFBTSxDQUFDLENBQUMsTUFBTSxFQUFFLEVBQUU7WUFDbEMsTUFBTSxHQUFHLElBQUksQ0FBQyxNQUFNLEVBQUUsQ0FBQztZQUN2QixJQUFJLFlBQVksR0FBRyxJQUFJLENBQUMsV0FBVyxFQUFFLENBQUMsYUFBYSxDQUFDLFlBQVksQ0FBQztZQUNqRSxJQUFJLENBQUMsTUFBTSxDQUFDLElBQUksRUFBRSxNQUFNLEVBQUUsQ0FBQztnQkFDekIsTUFBTSxDQUFDLElBQUksR0FBRyxDQUFDLEVBQUUsSUFBSSxFQUFFLEVBQUUsRUFBRSxDQUFDLENBQUM7WUFDL0IsQ0FBQztZQUNELElBQUksQ0FBQyxNQUFNLENBQUMsZUFBZSxFQUFFLENBQUM7Z0JBQzVCLE1BQU0sQ0FBQyxlQUFlLEdBQUcsWUFBWSxHQUFHLENBQUMsTUFBTSxDQUFDLGlCQUFpQixJQUFJLENBQUMsQ0FBQyxDQUFDO1lBQzFFLENBQUM7WUFDRCxJQUFJLE1BQU0sQ0FBQyxNQUFNLEVBQUUsQ0FBQztnQkFDbEIsWUFBWSxHQUFHLFlBQVksSUFBSSxNQUFNLENBQUMsZUFBZSxJQUFJLE1BQU0sQ0FBQyxpQkFBaUIsSUFBSSxDQUFDLENBQUM7Z0JBQ3ZGLE1BQU0sQ0FBQyxlQUFlLEdBQUcsWUFBWSxHQUFHLE1BQU0sQ0FBQyxNQUFNLEdBQUcsQ0FBQyxNQUFNLENBQUMsaUJBQWlCLElBQUksQ0FBQyxDQUFDLENBQUM7WUFDMUYsQ0FBQztZQUNELE1BQU0sQ0FBQyxNQUFNLEdBQUcsSUFBSSxDQUFDLElBQUksQ0FBQyxZQUFZLEdBQUcsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxlQUFlLElBQUksQ0FBQyxDQUFDLEdBQUcsQ0FBQyxNQUFNLENBQUMsaUJBQWlCLElBQUksQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDO1lBRTVHLE9BQU8sRUFBRSxHQUFHLE1BQU0sRUFBRSxDQUFDO1FBQ3ZCLENBQUMsQ0FBQyxDQUFDO0lBQ0wsQ0FBQzt3R0FsQ1UsaUNBQWlDOzRGQUFqQyxpQ0FBaUMsMFZDZjlDLCtsRUE2Q0EsMFREakNZLGdCQUFnQjs7NEZBR2YsaUNBQWlDO2tCQVQ3QyxTQUFTOytCQUVFLDZCQUE2QixjQUczQixJQUFJLFdBQ1AsQ0FBQyxnQkFBZ0IsQ0FBQyxtQkFDVix1QkFBdUIsQ0FBQyxNQUFNIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgTmdUZW1wbGF0ZU91dGxldCB9IGZyb20gJ0Bhbmd1bGFyL2NvbW1vbic7XG5pbXBvcnQgeyBBZnRlclZpZXdJbml0LCBDaGFuZ2VEZXRlY3Rpb25TdHJhdGVneSwgQ29tcG9uZW50LCBEZXN0cm95UmVmLCBFbGVtZW50UmVmLCBpbmplY3QsIGlucHV0LCBzaWduYWwsIHZpZXdDaGlsZCB9IGZyb20gJ0Bhbmd1bGFyL2NvcmUnO1xuaW1wb3J0IHsgdGFrZVVudGlsRGVzdHJveWVkIH0gZnJvbSAnQGFuZ3VsYXIvY29yZS9yeGpzLWludGVyb3AnO1xuaW1wb3J0IHsgZGVib3VuY2VUaW1lLCBmcm9tRXZlbnQsIG1lcmdlLCB0aW1lciB9IGZyb20gJ3J4anMnO1xuaW1wb3J0IHsgSVNrZWxldG9uQ29uZmlnIH0gZnJvbSAnLi9pbnRlcmZhY2VzL3NrZWxldG9uLWl0ZW0uaW50ZXJmYWNlJztcblxuQENvbXBvbmVudCh7XG4gIC8vIGVzbGludC1kaXNhYmxlLW5leHQtbGluZSBAYW5ndWxhci1lc2xpbnQvY29tcG9uZW50LXNlbGVjdG9yXG4gIHNlbGVjdG9yOiAnbGlic191aS1jb21wb25lbnRzLXNrZWxldG9uJyxcbiAgdGVtcGxhdGVVcmw6ICdza2VsZXRvbi5jb21wb25lbnQuaHRtbCcsXG4gIHN0eWxlVXJsczogWycuL3NrZWxldG9uLmNvbXBvbmVudC5zY3NzJ10sXG4gIHN0YW5kYWxvbmU6IHRydWUsXG4gIGltcG9ydHM6IFtOZ1RlbXBsYXRlT3V0bGV0XSxcbiAgY2hhbmdlRGV0ZWN0aW9uOiBDaGFuZ2VEZXRlY3Rpb25TdHJhdGVneS5PblB1c2gsXG59KVxuZXhwb3J0IGNsYXNzIExpYnNVaUNvbXBvbmVudHNTa2VsZXRvbkNvbXBvbmVudCBpbXBsZW1lbnRzIEFmdGVyVmlld0luaXQge1xuICBwcm90ZWN0ZWQgY29uZmlnUmVuZGVyID0gc2lnbmFsPElTa2VsZXRvbkNvbmZpZz4oe30pO1xuXG4gIHJlYWRvbmx5IGNvbmZpZyA9IGlucHV0PElTa2VsZXRvbkNvbmZpZywgSVNrZWxldG9uQ29uZmlnIHwgdW5kZWZpbmVkPih7fSwgeyB0cmFuc2Zvcm06ICh2YWx1ZSkgPT4gdmFsdWUgfHwge30gfSk7XG5cbiAgcmVhZG9ubHkgc2tlbGV0b25SZWYgPSB2aWV3Q2hpbGQucmVxdWlyZWQ8RWxlbWVudFJlZj4oJ3NrZWxldG9uUmVmJyk7XG5cbiAgcHJpdmF0ZSByZWFkb25seSBkZXN0cm95UmVmID0gaW5qZWN0KERlc3Ryb3lSZWYpO1xuXG4gIG5nQWZ0ZXJWaWV3SW5pdCgpOiB2b2lkIHtcbiAgICAvLyBUw61uaCAxIGzhuqduIHNhdSAxMDBtcyAoY2jhu50gbGF5b3V0IOG7lW4gxJHhu4tuaCDEkeG7gyDEkeG7jWMgb2Zmc2V0SGVpZ2h0IMSRw7puZykgKyB0w61uaCBs4bqhaSBt4buXaSBraGkgcmVzaXplLlxuICAgIG1lcmdlKHRpbWVyKDEwMCksIGZyb21FdmVudCh3aW5kb3csICdyZXNpemUnKS5waXBlKGRlYm91bmNlVGltZSgyMDApKSlcbiAgICAgIC5waXBlKHRha2VVbnRpbERlc3Ryb3llZCh0aGlzLmRlc3Ryb3lSZWYpKVxuICAgICAgLnN1YnNjcmliZSgoKSA9PiB0aGlzLmNhbGN1bGF0b3IoKSk7XG4gIH1cblxuICBwcml2YXRlIGNhbGN1bGF0b3IoKTogdm9pZCB7XG4gICAgdGhpcy5jb25maWdSZW5kZXIudXBkYXRlKChjb25maWcpID0+IHtcbiAgICAgIGNvbmZpZyA9IHRoaXMuY29uZmlnKCk7XG4gICAgICBsZXQgb2Zmc2V0SGVpZ2h0ID0gdGhpcy5za2VsZXRvblJlZigpLm5hdGl2ZUVsZW1lbnQub2Zmc2V0SGVpZ2h0O1xuICAgICAgaWYgKCFjb25maWcucm93cz8ubGVuZ3RoKSB7XG4gICAgICAgIGNvbmZpZy5yb3dzID0gW3sgaXRlbToge30gfV07XG4gICAgICB9XG4gICAgICBpZiAoIWNvbmZpZy5oZWlnaHRDb250YWluZXIpIHtcbiAgICAgICAgY29uZmlnLmhlaWdodENvbnRhaW5lciA9IG9mZnNldEhlaWdodCAtIChjb25maWcuc3R5bGVNYXJnaW5Cb3R0b20gfHwgMCk7XG4gICAgICB9XG4gICAgICBpZiAoY29uZmlnLnJlcGVhdCkge1xuICAgICAgICBvZmZzZXRIZWlnaHQgPSBvZmZzZXRIZWlnaHQgfHwgY29uZmlnLmhlaWdodENvbnRhaW5lciB8fCBjb25maWcuc3R5bGVNYXJnaW5Cb3R0b20gfHwgMDtcbiAgICAgICAgY29uZmlnLmhlaWdodENvbnRhaW5lciA9IG9mZnNldEhlaWdodCAvIGNvbmZpZy5yZXBlYXQgLSAoY29uZmlnLnN0eWxlTWFyZ2luQm90dG9tIHx8IDApO1xuICAgICAgfVxuICAgICAgY29uZmlnLnJlcGVhdCA9IE1hdGguY2VpbChvZmZzZXRIZWlnaHQgLyAoKGNvbmZpZy5oZWlnaHRDb250YWluZXIgfHwgMCkgKyAoY29uZmlnLnN0eWxlTWFyZ2luQm90dG9tIHx8IDApKSk7XG5cbiAgICAgIHJldHVybiB7IC4uLmNvbmZpZyB9O1xuICAgIH0pO1xuICB9XG59XG4iLCI8ZGl2XG4gICNza2VsZXRvblJlZlxuICBjbGFzcz1cInctZnVsbCBoLWZ1bGxcIj5cbiAgQGlmIChjb25maWdSZW5kZXIoKTsgYXMgY29uZmlnKSB7XG4gICAgQGZvciAoY29uZmlnUmVwZWF0IG9mIFtdLmNvbnN0cnVjdG9yKGNvbmZpZy5yZXBlYXQgfHwgMSk7IHRyYWNrICRpbmRleDsgbGV0IGxhc3RJdGVtID0gJGxhc3QpIHtcbiAgICAgIDxkaXZcbiAgICAgICAgW3N0eWxlLm1hcmdpbkJvdHRvbS5weF09XCJjb25maWcuc3R5bGVNYXJnaW5Cb3R0b20gJiYgIWxhc3RJdGVtID8gY29uZmlnLnN0eWxlTWFyZ2luQm90dG9tIDogMFwiXG4gICAgICAgIFtzdHlsZS5oZWlnaHRdPVwiY29uZmlnLmhlaWdodENvbnRhaW5lciA/IGNvbmZpZy5oZWlnaHRDb250YWluZXIgKyAncHgnIDogJzEwMCUnXCJcbiAgICAgICAgW2NsYXNzXT1cIihjb25maWcuY2xhc3NSb3dzIHx8ICcnKSArICcgZmxleCBmbGV4LWNvbCdcIj5cbiAgICAgICAgQGZvciAocm93IG9mIGNvbmZpZy5yb3dzOyB0cmFjayAkaW5kZXgpIHtcbiAgICAgICAgICBAZm9yIChyb3dSZXBlYXQgb2YgW10uY29uc3RydWN0b3Iocm93LnJlcGVhdCB8fCAxKTsgdHJhY2sgJGluZGV4OyBsZXQgZmlyc3RSb3cgPSAkZmlyc3Q7IGxldCBsYXN0Um93ID0gJGxhc3QpIHtcbiAgICAgICAgICAgIDxkaXYgW2NsYXNzXT1cIigobGFzdFJvdyB8fCAoZmlyc3RSb3cgJiYgbGFzdFJvdykgPyByb3cuY2xhc3NSb3dMYXN0IDogcm93LmNsYXNzUm93KSB8fCAnJykgKyAnIGZsZXggZmxleC1jb2wgdy1mdWxsIGgtZnVsbCdcIj5cbiAgICAgICAgICAgICAgQGlmIChyb3cuaXRlbSkge1xuICAgICAgICAgICAgICAgIDxuZy1jb250YWluZXIgKm5nVGVtcGxhdGVPdXRsZXQ9XCJ0ZW1wbGF0ZVNrZWxldG9uOyBjb250ZXh0OiB7IGNvbmZpZzogcm93Lml0ZW0gfVwiIC8+XG4gICAgICAgICAgICAgIH1cbiAgICAgICAgICAgICAgPGRpdiBbY2xhc3NdPVwiKChsYXN0Um93IHx8IChmaXJzdFJvdyAmJiBsYXN0Um93KSA/IHJvdy5jbGFzc0NvbHNMYXN0IDogcm93LmNsYXNzQ29scykgfHwgJycpICsgJ2ZsZXggdy1mdWxsJ1wiPlxuICAgICAgICAgICAgICAgIEBmb3IgKGNvbCBvZiByb3cuY29sczsgdHJhY2sgJGluZGV4KSB7XG4gICAgICAgICAgICAgICAgICBAZm9yIChjb2xSZXBlYXQgb2YgW10uY29uc3RydWN0b3IoY29sLnJlcGVhdCB8fCAxKTsgdHJhY2sgJGluZGV4OyBsZXQgZmlyc3RDb2wgPSAkZmlyc3Q7IGxldCBsYXN0Q29sID0gJGxhc3QpIHtcbiAgICAgICAgICAgICAgICAgICAgPGRpdiBbY2xhc3NdPVwiKChsYXN0Q29sIHx8IChmaXJzdENvbCAmJiBsYXN0Q29sKSA/IGNvbC5jbGFzc0NvbExhc3QgOiBjb2wuY2xhc3NDb2wpIHx8ICcnKSArICcgZmxleCB3LWZ1bGwnXCI+XG4gICAgICAgICAgICAgICAgICAgICAgQGlmIChjb2wuaXRlbSkge1xuICAgICAgICAgICAgICAgICAgICAgICAgPG5nLWNvbnRhaW5lciAqbmdUZW1wbGF0ZU91dGxldD1cInRlbXBsYXRlU2tlbGV0b247IGNvbnRleHQ6IHsgY29uZmlnOiBjb2wuaXRlbSB9XCIgLz5cbiAgICAgICAgICAgICAgICAgICAgICB9XG4gICAgICAgICAgICAgICAgICAgIDwvZGl2PlxuICAgICAgICAgICAgICAgICAgfVxuICAgICAgICAgICAgICAgIH1cbiAgICAgICAgICAgICAgPC9kaXY+XG4gICAgICAgICAgICA8L2Rpdj5cbiAgICAgICAgICB9XG4gICAgICAgIH1cbiAgICAgIDwvZGl2PlxuICAgIH1cbiAgfVxuPC9kaXY+XG5cbjxuZy10ZW1wbGF0ZVxuICAjdGVtcGxhdGVTa2VsZXRvblxuICBsZXQtY29uZmlnPVwiY29uZmlnXCI+XG4gIDxkaXYgW2NsYXNzXT1cImNvbmZpZz8uY2xhc3NJbmNsdWRlIHx8ICd3LWZ1bGwgaC1mdWxsJ1wiPlxuICAgIEBmb3IgKF8gb2YgW10uY29uc3RydWN0b3IoY29uZmlnLnJlcGVhdCB8fCAxKTsgdHJhY2sgJGluZGV4OyBsZXQgbGFzdCA9ICRsYXN0KSB7XG4gICAgICA8ZGl2XG4gICAgICAgIGNsYXNzPVwidy1mdWxsIGgtZnVsbCBza2VsZXRvbiByb3VuZGVkLVs4cHhdIHt7IGNvbmZpZz8uY2xhc3NJbmNsdWRlSXRlbSB8fCAnJyB9fVwiXG4gICAgICAgIFtjbGFzcy5tYi1bMTZweF1dPVwiY29uZmlnPy5zdHlsZURlZmF1bHQgJiYgIWxhc3RcIj48L2Rpdj5cbiAgICB9XG4gIDwvZGl2PlxuPC9uZy10ZW1wbGF0ZT5cbiJdfQ==
|
|
@@ -1,29 +1,36 @@
|
|
|
1
1
|
import { NgTemplateOutlet } from '@angular/common';
|
|
2
2
|
import * as i0 from '@angular/core';
|
|
3
|
-
import { signal, input, viewChild, Component, ChangeDetectionStrategy } from '@angular/core';
|
|
3
|
+
import { signal, input, viewChild, inject, DestroyRef, Component, ChangeDetectionStrategy } from '@angular/core';
|
|
4
|
+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
5
|
+
import { merge, timer, fromEvent, debounceTime } from 'rxjs';
|
|
4
6
|
|
|
5
7
|
class LibsUiComponentsSkeletonComponent {
|
|
6
8
|
configRender = signal({});
|
|
7
9
|
config = input({}, { transform: (value) => value || {} });
|
|
8
10
|
skeletonRef = viewChild.required('skeletonRef');
|
|
11
|
+
destroyRef = inject(DestroyRef);
|
|
9
12
|
ngAfterViewInit() {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
13
|
+
// Tính 1 lần sau 100ms (chờ layout ổn định để đọc offsetHeight đúng) + tính lại mỗi khi resize.
|
|
14
|
+
merge(timer(100), fromEvent(window, 'resize').pipe(debounceTime(200)))
|
|
15
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
16
|
+
.subscribe(() => this.calculator());
|
|
17
|
+
}
|
|
18
|
+
calculator() {
|
|
19
|
+
this.configRender.update((config) => {
|
|
20
|
+
config = this.config();
|
|
21
|
+
let offsetHeight = this.skeletonRef().nativeElement.offsetHeight;
|
|
22
|
+
if (!config.rows?.length) {
|
|
23
|
+
config.rows = [{ item: {} }];
|
|
24
|
+
}
|
|
25
|
+
if (!config.heightContainer) {
|
|
26
|
+
config.heightContainer = offsetHeight - (config.styleMarginBottom || 0);
|
|
27
|
+
}
|
|
28
|
+
if (config.repeat) {
|
|
29
|
+
offsetHeight = offsetHeight || config.heightContainer || config.styleMarginBottom || 0;
|
|
30
|
+
config.heightContainer = offsetHeight / config.repeat - (config.styleMarginBottom || 0);
|
|
31
|
+
}
|
|
32
|
+
config.repeat = Math.ceil(offsetHeight / ((config.heightContainer || 0) + (config.styleMarginBottom || 0)));
|
|
33
|
+
return { ...config };
|
|
27
34
|
});
|
|
28
35
|
}
|
|
29
36
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LibsUiComponentsSkeletonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
@@ -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
|
|
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, DestroyRef, ElementRef, inject, input, signal, viewChild } from '@angular/core';\nimport { takeUntilDestroyed } from '@angular/core/rxjs-interop';\nimport { debounceTime, fromEvent, merge, timer } from 'rxjs';\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 private readonly destroyRef = inject(DestroyRef);\n\n ngAfterViewInit(): void {\n // Tính 1 lần sau 100ms (chờ layout ổn định để đọc offsetHeight đúng) + tính lại mỗi khi resize.\n merge(timer(100), fromEvent(window, 'resize').pipe(debounceTime(200)))\n .pipe(takeUntilDestroyed(this.destroyRef))\n .subscribe(() => this.calculator());\n }\n\n private calculator(): void {\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","<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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"}
|
package/package.json
CHANGED
package/skeleton.component.d.ts
CHANGED
|
@@ -5,7 +5,9 @@ export declare class LibsUiComponentsSkeletonComponent implements AfterViewInit
|
|
|
5
5
|
protected configRender: import("@angular/core").WritableSignal<ISkeletonConfig>;
|
|
6
6
|
readonly config: import("@angular/core").InputSignalWithTransform<ISkeletonConfig, ISkeletonConfig | undefined>;
|
|
7
7
|
readonly skeletonRef: import("@angular/core").Signal<ElementRef<any>>;
|
|
8
|
+
private readonly destroyRef;
|
|
8
9
|
ngAfterViewInit(): void;
|
|
10
|
+
private calculator;
|
|
9
11
|
static ɵfac: i0.ɵɵFactoryDeclaration<LibsUiComponentsSkeletonComponent, never>;
|
|
10
12
|
static ɵcmp: i0.ɵɵComponentDeclaration<LibsUiComponentsSkeletonComponent, "libs_ui-components-skeleton", never, { "config": { "alias": "config"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
|
|
11
13
|
}
|