Skip to content

Commit

Permalink
feat(core): support deferred viewport triggers (#51874)
Browse files Browse the repository at this point in the history
Adds support for `on viewport` and `prefetch on viewport` triggers which will load the deferred content when the element comes into the view.

PR Close #51874
  • Loading branch information
crisbeto authored and dylhunn committed Sep 25, 2023
1 parent 687b961 commit 16f5fc4
Show file tree
Hide file tree
Showing 6 changed files with 468 additions and 14 deletions.
Expand Up @@ -9,7 +9,7 @@ function MyApp_Template(rf, ctx) {
$r3$.ɵɵdeferPrefetchOnTimer(1337);
$r3$.ɵɵdeferPrefetchOnHover(0, -1);
$r3$.ɵɵdeferPrefetchOnInteraction(0, -1);
$r3$.ɵɵdeferPrefetchOnViewport("button");
$r3$.ɵɵdeferPrefetchOnViewport(0, -1);
}
if (rf & 2) {
$r3$.ɵɵtextInterpolate1(" ", ctx.message, " ");
Expand Down
Expand Up @@ -8,7 +8,7 @@ function MyApp_Template(rf, ctx) {
$r3$.ɵɵdeferOnTimer(1337);
$r3$.ɵɵdeferOnHover(0, -1);
$r3$.ɵɵdeferOnInteraction(0, -1);
$r3$.ɵɵdeferOnViewport("button");
$r3$.ɵɵdeferOnViewport(0, -1);
}
if (rf & 2) {
$r3$.ɵɵtextInterpolate1(" ", ctx.message, " ");
Expand Down
13 changes: 6 additions & 7 deletions packages/compiler/src/render3/view/template.ts
Expand Up @@ -1416,18 +1416,17 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
prefetch ? R3.deferPrefetchOnInteraction : R3.deferOnInteraction);
}

// TODO(crisbeto): currently the reference is passed as a string.
// Update this once we figure out how we should refer to the target.
// `deferOnViewport(target)`
// `deferOnViewport(index, walkUpTimes)`
if (viewport) {
this.creationInstruction(
viewport.sourceSpan, prefetch ? R3.deferPrefetchOnViewport : R3.deferOnViewport,
[o.literal(viewport.reference)]);
this.domNodeBasedTrigger(
'viewport', viewport, metadata,
prefetch ? R3.deferPrefetchOnViewport : R3.deferOnViewport);
}
}

private domNodeBasedTrigger(
name: string, trigger: t.InteractionDeferredTrigger|t.HoverDeferredTrigger,
name: string,
trigger: t.InteractionDeferredTrigger|t.HoverDeferredTrigger|t.ViewportDeferredTrigger,
metadata: R3DeferBlockMetadata, instructionRef: o.ExternalReference) {
const triggerEl = metadata.triggerElements.get(trigger);

Expand Down
30 changes: 25 additions & 5 deletions packages/core/src/render3/instructions/defer.ts
Expand Up @@ -25,7 +25,7 @@ import {isPlatformBrowser} from '../util/misc_utils';
import {getConstant, getNativeByIndex, getTNode, removeLViewOnDestroy, storeLViewOnDestroy, walkUpViews} from '../util/view_utils';
import {addLViewToLContainer, createAndRenderEmbeddedLView, removeLViewFromLContainer, shouldAddViewToDom} from '../view_manipulation';

import {onHover, onInteraction} from './defer_events';
import {onHover, onInteraction, onViewport} from './defer_events';
import {ɵɵtemplate} from './template';

/**
Expand Down Expand Up @@ -299,17 +299,37 @@ export function ɵɵdeferPrefetchOnInteraction(triggerIndex: number, walkUpTimes

/**
* Creates runtime data structures for the `on viewport` deferred trigger.
* @param target Optional element on which to listen for hover events.
* @param triggerIndex Index at which to find the trigger element.
* @param walkUpTimes Number of times to walk up/down the tree hierarchy to find the trigger.
* @codeGenApi
*/
export function ɵɵdeferOnViewport(target?: unknown) {} // TODO: implement runtime logic.
export function ɵɵdeferOnViewport(triggerIndex: number, walkUpTimes?: number) {
const lView = getLView();
const tNode = getCurrentTNode()!;

renderPlaceholder(lView, tNode);
registerDomTrigger(
lView, tNode, triggerIndex, walkUpTimes, onViewport, () => triggerDeferBlock(lView, tNode));
}

/**
* Creates runtime data structures for the `prefetch on viewport` deferred trigger.
* @param target Optional element on which to listen for hover events.
* @param triggerIndex Index at which to find the trigger element.
* @param walkUpTimes Number of times to walk up/down the tree hierarchy to find the trigger.
* @codeGenApi
*/
export function ɵɵdeferPrefetchOnViewport(target?: unknown) {} // TODO: implement runtime logic.
export function ɵɵdeferPrefetchOnViewport(triggerIndex: number, walkUpTimes?: number) {
const lView = getLView();
const tNode = getCurrentTNode()!;
const tView = lView[TVIEW];
const tDetails = getTDeferBlockDetails(tView, tNode);

if (tDetails.loadingState === DeferDependenciesLoadingState.NOT_STARTED) {
registerDomTrigger(
lView, tNode, triggerIndex, walkUpTimes, onViewport,
() => triggerPrefetching(tDetails, lView));
}
}

/********** Helper functions **********/

Expand Down
77 changes: 77 additions & 0 deletions packages/core/src/render3/instructions/defer_events.ts
Expand Up @@ -6,6 +6,9 @@
* found in the LICENSE file at https://angular.io/license
*/

import {inject, Injector, ɵɵdefineInjectable} from '../../di';
import {NgZone} from '../../zone';

/** Configuration object used to register passive and capturing events. */
const eventListenerOptions: AddEventListenerOptions = {
passive: true,
Expand Down Expand Up @@ -104,3 +107,77 @@ export function onHover(trigger: Element, callback: VoidFunction): VoidFunction
}
};
}

