Skip to content

Commit ba1874d

Browse files
committed
feat(template): add runtime enable/disable support for VirtualView
Introduced runtime support for enabling/disabling `RxVirtualView`, including new configuration options (`enabled`, `enableAfterHydration`) and reactive signals. Improved platform detection with `PLATFORM` token, updated resize observer and visibility logic for server/browser context compatibility, and adjusted content/placeholder lifecycle handling.
1 parent d892044 commit ba1874d

4 files changed

Lines changed: 173 additions & 69 deletions

File tree

libs/template/virtual-view/src/lib/resize-observer.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { DestroyRef, inject, Injectable } from '@angular/core';
2-
import { Observable, ReplaySubject, Subject } from 'rxjs';
2+
import { EMPTY, Observable, ReplaySubject, Subject } from 'rxjs';
33
import { distinctUntilChanged, finalize } from 'rxjs/operators';
4+
import { PLATFORM } from './util';
45

56
/**
67
* A service that observes the resize of the elements.
@@ -10,35 +11,45 @@ import { distinctUntilChanged, finalize } from 'rxjs/operators';
1011
@Injectable()
1112
export class RxaResizeObserver {
1213
#destroyRef = inject(DestroyRef);
13-
#resizeObserver = new ResizeObserver((entries) => {
14-
entries.forEach((entry) => {
15-
if (this.#elements.has(entry.target))
16-
this.#elements.get(entry.target)!.next(entry);
17-
});
18-
});
14+
#platform = inject(PLATFORM);
15+
16+
#resizeObserver: ResizeObserver | null = null;
1917

2018
/** @internal */
2119
#elements = new Map<Element, Subject<ResizeObserverEntry>>();
2220

2321
constructor() {
22+
if (this.#platform.isBrowser) {
23+
this.#resizeObserver = new ResizeObserver((entries) => {
24+
entries.forEach((entry) => {
25+
if (this.#elements.has(entry.target))
26+
this.#elements.get(entry.target)!.next(entry);
27+
});
28+
});
29+
}
30+
2431
this.#destroyRef.onDestroy(() => {
2532
this.#elements.clear();
26-
this.#resizeObserver.disconnect();
33+
this.#resizeObserver?.disconnect();
2734
});
2835
}
2936

