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

@ -434,7 +434,7 @@ function extractHostBindings(
}); });
} }
const {attributes, listeners, properties, animations} = parseHostBindings(hostMetadata); const {attributes, listeners, properties} = parseHostBindings(hostMetadata);
filterToMembersWithDecorator(members, 'HostBinding', coreModule) filterToMembersWithDecorator(members, 'HostBinding', coreModule)
.forEach(({member, decorators}) => { .forEach(({member, decorators}) => {

View File

@ -434,14 +434,14 @@ describe('compiler compliance', () => {
$r3$.ɵallocHostVars(14); $r3$.ɵallocHostVars(14);
} }
if (rf & 2) { if (rf & 2) {
$r3$.ɵelementProperty(elIndex, "expansionHeight", $r3$.ɵcomponentHostSyntheticProperty(elIndex, "@expansionHeight",
$r3$.ɵbind( $r3$.ɵbind(
$r3$.ɵpureFunction2(5, $_c1$, ctx.getExpandedState(), $r3$.ɵpureFunction2(5, $_c1$, ctx.getExpandedState(),
$r3$.ɵpureFunction2(2, $_c0$, ctx.collapsedHeight, ctx.expandedHeight) $r3$.ɵpureFunction2(2, $_c0$, ctx.collapsedHeight, ctx.expandedHeight)
) )
), null, true ), null, true
); );
$r3$.ɵelementProperty(elIndex, "expansionWidth", $r3$.ɵcomponentHostSyntheticProperty(elIndex, "@expansionWidth",
$r3$.ɵbind( $r3$.ɵbind(
$r3$.ɵpureFunction2(11, $_c1$, ctx.getExpandedState(), $r3$.ɵpureFunction2(11, $_c1$, ctx.getExpandedState(),
$r3$.ɵpureFunction2(8, $_c2$, ctx.collapsedWidth, ctx.expandedWidth) $r3$.ɵpureFunction2(8, $_c2$, ctx.collapsedWidth, ctx.expandedWidth)

View File

@ -173,7 +173,7 @@ describe('r3_view_compiler', () => {
}; };
const template = ` const template = `
const _c0 = [3, "mySelector"]; const _c0 = [3, "@mySelector"];
// ... // ...
template: function MyApp_Template(rf, ctx) { template: function MyApp_Template(rf, ctx) {
if (rf & 1) { if (rf & 1) {

View File

@ -281,8 +281,8 @@ describe('compiler compliance: styling', () => {
template: function MyComponent_Template(rf, ctx) { template: function MyComponent_Template(rf, ctx) {
if (rf & 1) { if (rf & 1) {
$r3$.ɵelementStart(0, "div", _c0); $r3$.ɵelementStart(0, "div", _c0);
$r3$.ɵlistener("@myAnimation.start", function MyComponent_Template_div__myAnimation_start_0_listener($event) { return ctx.onStart($event); }); $r3$.ɵlistener("@myAnimation.start", function MyComponent_Template_div_animation_myAnimation_start_0_listener($event) { return ctx.onStart($event); });
$r3$.ɵlistener("@myAnimation.done", function MyComponent_Template_div__myAnimation_done_0_listener($event) { return ctx.onDone($event); }); $r3$.ɵlistener("@myAnimation.done", function MyComponent_Template_div_animation_myAnimation_done_0_listener($event) { return ctx.onDone($event); });
$r3$.ɵelementEnd(); $r3$.ɵelementEnd();
} if (rf & 2) { } if (rf & 2) {
$r3$.ɵelementProperty(0, "@myAnimation", $r3$.ɵbind(ctx.exp)); $r3$.ɵelementProperty(0, "@myAnimation", $r3$.ɵbind(ctx.exp));
@ -296,6 +296,64 @@ describe('compiler compliance: styling', () => {
const result = compile(files, angularFiles); const result = compile(files, angularFiles);
expectEmit(result.source, template, 'Incorrect template'); expectEmit(result.source, template, 'Incorrect template');
}); });
it('should generate animation host binding and listener code for directives', () => {
const files = {
app: {
'spec.ts': `
import {Directive, Component, NgModule} from '@angular/core';
@Directive({
selector: '[my-anim-dir]',
animations: [
{name: 'myAnim'}
],
host: {
'[@myAnim]': 'myAnimState',
'(@myAnim.start)': 'onStart()',
'(@myAnim.done)': 'onDone()'
}
})
class MyAnimDir {
onStart() {}
onDone() {}
myAnimState = '123';
}
@Component({
selector: 'my-cmp',
template: \`
<div my-anim-dir></div>
\`
})
class MyComponent {
}
@NgModule({declarations: [MyComponent, MyAnimDir]})
export class MyModule {}
`
}
};
const template = `
MyAnimDir.ngDirectiveDef = $r3$.ɵdefineDirective({
hostBindings: function MyAnimDir_HostBindings(rf, ctx, elIndex) {
if (rf & 1) {
$r3$.ɵallocHostVars(1);
$r3$.ɵlistener("@myAnim.start", function MyAnimDir_animation_myAnim_start_HostBindingHandler($event) { return ctx.onStart(); });
$r3$.ɵlistener("@myAnim.done", function MyAnimDir_animation_myAnim_done_HostBindingHandler($event) { return ctx.onDone(); });
} if (rf & 2) {
$r3$.ɵcomponentHostSyntheticProperty(elIndex, "@myAnim", $r3$.ɵbind(ctx.myAnimState), null, true);
}
}
});
`;
const result = compile(files, angularFiles);
expectEmit(result.source, template, 'Incorrect template');
});
}); });
describe('[style] and [style.prop]', () => { describe('[style] and [style.prop]', () => {

View File

@ -250,11 +250,7 @@ function extractHostBindings(host: {[key: string]: string}, propMetadata: {[key:
properties: StringMap, properties: StringMap,
} { } {
// First parse the declarations from the metadata. // First parse the declarations from the metadata.
const {attributes, listeners, properties, animations} = parseHostBindings(host || {}); const {attributes, listeners, properties} = parseHostBindings(host || {});
if (Object.keys(animations).length > 0) {
throw new Error(`Animation bindings are as-of-yet unsupported in Ivy`);
}
// Next, loop over the properties of the object, looking for @HostBinding and @HostListener. // Next, loop over the properties of the object, looking for @HostBinding and @HostListener.
for (const field in propMetadata) { for (const field in propMetadata) {

View File

@ -31,6 +31,9 @@ export class Identifiers {
static elementProperty: o.ExternalReference = {name: 'ɵelementProperty', moduleName: CORE}; 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 elementAttribute: o.ExternalReference = {name: 'ɵelementAttribute', moduleName: CORE};
static elementClassProp: o.ExternalReference = {name: 'ɵelementClassProp', moduleName: CORE}; static elementClassProp: o.ExternalReference = {name: 'ɵelementClassProp', moduleName: CORE};

View File

@ -52,3 +52,31 @@ export interface R3Reference {
value: o.Expression; value: o.Expression;
type: 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 {BindingForm, convertActionBinding, 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} from '../../expression_parser/ast'; import {AST, ParsedEvent, ParsedEventType, ParsedProperty} from '../../expression_parser/ast';
import {LifecycleHooks} from '../../lifecycle_reflector'; import {LifecycleHooks} from '../../lifecycle_reflector';
import {DEFAULT_INTERPOLATION_CONFIG} from '../../ml_parser/interpolation_config'; import {DEFAULT_INTERPOLATION_CONFIG} from '../../ml_parser/interpolation_config';
import * as o from '../../output/output_ast'; import * as o from '../../output/output_ast';
@ -25,7 +25,7 @@ import {OutputContext, error} from '../../util';
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 {typeWithParameters} from '../util'; import {prepareSyntheticListenerFunctionName, prepareSyntheticListenerName, 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';
@ -697,7 +697,7 @@ function createHostBindingsFunction(
const value = binding.expression.visit(valueConverter); const value = binding.expression.visit(valueConverter);
const bindingExpr = bindingFn(bindingContext, value); const bindingExpr = bindingFn(bindingContext, value);
const {bindingName, instruction, extraParams} = getBindingNameAndInstruction(name); const {bindingName, instruction, extraParams} = getBindingNameAndInstruction(binding);
const instructionParams: o.Expression[] = [ const instructionParams: o.Expression[] = [
elVarExp, o.literal(bindingName), o.importExpr(R3.bind).callFn([bindingExpr.currValExpr]) elVarExp, o.literal(bindingName), o.importExpr(R3.bind).callFn([bindingExpr.currValExpr])
@ -775,8 +775,9 @@ function createStylingStmt(
.toStmt(); .toStmt();
} }
function getBindingNameAndInstruction(bindingName: string): function getBindingNameAndInstruction(binding: ParsedProperty):
{bindingName: string, instruction: o.ExternalReference, extraParams: o.Expression[]} { {bindingName: string, instruction: o.ExternalReference, extraParams: o.Expression[]} {
let bindingName = binding.name;
let instruction !: o.ExternalReference; let instruction !: o.ExternalReference;
const extraParams: o.Expression[] = []; const extraParams: o.Expression[] = [];
@ -786,7 +787,15 @@ function getBindingNameAndInstruction(bindingName: string):
bindingName = attrMatches[1]; bindingName = attrMatches[1];
instruction = R3.elementAttribute; instruction = R3.elementAttribute;
} else { } 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( extraParams.push(
o.literal(null), // TODO: This should be a sanitizer fn (FW-785) o.literal(null), // TODO: This should be a sanitizer fn (FW-785)
o.literal(true) // host bindings must have nativeOnly prop set to true o.literal(true) // host bindings must have nativeOnly prop set to true
@ -802,14 +811,19 @@ function createHostListeners(
return eventBindings.map(binding => { return eventBindings.map(binding => {
const bindingExpr = convertActionBinding( const bindingExpr = convertActionBinding(
null, bindingContext, binding.handler, 'b', () => error('Unexpected interpolation')); 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 typeName = meta.name;
const functionName = const functionName =
typeName && bindingName ? `${typeName}_${bindingName}_HostBindingHandler` : null; typeName && bindingName ? `${typeName}_${bindingFnName}_HostBindingHandler` : null;
const handler = o.fn( const handler = o.fn(
[new o.FnParam('$event', o.DYNAMIC_TYPE)], [...bindingExpr.render3Stmts], o.INFERRED_TYPE, [new o.FnParam('$event', o.DYNAMIC_TYPE)], [...bindingExpr.render3Stmts], o.INFERRED_TYPE,
null, functionName); 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); return new Map(entries);
} }
const HOST_REG_EXP = /^(?:(?:\[([^\]]+)\])|(?:\(([^\)]+)\)))|(\@[-\w]+)$/; const HOST_REG_EXP = /^(?:\[([^\]]+)\])|(?:\(([^\)]+)\))$/;
// Represents the groups in the above regex. // Represents the groups in the above regex.
const enum HostBindingGroup { 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, Binding = 1,
// group 2: "event" from "(event)" // group 2: "event" from "(event)"
Event = 2, Event = 2,
// group 3: "@trigger" from "@trigger"
Animation = 3,
} }
export function parseHostBindings(host: {[key: string]: string}): { export function parseHostBindings(host: {[key: string]: string}): {
attributes: {[key: string]: string}, attributes: {[key: string]: string},
listeners: {[key: string]: string}, listeners: {[key: string]: string},
properties: {[key: string]: string}, properties: {[key: string]: string},
animations: {[key: string]: string},
} { } {
const attributes: {[key: string]: string} = {}; const attributes: {[key: string]: string} = {};
const listeners: {[key: string]: string} = {}; const listeners: {[key: string]: string} = {};
const properties: {[key: string]: string} = {}; const properties: {[key: string]: string} = {};
const animations: {[key: string]: string} = {};
Object.keys(host).forEach(key => { Object.keys(host).forEach(key => {
const value = host[key]; const value = host[key];
@ -863,15 +871,16 @@ export function parseHostBindings(host: {[key: string]: string}): {
if (matches === null) { if (matches === null) {
attributes[key] = value; attributes[key] = value;
} else if (matches[HostBindingGroup.Binding] != null) { } 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; properties[matches[HostBindingGroup.Binding]] = value;
} else if (matches[HostBindingGroup.Event] != null) { } else if (matches[HostBindingGroup.Event] != null) {
listeners[matches[HostBindingGroup.Event]] = value; 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[] { 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 {BindingForm, BuiltinFunctionCall, LocalResolver, convertActionBinding, convertPropertyBinding} from '../../compiler_util/expression_converter';
import {ConstantPool} from '../../constant_pool'; import {ConstantPool} from '../../constant_pool';
import * as core from '../../core'; 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 {Lexer} from '../../expression_parser/lexer';
import {Parser} from '../../expression_parser/parser'; import {Parser} from '../../expression_parser/parser';
import * as i18n from '../../i18n/i18n_ast'; import * as i18n from '../../i18n/i18n_ast';
@ -29,6 +29,7 @@ import {error} from '../../util';
import * as t from '../r3_ast'; import * as t from '../r3_ast';
import {Identifiers as R3} from '../r3_identifiers'; import {Identifiers as R3} from '../r3_identifiers';
import {htmlAstToRender3Ast} from '../r3_template_transform'; import {htmlAstToRender3Ast} from '../r3_template_transform';
import {getSyntheticPropertyName, prepareSyntheticListenerFunctionName, prepareSyntheticListenerName, prepareSyntheticPropertyName} from '../util';
import {R3QueryMetadata} from './api'; import {R3QueryMetadata} from './api';
import {I18nContext} from './i18n/context'; import {I18nContext} from './i18n/context';
@ -651,11 +652,12 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
const value = input.value.visit(this._valueConverter); const value = input.value.visit(this._valueConverter);
// setProperty without a value doesn't make any sense // setProperty without a value doesn't make any sense
if (value.name || value.value) { if (value.name || value.value) {
const bindingName = prepareSyntheticPropertyName(input.name);
this.allocateBindingSlots(value); this.allocateBindingSlots(value);
const name = prepareSyntheticAttributeName(input.name);
this.updateInstruction(input.sourceSpan, R3.elementProperty, () => { this.updateInstruction(input.sourceSpan, R3.elementProperty, () => {
return [ 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)) { if (isASTWithSource(valueExp)) {
const literal = valueExp.ast; const literal = valueExp.ast;
if (isLiteralPrimitive(literal) && literal.value === undefined) { if (isLiteralPrimitive(literal) && literal.value === undefined) {
addAttrExpr(prepareSyntheticAttributeName(input.name), EMPTY_STRING_EXPR); addAttrExpr(prepareSyntheticPropertyName(input.name), EMPTY_STRING_EXPR);
} }
} }
} else { } else {
@ -1021,7 +1023,11 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
if (nonSyntheticInputs.length || outputs.length) { if (nonSyntheticInputs.length || outputs.length) {
addAttrExpr(core.AttributeMarker.SelectOnly); addAttrExpr(core.AttributeMarker.SelectOnly);
nonSyntheticInputs.forEach((i: t.BoundAttribute) => addAttrExpr(i.name)); 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; return attrExprs;
@ -1064,14 +1070,19 @@ 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 eventName: string = outputAst.name;
let bindingFnName;
if (outputAst.type === ParsedEventType.Animation) { 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 tagNameSanitized = sanitizeIdentifier(tagName);
const functionName = const functionName =
`${this.templateName}_${tagNameSanitized}_${evNameSanitized}_${index}_listener`; `${this.templateName}_${tagNameSanitized}_${bindingFnName}_${index}_listener`;
return () => { return () => {
const listenerScope = this._bindingScope.nestedScope(this._bindingScope.bindingLevel); 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] { function isSingleElementTemplate(children: t.Node[]): children is[t.Element] {
return children.length === 1 && children[0] instanceof t.Element; return children.length === 1 && children[0] instanceof t.Element;
} }

View File

@ -440,4 +440,4 @@ export function calcPossibleSecurityContexts(
elementName => registry.securityContext(elementName, propName, isAttribute))); elementName => registry.securityContext(elementName, propName, isAttribute)));
}); });
return ctxs.length === 0 ? [SecurityContext.NONE] : Array.from(new Set(ctxs)).sort(); return ctxs.length === 0 ? [SecurityContext.NONE] : Array.from(new Set(ctxs)).sort();
} }

View File

@ -83,6 +83,7 @@ export {
loadQueryList as ɵloadQueryList, loadQueryList as ɵloadQueryList,
elementEnd as ɵelementEnd, elementEnd as ɵelementEnd,
elementProperty as ɵelementProperty, elementProperty as ɵelementProperty,
componentHostSyntheticProperty as ɵcomponentHostSyntheticProperty,
projectionDef as ɵprojectionDef, projectionDef as ɵprojectionDef,
reference as ɵreference, reference as ɵreference,
enableBindings as ɵenableBindings, enableBindings as ɵenableBindings,

View File

@ -44,6 +44,7 @@ export {
elementClassProp, elementClassProp,
elementEnd, elementEnd,
elementProperty, elementProperty,
componentHostSyntheticProperty,
elementStart, elementStart,
elementContainerStart, elementContainerStart,

View File

@ -987,10 +987,48 @@ export function elementAttribute(
* @param nativeOnly Whether or not we should only set native properties and skip input check * @param nativeOnly Whether or not we should only set native properties and skip input check
* (this is necessary for host property bindings) * (this is necessary for host property bindings)
*/ */
export function elementProperty<T>( export function elementProperty<T>(
index: number, propName: string, value: T | NO_CHANGE, sanitizer?: SanitizerFn | null, index: number, propName: string, value: T | NO_CHANGE, sanitizer?: SanitizerFn | null,
nativeOnly?: boolean): void { nativeOnly?: boolean): void {
elementPropertyInternal(index, propName, value, sanitizer, nativeOnly);
}
/**
* Updates a synthetic host binding (e.g. `[@foo]`) on a component.
*
* This instruction is for compatibility purposes and is designed to ensure that a
* synthetic host binding (e.g. `@HostBinding('@foo')`) properly gets rendered in
* the component's renderer. Normally all host bindings are evaluated with the parent
* component's renderer, but, in the case of animation @triggers, they need to be
* evaluated with the sub components renderer (because that's where the animation
* triggers are defined).
*
* Do not use this instruction as a replacement for `elementProperty`. This instruction
* only exists to ensure compatibility with the ViewEngine's host binding behavior.
*
* @param index The index of the element to update in the data array
* @param propName Name of property. Because it is going to DOM, this is not subject to
* renaming as part of minification.
* @param value New value to write.
* @param sanitizer An optional function used to sanitize the value.
* @param nativeOnly Whether or not we should only set native properties and skip input check
* (this is necessary for host property bindings)
*/
export function componentHostSyntheticProperty<T>(
index: number, propName: string, value: T | NO_CHANGE, sanitizer?: SanitizerFn | null,
nativeOnly?: boolean) {
elementPropertyInternal(index, propName, value, sanitizer, nativeOnly, loadComponentRenderer);
}
function loadComponentRenderer(tNode: TNode, lView: LView): Renderer3 {
const componentLView = lView[tNode.index] as LView;
return componentLView[RENDERER];
}
function elementPropertyInternal<T>(
index: number, propName: string, value: T | NO_CHANGE, sanitizer?: SanitizerFn | null,
nativeOnly?: boolean,
loadRendererFn?: ((tNode: TNode, lView: LView) => Renderer3) | null): void {
if (value === NO_CHANGE) return; if (value === NO_CHANGE) return;
const lView = getLView(); const lView = getLView();
const element = getNativeByIndex(index, lView) as RElement | RComment; const element = getNativeByIndex(index, lView) as RElement | RComment;
@ -1007,7 +1045,7 @@ export function elementProperty<T>(
} }
} }
} else if (tNode.type === TNodeType.Element) { } else if (tNode.type === TNodeType.Element) {
const renderer = lView[RENDERER]; const renderer = loadRendererFn ? loadRendererFn(tNode, lView) : lView[RENDERER];
// It is assumed that the sanitizer is only added when the compiler determines that the property // It is assumed that the sanitizer is only added when the compiler determines that the property
// is risky, so sanitization can be done without further checks. // is risky, so sanitization can be done without further checks.
value = sanitizer != null ? (sanitizer(value) as any) : value; value = sanitizer != null ? (sanitizer(value) as any) : value;

View File

@ -78,6 +78,7 @@ export const angularCoreEnv: {[name: string]: Function} = {
'ɵload': r3.load, 'ɵload': r3.load,
'ɵprojection': r3.projection, 'ɵprojection': r3.projection,
'ɵelementProperty': r3.elementProperty, 'ɵelementProperty': r3.elementProperty,
'ɵcomponentHostSyntheticProperty': r3.componentHostSyntheticProperty,
'ɵpipeBind1': r3.pipeBind1, 'ɵpipeBind1': r3.pipeBind1,
'ɵpipeBind2': r3.pipeBind2, 'ɵpipeBind2': r3.pipeBind2,
'ɵpipeBind3': r3.pipeBind3, 'ɵpipeBind3': r3.pipeBind3,

View File

@ -563,6 +563,9 @@
{ {
"name": "elementProperty" "name": "elementProperty"
}, },
{
"name": "elementPropertyInternal"
},
{ {
"name": "elementStart" "name": "elementStart"
}, },

View File

@ -2102,6 +2102,49 @@ describe('render3 integration test', () => {
const [elm, attr, value] = spy.calls.mostRecent().args; const [elm, attr, value] = spy.calls.mostRecent().args;
expect(attr).toEqual('@fooAnimation'); expect(attr).toEqual('@fooAnimation');
}); });
it('should allow host binding animations to be picked up and rendered', () => {
class ChildCompWithAnim {
static ngDirectiveDef = defineDirective({
type: ChildCompWithAnim,
factory: () => new ChildCompWithAnim(),
selectors: [['child-comp-with-anim']],
hostBindings: function(rf: RenderFlags, ctx: any, elementIndex: number): void {
if (rf & RenderFlags.Update) {
elementProperty(0, '@fooAnim', ctx.exp);
}
},
});
exp = 'go';
}
class ParentComp {
static ngComponentDef = defineComponent({
type: ParentComp,
consts: 1,
vars: 1,
selectors: [['foo']],
factory: () => new ParentComp(),
template: (rf: RenderFlags, ctx: ParentComp) => {
if (rf & RenderFlags.Create) {
element(0, 'child-comp-with-anim');
}
},
directives: [ChildCompWithAnim]
});
}
const rendererFactory = new MockRendererFactory(['setProperty']);
const fixture = new ComponentFixture(ParentComp, {rendererFactory});
const renderer = rendererFactory.lastRenderer !;
fixture.update();
const spy = renderer.spies['setProperty'];
const [elm, attr, value] = spy.calls.mostRecent().args;
expect(attr).toEqual('@fooAnim');
});
}); });
describe('element discovery', () => { describe('element discovery', () => {