@shimmer-from-structure/angular 2.2.1 → 2.2.3

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.
@@ -0,0 +1,298 @@
1
+ import * as i0 from '@angular/core';
2
+ import { InjectionToken, inject, input, viewChild, signal, PLATFORM_ID, computed, effect, ViewEncapsulation, ChangeDetectionStrategy, Component } from '@angular/core';
3
+ import { isPlatformBrowser, NgIf, NgFor } from '@angular/common';
4
+ import { shimmerDefaults, createResizeObserver, extractElementInfo } from '@shimmer-from-structure/core';
5
+ export { shimmerDefaults } from '@shimmer-from-structure/core';
6
+
7
+ /**
8
+ * Injection token for global shimmer configuration.
9
+ * Use `provideShimmerConfig()` to configure in your app.
10
+ */
11
+ const SHIMMER_CONFIG = new InjectionToken('SHIMMER_CONFIG');
12
+ /**
13
+ * Provider function for shimmer configuration.
14
+ * Use in your app's providers array.
15
+ *
16
+ * @example
17
+ * ```typescript
18
+ * bootstrapApplication(AppComponent, {
19
+ * providers: [
20
+ * provideShimmerConfig({
21
+ * shimmerColor: 'rgba(255, 255, 255, 0.3)',
22
+ * duration: 1.5
23
+ * })
24
+ * ]
25
+ * });
26
+ * ```
27
+ */
28
+ function provideShimmerConfig(config) {
29
+ return { provide: SHIMMER_CONFIG, useValue: config };
30
+ }
31
+ /**
32
+ * Inject and resolve shimmer configuration.
33
+ * Merges injected config with defaults.
34
+ * Returns fully resolved ShimmerContextValue with all properties defined.
35
+ */
36
+ function injectShimmerConfig() {
37
+ const config = inject(SHIMMER_CONFIG, { optional: true }) ?? {};
38
+ return {
39
+ shimmerColor: config.shimmerColor ?? shimmerDefaults.shimmerColor,
40
+ backgroundColor: config.backgroundColor ?? shimmerDefaults.backgroundColor,
41
+ duration: config.duration ?? shimmerDefaults.duration,
42
+ fallbackBorderRadius: config.fallbackBorderRadius ?? shimmerDefaults.fallbackBorderRadius,
43
+ };
44
+ }
45
+
46
+ /**
47
+ * Shimmer component that creates loading skeleton overlays based on content structure.
48
+ * Automatically measures projected content and creates matching shimmer blocks.
49
+ *
50
+ * @example
51
+ * ```html
52
+ * <shimmer [loading]="isLoading">
53
+ * <div class="card">
54
+ * <h2>{{ title }}</h2>
55
+ * <p>{{ description }}</p>
56
+ * </div>
57
+ * </shimmer>
58
+ * ```
59
+ */
60
+ class ShimmerComponent {
61
+ // Inputs using Angular signals
62
+ loading = input(true);
63
+ shimmerColor = input(undefined);
64
+ backgroundColor = input(undefined);
65
+ duration = input(undefined);
66
+ fallbackBorderRadius = input(undefined);
67
+ // View child reference
68
+ measureContainer = viewChild('measureContainer');
69
+ // Internal state
70
+ elements = signal([]);
71
+ // Inject dependencies
72
+ contextConfig = injectShimmerConfig();
73
+ platformId = inject(PLATFORM_ID);
74
+ isBrowser = isPlatformBrowser(this.platformId);
75
+ // Resolved values (props > context > defaults)
76
+ resolvedShimmerColor = computed(() => this.shimmerColor() ?? this.contextConfig.shimmerColor);
77
+ resolvedBackgroundColor = computed(() => this.backgroundColor() ?? this.contextConfig.backgroundColor);
78
+ resolvedDuration = computed(() => this.duration() ?? this.contextConfig.duration);
79
+ resolvedFallbackBorderRadius = computed(() => this.fallbackBorderRadius() ?? this.contextConfig.fallbackBorderRadius);
80
+ // Cleanup function for ResizeObserver
81
+ resizeCleanup;
82
+ mutationObserver;
83
+ constructor() {
84
+ // Effect to re-measure when loading state changes
85
+ effect((onCleanup) => {
86
+ // Skip effect on server
87
+ if (!this.isBrowser)
88
+ return;
89
+ const isLoading = this.loading();
90
+ const container = this.measureContainer();
91
+ if (isLoading && container) {
92
+ // Clean up existing observers before setting up new ones
93
+ this.cleanup();
94
+ // Set up observers for this loading session
95
+ this.setupObservers();
96
+ // Defer measurement to next frame to ensure content is rendered
97
+ requestAnimationFrame(() => this.measureElements());
98
+ }
99
+ else {
100
+ // Cleanup when not loading
101
+ this.cleanup();
102
+ }
103
+ // Cleanup on effect re-run or component destruction
104
+ onCleanup(() => {
105
+ this.cleanup();
106
+ });
107
+ });
108
+ }
109
+ ngAfterViewInit() {
110
+ // Effect will handle setup when container becomes available
111
+ }
112
+ ngOnDestroy() {
113
+ this.cleanup();
114
+ }
115
+ setupObservers() {
116
+ if (!this.isBrowser)
117
+ return;
118
+ const container = this.measureContainer()?.nativeElement;
119
+ if (!container)
120
+ return;
121
+ // Set up ResizeObserver
122
+ this.resizeCleanup = createResizeObserver(container, () => this.measureElements());
123
+ // Set up MutationObserver for content changes
124
+ this.mutationObserver = new MutationObserver(() => {
125
+ if (this.loading()) {
126
+ this.measureElements();
127
+ }
128
+ });
129
+ this.mutationObserver.observe(container, {
130
+ childList: true,
131
+ subtree: true,
132
+ characterData: true,
133
+ attributes: false,
134
+ });
135
+ }
136
+ measureElements() {
137
+ if (!this.isBrowser)
138
+ return;
139
+ const container = this.measureContainer()?.nativeElement;
140
+ if (!container || !this.loading())
141
+ return;
142
+ // Temporarily disconnect mutation observer to avoid recursion
143
+ this.mutationObserver?.disconnect();
144
+ const containerRect = container.getBoundingClientRect();
145
+ const extractedElements = [];
146
+ Array.from(container.children).forEach((child) => {
147
+ extractedElements.push(...extractElementInfo(child, containerRect));
148
+ });
149
+ this.elements.set(extractedElements);
150
+ // Reconnect mutation observer
151
+ if (this.mutationObserver && container) {
152
+ this.mutationObserver.observe(container, {
153
+ childList: true,
154
+ subtree: true,
155
+ characterData: true,
156
+ attributes: false,
157
+ });
158
+ }
159
+ }
160
+ cleanup() {
161
+ if (this.resizeCleanup) {
162
+ this.resizeCleanup();
163
+ this.resizeCleanup = undefined;
164
+ }
165
+ if (this.mutationObserver) {
166
+ this.mutationObserver.disconnect();
167
+ this.mutationObserver = undefined;
168
+ }
169
+ }
170
+ /**
171
+ * Manually trigger re-measurement of elements.
172
+ * Useful when content changes programmatically.
173
+ */
174
+ remeasure() {
175
+ if (this.isBrowser) {
176
+ this.measureElements();
177
+ }
178
+ }
179
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.18", ngImport: i0, type: ShimmerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
180
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.18", type: ShimmerComponent, isStandalone: true, selector: "shimmer", inputs: { loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, shimmerColor: { classPropertyName: "shimmerColor", publicName: "shimmerColor", isSignal: true, isRequired: false, transformFunction: null }, backgroundColor: { classPropertyName: "backgroundColor", publicName: "backgroundColor", isSignal: true, isRequired: false, transformFunction: null }, duration: { classPropertyName: "duration", publicName: "duration", isSignal: true, isRequired: false, transformFunction: null }, fallbackBorderRadius: { classPropertyName: "fallbackBorderRadius", publicName: "fallbackBorderRadius", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "measureContainer", first: true, predicate: ["measureContainer"], descendants: true, isSignal: true }], ngImport: i0, template: `
181
+ <div style="position: relative;">
182
+ <!-- Always render content -->
183
+ <div
184
+ #measureContainer
185
+ [class.shimmer-measure-container]="loading()"
186
+ [attr.aria-hidden]="loading() ? 'true' : null"
187
+ [style.pointer-events]="loading() ? 'none' : null"
188
+ >
189
+ <ng-content></ng-content>
190
+ </div>
191
+
192
+ <!-- Shimmer overlay - only when loading -->
193
+ @if (loading()) {
194
+ <div
195
+ style="
196
+ position: absolute;
197
+ top: 0;
198
+ left: 0;
199
+ right: 0;
200
+ bottom: 0;
201
+ overflow: hidden;
202
+ pointer-events: none;
203
+ "
204
+ >
205
+ @for (element of elements(); track $index) {
206
+ <div
207
+ [style.position]="'absolute'"
208
+ [style.left.px]="element.x"
209
+ [style.top.px]="element.y"
210
+ [style.width.px]="element.width"
211
+ [style.height.px]="element.height"
212
+ [style.backgroundColor]="resolvedBackgroundColor()"
213
+ [style.borderRadius]="
214
+ element.borderRadius === '0px'
215
+ ? resolvedFallbackBorderRadius() + 'px'
216
+ : element.borderRadius
217
+ "
218
+ [style.overflow]="'hidden'"
219
+ >
220
+ <div
221
+ class="shimmer-animation-element"
222
+ [style.background]="
223
+ 'linear-gradient(90deg, transparent, ' + resolvedShimmerColor() + ', transparent)'
224
+ "
225
+ [style.animationDuration]="resolvedDuration() + 's'"
226
+ ></div>
227
+ </div>
228
+ }
229
+ </div>
230
+ }
231
+ </div>
232
+ `, isInline: true, styles: [":host{display:contents}.shimmer-measure-container *{color:transparent!important}.shimmer-measure-container img,.shimmer-measure-container svg,.shimmer-measure-container video{opacity:0}.shimmer-animation-element{position:absolute;top:0;left:0;width:100%;height:100%;animation:shimmer-animation 1.5s infinite}@keyframes shimmer-animation{0%{transform:translate(-100%)}to{transform:translate(100%)}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
233
+ }
234
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.18", ngImport: i0, type: ShimmerComponent, decorators: [{
235
+ type: Component,
236
+ args: [{ selector: 'shimmer', standalone: true, imports: [NgIf, NgFor], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: `
237
+ <div style="position: relative;">
238
+ <!-- Always render content -->
239
+ <div
240
+ #measureContainer
241
+ [class.shimmer-measure-container]="loading()"
242
+ [attr.aria-hidden]="loading() ? 'true' : null"
243
+ [style.pointer-events]="loading() ? 'none' : null"
244
+ >
245
+ <ng-content></ng-content>
246
+ </div>
247
+
248
+ <!-- Shimmer overlay - only when loading -->
249
+ @if (loading()) {
250
+ <div
251
+ style="
252
+ position: absolute;
253
+ top: 0;
254
+ left: 0;
255
+ right: 0;
256
+ bottom: 0;
257
+ overflow: hidden;
258
+ pointer-events: none;
259
+ "
260
+ >
261
+ @for (element of elements(); track $index) {
262
+ <div
263
+ [style.position]="'absolute'"
264
+ [style.left.px]="element.x"
265
+ [style.top.px]="element.y"
266
+ [style.width.px]="element.width"
267
+ [style.height.px]="element.height"
268
+ [style.backgroundColor]="resolvedBackgroundColor()"
269
+ [style.borderRadius]="
270
+ element.borderRadius === '0px'
271
+ ? resolvedFallbackBorderRadius() + 'px'
272
+ : element.borderRadius
273
+ "
274
+ [style.overflow]="'hidden'"
275
+ >
276
+ <div
277
+ class="shimmer-animation-element"
278
+ [style.background]="
279
+ 'linear-gradient(90deg, transparent, ' + resolvedShimmerColor() + ', transparent)'
280
+ "
281
+ [style.animationDuration]="resolvedDuration() + 's'"
282
+ ></div>
283
+ </div>
284
+ }
285
+ </div>
286
+ }
287
+ </div>
288
+ `, styles: [":host{display:contents}.shimmer-measure-container *{color:transparent!important}.shimmer-measure-container img,.shimmer-measure-container svg,.shimmer-measure-container video{opacity:0}.shimmer-animation-element{position:absolute;top:0;left:0;width:100%;height:100%;animation:shimmer-animation 1.5s infinite}@keyframes shimmer-animation{0%{transform:translate(-100%)}to{transform:translate(100%)}}\n"] }]
289
+ }], ctorParameters: () => [] });
290
+
291
+ // Public API Surface
292
+
293
+ /**
294
+ * Generated bundle index. Do not edit.
295
+ */
296
+
297
+ export { SHIMMER_CONFIG, ShimmerComponent, injectShimmerConfig, provideShimmerConfig };
298
+ //# sourceMappingURL=shimmer-from-structure-angular.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shimmer-from-structure-angular.mjs","sources":["../../src/shimmer-config.service.ts","../../src/shimmer.component.ts","../../src/public-api.ts","../../src/shimmer-from-structure-angular.ts"],"sourcesContent":["import { InjectionToken, inject } from '@angular/core';\nimport type { ShimmerConfig, ShimmerContextValue } from '@shimmer-from-structure/core';\nimport { shimmerDefaults } from '@shimmer-from-structure/core';\n\n/**\n * Injection token for global shimmer configuration.\n * Use `provideShimmerConfig()` to configure in your app.\n */\nexport const SHIMMER_CONFIG = new InjectionToken<ShimmerConfig>('SHIMMER_CONFIG');\n\n/**\n * Provider function for shimmer configuration.\n * Use in your app's providers array.\n *\n * @example\n * ```typescript\n * bootstrapApplication(AppComponent, {\n * providers: [\n * provideShimmerConfig({\n * shimmerColor: 'rgba(255, 255, 255, 0.3)',\n * duration: 1.5\n * })\n * ]\n * });\n * ```\n */\nexport function provideShimmerConfig(config: ShimmerConfig) {\n return { provide: SHIMMER_CONFIG, useValue: config };\n}\n\n/**\n * Inject and resolve shimmer configuration.\n * Merges injected config with defaults.\n * Returns fully resolved ShimmerContextValue with all properties defined.\n */\nexport function injectShimmerConfig(): ShimmerContextValue {\n const config = inject(SHIMMER_CONFIG, { optional: true }) ?? {};\n\n return {\n shimmerColor: config.shimmerColor ?? shimmerDefaults.shimmerColor,\n backgroundColor: config.backgroundColor ?? shimmerDefaults.backgroundColor,\n duration: config.duration ?? shimmerDefaults.duration,\n fallbackBorderRadius: config.fallbackBorderRadius ?? shimmerDefaults.fallbackBorderRadius,\n };\n}\n\n// Re-export defaults for testing and reference\nexport { shimmerDefaults };\n","import {\n Component,\n input,\n signal,\n computed,\n effect,\n ElementRef,\n viewChild,\n AfterViewInit,\n OnDestroy,\n ChangeDetectionStrategy,\n ViewEncapsulation,\n inject,\n PLATFORM_ID,\n} from '@angular/core';\nimport { NgIf, NgFor, isPlatformBrowser } from '@angular/common';\nimport {\n extractElementInfo,\n createResizeObserver,\n type ElementInfo,\n} from '@shimmer-from-structure/core';\nimport { injectShimmerConfig } from './shimmer-config.service';\n\n/**\n * Shimmer component that creates loading skeleton overlays based on content structure.\n * Automatically measures projected content and creates matching shimmer blocks.\n *\n * @example\n * ```html\n * <shimmer [loading]=\"isLoading\">\n * <div class=\"card\">\n * <h2>{{ title }}</h2>\n * <p>{{ description }}</p>\n * </div>\n * </shimmer>\n * ```\n */\n@Component({\n selector: 'shimmer',\n standalone: true,\n imports: [NgIf, NgFor],\n changeDetection: ChangeDetectionStrategy.OnPush,\n encapsulation: ViewEncapsulation.None,\n template: `\n <div style=\"position: relative;\">\n <!-- Always render content -->\n <div\n #measureContainer\n [class.shimmer-measure-container]=\"loading()\"\n [attr.aria-hidden]=\"loading() ? 'true' : null\"\n [style.pointer-events]=\"loading() ? 'none' : null\"\n >\n <ng-content></ng-content>\n </div>\n\n <!-- Shimmer overlay - only when loading -->\n @if (loading()) {\n <div\n style=\"\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n overflow: hidden;\n pointer-events: none;\n \"\n >\n @for (element of elements(); track $index) {\n <div\n [style.position]=\"'absolute'\"\n [style.left.px]=\"element.x\"\n [style.top.px]=\"element.y\"\n [style.width.px]=\"element.width\"\n [style.height.px]=\"element.height\"\n [style.backgroundColor]=\"resolvedBackgroundColor()\"\n [style.borderRadius]=\"\n element.borderRadius === '0px'\n ? resolvedFallbackBorderRadius() + 'px'\n : element.borderRadius\n \"\n [style.overflow]=\"'hidden'\"\n >\n <div\n class=\"shimmer-animation-element\"\n [style.background]=\"\n 'linear-gradient(90deg, transparent, ' + resolvedShimmerColor() + ', transparent)'\n \"\n [style.animationDuration]=\"resolvedDuration() + 's'\"\n ></div>\n </div>\n }\n </div>\n }\n </div>\n `,\n styles: [\n `\n :host {\n display: contents;\n }\n\n .shimmer-measure-container * {\n color: transparent !important;\n }\n\n .shimmer-measure-container img,\n .shimmer-measure-container svg,\n .shimmer-measure-container video {\n opacity: 0;\n }\n\n .shimmer-animation-element {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n animation: shimmer-animation 1.5s infinite;\n }\n\n @keyframes shimmer-animation {\n 0% {\n transform: translateX(-100%);\n }\n 100% {\n transform: translateX(100%);\n }\n }\n `,\n ],\n})\nexport class ShimmerComponent implements AfterViewInit, OnDestroy {\n // Inputs using Angular signals\n loading = input<boolean>(true);\n shimmerColor = input<string | undefined>(undefined);\n backgroundColor = input<string | undefined>(undefined);\n duration = input<number | undefined>(undefined);\n fallbackBorderRadius = input<number | undefined>(undefined);\n\n // View child reference\n measureContainer = viewChild<ElementRef<HTMLDivElement>>('measureContainer');\n\n // Internal state\n elements = signal<ElementInfo[]>([]);\n\n // Inject dependencies\n private contextConfig = injectShimmerConfig();\n private platformId = inject(PLATFORM_ID);\n private isBrowser = isPlatformBrowser(this.platformId);\n\n // Resolved values (props > context > defaults)\n resolvedShimmerColor = computed(() => this.shimmerColor() ?? this.contextConfig.shimmerColor);\n resolvedBackgroundColor = computed(\n () => this.backgroundColor() ?? this.contextConfig.backgroundColor\n );\n resolvedDuration = computed(() => this.duration() ?? this.contextConfig.duration);\n resolvedFallbackBorderRadius = computed(\n () => this.fallbackBorderRadius() ?? this.contextConfig.fallbackBorderRadius\n );\n\n // Cleanup function for ResizeObserver\n private resizeCleanup: (() => void) | undefined;\n private mutationObserver: MutationObserver | undefined;\n\n constructor() {\n // Effect to re-measure when loading state changes\n effect((onCleanup) => {\n // Skip effect on server\n if (!this.isBrowser) return;\n\n const isLoading = this.loading();\n const container = this.measureContainer();\n\n if (isLoading && container) {\n // Clean up existing observers before setting up new ones\n this.cleanup();\n\n // Set up observers for this loading session\n this.setupObservers();\n\n // Defer measurement to next frame to ensure content is rendered\n requestAnimationFrame(() => this.measureElements());\n } else {\n // Cleanup when not loading\n this.cleanup();\n }\n\n // Cleanup on effect re-run or component destruction\n onCleanup(() => {\n this.cleanup();\n });\n });\n }\n\n ngAfterViewInit(): void {\n // Effect will handle setup when container becomes available\n }\n\n ngOnDestroy(): void {\n this.cleanup();\n }\n\n private setupObservers(): void {\n if (!this.isBrowser) return;\n\n const container = this.measureContainer()?.nativeElement;\n if (!container) return;\n\n // Set up ResizeObserver\n this.resizeCleanup = createResizeObserver(container, () => this.measureElements());\n\n // Set up MutationObserver for content changes\n this.mutationObserver = new MutationObserver(() => {\n if (this.loading()) {\n this.measureElements();\n }\n });\n\n this.mutationObserver.observe(container, {\n childList: true,\n subtree: true,\n characterData: true,\n attributes: false,\n });\n }\n\n private measureElements(): void {\n if (!this.isBrowser) return;\n\n const container = this.measureContainer()?.nativeElement;\n if (!container || !this.loading()) return;\n\n // Temporarily disconnect mutation observer to avoid recursion\n this.mutationObserver?.disconnect();\n\n const containerRect = container.getBoundingClientRect();\n const extractedElements: ElementInfo[] = [];\n\n Array.from(container.children).forEach((child) => {\n extractedElements.push(...extractElementInfo(child, containerRect));\n });\n\n this.elements.set(extractedElements);\n\n // Reconnect mutation observer\n if (this.mutationObserver && container) {\n this.mutationObserver.observe(container, {\n childList: true,\n subtree: true,\n characterData: true,\n attributes: false,\n });\n }\n }\n\n private cleanup(): void {\n if (this.resizeCleanup) {\n this.resizeCleanup();\n this.resizeCleanup = undefined;\n }\n if (this.mutationObserver) {\n this.mutationObserver.disconnect();\n this.mutationObserver = undefined;\n }\n }\n\n /**\n * Manually trigger re-measurement of elements.\n * Useful when content changes programmatically.\n */\n remeasure(): void {\n if (this.isBrowser) {\n this.measureElements();\n }\n }\n}\n","// Public API Surface\nexport { ShimmerComponent } from './shimmer.component';\nexport {\n SHIMMER_CONFIG,\n provideShimmerConfig,\n injectShimmerConfig,\n shimmerDefaults,\n} from './shimmer-config.service';\nexport type {\n ShimmerInputs,\n ShimmerConfig,\n ShimmerContextValue,\n ElementInfo,\n} from './types';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
package/index.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Generated bundle index. Do not edit.
3
+ */
4
+ /// <amd-module name="@shimmer-from-structure/angular" />
5
+ export * from './public-api';
package/package.json CHANGED
@@ -1,46 +1,38 @@
1
1
  {
2
- "name": "@shimmer-from-structure/angular",
3
- "version": "2.2.1",
4
- "description": "Angular adapter for shimmer-from-structure",
5
- "peerDependencies": {
6
- "@angular/core": "^19.0.0 || ^20.0.0 || ^21.0.0",
7
- "@angular/common": "^19.0.0 || ^20.0.0 || ^21.0.0"
2
+ "name": "@shimmer-from-structure/angular",
3
+ "version": "2.2.3",
4
+ "description": "Angular adapter for shimmer-from-structure",
5
+ "peerDependencies": {
6
+ "@angular/core": "^19.0.0 || ^20.0.0 || ^21.0.0",
7
+ "@angular/common": "^19.0.0 || ^20.0.0 || ^21.0.0"
8
+ },
9
+ "dependencies": {
10
+ "@shimmer-from-structure/core": "*",
11
+ "tslib": "^2.3.0"
12
+ },
13
+ "keywords": [
14
+ "angular",
15
+ "shimmer",
16
+ "skeleton",
17
+ "loading"
18
+ ],
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/darula-hpp/shimmer-from-structure.git",
22
+ "directory": "packages/angular"
23
+ },
24
+ "author": "Olebogeng Mbedzi",
25
+ "license": "MIT",
26
+ "module": "fesm2022/shimmer-from-structure-angular.mjs",
27
+ "typings": "index.d.ts",
28
+ "exports": {
29
+ "./package.json": {
30
+ "default": "./package.json"
8
31
  },
9
- "dependencies": {
10
- "@shimmer-from-structure/core": "*"
11
- },
12
- "devDependencies": {
13
- "@angular/compiler": "^19.0.0",
14
- "@angular/compiler-cli": "^19.0.0",
15
- "@angular/core": "^19.0.0",
16
- "@angular/common": "^19.0.0",
17
- "@angular/platform-browser": "^19.0.0",
18
- "@angular/platform-browser-dynamic": "^19.0.0",
19
- "ng-packagr": "^19.0.0",
20
- "typescript": "~5.6.0",
21
- "vitest": "^4.0.17",
22
- "jsdom": "^27.4.0",
23
- "@testing-library/dom": "^10.4.1",
24
- "zone.js": "~0.15.0",
25
- "rxjs": "~7.8.0"
26
- },
27
- "scripts": {
28
- "build": "ng-packagr -p ng-package.json",
29
- "test": "vitest run",
30
- "format": "prettier --write .",
31
- "prepublishOnly": "cp ../../README.md ."
32
- },
33
- "keywords": [
34
- "angular",
35
- "shimmer",
36
- "skeleton",
37
- "loading"
38
- ],
39
- "repository": {
40
- "type": "git",
41
- "url": "git+https://github.com/darula-hpp/shimmer-from-structure.git",
42
- "directory": "packages/angular"
43
- },
44
- "author": "Olebogeng Mbedzi",
45
- "license": "MIT"
46
- }
32
+ ".": {
33
+ "types": "./index.d.ts",
34
+ "default": "./fesm2022/shimmer-from-structure-angular.mjs"
35
+ }
36
+ },
37
+ "sideEffects": false
38
+ }
@@ -0,0 +1,3 @@
1
+ export { ShimmerComponent } from './shimmer.component';
2
+ export { SHIMMER_CONFIG, provideShimmerConfig, injectShimmerConfig, shimmerDefaults, } from './shimmer-config.service';
3
+ export type { ShimmerInputs, ShimmerConfig, ShimmerContextValue, ElementInfo, } from './types';
@@ -1,13 +1,11 @@
1
- import { InjectionToken, inject } from '@angular/core';
1
+ import { InjectionToken } from '@angular/core';
2
2
  import type { ShimmerConfig, ShimmerContextValue } from '@shimmer-from-structure/core';
