fix(ivy): ensure animation @trigger ordering is correctly delivered to the renderer (#28165)
In Ivy when elements are created a series of static attribute names are provided over to the construction instruction of that element. Static attribute names include non-binding attribues (like `<div selected>`) as well as animation bindings that do not have a RHS value (like `<div @foo>`). Because of this distinction, value-less animation triggers are rendered first before value-full animation bindings are and this improper ordering has caused various existing tests to fail. This patch ensures that animation bindings are evaluated in the order that they exist within the HTML template code (or host binding code). PR Close #28165
This commit is contained in:
parent
0d6913f037
commit
e172e97e13
|
@ -122,7 +122,7 @@ describe('r3_view_compiler', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('animations', () => {
|
describe('animations', () => {
|
||||||
it('should keep @attr but suppress [@attr]', () => {
|
it('should not register any @attr attributes as static attributes', () => {
|
||||||
const files: MockDirectory = {
|
const files: MockDirectory = {
|
||||||
app: {
|
app: {
|
||||||
'example.ts': `
|
'example.ts': `
|
||||||
|
@ -130,7 +130,7 @@ describe('r3_view_compiler', () => {
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-app',
|
selector: 'my-app',
|
||||||
template: '<div @attrOnly [@myAnimation]="exp"></div>'
|
template: '<div @attr [@binding]="exp"></div>'
|
||||||
})
|
})
|
||||||
export class MyApp {
|
export class MyApp {
|
||||||
}
|
}
|
||||||
|
@ -141,14 +141,14 @@ describe('r3_view_compiler', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const template = `
|
const template = `
|
||||||
const _c0 = ["@attrOnly", ""];
|
|
||||||
// ...
|
|
||||||
template: function MyApp_Template(rf, ctx) {
|
template: function MyApp_Template(rf, ctx) {
|
||||||
if (rf & 1) {
|
if (rf & 1) {
|
||||||
$i0$.ɵelement(0, "div", _c0);
|
$i0$.ɵelement(0, "div");
|
||||||
// ...
|
}
|
||||||
|
if (rf & 2) {
|
||||||
|
$i0$.ɵelementProperty(0, "@attr", …);
|
||||||
|
$i0$.ɵelementProperty(0, "@binding", …);
|
||||||
}
|
}
|
||||||
// ...
|
|
||||||
}`;
|
}`;
|
||||||
const result = compile(files, angularFiles);
|
const result = compile(files, angularFiles);
|
||||||
expectEmit(result.source, template, 'Incorrect initialization attributes');
|
expectEmit(result.source, template, 'Incorrect initialization attributes');
|
||||||
|
@ -173,14 +173,12 @@ describe('r3_view_compiler', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const template = `
|
const template = `
|
||||||
const _c0 = [3, "@mySelector"];
|
|
||||||
// ...
|
|
||||||
template: function MyApp_Template(rf, ctx) {
|
template: function MyApp_Template(rf, ctx) {
|
||||||
if (rf & 1) {
|
if (rf & 1) {
|
||||||
$i0$.ɵelementStart(0, "div", _c0);
|
$i0$.ɵelementStart(0, "div");
|
||||||
// ...
|
…
|
||||||
|
$i0$.ɵelementProperty(0, "@mySelector", …);
|
||||||
}
|
}
|
||||||
// ...
|
|
||||||
}`;
|
}`;
|
||||||
const result = compile(files, angularFiles);
|
const result = compile(files, angularFiles);
|
||||||
expectEmit(result.source, template, 'Incorrect initialization attributes');
|
expectEmit(result.source, template, 'Incorrect initialization attributes');
|
||||||
|
|
|
@ -214,21 +214,21 @@ describe('compiler compliance: styling', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const template = `
|
const template = `
|
||||||
const $e1_attrs$ = ["@bar", ""];
|
|
||||||
const $e2_attrs$ = ["@baz", ""];
|
|
||||||
…
|
…
|
||||||
MyComponent.ngComponentDef = $r3$.ɵdefineComponent({
|
MyComponent.ngComponentDef = $r3$.ɵdefineComponent({
|
||||||
…
|
…
|
||||||
consts: 3,
|
consts: 3,
|
||||||
vars: 1,
|
vars: 3,
|
||||||
template: function MyComponent_Template(rf, $ctx$) {
|
template: function MyComponent_Template(rf, $ctx$) {
|
||||||
if (rf & 1) {
|
if (rf & 1) {
|
||||||
$r3$.ɵelement(0, "div");
|
$r3$.ɵelement(0, "div");
|
||||||
$r3$.ɵelement(1, "div", $e1_attrs$);
|
$r3$.ɵelement(1, "div");
|
||||||
$r3$.ɵelement(2, "div", $e2_attrs$);
|
$r3$.ɵelement(2, "div");
|
||||||
}
|
}
|
||||||
if (rf & 2) {
|
if (rf & 2) {
|
||||||
$r3$.ɵelementProperty(0, "@foo", $r3$.ɵbind(ctx.exp));
|
$r3$.ɵelementProperty(0, "@foo", $r3$.ɵbind(ctx.exp));
|
||||||
|
$r3$.ɵelementProperty(1, "@bar", $r3$.ɵbind(undefined));
|
||||||
|
$r3$.ɵelementProperty(2, "@baz", $r3$.ɵbind(undefined));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
encapsulation: 2
|
encapsulation: 2
|
||||||
|
@ -280,7 +280,7 @@ describe('compiler compliance: styling', () => {
|
||||||
vars: 1,
|
vars: 1,
|
||||||
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");
|
||||||
$r3$.ɵlistener("@myAnimation.start", function MyComponent_Template_div_animation_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_animation_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();
|
||||||
|
|
|
@ -29,7 +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 {prepareSyntheticListenerFunctionName, prepareSyntheticListenerName, prepareSyntheticPropertyName} from '../util';
|
||||||
|
|
||||||
import {R3QueryMetadata} from './api';
|
import {R3QueryMetadata} from './api';
|
||||||
import {I18nContext} from './i18n/context';
|
import {I18nContext} from './i18n/context';
|
||||||
|
@ -571,8 +571,8 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||||
|
|
||||||
// this will build the instructions so that they fall into the following syntax
|
// this will build the instructions so that they fall into the following syntax
|
||||||
// add attributes for directive matching purposes
|
// add attributes for directive matching purposes
|
||||||
attributes.push(...this.prepareSyntheticAndSelectOnlyAttrs(
|
attributes.push(
|
||||||
allOtherInputs, element.outputs, stylingBuilder));
|
...this.prepareSelectOnlyAttrs(allOtherInputs, element.outputs, stylingBuilder));
|
||||||
parameters.push(this.toAttrsParam(attributes));
|
parameters.push(this.toAttrsParam(attributes));
|
||||||
|
|
||||||
// local refs (ex.: <div #foo #bar="baz">)
|
// local refs (ex.: <div #foo #bar="baz">)
|
||||||
|
@ -686,8 +686,14 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||||
this.processStylingInstruction(implicit, instruction, false);
|
this.processStylingInstruction(implicit, instruction, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// the reason why `undefined` is used is because the renderer understands this as a
|
||||||
|
// special value to symbolize that there is no RHS to this binding
|
||||||
|
// TODO (matsko): revisit this once FW-959 is approached
|
||||||
|
const emptyValueBindInstruction = o.importExpr(R3.bind).callFn([o.literal(undefined)]);
|
||||||
|
|
||||||
// Generate element input bindings
|
// Generate element input bindings
|
||||||
allOtherInputs.forEach((input: t.BoundAttribute) => {
|
allOtherInputs.forEach((input: t.BoundAttribute) => {
|
||||||
|
|
||||||
const instruction = mapBindingToInstruction(input.type);
|
const instruction = mapBindingToInstruction(input.type);
|
||||||
if (input.type === BindingType.Animation) {
|
if (input.type === BindingType.Animation) {
|
||||||
const value = input.value.visit(this._valueConverter);
|
const value = input.value.visit(this._valueConverter);
|
||||||
|
@ -696,21 +702,19 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||||
// 2. [@binding]="{value:fooExp, params:{...}}"
|
// 2. [@binding]="{value:fooExp, params:{...}}"
|
||||||
// 3. [@binding]
|
// 3. [@binding]
|
||||||
// 4. @binding
|
// 4. @binding
|
||||||
// only formats 1. and 2. include the actual binding of a value to
|
// All formats will be valid for when a synthetic binding is created.
|
||||||
// an expression and therefore only those should be the only two that
|
// The reasoning for this is because the renderer should get each
|
||||||
// are allowed. The check below ensures that a binding with no expression
|
// synthetic binding value in the order of the array that they are
|
||||||
// does not get an empty `elementProperty` instruction created for it.
|
// defined in...
|
||||||
const hasValue = value && (value instanceof LiteralPrimitive) ? !!value.value : true;
|
const hasValue = value instanceof LiteralPrimitive ? !!value.value : true;
|
||||||
if (hasValue) {
|
this.allocateBindingSlots(value);
|
||||||
this.allocateBindingSlots(value);
|
const bindingName = prepareSyntheticPropertyName(input.name);
|
||||||
const bindingName = prepareSyntheticPropertyName(input.name);
|
this.updateInstruction(input.sourceSpan, R3.elementProperty, () => {
|
||||||
this.updateInstruction(input.sourceSpan, R3.elementProperty, () => {
|
return [
|
||||||
return [
|
o.literal(elementIndex), o.literal(bindingName),
|
||||||
o.literal(elementIndex), o.literal(bindingName),
|
(hasValue ? this.convertPropertyBinding(implicit, value) : emptyValueBindInstruction)
|
||||||
this.convertPropertyBinding(implicit, value)
|
];
|
||||||
];
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if (instruction) {
|
} else if (instruction) {
|
||||||
const value = input.value.visit(this._valueConverter);
|
const value = input.value.visit(this._valueConverter);
|
||||||
if (value !== undefined) {
|
if (value !== undefined) {
|
||||||
|
@ -775,7 +779,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||||
const attrsExprs: o.Expression[] = [];
|
const attrsExprs: o.Expression[] = [];
|
||||||
template.attributes.forEach(
|
template.attributes.forEach(
|
||||||
(a: t.TextAttribute) => { attrsExprs.push(asLiteral(a.name), asLiteral(a.value)); });
|
(a: t.TextAttribute) => { attrsExprs.push(asLiteral(a.name), asLiteral(a.value)); });
|
||||||
attrsExprs.push(...this.prepareSyntheticAndSelectOnlyAttrs(template.inputs, template.outputs));
|
attrsExprs.push(...this.prepareSelectOnlyAttrs(template.inputs, template.outputs));
|
||||||
parameters.push(this.toAttrsParam(attrsExprs));
|
parameters.push(this.toAttrsParam(attrsExprs));
|
||||||
|
|
||||||
// local refs (ex.: <ng-template #foo>)
|
// local refs (ex.: <ng-template #foo>)
|
||||||
|
@ -969,7 +973,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||||
return originalSlots;
|
return originalSlots;
|
||||||
}
|
}
|
||||||
|
|
||||||
private allocateBindingSlots(value: AST) {
|
private allocateBindingSlots(value: AST|null) {
|
||||||
this._bindingSlots += value instanceof Interpolation ? value.expressions.length : 1;
|
this._bindingSlots += value instanceof Interpolation ? value.expressions.length : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1018,21 +1022,15 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||||
* STYLES, style1, value1, style2, value2,
|
* STYLES, style1, value1, style2, value2,
|
||||||
* SELECT_ONLY, name1, name2, name2, ...]
|
* SELECT_ONLY, name1, name2, name2, ...]
|
||||||
* ```
|
* ```
|
||||||
|
*
|
||||||
|
* Note that this function will fully ignore all synthetic (@foo) attribute values
|
||||||
|
* because those values are intended to always be generated as property instructions.
|
||||||
*/
|
*/
|
||||||
private prepareSyntheticAndSelectOnlyAttrs(
|
private prepareSelectOnlyAttrs(
|
||||||
inputs: t.BoundAttribute[], outputs: t.BoundEvent[],
|
inputs: t.BoundAttribute[], outputs: t.BoundEvent[],
|
||||||
styles?: StylingBuilder): o.Expression[] {
|
styles?: StylingBuilder): o.Expression[] {
|
||||||
const attrExprs: o.Expression[] = [];
|
|
||||||
const nonSyntheticInputs: t.BoundAttribute[] = [];
|
|
||||||
const alreadySeen = new Set<string>();
|
const alreadySeen = new Set<string>();
|
||||||
|
const attrExprs: o.Expression[] = [];
|
||||||
function isASTWithSource(ast: AST): ast is ASTWithSource {
|
|
||||||
return ast instanceof ASTWithSource;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isLiteralPrimitive(ast: AST): ast is LiteralPrimitive {
|
|
||||||
return ast instanceof LiteralPrimitive;
|
|
||||||
}
|
|
||||||
|
|
||||||
function addAttrExpr(key: string | number, value?: o.Expression): void {
|
function addAttrExpr(key: string | number, value?: o.Expression): void {
|
||||||
if (typeof key === 'string') {
|
if (typeof key === 'string') {
|
||||||
|
@ -1048,27 +1046,6 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (inputs.length) {
|
|
||||||
const EMPTY_STRING_EXPR = asLiteral('');
|
|
||||||
inputs.forEach(input => {
|
|
||||||
if (input.type === BindingType.Animation) {
|
|
||||||
// @attributes are for Renderer2 animation @triggers, but this feature
|
|
||||||
// may be supported differently in future versions of angular. However,
|
|
||||||
// @triggers should always just be treated as regular attributes (it's up
|
|
||||||
// to the renderer to detect and use them in a special way).
|
|
||||||
const valueExp = input.value;
|
|
||||||
if (isASTWithSource(valueExp)) {
|
|
||||||
const literal = valueExp.ast;
|
|
||||||
if (isLiteralPrimitive(literal) && literal.value === undefined) {
|
|
||||||
addAttrExpr(prepareSyntheticPropertyName(input.name), EMPTY_STRING_EXPR);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
nonSyntheticInputs.push(input);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// it's important that this occurs before SelectOnly because once `elementStart`
|
// it's important that this occurs before SelectOnly because once `elementStart`
|
||||||
// comes across the SelectOnly marker then it will continue reading each value as
|
// comes across the SelectOnly marker then it will continue reading each value as
|
||||||
// as single property value cell by cell.
|
// as single property value cell by cell.
|
||||||
|
@ -1076,14 +1053,30 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||||
styles.populateInitialStylingAttrs(attrExprs);
|
styles.populateInitialStylingAttrs(attrExprs);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nonSyntheticInputs.length || outputs.length) {
|
if (inputs.length || outputs.length) {
|
||||||
addAttrExpr(core.AttributeMarker.SelectOnly);
|
const attrsStartIndex = attrExprs.length;
|
||||||
nonSyntheticInputs.forEach((i: t.BoundAttribute) => addAttrExpr(i.name));
|
|
||||||
outputs.forEach((o: t.BoundEvent) => {
|
for (let i = 0; i < inputs.length; i++) {
|
||||||
const name =
|
const input = inputs[i];
|
||||||
o.type === ParsedEventType.Animation ? getSyntheticPropertyName(o.name) : o.name;
|
if (input.type !== BindingType.Animation) {
|
||||||
addAttrExpr(name);
|
addAttrExpr(input.name);
|
||||||
});
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < outputs.length; i++) {
|
||||||
|
const output = outputs[i];
|
||||||
|
if (output.type !== ParsedEventType.Animation) {
|
||||||
|
addAttrExpr(output.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is a cheap way of adding the marker only after all the input/output
|
||||||
|
// values have been filtered (by not including the animation ones) and added
|
||||||
|
// to the expressions. The marker is important because it tells the runtime
|
||||||
|
// code that this is where attributes without values start...
|
||||||
|
if (attrExprs.length) {
|
||||||
|
attrExprs.splice(attrsStartIndex, 0, o.literal(core.AttributeMarker.SelectOnly));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return attrExprs;
|
return attrExprs;
|
||||||
|
|
|
@ -1569,64 +1569,61 @@ const DEFAULT_COMPONENT_ID = '1';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
fixmeIvy(
|
it('should animate removals of nodes to the `void` state for each animation trigger, but treat all auto styles as pre styles',
|
||||||
'FW-932: Animation @triggers are not reported to the renderer in Ivy as they are in VE')
|
() => {
|
||||||
.it('should animate removals of nodes to the `void` state for each animation trigger, but treat all auto styles as pre styles',
|
@Component({
|
||||||
() => {
|
selector: 'ani-cmp',
|
||||||
@Component({
|
template: `
|
||||||
selector: 'ani-cmp',
|
|
||||||
template: `
|
|
||||||
<div *ngIf="exp" class="ng-if" [@trig1]="exp2" @trig2></div>
|
<div *ngIf="exp" class="ng-if" [@trig1]="exp2" @trig2></div>
|
||||||
`,
|
`,
|
||||||
animations: [
|
animations: [
|
||||||
trigger('trig1', [transition(
|
trigger(
|
||||||
'state => void', [animate(1000, style({opacity: 0}))])]),
|
'trig1', [transition('state => void', [animate(1000, style({opacity: 0}))])]),
|
||||||
trigger(
|
trigger('trig2', [transition(':leave', [animate(1000, style({width: '0px'}))])])
|
||||||
'trig2', [transition(':leave', [animate(1000, style({width: '0px'}))])])
|
]
|
||||||
]
|
})
|
||||||
})
|
class Cmp {
|
||||||
class Cmp {
|
public exp = true;
|
||||||
public exp = true;
|
public exp2 = 'state';
|
||||||
public exp2 = 'state';
|
}
|
||||||
}
|
|
||||||
|
|
||||||
TestBed.configureTestingModule({declarations: [Cmp]});
|
TestBed.configureTestingModule({declarations: [Cmp]});
|
||||||
|
|
||||||
const engine = TestBed.get(ɵAnimationEngine);
|
const engine = TestBed.get(ɵAnimationEngine);
|
||||||
const fixture = TestBed.createComponent(Cmp);
|
const fixture = TestBed.createComponent(Cmp);
|
||||||
const cmp = fixture.componentInstance;
|
const cmp = fixture.componentInstance;
|
||||||
cmp.exp = true;
|
cmp.exp = true;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
engine.flush();
|
engine.flush();
|
||||||
resetLog();
|
resetLog();
|
||||||
|
|
||||||
const element = getDOM().querySelector(fixture.nativeElement, '.ng-if');
|
const element = getDOM().querySelector(fixture.nativeElement, '.ng-if');
|
||||||
assertHasParent(element, true);
|
assertHasParent(element, true);
|
||||||
|
|
||||||
cmp.exp = false;
|
cmp.exp = false;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
engine.flush();
|
engine.flush();
|
||||||
|
|
||||||
assertHasParent(element, true);
|
assertHasParent(element, true);
|
||||||
|
|
||||||
expect(getLog().length).toEqual(2);
|
expect(getLog().length).toEqual(2);
|
||||||
|
|
||||||
const player2 = getLog().pop() !;
|
const player2 = getLog().pop() !;
|
||||||
const player1 = getLog().pop() !;
|
const player1 = getLog().pop() !;
|
||||||
|
|
||||||
expect(player2.keyframes).toEqual([
|
expect(player2.keyframes).toEqual([
|
||||||
{width: PRE_STYLE, offset: 0},
|
{width: PRE_STYLE, offset: 0},
|
||||||
{width: '0px', offset: 1},
|
{width: '0px', offset: 1},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(player1.keyframes).toEqual([
|
expect(player1.keyframes).toEqual([
|
||||||
{opacity: PRE_STYLE, offset: 0}, {opacity: '0', offset: 1}
|
{opacity: PRE_STYLE, offset: 0}, {opacity: '0', offset: 1}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
player2.finish();
|
player2.finish();
|
||||||
player1.finish();
|
player1.finish();
|
||||||
assertHasParent(element, false);
|
assertHasParent(element, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should properly cancel all existing animations when a removal occurs', () => {
|
it('should properly cancel all existing animations when a removal occurs', () => {
|
||||||
@Component({
|
@Component({
|
||||||
|
@ -3372,43 +3369,42 @@ const DEFAULT_COMPONENT_ID = '1';
|
||||||
expect(getLog().length).toEqual(1);
|
expect(getLog().length).toEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
fixmeIvy('FW-951 - Attribute-only synthetic properties are treated differently in Ivy')
|
it('should treat the property as true when the expression is missing', () => {
|
||||||
.it('should treat the property as true when the expression is missing', () => {
|
@Component({
|
||||||
@Component({
|
selector: 'parent-cmp',
|
||||||
selector: 'parent-cmp',
|
animations: [
|
||||||
animations: [
|
trigger(
|
||||||
trigger(
|
'myAnimation',
|
||||||
'myAnimation',
|
[
|
||||||
[
|
transition(
|
||||||
transition(
|
'* => go',
|
||||||
'* => go',
|
[
|
||||||
[
|
style({opacity: 0}),
|
||||||
style({opacity: 0}),
|
animate(500, style({opacity: 1})),
|
||||||
animate(500, style({opacity: 1})),
|
]),
|
||||||
]),
|
]),
|
||||||
]),
|
],
|
||||||
],
|
template: `
|
||||||
template: `
|
|
||||||
<div @.disabled>
|
<div @.disabled>
|
||||||
<div [@myAnimation]="exp"></div>
|
<div [@myAnimation]="exp"></div>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
})
|
})
|
||||||
class Cmp {
|
class Cmp {
|
||||||
exp = '';
|
exp = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
TestBed.configureTestingModule({declarations: [Cmp]});
|
TestBed.configureTestingModule({declarations: [Cmp]});
|
||||||
|
|
||||||
const fixture = TestBed.createComponent(Cmp);
|
const fixture = TestBed.createComponent(Cmp);
|
||||||
const cmp = fixture.componentInstance;
|
const cmp = fixture.componentInstance;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
resetLog();
|
resetLog();
|
||||||
|
|
||||||
cmp.exp = 'go';
|
cmp.exp = 'go';
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
expect(getLog().length).toEqual(0);
|
expect(getLog().length).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should respect parent/sub animations when the respective area in the DOM is disabled',
|
it('should respect parent/sub animations when the respective area in the DOM is disabled',
|
||||||
fakeAsync(() => {
|
fakeAsync(() => {
|
||||||
|
|
Loading…
Reference in New Issue