/
githubmirror
/
angular
Обзор
Документация
Войти
/
githubmirror
/
angular
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
packages/core/src/defer/dom_triggers.ts
210 строк
7 KB
Kristiyan Kostadinov
fix(core): do not register dom triggers when defer blocks are in manual mode
27 май 2026, 20:53
27 май 2026, 20:53
abc61aa
Код
Авторство
О чём код?
/*! * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.dev/license */ import type {Injector} from '../di'; import {AfterRenderRef} from '../render3/after_render/api'; import {afterEveryRender} from '../render3/after_render/hooks'; import {assertLContainer, assertLView} from '../render3/assert'; import {CONTAINER_HEADER_OFFSET} from '../render3/interfaces/container'; import {TNode} from '../render3/interfaces/node'; import {isDestroyed} from '../render3/interfaces/type_checks'; import {HEADER_OFFSET, INJECTOR, LView} from '../render3/interfaces/view'; import { getNativeByIndex, removeLViewOnDestroy, storeLViewOnDestroy, walkUpViews, } from '../render3/util/view_utils'; import {assertElement, assertEqual} from '../util/assert'; import {NgZone} from '../zone'; import {storeTriggerCleanupFn} from './cleanup'; import {onViewport, createIntersectionObserver} from '../../primitives/defer/src/triggers'; import { DEFER_BLOCK_STATE, DeferBlockInternalState, DeferBlockState, TriggerType, } from './interfaces'; import {getLDeferBlockDetails} from './utils'; import {shouldTriggerDeferBlock} from './rendering'; /** * Wrapper for onViewport trigger with angular specific Injector for resolving NgZone instance * and creating an IntersectionObserver which can run outside of Angular zone. * @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 onViewportWrapper( trigger: Element, callback: VoidFunction, injector: Injector, wrapperOptions?: IntersectionObserverInit, ) { const ngZone = injector.get(NgZone); return onViewport( trigger, () => ngZone.run(callback), (options) => ngZone.runOutsideAngular(() => createIntersectionObserver(options)), wrapperOptions, ); } /** * Helper function to get the LView in which a deferred block's trigger is rendered. * @param deferredHostLView LView in which the deferred block is defined. * @param deferredTNode TNode defining the deferred block. * @param walkUpTimes Number of times to go up in the view hierarchy to find the trigger's view. * A negative value means that the trigger is inside the block's placeholder, while an undefined * value means that the trigger is in the same LView as the deferred block. */ export function getTriggerLView( deferredHostLView: LView, deferredTNode: TNode, walkUpTimes: number | undefined | null, ): LView | null { // The trigger is in the same view, we don't need to traverse. if (walkUpTimes == null) { return deferredHostLView; } // A positive value or zero means that the trigger is in a parent view. if (walkUpTimes >= 0) { return walkUpViews(walkUpTimes, deferredHostLView); } // If the value is negative, it means that the trigger is inside the placeholder. const deferredContainer = deferredHostLView[deferredTNode.index]; ngDevMode && assertLContainer(deferredContainer); const triggerLView = deferredContainer[CONTAINER_HEADER_OFFSET] ?? null; // We need to null check, because the placeholder might not have been rendered yet. if (ngDevMode && triggerLView !== null) { const lDetails = getLDeferBlockDetails(deferredHostLView, deferredTNode); const renderedState = lDetails[DEFER_BLOCK_STATE]; assertEqual( renderedState, DeferBlockState.Placeholder, 'Expected a placeholder to be rendered in this defer block.', ); assertLView(triggerLView); } return triggerLView; } /** * Gets the element that a deferred block's trigger is pointing to. * @param triggerLView LView in which the trigger is defined. * @param triggerIndex Index at which the trigger element should've been rendered. */ export function getTriggerElement(triggerLView: LView, triggerIndex: number): Element { const element = getNativeByIndex(HEADER_OFFSET + triggerIndex, triggerLView); ngDevMode && assertElement(element); return element as Element; } /** * Registers a DOM-node based trigger. * @param initialLView LView in which the defer block is rendered. * @param tNode TNode representing the defer block. * @param triggerIndex Index at which to find the trigger element. * @param walkUpTimes Number of times to go up/down in the view hierarchy to find the trigger. * @param registerFn Function that will register the DOM events. * @param callback Callback to be invoked when the trigger receives the event that should render * the deferred block. * @param type Trigger type to distinguish between regular and prefetch triggers. */ export function registerDomTrigger<O>( initialLView: LView, tNode: TNode, triggerIndex: number, walkUpTimes: number | undefined | null, registerFn: ( element: Element, callback: VoidFunction, injector: Injector, options?: O, ) => VoidFunction, callback: VoidFunction, type: TriggerType, options?: O, ) { if (!shouldTriggerDeferBlock(type, initialLView)) { return; } const injector = initialLView[INJECTOR]; const zone = injector.get(NgZone); let poll: AfterRenderRef; function pollDomTrigger() { // If the initial view was destroyed, we don't need to do anything. if (isDestroyed(initialLView)) { poll.destroy(); return; } const lDetails = getLDeferBlockDetails(initialLView, tNode); const renderedState = lDetails[DEFER_BLOCK_STATE]; // If the block was loaded before the trigger was resolved, we don't need to do anything. if ( renderedState !== DeferBlockInternalState.Initial && renderedState !== DeferBlockState.Placeholder ) { poll.destroy(); return; } const triggerLView = getTriggerLView(initialLView, tNode, walkUpTimes); // Keep polling until we resolve the trigger's LView. if (!triggerLView) { // Keep polling. return; } poll.destroy(); // It's possible that the trigger's view was destroyed before we resolved the trigger element. if (isDestroyed(triggerLView)) { return; } const element = getTriggerElement(triggerLView, triggerIndex); const cleanup = registerFn( element, () => { // `pollDomTrigger` runs outside the zone (because of `afterNextRender`) and registers its // listeners outside the zone, so we jump back into the zone prior to running the callback. zone.run(() => { if (initialLView !== triggerLView) { removeLViewOnDestroy(triggerLView, cleanup); } callback(); }); }, injector, options, ); // The trigger and deferred block might be in different LViews. // For the main LView the cleanup would happen as a part of // `storeTriggerCleanupFn` logic. For trigger LView we register // a cleanup function there to remove event handlers in case an // LView gets destroyed before a trigger is invoked. if (initialLView !== triggerLView) { storeLViewOnDestroy(triggerLView, cleanup); } storeTriggerCleanupFn(type, lDetails, cleanup); } // Begin polling for the trigger. poll = afterEveryRender({read: pollDomTrigger}, {injector}); }