fix(ivy): ensure @animation host bindings/listeners work properly (#27896)

PR Close #27896
This commit is contained in:
Matias Niemelä
2019-01-03 18:24:21 -08:00
committed by Kara Erickson
parent 0bd9deb9f5
commit 5d3dcfc6ad
16 changed files with 234 additions and 46 deletions

View File

@ -31,6 +31,9 @@ export class Identifiers {
static elementProperty: o.ExternalReference = {name: 'ɵelementProperty', moduleName: CORE};
static componentHostSyntheticProperty:
o.ExternalReference = {name: 'ɵcomponentHostSyntheticProperty', moduleName: CORE};
static elementAttribute: o.ExternalReference = {name: 'ɵelementAttribute', moduleName: CORE};
static elementClassProp: o.ExternalReference = {name: 'ɵelementClassProp', moduleName: CORE};

View File

@ -52,3 +52,31 @@ export interface R3Reference {
value: o.Expression;
type: o.Expression;
}
const ANIMATE_SYMBOL_PREFIX = '@';
export function prepareSyntheticPropertyName(name: string) {
return `${ANIMATE_SYMBOL_PREFIX}${name}`;
}
export function prepareSyntheticListenerName(name: string, phase: string) {
return `${ANIMATE_SYMBOL_PREFIX}${name}.${phase}`;
}
export function isSyntheticPropertyOrListener(name: string) {
return name.charAt(0) == ANIMATE_SYMBOL_PREFIX;
}
export function getSyntheticPropertyName(name: string) {
// this will strip out listener phase values...
// @foo.start => @foo
const i = name.indexOf('.');
name = i > 0 ? name.substring(0, i) : name;
if (name.charAt(0) !== ANIMATE_SYMBOL_PREFIX) {
name = ANIMATE_SYMBOL_PREFIX + name;
}
return name;
}
export function prepareSyntheticListenerFunctionName(name: string, phase: string) {
return `animation_${name}_${phase}`;
}

View File

@ -12,7 +12,7 @@ import {CompileReflector} from '../../compile_reflector';
import {BindingForm, convertActionBinding, convertPropertyBinding} from '../../compiler_util/expression_converter';
import {ConstantPool, DefinitionKind} from '../../constant_pool';
import * as core from '../../core';
import {AST, ParsedEvent} from '../../expression_parser/ast';
import {AST, ParsedEvent, ParsedEventType, ParsedProperty} from '../../expression_parser/ast';
import {LifecycleHooks} from '../../lifecycle_reflector';
import {DEFAULT_INTERPOLATION_CONFIG} from '../../ml_parser/interpolation_config';
import * as o from '../../output/output_ast';
@ -25,7 +25,7 @@ import {OutputContext, error} from '../../util';
import {compileFactoryFunction, dependenciesFromGlobalMetadata} from '../r3_factory';
import {Identifiers as R3} from '../r3_identifiers';
import {Render3ParseResult} from '../r3_template_transform';
import {typeWithParameters} from '../util';
import {prepareSyntheticListenerFunctionName, prepareSyntheticListenerName, prepareSyntheticPropertyName, typeWithParameters} from '../util';
import {R3ComponentDef, R3ComponentMetadata, R3DirectiveDef, R3DirectiveMetadata, R3QueryMetadata} from './api';
import {StylingBuilder, StylingInstruction} from './styling_builder';
@ -697,7 +697,7 @@ function createHostBindingsFunction(
const value = binding.expression.visit(valueConverter);
const bindingExpr = bindingFn(bindingContext, value);
const {bindingName, instruction, extraParams} = getBindingNameAndInstruction(name);
const {bindingName, instruction, extraParams} = getBindingNameAndInstruction(binding);
const instructionParams: o.Expression[] = [
elVarExp, o.literal(bindingName), o.importExpr(R3.bind).callFn([bindingExpr.currValExpr])
@ -775,8 +775,9 @@ function createStylingStmt(
.toStmt();
}
function getBindingNameAndInstruction(bindingName: string):
function getBindingNameAndInstruction(binding: ParsedProperty):
{bindingName: string, instruction: o.ExternalReference, extraParams: o.Expression[]} {
let bindingName = binding.name;
let instruction !: o.ExternalReference;
const extraParams: o.Expression[] = [];
@ -786,7 +787,15 @@ function getBindingNameAndInstruction(bindingName: string):
bindingName = attrMatches[1];
instruction = R3.elementAttribute;
} else {
instruction = R3.elementProperty;
if (binding.isAnimation) {
bindingName = prepareSyntheticPropertyName(bindingName);
// host bindings that have a synthetic property (e.g. @foo) should always be rendered
// in the context of the component and not the parent. Therefore there is a special
// compatibility instruction available for this purpose.
instruction = R3.componentHostSyntheticProperty;
} else {
instruction = R3.elementProperty;
}
extraParams.push(
o.literal(null), // TODO: This should be a sanitizer fn (FW-785)
o.literal(true) // host bindings must have nativeOnly prop set to true
@ -802,14 +811,19 @@ function createHostListeners(
return eventBindings.map(binding => {
const bindingExpr = convertActionBinding(
null, bindingContext, binding.handler, 'b', () => error('Unexpected interpolation'));
const bindingName = binding.name && sanitizeIdentifier(binding.name);
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}_${bindingName}_HostBindingHandler` : null;
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(binding.name), handler]).toStmt();
return o.importExpr(R3.listener).callFn([o.literal(bindingName), handler]).toStmt();
});
}
@ -832,30 +846,24 @@ function typeMapToExpressionMap(
return new Map(entries);
}
const HOST_REG_EXP = /^(?:(?:\[([^\]]+)\])|(?:\(([^\)]+)\)))|(\@[-\w]+)$/;
const HOST_REG_EXP = /^(?:\[([^\]]+)\])|(?:\(([^\)]+)\))$/;
// Represents the groups in the above regex.
const enum HostBindingGroup {
// group 1: "prop" from "[prop]", or "attr.role" from "[attr.role]"
// group 1: "prop" from "[prop]", or "attr.role" from "[attr.role]", or @anim from [@anim]
Binding = 1,
// group 2: "event" from "(event)"
Event = 2,
// group 3: "@trigger" from "@trigger"
Animation = 3,
}
export function parseHostBindings(host: {[key: string]: string}): {
attributes: {[key: string]: string},
listeners: {[key: string]: string},
properties: {[key: string]: string},
animations: {[key: string]: string},
} {
const attributes: {[key: string]: string} = {};
const listeners: {[key: string]: string} = {};
const properties: {[key: string]: string} = {};
const animations: {[key: string]: string} = {};
Object.keys(host).forEach(key => {
const value = host[key];
@ -863,15 +871,16 @@ export function parseHostBindings(host: {[key: string]: string}): {
if (matches === null) {
attributes[key] = value;
} else if (matches[HostBindingGroup.Binding] != null) {
// synthetic properties (the ones that have a `@` as a prefix)
// are still treated the same as regular properties. Therefore
// there is no point in storing them in a separate map.
properties[matches[HostBindingGroup.Binding]] = value;
} else if (matches[HostBindingGroup.Event] != null) {
listeners[matches[HostBindingGroup.Event]] = value;
} else if (matches[HostBindingGroup.Animation] != null) {
animations[matches[HostBindingGroup.Animation]] = value;
}
});
return {attributes, listeners, properties, animations};
return {attributes, listeners, properties};
}
function compileStyles(styles: string[], selector: string, hostSelector: string): string[] {

View File

@ -10,7 +10,7 @@ import {flatten, sanitizeIdentifier} from '../../compile_metadata';
import {BindingForm, BuiltinFunctionCall, LocalResolver, convertActionBinding, convertPropertyBinding} from '../../compiler_util/expression_converter';
import {ConstantPool} from '../../constant_pool';
import * as core from '../../core';
import {AST, ASTWithSource, AstMemoryEfficientTransformer, BindingPipe, BindingType, FunctionCall, ImplicitReceiver, Interpolation, LiteralArray, LiteralMap, LiteralPrimitive, ParsedEventType, PropertyRead} from '../../expression_parser/ast';
import {AST, ASTWithSource, AstMemoryEfficientTransformer, BindingPipe, BindingType, FunctionCall, ImplicitReceiver, Interpolation, LiteralArray, LiteralMap, LiteralPrimitive, ParsedEvent, ParsedEventType, PropertyRead} from '../../expression_parser/ast';
import {Lexer} from '../../expression_parser/lexer';
import {Parser} from '../../expression_parser/parser';
import * as i18n from '../../i18n/i18n_ast';
@ -29,6 +29,7 @@ import {error} from '../../util';
import * as t from '../r3_ast';
import {Identifiers as R3} from '../r3_identifiers';
import {htmlAstToRender3Ast} from '../r3_template_transform';
import {getSyntheticPropertyName, prepareSyntheticListenerFunctionName, prepareSyntheticListenerName, prepareSyntheticPropertyName} from '../util';
import {R3QueryMetadata} from './api';
import {I18nContext} from './i18n/context';
@ -651,11 +652,12 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
const value = input.value.visit(this._valueConverter);
// setProperty without a value doesn't make any sense
if (value.name || value.value) {
const bindingName = prepareSyntheticPropertyName(input.name);
this.allocateBindingSlots(value);
const name = prepareSyntheticAttributeName(input.name);
this.updateInstruction(input.sourceSpan, R3.elementProperty, () => {
return [
o.literal(elementIndex), o.literal(name), this.convertPropertyBinding(implicit, value)
o.literal(elementIndex), o.literal(bindingName),
this.convertPropertyBinding(implicit, value)
];
});
}
@ -1002,7 +1004,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
if (isASTWithSource(valueExp)) {
const literal = valueExp.ast;
if (isLiteralPrimitive(literal) && literal.value === undefined) {
addAttrExpr(prepareSyntheticAttributeName(input.name), EMPTY_STRING_EXPR);
addAttrExpr(prepareSyntheticPropertyName(input.name), EMPTY_STRING_EXPR);
}
}
} else {
@ -1021,7 +1023,11 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
if (nonSyntheticInputs.length || outputs.length) {
addAttrExpr(core.AttributeMarker.SelectOnly);
nonSyntheticInputs.forEach((i: t.BoundAttribute) => addAttrExpr(i.name));
outputs.forEach((o: t.BoundEvent) => addAttrExpr(o.name));
outputs.forEach((o: t.BoundEvent) => {
const name =
o.type === ParsedEventType.Animation ? getSyntheticPropertyName(o.name) : o.name;
addAttrExpr(name);
});
}
return attrExprs;
@ -1064,14 +1070,19 @@ 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) {
eventName = prepareSyntheticAttributeName(`${outputAst.name}.${outputAst.phase}`);
// 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 evNameSanitized = sanitizeIdentifier(eventName);
const tagNameSanitized = sanitizeIdentifier(tagName);
const functionName =
`${this.templateName}_${tagNameSanitized}_${evNameSanitized}_${index}_listener`;
`${this.templateName}_${tagNameSanitized}_${bindingFnName}_${index}_listener`;
return () => {
const listenerScope = this._bindingScope.nestedScope(this._bindingScope.bindingLevel);
@ -1563,10 +1574,6 @@ function resolveSanitizationFn(input: t.BoundAttribute, context: core.SecurityCo
}
}
function prepareSyntheticAttributeName(name: string) {
return '@' + name;
}
function isSingleElementTemplate(children: t.Node[]): children is[t.Element] {
return children.length === 1 && children[0] instanceof t.Element;
}