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:
parent
917c09cfc8
commit
6e7c46af1b
|
@ -588,11 +588,14 @@ describe('ngtsc behavioral tests', () => {
|
||||||
template: 'Test'
|
template: 'Test'
|
||||||
})
|
})
|
||||||
class FooCmp {
|
class FooCmp {
|
||||||
|
@HostListener('click')
|
||||||
|
onClick(event: any): void {}
|
||||||
|
|
||||||
@HostListener('document:click', ['$event.target'])
|
@HostListener('document:click', ['$event.target'])
|
||||||
onClick(eventTarget: HTMLElement): void {}
|
onDocumentClick(eventTarget: HTMLElement): void {}
|
||||||
|
|
||||||
@HostListener('window:scroll')
|
@HostListener('window:scroll')
|
||||||
onScroll(event: any): void {}
|
onWindowScroll(event: any): void {}
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
@ -601,14 +604,35 @@ describe('ngtsc behavioral tests', () => {
|
||||||
const hostBindingsFn = `
|
const hostBindingsFn = `
|
||||||
hostBindings: function FooCmp_HostBindings(rf, ctx, elIndex) {
|
hostBindings: function FooCmp_HostBindings(rf, ctx, elIndex) {
|
||||||
if (rf & 1) {
|
if (rf & 1) {
|
||||||
i0.ɵlistener("click", function FooCmp_click_HostBindingHandler($event) { return ctx.onClick($event.target); });
|
i0.ɵlistener("click", function FooCmp_click_HostBindingHandler($event) { return ctx.onClick(); });
|
||||||
i0.ɵlistener("scroll", function FooCmp_scroll_HostBindingHandler($event) { return ctx.onScroll(); });
|
i0.ɵlistener("click", function FooCmp_click_HostBindingHandler($event) { return ctx.onDocumentClick($event.target); }, false, i0.ɵresolveDocument);
|
||||||
|
i0.ɵlistener("scroll", function FooCmp_scroll_HostBindingHandler($event) { return ctx.onWindowScroll(); }, false, i0.ɵresolveWindow);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
expect(trim(jsContents)).toContain(trim(hostBindingsFn));
|
expect(trim(jsContents)).toContain(trim(hostBindingsFn));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should throw in case unknown global target is provided', () => {
|
||||||
|
env.tsconfig();
|
||||||
|
env.write(`test.ts`, `
|
||||||
|
import {Component, HostListener} from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'test',
|
||||||
|
template: 'Test'
|
||||||
|
})
|
||||||
|
class FooCmp {
|
||||||
|
@HostListener('UnknownTarget:click')
|
||||||
|
onClick(event: any): void {}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
const errors = env.driveDiagnostics();
|
||||||
|
expect(trim(errors[0].messageText as string))
|
||||||
|
.toContain(
|
||||||
|
`Unexpected global target 'UnknownTarget' defined for 'click' event. Supported list of global targets: window,document,body.`);
|
||||||
|
});
|
||||||
|
|
||||||
it('should generate host bindings for directives', () => {
|
it('should generate host bindings for directives', () => {
|
||||||
env.tsconfig();
|
env.tsconfig();
|
||||||
env.write(`test.ts`, `
|
env.write(`test.ts`, `
|
||||||
|
@ -620,6 +644,7 @@ describe('ngtsc behavioral tests', () => {
|
||||||
host: {
|
host: {
|
||||||
'[attr.hello]': 'foo',
|
'[attr.hello]': 'foo',
|
||||||
'(click)': 'onClick($event)',
|
'(click)': 'onClick($event)',
|
||||||
|
'(body:click)': 'onBodyClick($event)',
|
||||||
'[prop]': 'bar',
|
'[prop]': 'bar',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -641,6 +666,7 @@ describe('ngtsc behavioral tests', () => {
|
||||||
if (rf & 1) {
|
if (rf & 1) {
|
||||||
i0.ɵallocHostVars(2);
|
i0.ɵallocHostVars(2);
|
||||||
i0.ɵlistener("click", function FooCmp_click_HostBindingHandler($event) { return ctx.onClick($event); });
|
i0.ɵlistener("click", function FooCmp_click_HostBindingHandler($event) { return ctx.onClick($event); });
|
||||||
|
i0.ɵlistener("click", function FooCmp_click_HostBindingHandler($event) { return ctx.onBodyClick($event); }, false, i0.ɵresolveBody);
|
||||||
i0.ɵlistener("change", function FooCmp_change_HostBindingHandler($event) { return ctx.onChange(ctx.arg1, ctx.arg2, ctx.arg3); });
|
i0.ɵlistener("change", function FooCmp_change_HostBindingHandler($event) { return ctx.onChange(ctx.arg1, ctx.arg2, ctx.arg3); });
|
||||||
i0.ɵelementStyling(_c0, null, null, ctx);
|
i0.ɵelementStyling(_c0, null, null, ctx);
|
||||||
}
|
}
|
||||||
|
|
|
@ -131,6 +131,10 @@ export class Identifiers {
|
||||||
static templateRefExtractor:
|
static templateRefExtractor:
|
||||||
o.ExternalReference = {name: 'ɵtemplateRefExtractor', moduleName: CORE};
|
o.ExternalReference = {name: 'ɵtemplateRefExtractor', moduleName: CORE};
|
||||||
|
|
||||||
|
static resolveWindow: o.ExternalReference = {name: 'ɵresolveWindow', moduleName: CORE};
|
||||||
|
static resolveDocument: o.ExternalReference = {name: 'ɵresolveDocument', moduleName: CORE};
|
||||||
|
static resolveBody: o.ExternalReference = {name: 'ɵresolveBody', moduleName: CORE};
|
||||||
|
|
||||||
static defineBase: o.ExternalReference = {name: 'ɵdefineBase', moduleName: CORE};
|
static defineBase: o.ExternalReference = {name: 'ɵdefineBase', moduleName: CORE};
|
||||||
|
|
||||||
static BaseDef: o.ExternalReference = {
|
static BaseDef: o.ExternalReference = {
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
import {StaticSymbol} from '../../aot/static_symbol';
|
import {StaticSymbol} from '../../aot/static_symbol';
|
||||||
import {CompileDirectiveMetadata, CompileDirectiveSummary, CompileQueryMetadata, CompileTokenMetadata, identifierName, sanitizeIdentifier} from '../../compile_metadata';
|
import {CompileDirectiveMetadata, CompileDirectiveSummary, CompileQueryMetadata, CompileTokenMetadata, identifierName, sanitizeIdentifier} from '../../compile_metadata';
|
||||||
import {CompileReflector} from '../../compile_reflector';
|
import {CompileReflector} from '../../compile_reflector';
|
||||||
import {BindingForm, convertActionBinding, convertPropertyBinding} from '../../compiler_util/expression_converter';
|
import {BindingForm, convertPropertyBinding} from '../../compiler_util/expression_converter';
|
||||||
import {ConstantPool, DefinitionKind} from '../../constant_pool';
|
import {ConstantPool, DefinitionKind} from '../../constant_pool';
|
||||||
import * as core from '../../core';
|
import * as core from '../../core';
|
||||||
import {AST, ParsedEvent, ParsedEventType, ParsedProperty} from '../../expression_parser/ast';
|
import {AST, ParsedEvent, ParsedEventType, ParsedProperty} from '../../expression_parser/ast';
|
||||||
|
@ -22,14 +22,15 @@ import {ShadowCss} from '../../shadow_css';
|
||||||
import {CONTENT_ATTR, HOST_ATTR} from '../../style_compiler';
|
import {CONTENT_ATTR, HOST_ATTR} from '../../style_compiler';
|
||||||
import {BindingParser} from '../../template_parser/binding_parser';
|
import {BindingParser} from '../../template_parser/binding_parser';
|
||||||
import {OutputContext, error} from '../../util';
|
import {OutputContext, error} from '../../util';
|
||||||
|
import {BoundEvent} from '../r3_ast';
|
||||||
import {compileFactoryFunction, dependenciesFromGlobalMetadata} from '../r3_factory';
|
import {compileFactoryFunction, dependenciesFromGlobalMetadata} from '../r3_factory';
|
||||||
import {Identifiers as R3} from '../r3_identifiers';
|
import {Identifiers as R3} from '../r3_identifiers';
|
||||||
import {Render3ParseResult} from '../r3_template_transform';
|
import {Render3ParseResult} from '../r3_template_transform';
|
||||||
import {prepareSyntheticListenerFunctionName, prepareSyntheticListenerName, prepareSyntheticPropertyName, typeWithParameters} from '../util';
|
import {prepareSyntheticListenerFunctionName, prepareSyntheticPropertyName, typeWithParameters} from '../util';
|
||||||
|
|
||||||
import {R3ComponentDef, R3ComponentMetadata, R3DirectiveDef, R3DirectiveMetadata, R3QueryMetadata} from './api';
|
import {R3ComponentDef, R3ComponentMetadata, R3DirectiveDef, R3DirectiveMetadata, R3QueryMetadata} from './api';
|
||||||
import {StylingBuilder, StylingInstruction} from './styling_builder';
|
import {StylingBuilder, StylingInstruction} from './styling_builder';
|
||||||
import {BindingScope, TemplateDefinitionBuilder, ValueConverter, renderFlagCheckIfStmt} from './template';
|
import {BindingScope, TemplateDefinitionBuilder, ValueConverter, prepareEventListenerParameters, renderFlagCheckIfStmt} from './template';
|
||||||
import {CONTEXT_NAME, DefinitionMap, RENDER_FLAGS, TEMPORARY_NAME, asLiteral, conditionallyCreateMapObjectLiteral, getQueryPredicate, temporaryAllocator} from './util';
|
import {CONTEXT_NAME, DefinitionMap, RENDER_FLAGS, TEMPORARY_NAME, asLiteral, conditionallyCreateMapObjectLiteral, getQueryPredicate, temporaryAllocator} from './util';
|
||||||
|
|
||||||
const EMPTY_ARRAY: any[] = [];
|
const EMPTY_ARRAY: any[] = [];
|
||||||
|
@ -809,21 +810,15 @@ function createHostListeners(
|
||||||
bindingContext: o.Expression, eventBindings: ParsedEvent[],
|
bindingContext: o.Expression, eventBindings: ParsedEvent[],
|
||||||
meta: R3DirectiveMetadata): o.Statement[] {
|
meta: R3DirectiveMetadata): o.Statement[] {
|
||||||
return eventBindings.map(binding => {
|
return eventBindings.map(binding => {
|
||||||
const bindingExpr = convertActionBinding(
|
|
||||||
null, bindingContext, binding.handler, 'b', () => error('Unexpected interpolation'));
|
|
||||||
let bindingName = binding.name && sanitizeIdentifier(binding.name);
|
let bindingName = binding.name && sanitizeIdentifier(binding.name);
|
||||||
let bindingFnName = bindingName;
|
const bindingFnName = binding.type === ParsedEventType.Animation ?
|
||||||
if (binding.type === ParsedEventType.Animation) {
|
prepareSyntheticListenerFunctionName(bindingName, binding.targetOrPhase) :
|
||||||
bindingFnName = prepareSyntheticListenerFunctionName(bindingName, binding.targetOrPhase);
|
bindingName;
|
||||||
bindingName = prepareSyntheticListenerName(bindingName, binding.targetOrPhase);
|
const handlerName =
|
||||||
}
|
meta.name && bindingName ? `${meta.name}_${bindingFnName}_HostBindingHandler` : null;
|
||||||
const typeName = meta.name;
|
const params = prepareEventListenerParameters(
|
||||||
const functionName =
|
BoundEvent.fromParsedEvent(binding), bindingContext, handlerName);
|
||||||
typeName && bindingName ? `${typeName}_${bindingFnName}_HostBindingHandler` : null;
|
return o.importExpr(R3.listener).callFn(params).toStmt();
|
||||||
const handler = o.fn(
|
|
||||||
[new o.FnParam('$event', o.DYNAMIC_TYPE)], [...bindingExpr.render3Stmts], o.INFERRED_TYPE,
|
|
||||||
null, functionName);
|
|
||||||
return o.importExpr(R3.listener).callFn([o.literal(bindingName), handler]).toStmt();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -39,6 +39,16 @@ import {I18N_ICU_MAPPING_PREFIX, assembleBoundTextPlaceholders, assembleI18nBoun
|
||||||
import {StylingBuilder, StylingInstruction} from './styling_builder';
|
import {StylingBuilder, StylingInstruction} from './styling_builder';
|
||||||
import {CONTEXT_NAME, IMPLICIT_REFERENCE, NON_BINDABLE_ATTR, REFERENCE_PREFIX, RENDER_FLAGS, asLiteral, getAttrsForDirectiveMatching, invalid, trimTrailingNulls, unsupported} from './util';
|
import {CONTEXT_NAME, IMPLICIT_REFERENCE, NON_BINDABLE_ATTR, REFERENCE_PREFIX, RENDER_FLAGS, asLiteral, getAttrsForDirectiveMatching, invalid, trimTrailingNulls, unsupported} from './util';
|
||||||
|
|
||||||
|
// Default selector used by `<ng-content>` if none specified
|
||||||
|
const DEFAULT_NG_CONTENT_SELECTOR = '*';
|
||||||
|
|
||||||
|
// Selector attribute name of `<ng-content>`
|
||||||
|
const NG_CONTENT_SELECT_ATTR = 'select';
|
||||||
|
|
||||||
|
// List of supported global targets for event listeners
|
||||||
|
const GLOBAL_TARGET_RESOLVERS = new Map<string, o.ExternalReference>(
|
||||||
|
[['window', R3.resolveWindow], ['document', R3.resolveDocument], ['body', R3.resolveBody]]);
|
||||||
|
|
||||||
function mapBindingToInstruction(type: BindingType): o.ExternalReference|undefined {
|
function mapBindingToInstruction(type: BindingType): o.ExternalReference|undefined {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case BindingType.Property:
|
case BindingType.Property:
|
||||||
|
@ -59,11 +69,39 @@ export function renderFlagCheckIfStmt(
|
||||||
return o.ifStmt(o.variable(RENDER_FLAGS).bitwiseAnd(o.literal(flags), null, false), statements);
|
return o.ifStmt(o.variable(RENDER_FLAGS).bitwiseAnd(o.literal(flags), null, false), statements);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default selector used by `<ng-content>` if none specified
|
export function prepareEventListenerParameters(
|
||||||
const DEFAULT_NG_CONTENT_SELECTOR = '*';
|
eventAst: t.BoundEvent, bindingContext: o.Expression, handlerName: string | null = null,
|
||||||
|
scope: BindingScope | null = null): o.Expression[] {
|
||||||
|
const {type, name, target, phase, handler} = eventAst;
|
||||||
|
if (target && !GLOBAL_TARGET_RESOLVERS.has(target)) {
|
||||||
|
throw new Error(`Unexpected global target '${target}' defined for '${name}' event.
|
||||||
|
Supported list of global targets: ${Array.from(GLOBAL_TARGET_RESOLVERS.keys())}.`);
|
||||||
|
}
|
||||||
|
|
||||||
// Selector attribute name of `<ng-content>`
|
const bindingExpr = convertActionBinding(
|
||||||
const NG_CONTENT_SELECT_ATTR = 'select';
|
scope, bindingContext, handler, 'b', () => error('Unexpected interpolation'));
|
||||||
|
|
||||||
|
const statements = [];
|
||||||
|
if (scope) {
|
||||||
|
statements.push(...scope.restoreViewStatement());
|
||||||
|
statements.push(...scope.variableDeclarations());
|
||||||
|
}
|
||||||
|
statements.push(...bindingExpr.render3Stmts);
|
||||||
|
|
||||||
|
const eventName: string =
|
||||||
|
type === ParsedEventType.Animation ? prepareSyntheticListenerName(name, phase !) : name;
|
||||||
|
const fnName = handlerName && sanitizeIdentifier(handlerName);
|
||||||
|
const fnArgs = [new o.FnParam('$event', o.DYNAMIC_TYPE)];
|
||||||
|
const handlerFn = o.fn(fnArgs, statements, o.INFERRED_TYPE, null, fnName);
|
||||||
|
|
||||||
|
const params: o.Expression[] = [o.literal(eventName), handlerFn];
|
||||||
|
if (target) {
|
||||||
|
params.push(
|
||||||
|
o.literal(false), // `useCapture` flag, defaults to `false`
|
||||||
|
o.importExpr(GLOBAL_TARGET_RESOLVERS.get(target) !));
|
||||||
|
}
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver {
|
export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver {
|
||||||
private _dataIndex = 0;
|
private _dataIndex = 0;
|
||||||
|
@ -1069,37 +1107,16 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||||
|
|
||||||
private prepareListenerParameter(tagName: string, outputAst: t.BoundEvent, index: number):
|
private prepareListenerParameter(tagName: string, outputAst: t.BoundEvent, index: number):
|
||||||
() => o.Expression[] {
|
() => o.Expression[] {
|
||||||
let eventName: string = outputAst.name;
|
|
||||||
|
|
||||||
let bindingFnName;
|
|
||||||
if (outputAst.type === ParsedEventType.Animation) {
|
|
||||||
// synthetic @listener.foo values are treated the exact same as are standard listeners
|
|
||||||
bindingFnName = prepareSyntheticListenerFunctionName(eventName, outputAst.phase !);
|
|
||||||
eventName = prepareSyntheticListenerName(eventName, outputAst.phase !);
|
|
||||||
} else {
|
|
||||||
bindingFnName = sanitizeIdentifier(eventName);
|
|
||||||
}
|
|
||||||
|
|
||||||
const tagNameSanitized = sanitizeIdentifier(tagName);
|
|
||||||
const functionName =
|
|
||||||
`${this.templateName}_${tagNameSanitized}_${bindingFnName}_${index}_listener`;
|
|
||||||
return () => {
|
return () => {
|
||||||
|
const eventName: string = outputAst.name;
|
||||||
const listenerScope = this._bindingScope.nestedScope(this._bindingScope.bindingLevel);
|
const bindingFnName = outputAst.type === ParsedEventType.Animation ?
|
||||||
|
// synthetic @listener.foo values are treated the exact same as are standard listeners
|
||||||
const bindingExpr = convertActionBinding(
|
prepareSyntheticListenerFunctionName(eventName, outputAst.phase !) :
|
||||||
listenerScope, o.variable(CONTEXT_NAME), outputAst.handler, 'b',
|
sanitizeIdentifier(eventName);
|
||||||
() => error('Unexpected interpolation'));
|
const handlerName = `${this.templateName}_${tagName}_${bindingFnName}_${index}_listener`;
|
||||||
|
const scope = this._bindingScope.nestedScope(this._bindingScope.bindingLevel);
|
||||||
const statements = [
|
const context = o.variable(CONTEXT_NAME);
|
||||||
...listenerScope.restoreViewStatement(), ...listenerScope.variableDeclarations(),
|
return prepareEventListenerParameters(outputAst, context, handlerName, scope);
|
||||||
...bindingExpr.render3Stmts
|
|
||||||
];
|
|
||||||
|
|
||||||
const handler = o.fn(
|
|
||||||
[new o.FnParam('$event', o.DYNAMIC_TYPE)], statements, o.INFERRED_TYPE, null,
|
|
||||||
functionName);
|
|
||||||
return [o.literal(eventName), handler];
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -120,6 +120,9 @@ export {
|
||||||
i18nApply as ɵi18nApply,
|
i18nApply as ɵi18nApply,
|
||||||
i18nPostprocess as ɵi18nPostprocess,
|
i18nPostprocess as ɵi18nPostprocess,
|
||||||
setClassMetadata as ɵsetClassMetadata,
|
setClassMetadata as ɵsetClassMetadata,
|
||||||
|
resolveWindow as ɵresolveWindow,
|
||||||
|
resolveDocument as ɵresolveDocument,
|
||||||
|
resolveBody as ɵresolveBody,
|
||||||
} from './render3/index';
|
} from './render3/index';
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -144,6 +144,7 @@ export {
|
||||||
|
|
||||||
export {templateRefExtractor} from './view_engine_compatibility_prebound';
|
export {templateRefExtractor} from './view_engine_compatibility_prebound';
|
||||||
|
|
||||||
|
export {resolveWindow, resolveDocument, resolveBody} from './util';
|
||||||
|
|
||||||
// clang-format on
|
// clang-format on
|
||||||
|
|
||||||
|
|
|
@ -29,7 +29,7 @@ import {AttributeMarker, InitialInputData, InitialInputs, LocalRefExtractor, Pro
|
||||||
import {PlayerFactory} from './interfaces/player';
|
import {PlayerFactory} from './interfaces/player';
|
||||||
import {CssSelectorList, NG_PROJECT_AS_ATTR_NAME} from './interfaces/projection';
|
import {CssSelectorList, NG_PROJECT_AS_ATTR_NAME} from './interfaces/projection';
|
||||||
import {LQueries} from './interfaces/query';
|
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 {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 {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';
|
import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert';
|
||||||
|
@ -822,10 +822,13 @@ export function locateHostElement(
|
||||||
*
|
*
|
||||||
* @param eventName Name of the event
|
* @param eventName Name of the event
|
||||||
* @param listenerFn The function to be called when event emits
|
* @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(
|
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 lView = getLView();
|
||||||
const tNode = getPreviousOrParentTNode();
|
const tNode = getPreviousOrParentTNode();
|
||||||
const tView = lView[TVIEW];
|
const tView = lView[TVIEW];
|
||||||
|
@ -837,6 +840,8 @@ export function listener(
|
||||||
// add native event listener - applicable to elements only
|
// add native event listener - applicable to elements only
|
||||||
if (tNode.type === TNodeType.Element) {
|
if (tNode.type === TNodeType.Element) {
|
||||||
const native = getNativeByTNode(tNode, lView) as RElement;
|
const native = getNativeByTNode(tNode, lView) as RElement;
|
||||||
|
const resolved = eventTargetResolver ? eventTargetResolver(native) : {} as any;
|
||||||
|
const target = resolved.target || native;
|
||||||
ngDevMode && ngDevMode.rendererAddEventListener++;
|
ngDevMode && ngDevMode.rendererAddEventListener++;
|
||||||
const renderer = lView[RENDERER];
|
const renderer = lView[RENDERER];
|
||||||
const lCleanup = getCleanup(lView);
|
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
|
// In order to match current behavior, native DOM event listeners must be added for all
|
||||||
// events (including outputs).
|
// events (including outputs).
|
||||||
if (isProceduralRenderer(renderer)) {
|
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);
|
lCleanup.push(listenerFn, cleanupFn);
|
||||||
useCaptureOrSubIdx = lCleanupIndex + 1;
|
useCaptureOrSubIdx = lCleanupIndex + 1;
|
||||||
} else {
|
} else {
|
||||||
const wrappedListener = wrapListenerWithPreventDefault(listenerFn);
|
const wrappedListener = wrapListenerWithPreventDefault(listenerFn);
|
||||||
native.addEventListener(eventName, wrappedListener, useCapture);
|
target.addEventListener(eventName, wrappedListener, useCapture);
|
||||||
lCleanup.push(wrappedListener);
|
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
|
// subscribe to directive outputs
|
||||||
|
|
|
@ -26,6 +26,12 @@ export enum RendererStyleFlags3 {
|
||||||
|
|
||||||
export type Renderer3 = ObjectOrientedRenderer3 | ProceduralRenderer3;
|
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.
|
* 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;
|
setValue(node: RText|RComment, value: string): void;
|
||||||
|
|
||||||
// TODO(misko): Deprecate in favor of addEventListener/removeEventListener
|
// 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 {
|
export interface RendererFactory3 {
|
||||||
|
|
|
@ -446,7 +446,11 @@ export interface TView {
|
||||||
*
|
*
|
||||||
* If it's a native DOM listener or output subscription being stored:
|
* If it's a native DOM listener or output subscription being stored:
|
||||||
* 1st index is: event name `name = tView.cleanup[i+0]`
|
* 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]]`
|
* 3rd index is: index of listener function `listener = lView[CLEANUP][tView.cleanup[i+2]]`
|
||||||
* 4th index is: `useCaptureOrIndx = tView.cleanup[i+3]`
|
* 4th index is: `useCaptureOrIndx = tView.cleanup[i+3]`
|
||||||
* `typeof useCaptureOrIndx == 'boolean' : useCapture boolean
|
* `typeof useCaptureOrIndx == 'boolean' : useCapture boolean
|
||||||
|
|
|
@ -107,6 +107,9 @@ export const angularCoreEnv: {[name: string]: Function} = {
|
||||||
'ɵi18nEnd': r3.i18nEnd,
|
'ɵi18nEnd': r3.i18nEnd,
|
||||||
'ɵi18nApply': r3.i18nApply,
|
'ɵi18nApply': r3.i18nApply,
|
||||||
'ɵi18nPostprocess': r3.i18nPostprocess,
|
'ɵi18nPostprocess': r3.i18nPostprocess,
|
||||||
|
'ɵresolveWindow': r3.resolveWindow,
|
||||||
|
'ɵresolveDocument': r3.resolveDocument,
|
||||||
|
'ɵresolveBody': r3.resolveBody,
|
||||||
|
|
||||||
'ɵsanitizeHtml': sanitization.sanitizeHtml,
|
'ɵsanitizeHtml': sanitization.sanitizeHtml,
|
||||||
'ɵsanitizeStyle': sanitization.sanitizeStyle,
|
'ɵsanitizeStyle': sanitization.sanitizeStyle,
|
||||||
|
|
|
@ -445,13 +445,15 @@ function removeListeners(lView: LView): void {
|
||||||
for (let i = 0; i < tCleanup.length - 1; i += 2) {
|
for (let i = 0; i < tCleanup.length - 1; i += 2) {
|
||||||
if (typeof tCleanup[i] === 'string') {
|
if (typeof tCleanup[i] === 'string') {
|
||||||
// This is a listener with the native renderer
|
// 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 listener = lCleanup[tCleanup[i + 2]];
|
||||||
const native = readElementValue(lView[idx]);
|
|
||||||
const useCaptureOrSubIdx = tCleanup[i + 3];
|
const useCaptureOrSubIdx = tCleanup[i + 3];
|
||||||
if (typeof useCaptureOrSubIdx === 'boolean') {
|
if (typeof useCaptureOrSubIdx === 'boolean') {
|
||||||
// DOM listener
|
// DOM listener
|
||||||
native.removeEventListener(tCleanup[i], listener, useCaptureOrSubIdx);
|
target.removeEventListener(tCleanup[i], listener, useCaptureOrSubIdx);
|
||||||
} else {
|
} else {
|
||||||
if (useCaptureOrSubIdx >= 0) {
|
if (useCaptureOrSubIdx >= 0) {
|
||||||
// unregister
|
// unregister
|
||||||
|
|
|
@ -14,7 +14,7 @@ import {LContext, MONKEY_PATCH_KEY_NAME} from './interfaces/context';
|
||||||
import {ComponentDef, DirectiveDef} from './interfaces/definition';
|
import {ComponentDef, DirectiveDef} from './interfaces/definition';
|
||||||
import {NO_PARENT_INJECTOR, RelativeInjectorLocation, RelativeInjectorLocationFlags} from './interfaces/injector';
|
import {NO_PARENT_INJECTOR, RelativeInjectorLocation, RelativeInjectorLocationFlags} from './interfaces/injector';
|
||||||
import {TContainerNode, TElementNode, TNode, TNodeFlags, TNodeType} from './interfaces/node';
|
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 {StylingContext} from './interfaces/styling';
|
||||||
import {CONTEXT, DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, HOST_NODE, LView, LViewFlags, PARENT, RootContext, TData, TVIEW, TView} from './interfaces/view';
|
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;
|
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};
|
||||||
|
}
|
|
@ -845,52 +845,48 @@ function declareTests(config?: {useJit: boolean}) {
|
||||||
dir.triggerChange('two');
|
dir.triggerChange('two');
|
||||||
}));
|
}));
|
||||||
|
|
||||||
fixmeIvy(
|
it('should support render events', () => {
|
||||||
'FW-743: Registering events on global objects (document, window, body) is not supported')
|
TestBed.configureTestingModule({declarations: [MyComp, DirectiveListeningDomEvent]});
|
||||||
.it('should support render events', () => {
|
const template = '<div listener></div>';
|
||||||
TestBed.configureTestingModule({declarations: [MyComp, DirectiveListeningDomEvent]});
|
TestBed.overrideComponent(MyComp, {set: {template}});
|
||||||
const template = '<div listener></div>';
|
const fixture = TestBed.createComponent(MyComp);
|
||||||
TestBed.overrideComponent(MyComp, {set: {template}});
|
|
||||||
const fixture = TestBed.createComponent(MyComp);
|
|
||||||
|
|
||||||
const tc = fixture.debugElement.children[0];
|
const tc = fixture.debugElement.children[0];
|
||||||
const listener = tc.injector.get(DirectiveListeningDomEvent);
|
const listener = tc.injector.get(DirectiveListeningDomEvent);
|
||||||
|
|
||||||
dispatchEvent(tc.nativeElement, 'domEvent');
|
dispatchEvent(tc.nativeElement, 'domEvent');
|
||||||
|
|
||||||
expect(listener.eventTypes).toEqual([
|
expect(listener.eventTypes).toEqual([
|
||||||
'domEvent', 'body_domEvent', 'document_domEvent', 'window_domEvent'
|
'domEvent', 'body_domEvent', 'document_domEvent', 'window_domEvent'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
fixture.destroy();
|
fixture.destroy();
|
||||||
listener.eventTypes = [];
|
listener.eventTypes = [];
|
||||||
dispatchEvent(tc.nativeElement, 'domEvent');
|
dispatchEvent(tc.nativeElement, 'domEvent');
|
||||||
expect(listener.eventTypes).toEqual([]);
|
expect(listener.eventTypes).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
fixmeIvy(
|
it('should support render global events', () => {
|
||||||
'FW-743: Registering events on global objects (document, window, body) is not supported')
|
TestBed.configureTestingModule({declarations: [MyComp, DirectiveListeningDomEvent]});
|
||||||
.it('should support render global events', () => {
|
const template = '<div listener></div>';
|
||||||
TestBed.configureTestingModule({declarations: [MyComp, DirectiveListeningDomEvent]});
|
TestBed.overrideComponent(MyComp, {set: {template}});
|
||||||
const template = '<div listener></div>';
|
const fixture = TestBed.createComponent(MyComp);
|
||||||
TestBed.overrideComponent(MyComp, {set: {template}});
|
const doc = TestBed.get(DOCUMENT);
|
||||||
const fixture = TestBed.createComponent(MyComp);
|
|
||||||
const doc = TestBed.get(DOCUMENT);
|
|
||||||
|
|
||||||
const tc = fixture.debugElement.children[0];
|
const tc = fixture.debugElement.children[0];
|
||||||
const listener = tc.injector.get(DirectiveListeningDomEvent);
|
const listener = tc.injector.get(DirectiveListeningDomEvent);
|
||||||
dispatchEvent(getDOM().getGlobalEventTarget(doc, 'window'), 'domEvent');
|
dispatchEvent(getDOM().getGlobalEventTarget(doc, 'window'), 'domEvent');
|
||||||
expect(listener.eventTypes).toEqual(['window_domEvent']);
|
expect(listener.eventTypes).toEqual(['window_domEvent']);
|
||||||
|
|
||||||
listener.eventTypes = [];
|
listener.eventTypes = [];
|
||||||
dispatchEvent(getDOM().getGlobalEventTarget(doc, 'document'), 'domEvent');
|
dispatchEvent(getDOM().getGlobalEventTarget(doc, 'document'), 'domEvent');
|
||||||
expect(listener.eventTypes).toEqual(['document_domEvent', 'window_domEvent']);
|
expect(listener.eventTypes).toEqual(['document_domEvent', 'window_domEvent']);
|
||||||
|
|
||||||
fixture.destroy();
|
fixture.destroy();
|
||||||
listener.eventTypes = [];
|
listener.eventTypes = [];
|
||||||
dispatchEvent(getDOM().getGlobalEventTarget(doc, 'body'), 'domEvent');
|
dispatchEvent(getDOM().getGlobalEventTarget(doc, 'body'), 'domEvent');
|
||||||
expect(listener.eventTypes).toEqual([]);
|
expect(listener.eventTypes).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should support updating host element via hostAttributes on root elements', () => {
|
it('should support updating host element via hostAttributes on root elements', () => {
|
||||||
@Component({host: {'role': 'button'}, template: ''})
|
@Component({host: {'role': 'button'}, template: ''})
|
||||||
|
@ -1027,44 +1023,41 @@ function declareTests(config?: {useJit: boolean}) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fixmeIvy(
|
it('should support render global events from multiple directives', () => {
|
||||||
'FW-743: Registering events on global objects (document, window, body) is not supported')
|
TestBed.configureTestingModule(
|
||||||
.it('should support render global events from multiple directives', () => {
|
{declarations: [MyComp, DirectiveListeningDomEvent, DirectiveListeningDomEventOther]});
|
||||||
TestBed.configureTestingModule({
|
const template = '<div *ngIf="ctxBoolProp" listener listenerother></div>';
|
||||||
declarations: [MyComp, DirectiveListeningDomEvent, DirectiveListeningDomEventOther]
|
TestBed.overrideComponent(MyComp, {set: {template}});
|
||||||
});
|
const fixture = TestBed.createComponent(MyComp);
|
||||||
const template = '<div *ngIf="ctxBoolProp" listener listenerother></div>';
|
const doc = TestBed.get(DOCUMENT);
|
||||||
TestBed.overrideComponent(MyComp, {set: {template}});
|
|
||||||
const fixture = TestBed.createComponent(MyComp);
|
|
||||||
const doc = TestBed.get(DOCUMENT);
|
|
||||||
|
|
||||||
globalCounter = 0;
|
globalCounter = 0;
|
||||||
fixture.componentInstance.ctxBoolProp = true;
|
fixture.componentInstance.ctxBoolProp = true;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
const tc = fixture.debugElement.children[0];
|
const tc = fixture.debugElement.children[0];
|
||||||
|
|
||||||
const listener = tc.injector.get(DirectiveListeningDomEvent);
|
const listener = tc.injector.get(DirectiveListeningDomEvent);
|
||||||
const listenerother = tc.injector.get(DirectiveListeningDomEventOther);
|
const listenerother = tc.injector.get(DirectiveListeningDomEventOther);
|
||||||
dispatchEvent(getDOM().getGlobalEventTarget(doc, 'window'), 'domEvent');
|
dispatchEvent(getDOM().getGlobalEventTarget(doc, 'window'), 'domEvent');
|
||||||
expect(listener.eventTypes).toEqual(['window_domEvent']);
|
expect(listener.eventTypes).toEqual(['window_domEvent']);
|
||||||
expect(listenerother.eventType).toEqual('other_domEvent');
|
expect(listenerother.eventType).toEqual('other_domEvent');
|
||||||
expect(globalCounter).toEqual(1);
|
expect(globalCounter).toEqual(1);
|
||||||
|
|
||||||
|
|
||||||
fixture.componentInstance.ctxBoolProp = false;
|
fixture.componentInstance.ctxBoolProp = false;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
dispatchEvent(getDOM().getGlobalEventTarget(doc, 'window'), 'domEvent');
|
dispatchEvent(getDOM().getGlobalEventTarget(doc, 'window'), 'domEvent');
|
||||||
expect(globalCounter).toEqual(1);
|
expect(globalCounter).toEqual(1);
|
||||||
|
|
||||||
fixture.componentInstance.ctxBoolProp = true;
|
fixture.componentInstance.ctxBoolProp = true;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
dispatchEvent(getDOM().getGlobalEventTarget(doc, 'window'), 'domEvent');
|
dispatchEvent(getDOM().getGlobalEventTarget(doc, 'window'), 'domEvent');
|
||||||
expect(globalCounter).toEqual(2);
|
expect(globalCounter).toEqual(2);
|
||||||
|
|
||||||
// need to destroy to release all remaining global event listeners
|
// need to destroy to release all remaining global event listeners
|
||||||
fixture.destroy();
|
fixture.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('ViewContainerRef', () => {
|
describe('ViewContainerRef', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|
|
@ -6,17 +6,21 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {bind, defineComponent, defineDirective, markDirty, reference, textBinding} from '../../src/render3/index';
|
import {dispatchEvent} from '@angular/platform-browser/testing/src/browser_util';
|
||||||
|
|
||||||
|
import {bind, defineComponent, defineDirective, markDirty, reference, resolveBody, resolveDocument, textBinding} from '../../src/render3/index';
|
||||||
import {container, containerRefreshEnd, containerRefreshStart, element, elementEnd, elementStart, embeddedViewEnd, embeddedViewStart, getCurrentView, listener, text} from '../../src/render3/instructions';
|
import {container, containerRefreshEnd, containerRefreshStart, element, elementEnd, elementStart, embeddedViewEnd, embeddedViewStart, getCurrentView, listener, text} from '../../src/render3/instructions';
|
||||||
import {RenderFlags} from '../../src/render3/interfaces/definition';
|
import {RenderFlags} from '../../src/render3/interfaces/definition';
|
||||||
|
import {GlobalTargetResolver} from '../../src/render3/interfaces/renderer';
|
||||||
import {restoreView} from '../../src/render3/state';
|
import {restoreView} from '../../src/render3/state';
|
||||||
|
|
||||||
import {getRendererFactory2} from './imported_renderer2';
|
import {getRendererFactory2} from './imported_renderer2';
|
||||||
import {ComponentFixture, containerEl, createComponent, getDirectiveOnNode, renderToHtml, requestAnimationFrame} from './render_util';
|
import {ComponentFixture, TemplateFixture, containerEl, createComponent, getDirectiveOnNode, renderToHtml, requestAnimationFrame} from './render_util';
|
||||||
|
|
||||||
|
|
||||||
describe('event listeners', () => {
|
describe('event listeners', () => {
|
||||||
let comps: MyComp[] = [];
|
let comps: any[] = [];
|
||||||
|
let events: any[] = [];
|
||||||
|
|
||||||
class MyComp {
|
class MyComp {
|
||||||
showing = true;
|
showing = true;
|
||||||
|
@ -48,6 +52,67 @@ describe('event listeners', () => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class MyCompWithGlobalListeners {
|
||||||
|
/* @HostListener('document:custom') */
|
||||||
|
onDocumentCustomEvent() { events.push('component - document:custom'); }
|
||||||
|
|
||||||
|
/* @HostListener('body:click') */
|
||||||
|
onBodyClick() { events.push('component - body:click'); }
|
||||||
|
|
||||||
|
static ngComponentDef = defineComponent({
|
||||||
|
type: MyCompWithGlobalListeners,
|
||||||
|
selectors: [['comp']],
|
||||||
|
consts: 1,
|
||||||
|
vars: 0,
|
||||||
|
template: function CompTemplate(rf: RenderFlags, ctx: any) {
|
||||||
|
if (rf & RenderFlags.Create) {
|
||||||
|
text(0, 'Some text');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
factory: () => {
|
||||||
|
let comp = new MyCompWithGlobalListeners();
|
||||||
|
comps.push(comp);
|
||||||
|
return comp;
|
||||||
|
},
|
||||||
|
hostBindings: function HostListenerDir_HostBindings(
|
||||||
|
rf: RenderFlags, ctx: any, elIndex: number) {
|
||||||
|
if (rf & RenderFlags.Create) {
|
||||||
|
listener('custom', function() {
|
||||||
|
return ctx.onDocumentCustomEvent();
|
||||||
|
}, false, resolveDocument as GlobalTargetResolver);
|
||||||
|
listener('click', function() {
|
||||||
|
return ctx.onBodyClick();
|
||||||
|
}, false, resolveBody as GlobalTargetResolver);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class GlobalHostListenerDir {
|
||||||
|
/* @HostListener('document:custom') */
|
||||||
|
onDocumentCustomEvent() { events.push('directive - document:custom'); }
|
||||||
|
|
||||||
|
/* @HostListener('body:click') */
|
||||||
|
onBodyClick() { events.push('directive - body:click'); }
|
||||||
|
|
||||||
|
static ngDirectiveDef = defineDirective({
|
||||||
|
type: GlobalHostListenerDir,
|
||||||
|
selectors: [['', 'hostListenerDir', '']],
|
||||||
|
factory: function HostListenerDir_Factory() { return new GlobalHostListenerDir(); },
|
||||||
|
hostBindings: function HostListenerDir_HostBindings(
|
||||||
|
rf: RenderFlags, ctx: any, elIndex: number) {
|
||||||
|
if (rf & RenderFlags.Create) {
|
||||||
|
listener('custom', function() {
|
||||||
|
return ctx.onDocumentCustomEvent();
|
||||||
|
}, false, resolveDocument as GlobalTargetResolver);
|
||||||
|
listener('click', function() {
|
||||||
|
return ctx.onBodyClick();
|
||||||
|
}, false, resolveBody as GlobalTargetResolver);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
class PreventDefaultComp {
|
class PreventDefaultComp {
|
||||||
handlerReturnValue: any = true;
|
handlerReturnValue: any = true;
|
||||||
// TODO(issue/24571): remove '!'.
|
// TODO(issue/24571): remove '!'.
|
||||||
|
@ -84,7 +149,10 @@ describe('event listeners', () => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => { comps = []; });
|
beforeEach(() => {
|
||||||
|
comps = [];
|
||||||
|
events = [];
|
||||||
|
});
|
||||||
|
|
||||||
it('should call function on event emit', () => {
|
it('should call function on event emit', () => {
|
||||||
const fixture = new ComponentFixture(MyComp);
|
const fixture = new ComponentFixture(MyComp);
|
||||||
|
@ -477,6 +545,7 @@ describe('event listeners', () => {
|
||||||
|
|
||||||
const fixture = new ComponentFixture(MyComp);
|
const fixture = new ComponentFixture(MyComp);
|
||||||
const host = fixture.hostElement;
|
const host = fixture.hostElement;
|
||||||
|
|
||||||
host.click();
|
host.click();
|
||||||
expect(events).toEqual(['click!']);
|
expect(events).toEqual(['click!']);
|
||||||
|
|
||||||
|
@ -484,6 +553,20 @@ describe('event listeners', () => {
|
||||||
expect(events).toEqual(['click!', 'click!']);
|
expect(events).toEqual(['click!', 'click!']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should support global host listeners on components', () => {
|
||||||
|
const fixture = new ComponentFixture(MyCompWithGlobalListeners);
|
||||||
|
const doc = fixture.hostElement.ownerDocument !;
|
||||||
|
|
||||||
|
dispatchEvent(doc, 'custom');
|
||||||
|
expect(events).toEqual(['component - document:custom']);
|
||||||
|
|
||||||
|
dispatchEvent(doc.body, 'click');
|
||||||
|
expect(events).toEqual(['component - document:custom', 'component - body:click']);
|
||||||
|
|
||||||
|
// invoke destroy for this fixture to cleanup all listeners setup for global objects
|
||||||
|
fixture.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
it('should support host listeners on directives', () => {
|
it('should support host listeners on directives', () => {
|
||||||
let events: string[] = [];
|
let events: string[] = [];
|
||||||
|
|
||||||
|
@ -504,16 +587,14 @@ describe('event listeners', () => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function Template(rf: RenderFlags, ctx: any) {
|
const fixture = new TemplateFixture(() => {
|
||||||
if (rf & RenderFlags.Create) {
|
elementStart(0, 'button', ['hostListenerDir', '']);
|
||||||
elementStart(0, 'button', ['hostListenerDir', '']);
|
text(1, 'Click');
|
||||||
text(1, 'Click');
|
elementEnd();
|
||||||
elementEnd();
|
}, () => {}, 2, 0, [HostListenerDir]);
|
||||||
}
|
|
||||||
}
|
const button = fixture.hostElement.querySelector('button') !;
|
||||||
|
|
||||||
renderToHtml(Template, {}, 2, 0, [HostListenerDir]);
|
|
||||||
const button = containerEl.querySelector('button') !;
|
|
||||||
button.click();
|
button.click();
|
||||||
expect(events).toEqual(['click!']);
|
expect(events).toEqual(['click!']);
|
||||||
|
|
||||||
|
@ -521,6 +602,23 @@ describe('event listeners', () => {
|
||||||
expect(events).toEqual(['click!', 'click!']);
|
expect(events).toEqual(['click!', 'click!']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should support global host listeners on directives', () => {
|
||||||
|
const fixture = new TemplateFixture(() => {
|
||||||
|
element(0, 'div', ['hostListenerDir', '']);
|
||||||
|
}, () => {}, 1, 0, [GlobalHostListenerDir]);
|
||||||
|
|
||||||
|
const doc = fixture.hostElement.ownerDocument !;
|
||||||
|
|
||||||
|
dispatchEvent(doc, 'custom');
|
||||||
|
expect(events).toEqual(['directive - document:custom']);
|
||||||
|
|
||||||
|
dispatchEvent(doc.body, 'click');
|
||||||
|
expect(events).toEqual(['directive - document:custom', 'directive - body:click']);
|
||||||
|
|
||||||
|
// invoke destroy for this fixture to cleanup all listeners setup for global objects
|
||||||
|
fixture.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
it('should support listeners with specified set of args', () => {
|
it('should support listeners with specified set of args', () => {
|
||||||
class MyComp {
|
class MyComp {
|
||||||
counter = 0;
|
counter = 0;
|
||||||
|
@ -673,6 +771,40 @@ describe('event listeners', () => {
|
||||||
expect(comps[1] !.counter).toEqual(1);
|
expect(comps[1] !.counter).toEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should destroy global listeners in component views', () => {
|
||||||
|
const ctx = {showing: true};
|
||||||
|
|
||||||
|
const fixture = new TemplateFixture(
|
||||||
|
() => { container(0); },
|
||||||
|
() => {
|
||||||
|
containerRefreshStart(0);
|
||||||
|
{
|
||||||
|
if (ctx.showing) {
|
||||||
|
let rf1 = embeddedViewStart(0, 1, 0);
|
||||||
|
if (rf1 & RenderFlags.Create) {
|
||||||
|
element(0, 'comp');
|
||||||
|
}
|
||||||
|
embeddedViewEnd();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
containerRefreshEnd();
|
||||||
|
},
|
||||||
|
1, 0, [MyCompWithGlobalListeners]);
|
||||||
|
|
||||||
|
const body = fixture.hostElement.ownerDocument !.body;
|
||||||
|
|
||||||
|
body.click();
|
||||||
|
expect(events).toEqual(['component - body:click']);
|
||||||
|
|
||||||
|
// the child view listener should be removed when the parent view is removed
|
||||||
|
ctx.showing = false;
|
||||||
|
fixture.update();
|
||||||
|
|
||||||
|
body.click();
|
||||||
|
// expecting no changes in events array
|
||||||
|
expect(events).toEqual(['component - body:click']);
|
||||||
|
});
|
||||||
|
|
||||||
it('should support listeners with sibling nested containers', () => {
|
it('should support listeners with sibling nested containers', () => {
|
||||||
/**
|
/**
|
||||||
* % if (condition) {
|
* % if (condition) {
|
||||||
|
|
|
@ -31,6 +31,8 @@ import {DirectiveDefList, DirectiveTypesOrFactory, PipeDef, PipeDefList, PipeTyp
|
||||||
import {PlayerHandler} from '../../src/render3/interfaces/player';
|
import {PlayerHandler} from '../../src/render3/interfaces/player';
|
||||||
import {ProceduralRenderer3, RComment, RElement, RNode, RText, Renderer3, RendererFactory3, RendererStyleFlags3, domRendererFactory3} from '../../src/render3/interfaces/renderer';
|
import {ProceduralRenderer3, RComment, RElement, RNode, RText, Renderer3, RendererFactory3, RendererStyleFlags3, domRendererFactory3} from '../../src/render3/interfaces/renderer';
|
||||||
import {HEADER_OFFSET, LView} from '../../src/render3/interfaces/view';
|
import {HEADER_OFFSET, LView} from '../../src/render3/interfaces/view';
|
||||||
|
import {destroyLView} from '../../src/render3/node_manipulation';
|
||||||
|
import {getRootView} from '../../src/render3/util';
|
||||||
import {Sanitizer} from '../../src/sanitization/security';
|
import {Sanitizer} from '../../src/sanitization/security';
|
||||||
import {Type} from '../../src/type';
|
import {Type} from '../../src/type';
|
||||||
|
|
||||||
|
@ -130,6 +132,11 @@ export class TemplateFixture extends BaseFixture {
|
||||||
this.hostElement, updateBlock || this.updateBlock, 0, this.vars, null !,
|
this.hostElement, updateBlock || this.updateBlock, 0, this.vars, null !,
|
||||||
this._rendererFactory, this.hostView, this._directiveDefs, this._pipeDefs, this._sanitizer);
|
this._rendererFactory, this.hostView, this._directiveDefs, this._pipeDefs, this._sanitizer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
destroy(): void {
|
||||||
|
this.containerElement.removeChild(this.hostElement);
|
||||||
|
destroyLView(this.hostView);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -171,6 +178,11 @@ export class ComponentFixture<T> extends BaseFixture {
|
||||||
tick(this.component);
|
tick(this.component);
|
||||||
this.requestAnimationFrame.flush();
|
this.requestAnimationFrame.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
destroy(): void {
|
||||||
|
this.containerElement.removeChild(this.hostElement);
|
||||||
|
destroyLView(getRootView(this.component));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
Loading…
Reference in New Issue