fix(ivy): adding event listeners for global objects (window, document, body) (#27772)

This update introduces support for global object (window, document, body) listeners, that can be defined via host listeners on Components and Directives.

PR Close #27772
This commit is contained in:
Andrew Kushnir
2018-12-19 15:03:47 -08:00
committed by Kara Erickson
parent 917c09cfc8
commit 6e7c46af1b
15 changed files with 373 additions and 149 deletions

View File

@ -144,6 +144,7 @@ export {
export {templateRefExtractor} from './view_engine_compatibility_prebound';
export {resolveWindow, resolveDocument, resolveBody} from './util';
// clang-format on

View File

@ -29,7 +29,7 @@ import {AttributeMarker, InitialInputData, InitialInputs, LocalRefExtractor, Pro
import {PlayerFactory} from './interfaces/player';
import {CssSelectorList, NG_PROJECT_AS_ATTR_NAME} from './interfaces/projection';
import {LQueries} from './interfaces/query';
import {ProceduralRenderer3, RComment, RElement, RText, Renderer3, RendererFactory3, isProceduralRenderer} from './interfaces/renderer';
import {GlobalTargetResolver, ProceduralRenderer3, RComment, RElement, RText, Renderer3, RendererFactory3, isProceduralRenderer} from './interfaces/renderer';
import {SanitizerFn} from './interfaces/sanitization';
import {BINDING_INDEX, CLEANUP, CONTAINER_INDEX, CONTENT_QUERIES, CONTEXT, DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, HOST_NODE, INJECTOR, LView, LViewFlags, NEXT, OpaqueViewState, PARENT, QUERIES, RENDERER, RENDERER_FACTORY, RootContext, RootContextFlags, SANITIZER, TAIL, TVIEW, TView} from './interfaces/view';
import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert';
@ -822,10 +822,13 @@ export function locateHostElement(
*
* @param eventName Name of the event
* @param listenerFn The function to be called when event emits
* @param useCapture Whether or not to use capture in event listener.
* @param useCapture Whether or not to use capture in event listener
* @param eventTargetResolver Function that returns global target information in case this listener
* should be attached to a global object like window, document or body
*/
export function listener(
eventName: string, listenerFn: (e?: any) => any, useCapture = false): void {
eventName: string, listenerFn: (e?: any) => any, useCapture = false,
eventTargetResolver?: GlobalTargetResolver): void {
const lView = getLView();
const tNode = getPreviousOrParentTNode();
const tView = lView[TVIEW];
@ -837,6 +840,8 @@ export function listener(
// add native event listener - applicable to elements only
if (tNode.type === TNodeType.Element) {
const native = getNativeByTNode(tNode, lView) as RElement;
const resolved = eventTargetResolver ? eventTargetResolver(native) : {} as any;
const target = resolved.target || native;
ngDevMode && ngDevMode.rendererAddEventListener++;
const renderer = lView[RENDERER];
const lCleanup = getCleanup(lView);
@ -846,15 +851,22 @@ export function listener(
// In order to match current behavior, native DOM event listeners must be added for all
// events (including outputs).
if (isProceduralRenderer(renderer)) {
const cleanupFn = renderer.listen(native, eventName, listenerFn);
// The first argument of `listen` function in Procedural Renderer is:
// - either a target name (as a string) in case of global target (window, document, body)
// - or element reference (in all other cases)
const cleanupFn = renderer.listen(resolved.name || target, eventName, listenerFn);
lCleanup.push(listenerFn, cleanupFn);
useCaptureOrSubIdx = lCleanupIndex + 1;
} else {
const wrappedListener = wrapListenerWithPreventDefault(listenerFn);
native.addEventListener(eventName, wrappedListener, useCapture);
target.addEventListener(eventName, wrappedListener, useCapture);
lCleanup.push(wrappedListener);
}
tCleanup && tCleanup.push(eventName, tNode.index, lCleanupIndex, useCaptureOrSubIdx);
const idxOrTargetGetter = eventTargetResolver ?
(_lView: LView) => eventTargetResolver(readElementValue(_lView[tNode.index])).target :
tNode.index;
tCleanup && tCleanup.push(eventName, idxOrTargetGetter, lCleanupIndex, useCaptureOrSubIdx);
}
// subscribe to directive outputs

View File

@ -26,6 +26,12 @@ export enum RendererStyleFlags3 {
export type Renderer3 = ObjectOrientedRenderer3 | ProceduralRenderer3;
export type GlobalTargetName = 'document' | 'window' | 'body';
export type GlobalTargetResolver = (element: any) => {
name: GlobalTargetName, target: EventTarget
};
/**
* Object Oriented style of API needed to create elements and text nodes.
*
@ -86,7 +92,9 @@ export interface ProceduralRenderer3 {
setValue(node: RText|RComment, value: string): void;
// TODO(misko): Deprecate in favor of addEventListener/removeEventListener
listen(target: RNode, eventName: string, callback: (event: any) => boolean | void): () => void;
listen(
target: GlobalTargetName|RNode, eventName: string,
callback: (event: any) => boolean | void): () => void;
}
export interface RendererFactory3 {

View File

@ -446,7 +446,11 @@ export interface TView {
*
* If it's a native DOM listener or output subscription being stored:
* 1st index is: event name `name = tView.cleanup[i+0]`
* 2nd index is: index of native element `element = lView[tView.cleanup[i+1]]`
* 2nd index is: index of native element or a function that retrieves global target (window,
* document or body) reference based on the native element:
* `typeof idxOrTargetGetter === 'function'`: global target getter function
* `typeof idxOrTargetGetter === 'number'`: index of native element
*
* 3rd index is: index of listener function `listener = lView[CLEANUP][tView.cleanup[i+2]]`
* 4th index is: `useCaptureOrIndx = tView.cleanup[i+3]`
* `typeof useCaptureOrIndx == 'boolean' : useCapture boolean

View File

@ -107,6 +107,9 @@ export const angularCoreEnv: {[name: string]: Function} = {
'ɵi18nEnd': r3.i18nEnd,
'ɵi18nApply': r3.i18nApply,
'ɵi18nPostprocess': r3.i18nPostprocess,
'ɵresolveWindow': r3.resolveWindow,
'ɵresolveDocument': r3.resolveDocument,
'ɵresolveBody': r3.resolveBody,
'ɵsanitizeHtml': sanitization.sanitizeHtml,
'ɵsanitizeStyle': sanitization.sanitizeStyle,

View File

@ -445,13 +445,15 @@ function removeListeners(lView: LView): void {
for (let i = 0; i < tCleanup.length - 1; i += 2) {
if (typeof tCleanup[i] === 'string') {
// This is a listener with the native renderer
const idx = tCleanup[i + 1];
const idxOrTargetGetter = tCleanup[i + 1];
const target = typeof idxOrTargetGetter === 'function' ?
idxOrTargetGetter(lView) :
readElementValue(lView[idxOrTargetGetter]);
const listener = lCleanup[tCleanup[i + 2]];
const native = readElementValue(lView[idx]);
const useCaptureOrSubIdx = tCleanup[i + 3];
if (typeof useCaptureOrSubIdx === 'boolean') {
// DOM listener
native.removeEventListener(tCleanup[i], listener, useCaptureOrSubIdx);
target.removeEventListener(tCleanup[i], listener, useCaptureOrSubIdx);
} else {
if (useCaptureOrSubIdx >= 0) {
// unregister

View File

@ -14,7 +14,7 @@ import {LContext, MONKEY_PATCH_KEY_NAME} from './interfaces/context';
import {ComponentDef, DirectiveDef} from './interfaces/definition';
import {NO_PARENT_INJECTOR, RelativeInjectorLocation, RelativeInjectorLocationFlags} from './interfaces/injector';
import {TContainerNode, TElementNode, TNode, TNodeFlags, TNodeType} from './interfaces/node';
import {RComment, RElement, RText} from './interfaces/renderer';
import {GlobalTargetName, GlobalTargetResolver, RComment, RElement, RText} from './interfaces/renderer';
import {StylingContext} from './interfaces/styling';
import {CONTEXT, DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, HOST_NODE, LView, LViewFlags, PARENT, RootContext, TData, TVIEW, TView} from './interfaces/view';
@ -276,3 +276,15 @@ export function findComponentView(lView: LView): LView {
return lView;
}
export function resolveWindow(element: RElement & {ownerDocument: Document}) {
return {name: 'window', target: element.ownerDocument.defaultView};
}
export function resolveDocument(element: RElement & {ownerDocument: Document}) {
return {name: 'document', target: element.ownerDocument};
}
export function resolveBody(element: RElement & {ownerDocument: Document}) {
return {name: 'body', target: element.ownerDocument.body};
}