3037
observeElement(
3138
element: Element,
3239
options?: ResizeObserverOptions,
3340
): Observable<ResizeObserverEntry> {
41+
if (!this.#resizeObserver) {
42+
return EMPTY;
43+
}
44+
3445
const resizeEvent$ = new ReplaySubject<ResizeObserverEntry>(1);
3546
this.#elements.set(element, resizeEvent$);
3647
this.#resizeObserver.observe(element, options);
3748

3849
return resizeEvent$.pipe(
3950
distinctUntilChanged(),
4051
finalize(() => {
41-
this.#resizeObserver.unobserve(element);
52+
this.#resizeObserver?.unobserve(element);
4253
this.#elements.delete(element);
4354
}),
4455
);

libs/template/virtual-view/src/lib/virtual-view-observer.directive.ts

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@ import {
1111
BehaviorSubject,
1212
combineLatest,
1313
Observable,
14+
of,
1415
ReplaySubject,
1516
Subject,
1617
} from 'rxjs';
1718
import { distinctUntilChanged, finalize, map } from 'rxjs/operators';
1819
import { _RxVirtualViewObserver } from './model';
1920
import { RxaResizeObserver } from './resize-observer';
21+
import { PLATFORM } from './util';
2022
import { VirtualViewCache } from './virtual-view-cache';
2123

2224
/**
@@ -50,6 +52,7 @@ export class RxVirtualViewObserver
5052
extends _RxVirtualViewObserver
5153
implements OnInit, OnDestroy
5254
{
55+
#platform = inject(PLATFORM);
5356
#elementRef = inject<ElementRef<HTMLElement>>(ElementRef);
5457

5558
#observer: IntersectionObserver | null = null;
@@ -97,19 +100,21 @@ export class RxVirtualViewObserver
97100
#forcedHidden$ = new BehaviorSubject(false);
98101

99102
ngOnInit(): void {
100-
this.#observer = new IntersectionObserver(
101-
(entries) => {
102-
entries.forEach((entry) => {
103-
if (this.#elements.has(entry.target))
104-
this.#elements.get(entry.target)?.next(entry.isIntersecting);
105-
});
106-
},
107-
{
108-
root: this.#rootElement(),
109-
rootMargin: this.rootMargin(),
110-
threshold: this.threshold(),
111-
},
112-
);
103+
if (this.#platform.isBrowser) {
104+
this.#observer = new IntersectionObserver(
105+
(entries) => {
106+
entries.forEach((entry) => {
107+
if (this.#elements.has(entry.target))
108+
this.#elements.get(entry.target)?.next(entry.isIntersecting);
109+
});
110+
},
111+
{
112+
root: this.#rootElement(),
113+
rootMargin: this.rootMargin(),
114+
threshold: this.threshold(),
115+
},
116+
);
117+
}
113118
}
114119

115120
ngOnDestroy() {
@@ -143,6 +148,10 @@ export class RxVirtualViewObserver
143148
}
144149

145150
observeElementVisibility(virtualView: HTMLElement) {
151+
if (this.#platform.isServer) {
152+
return of(true);
153+
}
154+
146155
const isVisible$ = new ReplaySubject<boolean>(1);
147156

148157
// Store the view and the visibility state in the map.

libs/template/virtual-view/src/lib/virtual-view.config.ts

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { InjectionToken, Provider } from '@angular/core';
1+
import { InjectionToken, Provider, Signal } from '@angular/core';
22
import { RxStrategyNames } from '@rx-angular/cdk/render-strategies';
33

44
export const VIRTUAL_VIEW_CONFIG_TOKEN =
@@ -8,13 +8,23 @@ export const VIRTUAL_VIEW_CONFIG_TOKEN =
88
});
99

1010
export interface RxVirtualViewConfig {
11+
enabled: boolean | Signal<boolean>;
1112
keepLastKnownSize: boolean;
1213
useContentVisibility: boolean;
1314
useContainment: boolean;
1415
placeholderStrategy: RxStrategyNames<string>;
1516
contentStrategy: RxStrategyNames<string>;
1617
cacheEnabled: boolean;
1718
startWithPlaceholderAsap: boolean;
19+
20+
/**
21+
* Whether to enable the visibility after hydration. (DEFAULT: true)
22+
*
23+
* If `false`, the elements that were hydrated, won't go back to render placeholder anymore on hydration.
24+
* You can disable this to avoid destroying components that were just hydrated.
25+
*/
26+
enableAfterHydration: boolean;
27+
1828
cache: {
1929
/**
2030
* The maximum number of contents that can be stored in the cache.
@@ -31,13 +41,16 @@ export interface RxVirtualViewConfig {
3141
}
3242

3343
export const VIRTUAL_VIEW_CONFIG_DEFAULT: RxVirtualViewConfig = {
44+
enabled: true,
3445
keepLastKnownSize: false,
3546
useContentVisibility: false,
3647
useContainment: true,
3748
placeholderStrategy: 'low',
3849
contentStrategy: 'normal',
3950
startWithPlaceholderAsap: false,
4051
cacheEnabled: true,
52+
enableAfterHydration: true,
53+
4154
cache: {
4255
contentCacheSize: 20,
4356
placeholderCacheSize: 20,
@@ -74,10 +87,25 @@ export const VIRTUAL_VIEW_CONFIG_DEFAULT: RxVirtualViewConfig = {
7487
* @returns An object that can be provided to the `VirtualView` service.
7588
*/
7689
export function provideVirtualViewConfig(
77-
config: Partial<
78-
RxVirtualViewConfig & { cache?: Partial<RxVirtualViewConfig['cache']> }
79-
>,
90+
config: VVConfig | (() => VVConfig),
8091
): Provider {
92+
if (typeof config === 'function') {
93+
return {
94+
provide: VIRTUAL_VIEW_CONFIG_TOKEN,
95+
useFactory: () => {
96+
const cfg = config();
97+
return {
98+
...VIRTUAL_VIEW_CONFIG_DEFAULT,
99+
...cfg,
100+
cache: {
101+
...VIRTUAL_VIEW_CONFIG_DEFAULT.cache,
102+
...(cfg?.cache ?? {}),
103+
},
104+
};
105+
},
106+
} satisfies Provider;
107+
}
108+
81109
return {
82110
provide: VIRTUAL_VIEW_CONFIG_TOKEN,
83111
useValue: {
@@ -87,3 +115,7 @@ export function provideVirtualViewConfig(
87115
},
88116
} satisfies Provider;
89117
}
118+
119+
type VVConfig = Partial<
120+
RxVirtualViewConfig & { cache?: Partial<RxVirtualViewConfig['cache']> }
121+
>;

0 commit comments

Comments
 (0)