From 86fcf518f1ff5464e732d73f116bdeefc8a22fdb Mon Sep 17 00:00:00 2001 From: Michael Berger Date: Sat, 21 Jan 2023 22:23:49 +0100 Subject: [PATCH] feat(rx): create rxRenderInViewPort directive --- libs/rx/platform/src/index.ts | 3 +- .../lib/directives/in-view.directive.spec.ts | 4 - .../src/lib/directives/in-view.directive.ts | 151 ------------------ .../rx-render-in-view-port.directive.spec.ts | 4 + .../rx-render-in-view-port.directive.ts | 99 ++++++++++++ 5 files changed, 104 insertions(+), 157 deletions(-) delete mode 100644 libs/rx/platform/src/lib/directives/in-view.directive.spec.ts delete mode 100644 libs/rx/platform/src/lib/directives/in-view.directive.ts create mode 100644 libs/rx/platform/src/lib/directives/rx-render-in-view-port.directive.spec.ts create mode 100644 libs/rx/platform/src/lib/directives/rx-render-in-view-port.directive.ts diff --git a/libs/rx/platform/src/index.ts b/libs/rx/platform/src/index.ts index be63575..dda36d2 100644 --- a/libs/rx/platform/src/index.ts +++ b/libs/rx/platform/src/index.ts @@ -4,5 +4,4 @@ export * from './lib/create-mutation-observer'; export * from './lib/directives/rx-observe-resize.directive'; export * from './lib/directives/observe-intersection.directive'; - -export * from './lib/directives/rx-observe-visibility.directive'; +export * from './lib/directives/observe-intersection.directive'; diff --git a/libs/rx/platform/src/lib/directives/in-view.directive.spec.ts b/libs/rx/platform/src/lib/directives/in-view.directive.spec.ts deleted file mode 100644 index 447da73..0000000 --- a/libs/rx/platform/src/lib/directives/in-view.directive.spec.ts +++ /dev/null @@ -1,4 +0,0 @@ -// todo -describe('InViewDirective', () => { - -}); diff --git a/libs/rx/platform/src/lib/directives/in-view.directive.ts b/libs/rx/platform/src/lib/directives/in-view.directive.ts deleted file mode 100644 index feb2f5f..0000000 --- a/libs/rx/platform/src/lib/directives/in-view.directive.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { - AfterViewInit, - Directive, - EmbeddedViewRef, - Input, - NgModule, - OnDestroy, - TemplateRef, - ViewContainerRef -} from '@angular/core'; -import {CommonModule} from '@angular/common'; -import {debounceTime, filter, Observable, Subject, Subscription} from "rxjs"; - -@Directive({ - selector: '[rxInView]', -}) -export class RxInViewDirective implements AfterViewInit, OnDestroy { - alreadyRendered: boolean = false; // cheking if visible already - embeddedViewRef: EmbeddedViewRef | null = null; - - private sub = new Subscription(); - @Input() inView: any = null; - @Input() inViewDebounce = 250; - @Input() inViewRootMargin = '0px'; - //@Input() inViewRoot: HTMLElement | undefined; - @Input() inViewThreshold: number | number[] = 0; - - constructor( - private vcRef: ViewContainerRef, - private tplRef: TemplateRef - ) {} - - ngAfterViewInit() { - const commentEl = this.vcRef.element.nativeElement; // template - const elToObserve = commentEl.parentElement; - const config = { - //root: this.intersectionRoot, - rootMargin: this.inViewRootMargin, - threshold: this.inViewThreshold, - }; - this.setMinWidthHeight(elToObserve); - - this.sub = fromIntersectionObserver(elToObserve, config, this.inViewDebounce) - .pipe() - .subscribe((status) => { - this.renderContents(status === 'Visible'); - }); - } - - ngOnDestroy() { - this.embeddedViewRef?.destroy(); - this.vcRef?.clear(); - this.sub.unsubscribe(); - } - - renderContents(isInView: boolean) { - if (isInView && !this.alreadyRendered) { - this.vcRef.clear(); - this.embeddedViewRef = this.vcRef.createEmbeddedView(this.tplRef); - this.embeddedViewRef.detectChanges(); - this.alreadyRendered = true; - } - } - - setMinWidthHeight(el: HTMLElement) { - // prevent issue being visible all together - const style = window.getComputedStyle(el); - const [width, height] = [parseInt(style.width), parseInt(style.height)]; - !width && (el.style.minWidth = '40px'); - !height && (el.style.minHeight = '40px'); - } -} - -@NgModule({ - imports: [CommonModule], - declarations: [RxInViewDirective], - exports: [RxInViewDirective], -}) -export class RxInViewDirectiveModule {} - -export enum IntersectionStatus { - Visible = 'Visible', - Pending = 'Pending', - NotVisible = 'NotVisible' -} - -export const fromIntersectionObserver = ( - element: HTMLElement, - config: IntersectionObserverInit, - debounce = 0 -) => - new Observable(subscriber => { - const subject$ = new Subject<{ - entry: IntersectionObserverEntry; - observer: IntersectionObserver; - }>(); - - const intersectionObserver = new IntersectionObserver( - (entries, observer) => { - entries.forEach(entry => { - /* if (isIntersecting(entry)) { - subject$.next({ entry, observer }); - }*/ - // with this we also get notified when element is hidden - subject$.next({ entry, observer }); - }); - }, - config - ); - - subject$.subscribe(() => { - subscriber.next(IntersectionStatus.Pending); - }); - - subject$ - .pipe( - debounceTime(debounce), - filter(Boolean) - ) - .subscribe(async ({ entry, observer }) => { - const isEntryVisible = await isVisible(entry.target as HTMLElement); - - if (isEntryVisible) { - subscriber.next(IntersectionStatus.Visible); - //observer.unobserve(entry.target); - } else { - subscriber.next(IntersectionStatus.NotVisible); - } - }); - - intersectionObserver.observe(element); - - return { - unsubscribe() { - intersectionObserver.disconnect(); - subject$.unsubscribe(); - } - }; - }); - -async function isVisible(element: HTMLElement) { - return new Promise(resolve => { - const observer = new IntersectionObserver(([entry]) => { - resolve(entry.isIntersecting); - //observer.disconnect(); - }); - - observer.observe(element); - }); -} - diff --git a/libs/rx/platform/src/lib/directives/rx-render-in-view-port.directive.spec.ts b/libs/rx/platform/src/lib/directives/rx-render-in-view-port.directive.spec.ts new file mode 100644 index 0000000..9b39865 --- /dev/null +++ b/libs/rx/platform/src/lib/directives/rx-render-in-view-port.directive.spec.ts @@ -0,0 +1,4 @@ +// todo +describe('RxRenderInViewDirective', () => { + +}); diff --git a/libs/rx/platform/src/lib/directives/rx-render-in-view-port.directive.ts b/libs/rx/platform/src/lib/directives/rx-render-in-view-port.directive.ts new file mode 100644 index 0000000..31fb22b --- /dev/null +++ b/libs/rx/platform/src/lib/directives/rx-render-in-view-port.directive.ts @@ -0,0 +1,99 @@ +import { + AfterViewInit, + Directive, + EmbeddedViewRef, + Input, + NgModule, + OnDestroy, + TemplateRef, + ViewContainerRef, +} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {combineLatest, mergeMap, SchedulerLike, startWith, Subscription} from 'rxjs'; +import {createSignal} from '../../../../signal/src'; +import {createIntersectionObserver} from '../create-intersection-observer'; + +@Directive({ + selector: '[rxRenderInViewport]', +}) +export class RxRenderInViewportDirective implements AfterViewInit, OnDestroy { + private alreadyRendered = false; // cheking if visible already + private embeddedViewRef: EmbeddedViewRef | null = null; + private sub = new Subscription(); + + private rxObserveVisibilityDebounceSignal = createSignal(0); + private rxObserveVisibilityRootMarginSignal = createSignal('0px'); + private rxObserveVisibilityRootSignal = createSignal(undefined); + private rxObserveVisibilityThresholdSignal = createSignal(0); + private rxObserveVisibilitySchedulerSignal = createSignal(undefined); + + @Input() set rxRenderInViewport(rootMargin: string) { + this.rxObserveVisibilityRootMarginSignal.send(rootMargin); + } + @Input() set rxRenderInViewportDebounce(debounceInMs: number) { + this.rxObserveVisibilityDebounceSignal.send(debounceInMs); + } + @Input() set rxRenderInViewportRoot(root: HTMLElement | undefined) { + this.rxObserveVisibilityRootSignal.send(root); + } + @Input() set rxRenderInViewportThreshold(threshold: number | number[]) { + this.rxObserveVisibilityThresholdSignal.send(threshold); + } + @Input() set rxRenderInViewportScheduler(scheduler: SchedulerLike) { + this.rxObserveVisibilitySchedulerSignal.send(scheduler); + } + + constructor(private vcRef: ViewContainerRef, private tplRef: TemplateRef) {} + + ngAfterViewInit() { + this.sub = combineLatest([ + this.rxObserveVisibilityDebounceSignal.$.pipe(startWith(0)), + this.rxObserveVisibilityRootMarginSignal.$.pipe(startWith('0px')), + this.rxObserveVisibilityRootSignal.$.pipe(startWith(undefined)), + this.rxObserveVisibilityThresholdSignal.$.pipe(startWith(0)), + this.rxObserveVisibilitySchedulerSignal.$.pipe(startWith(undefined)), + ]) + .pipe( + mergeMap(([debounceInMs, rootMargin, root, threshold, scheduler]) => + createIntersectionObserver( + this.vcRef.element.nativeElement.parentElement, + { + root: root, + rootMargin: rootMargin, + threshold: threshold, + }, + { + throttleMs: debounceInMs, + scheduler: scheduler, + } + ) + ) + ) + .subscribe((entries) => { + const entry = entries[0]; + this.renderContents(entry.isIntersecting); + }); + } + + ngOnDestroy() { + this.embeddedViewRef?.destroy(); + this.vcRef?.clear(); + this.sub.unsubscribe(); + } + + renderContents(isInView: boolean) { + if (isInView && !this.alreadyRendered) { + this.vcRef.clear(); + this.embeddedViewRef = this.vcRef.createEmbeddedView(this.tplRef); + this.embeddedViewRef.detectChanges(); + this.alreadyRendered = true; + } + } +} + +@NgModule({ + imports: [CommonModule], + declarations: [RxRenderInViewportDirective], + exports: [RxRenderInViewportDirective], +}) +export class RxRenderInViewportDirectiveModule {}