/**
* Registers a viewport trigger.
* @param trigger Element that is the trigger.
* @param callback Callback to be invoked when the trigger comes into the viewport.
* @param injector Injector that can be used by the trigger to resolve DI tokens.
*/
export function onViewport(
trigger: Element, callback: VoidFunction, injector: Injector): VoidFunction {
return injector.get(DeferIntersectionManager).register(trigger, callback);
}

/** Keeps track of the registered `viewport` triggers. */
class DeferIntersectionManager {
/** @nocollapse */
static ɵprov = /** @pureOrBreakMyCode */ ɵɵdefineInjectable({
token: DeferIntersectionManager,
providedIn: 'root',
factory: () => new DeferIntersectionManager(inject(NgZone)),
});

/** `IntersectionObserver` used to observe `viewport` triggers. */
private intersectionObserver: IntersectionObserver|null = null;

/** Number of elements currently observed with `viewport` triggers. */
private observedViewportElements = 0;

/** Currently-registered `viewport` triggers. */
private viewportTriggers = new WeakMap<Element, DeferEventEntry>();

constructor(private ngZone: NgZone) {}

register(trigger: Element, callback: VoidFunction): VoidFunction {
let entry = this.viewportTriggers.get(trigger);

if (!this.intersectionObserver) {
this.intersectionObserver =
this.ngZone.runOutsideAngular(() => new IntersectionObserver(this.intersectionCallback));
}

if (!entry) {
entry = new DeferEventEntry();
this.ngZone.runOutsideAngular(() => this.intersectionObserver!.observe(trigger));
this.viewportTriggers.set(trigger, entry);
this.observedViewportElements++;
}

entry.callbacks.add(callback);

return () => {
entry!.callbacks.delete(callback);

if (entry!.callbacks.size === 0) {
this.intersectionObserver?.unobserve(trigger);
this.viewportTriggers.delete(trigger);
this.observedViewportElements--;
}

if (this.observedViewportElements === 0) {
this.intersectionObserver?.disconnect();
this.intersectionObserver = null;
}
};
}

private intersectionCallback: IntersectionObserverCallback = entries => {
for (const current of entries) {
// Only invoke the callbacks if the specific element is intersecting.
if (current.isIntersecting && this.viewportTriggers.has(current.target)) {
this.ngZone.run(this.viewportTriggers.get(current.target)!.listener);
}
}
}
}

0 comments on commit 16f5fc4

Please sign in to comment.