fix(ivy): ensure @animation host bindings/listeners work properly (#27896)
PR Close #27896
This commit is contained in:
parent
0bd9deb9f5
commit
5d3dcfc6ad
@ -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}) => {
|
||||||
|
@ -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)
|
||||||
|
@ -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) {
|
||||||
|
@ -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]', () => {
|
||||||
|
@ -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) {
|
||||||
|
@ -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};
|
||||||
|
@ -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}`;
|
||||||
|
}
|
@ -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[] {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
@ -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,
|
||||||
|
@ -44,6 +44,7 @@ export {
|
|||||||
elementClassProp,
|
elementClassProp,
|
||||||
elementEnd,
|
elementEnd,
|
||||||
elementProperty,
|
elementProperty,
|
||||||
|
componentHostSyntheticProperty,
|
||||||
elementStart,
|
elementStart,
|
||||||
|
|
||||||
elementContainerStart,
|
elementContainerStart,
|
||||||
|
@ -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;
|
||||||
|
@ -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,
|
||||||
|
@ -563,6 +563,9 @@
|
|||||||
{
|
{
|
||||||
"name": "elementProperty"
|
"name": "elementProperty"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "elementPropertyInternal"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "elementStart"
|
"name": "elementStart"
|
||||||
},
|
},
|
||||||
|
@ -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', () => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user