3
3
  import { shimmerDefaults } from '@shimmer-from-structure/core';
4
-
5
4
  /**
6
5
  * Injection token for global shimmer configuration.
7
6
  * Use `provideShimmerConfig()` to configure in your app.
8
7
  */
9
- export const SHIMMER_CONFIG = new InjectionToken<ShimmerConfig>('SHIMMER_CONFIG');
10
-
8
+ export declare const SHIMMER_CONFIG: InjectionToken<ShimmerConfig>;
11
9
  /**
12
10
  * Provider function for shimmer configuration.
13
11
  * Use in your app's providers array.
@@ -24,25 +22,14 @@ export const SHIMMER_CONFIG = new InjectionToken<ShimmerConfig>('SHIMMER_CONFIG'
24
22
  * });
25
23
  * ```
26
24
  */
27
- export function provideShimmerConfig(config: ShimmerConfig) {
28
- return { provide: SHIMMER_CONFIG, useValue: config };
29
- }
30
-
25
+ export declare function provideShimmerConfig(config: ShimmerConfig): {
26
+ provide: InjectionToken<ShimmerConfig>;
27
+ useValue: ShimmerConfig;
28
+ };
31
29
  /**
32
30
  * Inject and resolve shimmer configuration.
33
31
  * Merges injected config with defaults.
34
32
  * Returns fully resolved ShimmerContextValue with all properties defined.
35
33
  */
