refactor(compiler): generate host listeners in DirectiveWrappers
Part of #11683
This commit is contained in:
@ -6,183 +6,136 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {CompileDirectiveMetadata} from '../compile_metadata';
|
||||
import {EventHandlerVars, convertActionBinding} from '../compiler_util/expression_converter';
|
||||
import {createInlineArray} from '../compiler_util/identifier_util';
|
||||
import {DirectiveWrapperExpressions} from '../directive_wrapper_compiler';
|
||||
import {MapWrapper} from '../facade/collection';
|
||||
import {isPresent} from '../facade/lang';
|
||||
import {identifierToken} from '../identifiers';
|
||||
import {Identifiers, resolveIdentifier} from '../identifiers';
|
||||
import * as o from '../output/output_ast';
|
||||
import {BoundEventAst, DirectiveAst} from '../template_parser/template_ast';
|
||||
|
||||
import {CompileElement} from './compile_element';
|
||||
import {CompileMethod} from './compile_method';
|
||||
import {ViewProperties} from './constants';
|
||||
import {getHandleEventMethodName} from './util';
|
||||
|
||||
export class CompileEventListener {
|
||||
private _method: CompileMethod;
|
||||
private _hasComponentHostListener: boolean = false;
|
||||
private _methodName: string;
|
||||
private _eventParam: o.FnParam;
|
||||
private _actionResultExprs: o.Expression[] = [];
|
||||
|
||||
static getOrCreate(
|
||||
compileElement: CompileElement, eventTarget: string, eventName: string, eventPhase: string,
|
||||
targetEventListeners: CompileEventListener[]): CompileEventListener {
|
||||
var listener = targetEventListeners.find(
|
||||
listener => listener.eventTarget == eventTarget && listener.eventName == eventName &&
|
||||
listener.eventPhase == eventPhase);
|
||||
if (!listener) {
|
||||
listener = new CompileEventListener(
|
||||
compileElement, eventTarget, eventName, eventPhase, targetEventListeners.length);
|
||||
targetEventListeners.push(listener);
|
||||
}
|
||||
return listener;
|
||||
export function bindOutputs(
|
||||
boundEvents: BoundEventAst[], directives: DirectiveAst[], compileElement: CompileElement,
|
||||
bindToRenderer: boolean): boolean {
|
||||
const usedEvents = collectEvents(boundEvents, directives);
|
||||
if (!usedEvents.size) {
|
||||
return false;
|
||||
}
|
||||
|
||||
get methodName() { return this._methodName; }
|
||||
get isAnimation() { return !!this.eventPhase; }
|
||||
|
||||
constructor(
|
||||
public compileElement: CompileElement, public eventTarget: string, public eventName: string,
|
||||
public eventPhase: string, listenerIndex: number) {
|
||||
this._method = new CompileMethod(compileElement.view);
|
||||
this._methodName =
|
||||
`_handle_${sanitizeEventName(eventName)}_${compileElement.nodeIndex}_${listenerIndex}`;
|
||||
this._eventParam = new o.FnParam(
|
||||
EventHandlerVars.event.name,
|
||||
o.importType(this.compileElement.view.genConfig.renderTypes.renderEvent));
|
||||
if (bindToRenderer) {
|
||||
subscribeToRenderEvents(usedEvents, compileElement);
|
||||
}
|
||||
subscribeToDirectiveEvents(usedEvents, directives, compileElement);
|
||||
generateHandleEventMethod(boundEvents, directives, compileElement);
|
||||
return true;
|
||||
}
|
||||
|
||||
addAction(
|
||||
hostEvent: BoundEventAst, directive: CompileDirectiveMetadata,
|
||||
directiveInstance: o.Expression) {
|
||||
if (isPresent(directive) && directive.isComponent) {
|
||||
this._hasComponentHostListener = true;
|
||||
function collectEvents(
|
||||
boundEvents: BoundEventAst[], directives: DirectiveAst[]): Map<string, EventSummary> {
|
||||
const usedEvents = new Map<string, EventSummary>();
|
||||
boundEvents.forEach((event) => { usedEvents.set(event.fullName, event); });
|
||||
directives.forEach((dirAst) => {
|
||||
dirAst.hostEvents.forEach((event) => { usedEvents.set(event.fullName, event); });
|
||||
});
|
||||
return usedEvents;
|
||||
}
|
||||
|
||||
function subscribeToRenderEvents(
|
||||
usedEvents: Map<string, EventSummary>, compileElement: CompileElement) {
|
||||
const eventAndTargetExprs: o.Expression[] = [];
|
||||
usedEvents.forEach((event) => {
|
||||
if (!event.phase) {
|
||||
eventAndTargetExprs.push(o.literal(event.name), o.literal(event.target));
|
||||
}
|
||||
this._method.resetDebugInfo(this.compileElement.nodeIndex, hostEvent);
|
||||
var context = directiveInstance || this.compileElement.view.componentContext;
|
||||
const view = this.compileElement.view;
|
||||
});
|
||||
if (eventAndTargetExprs.length) {
|
||||
const disposableVar = o.variable(`disposable_${compileElement.view.disposables.length}`);
|
||||
compileElement.view.disposables.push(disposableVar);
|
||||
compileElement.view.createMethod.addStmt(
|
||||
disposableVar
|
||||
.set(o.importExpr(resolveIdentifier(Identifiers.subscribeToRenderElement)).callFn([
|
||||
ViewProperties.renderer, compileElement.renderNode,
|
||||
createInlineArray(eventAndTargetExprs), handleEventClosure(compileElement)
|
||||
]))
|
||||
.toDeclStmt(o.FUNCTION_TYPE, [o.StmtModifier.Private]));
|
||||
}
|
||||
}
|
||||
|
||||
function subscribeToDirectiveEvents(
|
||||
usedEvents: Map<string, EventSummary>, directives: DirectiveAst[],
|
||||
compileElement: CompileElement) {
|
||||
const usedEventNames = MapWrapper.keys(usedEvents);
|
||||
directives.forEach((dirAst) => {
|
||||
const dirWrapper = compileElement.directiveWrapperInstance.get(dirAst.directive.type.reference);
|
||||
compileElement.view.createMethod.addStmts(DirectiveWrapperExpressions.subscribe(
|
||||
dirAst.directive, dirAst.hostProperties, usedEventNames, dirWrapper,
|
||||
handleEventClosure(compileElement)));
|
||||
});
|
||||
}
|
||||
|
||||
function generateHandleEventMethod(
|
||||
boundEvents: BoundEventAst[], directives: DirectiveAst[], compileElement: CompileElement) {
|
||||
const hasComponentHostListener =
|
||||
directives.some((dirAst) => dirAst.hostEvents.some((event) => dirAst.directive.isComponent));
|
||||
|
||||
const markPathToRootStart =
|
||||
hasComponentHostListener ? compileElement.appElement.prop('componentView') : o.THIS_EXPR;
|
||||
const handleEventStmts = new CompileMethod(compileElement.view);
|
||||
handleEventStmts.resetDebugInfo(compileElement.nodeIndex, compileElement.sourceAst);
|
||||
handleEventStmts.push(markPathToRootStart.callMethod('markPathToRootAsCheckOnce', []).toStmt());
|
||||
const eventNameVar = o.variable('eventName');
|
||||
const resultVar = o.variable('result');
|
||||
handleEventStmts.push(resultVar.set(o.literal(true)).toDeclStmt(o.BOOL_TYPE));
|
||||
|
||||
directives.forEach((dirAst, dirIdx) => {
|
||||
const dirWrapper = compileElement.directiveWrapperInstance.get(dirAst.directive.type.reference);
|
||||
if (dirAst.hostEvents.length > 0) {
|
||||
handleEventStmts.push(
|
||||
resultVar
|
||||
.set(DirectiveWrapperExpressions
|
||||
.handleEvent(
|
||||
dirAst.hostEvents, dirWrapper, eventNameVar, EventHandlerVars.event)
|
||||
.and(resultVar))
|
||||
.toStmt());
|
||||
}
|
||||
});
|
||||
boundEvents.forEach((renderEvent, renderEventIdx) => {
|
||||
const evalResult = convertActionBinding(
|
||||
view, directive ? null : view, context, hostEvent.handler,
|
||||
`${this.compileElement.nodeIndex}_${this._actionResultExprs.length}`);
|
||||
compileElement.view, compileElement.view, compileElement.view.componentContext,
|
||||
renderEvent.handler, `sub_${renderEventIdx}`);
|
||||
const trueStmts = evalResult.stmts;
|
||||
if (evalResult.preventDefault) {
|
||||
this._actionResultExprs.push(evalResult.preventDefault);
|
||||
trueStmts.push(resultVar.set(evalResult.preventDefault.and(resultVar)).toStmt());
|
||||
}
|
||||
this._method.addStmts(evalResult.stmts);
|
||||
}
|
||||
|
||||
finishMethod() {
|
||||
var markPathToRootStart = this._hasComponentHostListener ?
|
||||
this.compileElement.appElement.prop('componentView') :
|
||||
o.THIS_EXPR;
|
||||
var resultExpr: o.Expression = o.literal(true);
|
||||
this._actionResultExprs.forEach((expr) => { resultExpr = resultExpr.and(expr); });
|
||||
var stmts =
|
||||
(<o.Statement[]>[markPathToRootStart.callMethod('markPathToRootAsCheckOnce', []).toStmt()])
|
||||
.concat(this._method.finish())
|
||||
.concat([new o.ReturnStatement(resultExpr)]);
|
||||
// private is fine here as no child view will reference the event handler...
|
||||
this.compileElement.view.methods.push(new o.ClassMethod(
|
||||
this._methodName, [this._eventParam], stmts, o.BOOL_TYPE, [o.StmtModifier.Private]));
|
||||
}
|
||||
|
||||
listenToRenderer() {
|
||||
var listenExpr: o.Expression;
|
||||
var eventListener = o.THIS_EXPR.callMethod(
|
||||
'eventHandler',
|
||||
[o.THIS_EXPR.prop(this._methodName).callMethod(o.BuiltinMethod.Bind, [o.THIS_EXPR])]);
|
||||
if (isPresent(this.eventTarget)) {
|
||||
listenExpr = ViewProperties.renderer.callMethod(
|
||||
'listenGlobal', [o.literal(this.eventTarget), o.literal(this.eventName), eventListener]);
|
||||
} else {
|
||||
listenExpr = ViewProperties.renderer.callMethod(
|
||||
'listen', [this.compileElement.renderNode, o.literal(this.eventName), eventListener]);
|
||||
}
|
||||
var disposable = o.variable(`disposable_${this.compileElement.view.disposables.length}`);
|
||||
this.compileElement.view.disposables.push(disposable);
|
||||
// private is fine here as no child view will reference the event handler...
|
||||
this.compileElement.view.createMethod.addStmt(
|
||||
disposable.set(listenExpr).toDeclStmt(o.FUNCTION_TYPE, [o.StmtModifier.Private]));
|
||||
}
|
||||
|
||||
listenToAnimation(animationTransitionVar: o.ReadVarExpr): o.Statement {
|
||||
const callbackMethod = this.eventPhase == 'start' ? 'onStart' : 'onDone';
|
||||
return animationTransitionVar
|
||||
.callMethod(
|
||||
callbackMethod,
|
||||
[o.THIS_EXPR.prop(this.methodName).callMethod(o.BuiltinMethod.Bind, [o.THIS_EXPR])])
|
||||
.toStmt();
|
||||
}
|
||||
|
||||
listenToDirective(directiveInstance: o.Expression, observablePropName: string) {
|
||||
var subscription = o.variable(`subscription_${this.compileElement.view.subscriptions.length}`);
|
||||
this.compileElement.view.subscriptions.push(subscription);
|
||||
var eventListener = o.THIS_EXPR.callMethod(
|
||||
'eventHandler',
|
||||
[o.THIS_EXPR.prop(this._methodName).callMethod(o.BuiltinMethod.Bind, [o.THIS_EXPR])]);
|
||||
this.compileElement.view.createMethod.addStmt(
|
||||
subscription
|
||||
.set(directiveInstance.prop(observablePropName)
|
||||
.callMethod(o.BuiltinMethod.SubscribeObservable, [eventListener]))
|
||||
.toDeclStmt(null, [o.StmtModifier.Final]));
|
||||
}
|
||||
}
|
||||
|
||||
export function collectEventListeners(
|
||||
hostEvents: BoundEventAst[], dirs: DirectiveAst[],
|
||||
compileElement: CompileElement): CompileEventListener[] {
|
||||
const eventListeners: CompileEventListener[] = [];
|
||||
|
||||
hostEvents.forEach((hostEvent) => {
|
||||
var listener = CompileEventListener.getOrCreate(
|
||||
compileElement, hostEvent.target, hostEvent.name, hostEvent.phase, eventListeners);
|
||||
listener.addAction(hostEvent, null, null);
|
||||
// TODO(tbosch): convert this into a `switch` once our OutputAst supports it.
|
||||
handleEventStmts.push(
|
||||
new o.IfStmt(eventNameVar.equals(o.literal(renderEvent.fullName)), trueStmts));
|
||||
});
|
||||
|
||||
dirs.forEach((directiveAst) => {
|
||||
var directiveInstance =
|
||||
compileElement.instances.get(identifierToken(directiveAst.directive.type).reference);
|
||||
directiveAst.hostEvents.forEach((hostEvent) => {
|
||||
var listener = CompileEventListener.getOrCreate(
|
||||
compileElement, hostEvent.target, hostEvent.name, hostEvent.phase, eventListeners);
|
||||
listener.addAction(hostEvent, directiveAst.directive, directiveInstance);
|
||||
});
|
||||
});
|
||||
|
||||
eventListeners.forEach((listener) => listener.finishMethod());
|
||||
return eventListeners;
|
||||
handleEventStmts.push(new o.ReturnStatement(resultVar));
|
||||
compileElement.view.methods.push(new o.ClassMethod(
|
||||
getHandleEventMethodName(compileElement.nodeIndex),
|
||||
[
|
||||
new o.FnParam(eventNameVar.name, o.STRING_TYPE),
|
||||
new o.FnParam(EventHandlerVars.event.name, o.DYNAMIC_TYPE)
|
||||
],
|
||||
handleEventStmts.finish(), o.BOOL_TYPE));
|
||||
}
|
||||
|
||||
export function bindDirectiveOutputs(
|
||||
directiveAst: DirectiveAst, directiveInstance: o.Expression,
|
||||
eventListeners: CompileEventListener[]) {
|
||||
Object.keys(directiveAst.directive.outputs).forEach(observablePropName => {
|
||||
const eventName = directiveAst.directive.outputs[observablePropName];
|
||||
|
||||
eventListeners.filter(listener => listener.eventName == eventName).forEach((listener) => {
|
||||
listener.listenToDirective(directiveInstance, observablePropName);
|
||||
});
|
||||
});
|
||||
function handleEventClosure(compileElement: CompileElement) {
|
||||
const handleEventMethodName = getHandleEventMethodName(compileElement.nodeIndex);
|
||||
return o.THIS_EXPR.callMethod(
|
||||
'eventHandler',
|
||||
[o.THIS_EXPR.prop(handleEventMethodName).callMethod(o.BuiltinMethod.Bind, [o.THIS_EXPR])]);
|
||||
}
|
||||
|
||||
export function bindRenderOutputs(eventListeners: CompileEventListener[]) {
|
||||
eventListeners.forEach(listener => {
|
||||
// the animation listeners are handled within property_binder.ts to
|
||||
// allow them to be placed next to the animation factory statements
|
||||
if (!listener.isAnimation) {
|
||||
listener.listenToRenderer();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function convertStmtIntoExpression(stmt: o.Statement): o.Expression {
|
||||
if (stmt instanceof o.ExpressionStatement) {
|
||||
return stmt.expr;
|
||||
} else if (stmt instanceof o.ReturnStatement) {
|
||||
return stmt.value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function sanitizeEventName(name: string): string {
|
||||
return name.replace(/[^a-zA-Z_]/g, '_');
|
||||
}
|
||||
type EventSummary = {
|
||||
name: string,
|
||||
target: string,
|
||||
phase: string
|
||||
}
|
Reference in New Issue
Block a user