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:

committed by
Kara Erickson

parent
917c09cfc8
commit
6e7c46af1b
@ -131,6 +131,10 @@ export class Identifiers {
|
||||
static templateRefExtractor:
|
||||
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 BaseDef: o.ExternalReference = {
|
||||
|
@ -9,7 +9,7 @@
|
||||
import {StaticSymbol} from '../../aot/static_symbol';
|
||||
import {CompileDirectiveMetadata, CompileDirectiveSummary, CompileQueryMetadata, CompileTokenMetadata, identifierName, sanitizeIdentifier} from '../../compile_metadata';
|
||||
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 * as core from '../../core';
|
||||
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 {BindingParser} from '../../template_parser/binding_parser';
|
||||
import {OutputContext, error} from '../../util';
|
||||
import {BoundEvent} from '../r3_ast';
|
||||
import {compileFactoryFunction, dependenciesFromGlobalMetadata} from '../r3_factory';
|
||||
import {Identifiers as R3} from '../r3_identifiers';
|
||||
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 {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';
|
||||
|
||||
const EMPTY_ARRAY: any[] = [];
|
||||
@ -809,21 +810,15 @@ function createHostListeners(
|
||||
bindingContext: o.Expression, eventBindings: ParsedEvent[],
|
||||
meta: R3DirectiveMetadata): o.Statement[] {
|
||||
return eventBindings.map(binding => {
|
||||
const bindingExpr = convertActionBinding(
|
||||
null, bindingContext, binding.handler, 'b', () => error('Unexpected interpolation'));
|
||||
let bindingName = binding.name && sanitizeIdentifier(binding.name);
|
||||
let bindingFnName = bindingName;
|
||||
if (binding.type === ParsedEventType.Animation) {
|
||||
bindingFnName = prepareSyntheticListenerFunctionName(bindingName, binding.targetOrPhase);
|
||||
bindingName = prepareSyntheticListenerName(bindingName, binding.targetOrPhase);
|
||||
}
|
||||
const typeName = meta.name;
|
||||
const functionName =
|
||||
typeName && bindingName ? `${typeName}_${bindingFnName}_HostBindingHandler` : null;
|
||||
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();
|
||||
const bindingFnName = binding.type === ParsedEventType.Animation ?
|
||||
prepareSyntheticListenerFunctionName(bindingName, binding.targetOrPhase) :
|
||||
bindingName;
|
||||
const handlerName =
|
||||
meta.name && bindingName ? `${meta.name}_${bindingFnName}_HostBindingHandler` : null;
|
||||
const params = prepareEventListenerParameters(
|
||||
BoundEvent.fromParsedEvent(binding), bindingContext, handlerName);
|
||||
return o.importExpr(R3.listener).callFn(params).toStmt();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -39,6 +39,16 @@ import {I18N_ICU_MAPPING_PREFIX, assembleBoundTextPlaceholders, assembleI18nBoun
|
||||
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';
|
||||
|
||||
// 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 {
|
||||
switch (type) {
|
||||
case BindingType.Property:
|
||||
@ -59,11 +69,39 @@ export function renderFlagCheckIfStmt(
|
||||
return o.ifStmt(o.variable(RENDER_FLAGS).bitwiseAnd(o.literal(flags), null, false), statements);
|
||||
}
|
||||
|
||||
// Default selector used by `<ng-content>` if none specified
|
||||
const DEFAULT_NG_CONTENT_SELECTOR = '*';
|
||||
export function prepareEventListenerParameters(
|
||||
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 NG_CONTENT_SELECT_ATTR = 'select';
|
||||
const bindingExpr = convertActionBinding(
|
||||
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 {
|
||||
private _dataIndex = 0;
|
||||
@ -1069,37 +1107,16 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||
|
||||
private prepareListenerParameter(tagName: string, outputAst: t.BoundEvent, index: number):
|
||||
() => 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 () => {
|
||||
|
||||
const listenerScope = this._bindingScope.nestedScope(this._bindingScope.bindingLevel);
|
||||
|
||||
const bindingExpr = convertActionBinding(
|
||||
listenerScope, o.variable(CONTEXT_NAME), outputAst.handler, 'b',
|
||||
() => error('Unexpected interpolation'));
|
||||
|
||||
const statements = [
|
||||
...listenerScope.restoreViewStatement(), ...listenerScope.variableDeclarations(),
|
||||
...bindingExpr.render3Stmts
|
||||
];
|
||||
|
||||
const handler = o.fn(
|
||||
[new o.FnParam('$event', o.DYNAMIC_TYPE)], statements, o.INFERRED_TYPE, null,
|
||||
functionName);
|
||||
return [o.literal(eventName), handler];
|
||||
const eventName: string = outputAst.name;
|
||||
const bindingFnName = outputAst.type === ParsedEventType.Animation ?
|
||||
// synthetic @listener.foo values are treated the exact same as are standard listeners
|
||||
prepareSyntheticListenerFunctionName(eventName, outputAst.phase !) :
|
||||
sanitizeIdentifier(eventName);
|
||||
const handlerName = `${this.templateName}_${tagName}_${bindingFnName}_${index}_listener`;
|
||||
const scope = this._bindingScope.nestedScope(this._bindingScope.bindingLevel);
|
||||
const context = o.variable(CONTEXT_NAME);
|
||||
return prepareEventListenerParameters(outputAst, context, handlerName, scope);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user