36
- export function injectShimmerConfig(): ShimmerContextValue {
37
- const config = inject(SHIMMER_CONFIG, { optional: true }) ?? {};
38
-
39
- return {
40
- shimmerColor: config.shimmerColor ?? shimmerDefaults.shimmerColor,
41
- backgroundColor: config.backgroundColor ?? shimmerDefaults.backgroundColor,
42
- duration: config.duration ?? shimmerDefaults.duration,
43
- fallbackBorderRadius: config.fallbackBorderRadius ?? shimmerDefaults.fallbackBorderRadius,
44
- };
45
- }
46
-
47
- // Re-export defaults for testing and reference
34
+ export declare function injectShimmerConfig(): ShimmerContextValue;
48
35
  export { shimmerDefaults };
@@ -0,0 +1,48 @@
1
+ import { ElementRef, AfterViewInit, OnDestroy } from '@angular/core';
2
+ import { type ElementInfo } from '@shimmer-from-structure/core';
3
+ import * as i0 from "@angular/core";
4
+ /**
5
+ * Shimmer component that creates loading skeleton overlays based on content structure.
6
+ * Automatically measures projected content and creates matching shimmer blocks.
7
+ *
8
+ * @example
9
+ * ```html
10
+ * <shimmer [loading]="isLoading">
11
+ * <div class="card">
12
+ * <h2>{{ title }}</h2>
13
+ * <p>{{ description }}</p>
14
+ * </div>
15
+ * </shimmer>
16
+ * ```
17
+ */
18
+ export declare class ShimmerComponent implements AfterViewInit, OnDestroy {
19
+ loading: import("@angular/core").InputSignal<boolean>;
20
+ shimmerColor: import("@angular/core").InputSignal<string>;
21
+ backgroundColor: import("@angular/core").InputSignal<string>;
22
+ duration: import("@angular/core").InputSignal<number>;
23
+ fallbackBorderRadius: import("@angular/core").InputSignal<number>;
24
+ measureContainer: import("@angular/core").Signal<ElementRef<HTMLDivElement>>;
25
+ elements: import("@angular/core").WritableSignal<ElementInfo[]>;
26
+ private contextConfig;
27
+ private platformId;
28
+ private isBrowser;
29
+ resolvedShimmerColor: import("@angular/core").Signal<string>;
30
+ resolvedBackgroundColor: import("@angular/core").Signal<string>;
31
+ resolvedDuration: import("@angular/core").Signal<number>;
32
+ resolvedFallbackBorderRadius: import("@angular/core").Signal<number>;
33
+ private resizeCleanup;
34
+ private mutationObserver;
35
+ constructor();
36
+ ngAfterViewInit(): void;
37
+ ngOnDestroy(): void;
38
+ private setupObservers;
39
+ private measureElements;
40
+ private cleanup;
41
+ /**
42
+ * Manually trigger re-measurement of elements.
43
+ * Useful when content changes programmatically.
44
+ */
45
+ remeasure(): void;
46
+ static ɵfac: i0.ɵɵFactoryDeclaration<ShimmerComponent, never>;
47
+ static ɵcmp: i0.ɵɵComponentDeclaration<ShimmerComponent, "shimmer", never, { "loading": { "alias": "loading"; "required": false; "isSignal": true; }; "shimmerColor": { "alias": "shimmerColor"; "required": false; "isSignal": true; }; "backgroundColor": { "alias": "backgroundColor"; "required": false; "isSignal": true; }; "duration": { "alias": "duration"; "required": false; "isSignal": true; }; "fallbackBorderRadius": { "alias": "fallbackBorderRadius"; "required": false; "isSignal": true; }; }, {}, never, ["*"], true, never>;
48
+ }
@@ -1,4 +1,3 @@
1
-
2
1
  /**
3
2
  * Input properties for the Shimmer component
4
3
  */
@@ -8,28 +7,22 @@ export interface ShimmerInputs {
8
7
  * @default true
9
8
  */
10
9
  loading?: boolean;
11
-
12
10
  /**
13
11
  * Color of the shimmer effect gradient
14
12
  */
15
13
  shimmerColor?: string;
16
-
17
14
  /**
18
15
  * Background color of the shimmer blocks
19
16
  */
20
17
  backgroundColor?: string;
21
-
22
18
  /**
23
19
  * Duration of one shimmer animation cycle in seconds
24
20
  */
25
21
  duration?: number;
26
-
27
22
  /**
28
23
  * Fallback border radius (in pixels) when element has no border-radius
29
24
  * @default 4
30
25
  */
31
26
  fallbackBorderRadius?: number;
32
27
  }
33
-
34
- // Re-export core types for convenience
35
28
  export type { ShimmerConfig, ShimmerContextValue, ElementInfo } from '@shimmer-from-structure/core';
package/ng-package.json DELETED
@@ -1,8 +0,0 @@
1
- {
2
- "$schema": "./node_modules/ng-packagr/ng-package.schema.json",
3
- "dest": "./dist",
4
- "lib": {
5
- "entryFile": "src/public-api.ts"
6
- },
7
- "allowedNonPeerDependencies": ["@shimmer-from-structure/core"]
8
- }
package/src/public-api.ts DELETED
@@ -1,14 +0,0 @@
1
- // Public API Surface
2
- export { ShimmerComponent } from './shimmer.component';
3
- export {
4
- SHIMMER_CONFIG,
5
- provideShimmerConfig,
6
- injectShimmerConfig,
7
- shimmerDefaults,
8
- } from './shimmer-config.service';
9
- export type {
10
- ShimmerInputs,
11
- ShimmerConfig,
12
- ShimmerContextValue,
13
- ElementInfo,
14
- } from './types';
@@ -1,88 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { TestBed } from '@angular/core/testing';
3
- import {
4
- provideShimmerConfig,
5
- injectShimmerConfig,
6
- shimmerDefaults,
7
- SHIMMER_CONFIG,
8
- } from './shimmer-config.service';
9
-
10
- describe('shimmer-config.service', () => {
11
- describe('shimmerDefaults', () => {
12
- it('exports default values', () => {
13
- expect(shimmerDefaults).toBeDefined();
14
- expect(shimmerDefaults.shimmerColor).toBeDefined();
15
- expect(shimmerDefaults.backgroundColor).toBeDefined();
16
- expect(shimmerDefaults.duration).toBeDefined();
17
- expect(shimmerDefaults.fallbackBorderRadius).toBeDefined();
18
- });
19
- });
20
-
21
- describe('provideShimmerConfig', () => {
22
- it('creates a provider object', () => {
23
- const config = { shimmerColor: '#fff', duration: 2 };
24
- const provider = provideShimmerConfig(config);
25
-
26
- expect(provider.provide).toBe(SHIMMER_CONFIG);
27
- expect(provider.useValue).toBe(config);
28
- });
29
- });
30
-
31
- describe('injectShimmerConfig', () => {
32
- it('returns defaults when no provider is configured', () => {
33
- TestBed.configureTestingModule({});
34
-
35
- TestBed.runInInjectionContext(() => {
36
- const config = injectShimmerConfig();
37
-
38
- expect(config.shimmerColor).toBe(shimmerDefaults.shimmerColor);
39
- expect(config.backgroundColor).toBe(shimmerDefaults.backgroundColor);
40
- expect(config.duration).toBe(shimmerDefaults.duration);
41
- expect(config.fallbackBorderRadius).toBe(shimmerDefaults.fallbackBorderRadius);
42
- });
43
- });
44
-
45
- it('merges provided config with defaults', () => {
46
- TestBed.configureTestingModule({
47
- providers: [
48
- provideShimmerConfig({
49
- shimmerColor: 'rgba(255, 255, 255, 0.8)',
50
- duration: 3,
51
- }),
52
- ],
53
- });
54
-
55
- TestBed.runInInjectionContext(() => {
56
- const config = injectShimmerConfig();
57
-
58
- expect(config.shimmerColor).toBe('rgba(255, 255, 255, 0.8)');
59
- expect(config.duration).toBe(3);
60
- // Should use defaults for non-provided values
61
- expect(config.backgroundColor).toBe(shimmerDefaults.backgroundColor);
62
- expect(config.fallbackBorderRadius).toBe(shimmerDefaults.fallbackBorderRadius);
63
- });
64
- });
65
-
66
- it('returns fully resolved ShimmerContextValue', () => {
67
- TestBed.configureTestingModule({
68
- providers: [
69
- provideShimmerConfig({
70
- shimmerColor: '#aaa',
71
- backgroundColor: '#bbb',
72
- duration: 1.5,
73
- fallbackBorderRadius: 10,
74
- }),
75
- ],
76
- });
77
-
78
- TestBed.runInInjectionContext(() => {
79
- const config = injectShimmerConfig();
80
-
81
- expect(config.shimmerColor).toBe('#aaa');
82
- expect(config.backgroundColor).toBe('#bbb');
83
- expect(config.duration).toBe(1.5);
84
- expect(config.fallbackBorderRadius).toBe(10);
85
- });
86
- });
87
- });
88
- });
@@ -1,153 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { Component } from '@angular/core';
3
- import { TestBed, ComponentFixture, fakeAsync, tick } from '@angular/core/testing';
4
- import { ShimmerComponent } from './shimmer.component';
5
- import { provideShimmerConfig, SHIMMER_CONFIG } from './shimmer-config.service';
6
-
7
- // Test wrapper component for content projection
8
- @Component({
9
- selector: 'test-host',
10
- standalone: true,
11
- imports: [ShimmerComponent],
12
- template: `
13
- <shimmer [loading]="loading">
14
- <div class="test-content" style="width: 100px; height: 50px;">Content</div>
15
- </shimmer>
16
- `,
17
- })
18
- class TestHostComponent {
19
- loading = true;
20
- }
21
-
22
- describe('ShimmerComponent', () => {
23
- let fixture: ComponentFixture<TestHostComponent>;
24
- let hostComponent: TestHostComponent;
25
-
26
- beforeEach(async () => {
27
- vi.useFakeTimers();
28
-
29
- await TestBed.configureTestingModule({
30
- imports: [TestHostComponent, ShimmerComponent],
31
- }).compileComponents();
32
-
33
- fixture = TestBed.createComponent(TestHostComponent);
34
- hostComponent = fixture.componentInstance;
35
- });
36
-
37
- it('renders children normally when loading=false', fakeAsync(() => {
38
- hostComponent.loading = false;
39
- fixture.detectChanges();
40
- tick();
41
-
42
- const content = fixture.nativeElement.querySelector('.test-content');
43
- expect(content).toBeTruthy();
44
- expect(content.textContent).toBe('Content');
45
-
46
- // Should not have the measure container when not loading
47
- const measureContainer = fixture.nativeElement.querySelector('.shimmer-measure-container');
48
- expect(measureContainer).toBeFalsy();
49
- }));
50
-
51
- it('renders shimmer structure when loading=true', fakeAsync(() => {
52
- hostComponent.loading = true;
53
- fixture.detectChanges();
54
- tick();
55
-
56
- // Should render the measure container
57
- const measureContainer = fixture.nativeElement.querySelector('.shimmer-measure-container');
58
- expect(measureContainer).toBeTruthy();
59
- }));
60
-
61
- it('applies transparent text style to measure container', fakeAsync(() => {
62
- hostComponent.loading = true;
63
- fixture.detectChanges();
64
- tick();
65
-
66
- // Check that the measure container has the class
67
- const measureContainer = fixture.nativeElement.querySelector('.shimmer-measure-container');
68
- expect(measureContainer).toBeTruthy();
69
- expect(measureContainer.classList.contains('shimmer-measure-container')).toBe(true);
70
- }));
71
- });
72
-
73
- describe('ShimmerComponent with config provider', () => {
74
- let fixture: ComponentFixture<TestHostComponent>;
75
-
76
- beforeEach(async () => {
77
- vi.useFakeTimers();
78
-
79
- await TestBed.configureTestingModule({
80
- imports: [TestHostComponent, ShimmerComponent],
81
- providers: [
82
- provideShimmerConfig({
83
- shimmerColor: 'rgba(255, 0, 0, 0.5)',
84
- backgroundColor: '#ff0000',
85
- duration: 2,
86
- fallbackBorderRadius: 8,
87
- }),
88
- ],
89
- }).compileComponents();
90
-
91
- fixture = TestBed.createComponent(TestHostComponent);
92
- });
93
-
94
- it('uses provided config values', fakeAsync(() => {
95
- fixture.componentInstance.loading = true;
96
- fixture.detectChanges();
97
- tick();
98
-
99
- // The config should be injected - we can verify by checking the SHIMMER_CONFIG token
100
- const config = TestBed.inject(SHIMMER_CONFIG);
101
- expect(config.shimmerColor).toBe('rgba(255, 0, 0, 0.5)');
102
- expect(config.backgroundColor).toBe('#ff0000');
103
- expect(config.duration).toBe(2);
104
- expect(config.fallbackBorderRadius).toBe(8);
105
- }));
106
- });
107
-
108
- describe('ShimmerComponent input overrides', () => {
109
- @Component({
110
- selector: 'test-override-host',
111
- standalone: true,
112
- imports: [ShimmerComponent],
113
- template: `
114
- <shimmer
115
- [loading]="true"
116
- [shimmerColor]="'#00ff00'"
117
- [backgroundColor]="'#0000ff'"
118
- [duration]="3"
119
- [fallbackBorderRadius]="12"
120
- >
121
- <div>Content</div>
122
- </shimmer>
123
- `,
124
- })
125
- class TestOverrideHostComponent { }
126
-
127
- beforeEach(async () => {
128
- vi.useFakeTimers();
129
-
130
- await TestBed.configureTestingModule({
131
- imports: [TestOverrideHostComponent, ShimmerComponent],
132
- providers: [
133
- provideShimmerConfig({
134
- shimmerColor: 'rgba(255, 0, 0, 0.5)',
135
- duration: 1,
136
- }),
137
- ],
138
- }).compileComponents();
139
- });
140
-
141
- it('component inputs override provider config', fakeAsync(() => {
142
- const fixture = TestBed.createComponent(TestOverrideHostComponent);
143
- fixture.detectChanges();
144
- tick();
145
-
146
- // The shimmer component should use input values over config
147
- const shimmer = fixture.debugElement.children[0].componentInstance as ShimmerComponent;
148
- expect(shimmer.resolvedShimmerColor()).toBe('#00ff00');
149
- expect(shimmer.resolvedBackgroundColor()).toBe('#0000ff');
150
- expect(shimmer.resolvedDuration()).toBe(3);
151
- expect(shimmer.resolvedFallbackBorderRadius()).toBe(12);
152
- }));
153
- });
@@ -1,264 +0,0 @@
1
- import {
2
- Component,
3
- input,
4
- signal,
5
- computed,
6
- effect,
7
- ElementRef,
8
- viewChild,
9
- AfterViewInit,
10
- OnDestroy,
11
- ChangeDetectionStrategy,
12
- ViewEncapsulation,
13
- } from '@angular/core';
14
- import { NgIf, NgFor } from '@angular/common';
15
- import {
16
- extractElementInfo,
17
- createResizeObserver,
18
- type ElementInfo,
19
- } from '@shimmer-from-structure/core';
20
- import { injectShimmerConfig } from './shimmer-config.service';
21
-
22
- /**
23
- * Shimmer component that creates loading skeleton overlays based on content structure.
24
- * Automatically measures projected content and creates matching shimmer blocks.
25
- *
26
- * @example
27
- * ```html
28
- * <shimmer [loading]="isLoading">
29
- * <div class="card">
30
- * <h2>{{ title }}</h2>
31
- * <p>{{ description }}</p>
32
- * </div>
33
- * </shimmer>
34
- * ```
35
- */
36
- @Component({
37
- selector: 'shimmer',
38
- standalone: true,
39
- imports: [NgIf, NgFor],
40
- changeDetection: ChangeDetectionStrategy.OnPush,
41
- encapsulation: ViewEncapsulation.None,
42
- template: `
43
- <div style="position: relative;">
44
- <!-- Always render content -->
45
- <div
46
- #measureContainer
47
- [class.shimmer-measure-container]="loading()"
48
- [attr.aria-hidden]="loading() ? 'true' : null"
49
- [style.pointer-events]="loading() ? 'none' : null"
50
- >
51
- <ng-content></ng-content>
52
- </div>
53
-
54
- <!-- Shimmer overlay - only when loading -->
55
- @if (loading()) {
56
- <div
57
- style="
58
- position: absolute;
59
- top: 0;
60
- left: 0;
61
- right: 0;
62
- bottom: 0;
63
- overflow: hidden;
64
- pointer-events: none;
65
- "
66
- >
67
- @for (element of elements(); track $index) {
68
- <div
69
- [style.position]="'absolute'"
70
- [style.left.px]="element.x"
71
- [style.top.px]="element.y"
72
- [style.width.px]="element.width"
73
- [style.height.px]="element.height"
74
- [style.backgroundColor]="resolvedBackgroundColor()"
75
- [style.borderRadius]="
76
- element.borderRadius === '0px'
77
- ? resolvedFallbackBorderRadius() + 'px'
78
- : element.borderRadius
79
- "
80
- [style.overflow]="'hidden'"
81
- >
82
- <div
83
- class="shimmer-animation-element"
84
- [style.background]="
85
- 'linear-gradient(90deg, transparent, ' + resolvedShimmerColor() + ', transparent)'
86
- "
87
- [style.animationDuration]="resolvedDuration() + 's'"
88
- ></div>
89
- </div>
90
- }
91
- </div>
92
- }
93
- </div>
94
- `,
95
- styles: [
96
- `
97
- :host {
98
- display: contents;
99
- }
100
-
101
- .shimmer-measure-container * {
102
- color: transparent !important;
103
- }
104
-
105
- .shimmer-measure-container img,
106
- .shimmer-measure-container svg,
107
- .shimmer-measure-container video {
108
- opacity: 0;
109
- }
110
-
111
- .shimmer-animation-element {
112
- position: absolute;
113
- top: 0;
114
- left: 0;
115
- width: 100%;
116
- height: 100%;
117
- animation: shimmer-animation 1.5s infinite;
118
- }
119
-
120
- @keyframes shimmer-animation {
121
- 0% {
122
- transform: translateX(-100%);
123
- }
124
- 100% {
125
- transform: translateX(100%);
126
- }
127
- }
128
- `,
129
- ],
130
- })
131
- export class ShimmerComponent implements AfterViewInit, OnDestroy {
132
- // Inputs using Angular signals
133
- loading = input<boolean>(true);
134
- shimmerColor = input<string | undefined>(undefined);
135
- backgroundColor = input<string | undefined>(undefined);
136
- duration = input<number | undefined>(undefined);
137
- fallbackBorderRadius = input<number | undefined>(undefined);
138
-
139
- // View child reference
140
- measureContainer = viewChild<ElementRef<HTMLDivElement>>('measureContainer');
141
-
142
- // Internal state
143
- elements = signal<ElementInfo[]>([]);
144
-
145
- // Inject global config
146
- private contextConfig = injectShimmerConfig();
147
-
148
- // Resolved values (props > context > defaults)
149
- resolvedShimmerColor = computed(() => this.shimmerColor() ?? this.contextConfig.shimmerColor);
150
- resolvedBackgroundColor = computed(
151
- () => this.backgroundColor() ?? this.contextConfig.backgroundColor
152
- );
153
- resolvedDuration = computed(() => this.duration() ?? this.contextConfig.duration);
154
- resolvedFallbackBorderRadius = computed(
155
- () => this.fallbackBorderRadius() ?? this.contextConfig.fallbackBorderRadius
156
- );
157
-
158
- // Cleanup function for ResizeObserver
159
- private resizeCleanup: (() => void) | undefined;
160
- private mutationObserver: MutationObserver | undefined;
161
-
162
- constructor() {
163
- // Effect to re-measure when loading state changes
164
- effect((onCleanup) => {
165
- const isLoading = this.loading();
166
- const container = this.measureContainer();
167
-
168
- if (isLoading && container) {
169
- // Clean up existing observers before setting up new ones
170
- this.cleanup();
171
-
172
- // Set up observers for this loading session
173
- this.setupObservers();
174
-
175
- // Defer measurement to next frame to ensure content is rendered
176
- requestAnimationFrame(() => this.measureElements());
177
- } else {
178
- // Cleanup when not loading
179
- this.cleanup();
180
- }
181
-
182
- // Cleanup on effect re-run or component destruction
183
- onCleanup(() => {
184
- this.cleanup();
185
- });
186
- });
187
- }
188
-
189
- ngAfterViewInit(): void {
190
- // Effect will handle setup when container becomes available
191
- }
192
-
193
- ngOnDestroy(): void {
194
- this.cleanup();
195
- }
196
-
197
- private setupObservers(): void {
198
- const container = this.measureContainer()?.nativeElement;
199
- if (!container) return;
200
-
201
- // Set up ResizeObserver
202
- this.resizeCleanup = createResizeObserver(container, () => this.measureElements());
203
-
204
- // Set up MutationObserver for content changes
205
- this.mutationObserver = new MutationObserver(() => {
206
- if (this.loading()) {
207
- this.measureElements();
208
- }
209
- });
210
-
211
- this.mutationObserver.observe(container, {
212
- childList: true,
213
- subtree: true,
214
- characterData: true,
215
- attributes: false,
216
- });
217
- }
218
-
219
- private measureElements(): void {
220
- const container = this.measureContainer()?.nativeElement;
221
- if (!container || !this.loading()) return;
222
-
223
- // Temporarily disconnect mutation observer to avoid recursion
224
- this.mutationObserver?.disconnect();
225
-
226
- const containerRect = container.getBoundingClientRect();
227
- const extractedElements: ElementInfo[] = [];
228
-
229
- Array.from(container.children).forEach((child) => {
230
- extractedElements.push(...extractElementInfo(child, containerRect));
231
- });
232
-
233
- this.elements.set(extractedElements);
234
-
235
- // Reconnect mutation observer
236
- if (this.mutationObserver && container) {
237
- this.mutationObserver.observe(container, {
238
- childList: true,
239
- subtree: true,
240
- characterData: true,
241
- attributes: false,
242
- });
243
- }
244
- }
245
-
246
- private cleanup(): void {
247
- if (this.resizeCleanup) {
248
- this.resizeCleanup();
249
- this.resizeCleanup = undefined;
250
- }
251
- if (this.mutationObserver) {
252
- this.mutationObserver.disconnect();
253
- this.mutationObserver = undefined;
254
- }
255
- }
256
-
257
- /**
258
- * Manually trigger re-measurement of elements.
259
- * Useful when content changes programmatically.
260
- */
261
- remeasure(): void {
262
- this.measureElements();
263
- }
264
- }
package/src/test/setup.ts DELETED
@@ -1,42 +0,0 @@
1
- import 'zone.js';
2
- import { vi } from 'vitest';
3
-
4
- // Mock getBoundingClientRect since jsdom doesn't support layout
5
- Element.prototype.getBoundingClientRect = vi.fn(() => ({
6
- width: 100,
7
- height: 50,
8
- top: 0,
9
- left: 0,
10
- bottom: 50,
11
- right: 100,
12
- x: 0,
13
- y: 0,
14
- toJSON: () => { },
15
- }));
16
-
17
- // Mock ResizeObserver
18
- global.ResizeObserver = class ResizeObserver {
19
- observe() { }
20
- unobserve() { }
21
- disconnect() { }
22
- };
23
-
24
- // Mock getComputedStyle
25
- const originalGetComputedStyle = window.getComputedStyle;
26
- window.getComputedStyle = (elt) => {
27
- const styles = originalGetComputedStyle(elt);
28
- // Add borderRadius support for our tests
29
- if (!styles.borderRadius) {
30
- Object.defineProperty(styles, 'borderRadius', {
31
- value: '4px',
32
- writable: true,
33
- });
34
- }
35
- return styles;
36
- };
37
-
38
- // Mock requestAnimationFrame
39
- global.requestAnimationFrame = (cb) => {
40
- setTimeout(cb, 0);
41
- return 0;
42
- };
package/tsconfig.json DELETED
@@ -1,26 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.base.json",
3
- "compilerOptions": {
4
- "outDir": "./dist",
5
- "declaration": true,
6
- "declarationMap": true,
7
- "experimentalDecorators": true,
8
- "emitDecoratorMetadata": true,
9
- "target": "ES2022",
10
- "module": "ES2022",
11
- "moduleResolution": "bundler",
12
- "useDefineForClassFields": false,
13
- "lib": [
14
- "ES2022",
15
- "DOM"
16
- ]
17
- },
18
- "include": [
19
- "src/**/*.ts"
20
- ],
21
- "exclude": [
22
- "src/**/*.test.ts",
23
- "node_modules",
24
- "dist"
25
- ]
26
- }
package/vitest.config.ts DELETED
@@ -1,12 +0,0 @@
1
- /// <reference types="vitest" />
2
- import { defineConfig } from 'vitest/config';
3
-
4
- export default defineConfig({
5
- test: {
6
- globals: true,
7
- environment: 'jsdom',
8
- setupFiles: './src/test/setup.ts',
9
- include: ['src/**/*.test.ts'],
10
- css: true,
11
- },
12
- });