Skip to content

Commit d7b6045

Browse files
thePunderWomanAndrewKushnir
authored andcommitted
fix(compiler): fixes animations on elements with structural directives (#63390)
The animate instructions were getting applied to the container comment nodes as well as the element nodes. This prevents that on the compiler level. fixes: #63371 PR Close #63390
1 parent c9b0f45 commit d7b6045

File tree

6 files changed

+129
-5
lines changed

6 files changed

+129
-5
lines changed

packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/GOLDEN_PARTIAL.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,57 @@ export declare class MyComponent {
3232
static ɵcmp: i0.ɵɵComponentDeclaration<MyComponent, "my-component", never, {}, {}, never, never, true, never>;
3333
}
3434

35+
/****************************************************************************************************
36+
* PARTIAL FILE: animate_enter_with_structural_directive.js
37+
****************************************************************************************************/
38+
import { Component, Directive } from '@angular/core';
39+
import * as i0 from "@angular/core";
40+
export class AnyStructuralDirective {
41+
}
42+
AnyStructuralDirective.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: AnyStructuralDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
43+
AnyStructuralDirective.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: AnyStructuralDirective, isStandalone: true, selector: "[any-structural-directive]", ngImport: i0 });
44+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: AnyStructuralDirective, decorators: [{
45+
type: Directive,
46+
args: [{
47+
selector: '[any-structural-directive]',
48+
standalone: true,
49+
}]
50+
}] });
51+
export class MyComponent {
52+
}
53+
MyComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
54+
MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: MyComponent, isStandalone: true, selector: "my-component", ngImport: i0, template: `
55+
<div>
56+
<p *any-structural-directive animate.enter="slide">Sliding Content</p>
57+
</div>
58+
`, isInline: true, dependencies: [{ kind: "directive", type: AnyStructuralDirective, selector: "[any-structural-directive]" }] });
59+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, decorators: [{
60+
type: Component,
61+
args: [{
62+
selector: 'my-component',
63+
imports: [AnyStructuralDirective],
64+
standalone: true,
65+
template: `
66+
<div>
67+
<p *any-structural-directive animate.enter="slide">Sliding Content</p>
68+
</div>
69+
`,
70+
}]
71+
}] });
72+
73+
/****************************************************************************************************
74+
* PARTIAL FILE: animate_enter_with_structural_directive.d.ts
75+
****************************************************************************************************/
76+
import * as i0 from "@angular/core";
77+
export declare class AnyStructuralDirective {
78+
static ɵfac: i0.ɵɵFactoryDeclaration<AnyStructuralDirective, never>;
79+
static ɵdir: i0.ɵɵDirectiveDeclaration<AnyStructuralDirective, "[any-structural-directive]", never, {}, {}, never, never, true, never>;
80+
}
81+
export declare class MyComponent {
82+
static ɵfac: i0.ɵɵFactoryDeclaration<MyComponent, never>;
83+
static ɵcmp: i0.ɵɵComponentDeclaration<MyComponent, "my-component", never, {}, {}, never, never, true, never>;
84+
}
85+
3586
/****************************************************************************************************
3687
* PARTIAL FILE: animate_enter_with_string_host_bindings.js
3788
****************************************************************************************************/

packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/TEST_CASES.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,23 @@
1818
}
1919
]
2020
},
21+
{
22+
"description": "should generate animate enter instructions on element with a structural directive",
23+
"inputFiles": [
24+
"animate_enter_with_structural_directive.ts"
25+
],
26+
"expectations": [
27+
{
28+
"files": [
29+
{
30+
"expected": "animate_enter_with_structural_directive_template.js",
31+
"generated": "animate_enter_with_structural_directive.js"
32+
}
33+
],
34+
"failureMessage": "Incorrect ɵɵanimateEnter() call"
35+
}
36+
]
37+
},
2138
{
2239
"description": "should generate animate enter instructions with host binding and simple string",
2340
"inputFiles": [
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import {Component, Directive} from '@angular/core';
2+
3+
@Directive({
4+
selector: '[any-structural-directive]',
5+
standalone: true,
6+
})
7+
export class AnyStructuralDirective {}
8+
9+
10+
@Component({
11+
selector: 'my-component',
12+
imports: [AnyStructuralDirective],
13+
standalone: true,
14+
template: `
15+
<div>
16+
<p *any-structural-directive animate.enter="slide">Sliding Content</p>
17+
</div>
18+
`,
19+
})
20+
export class MyComponent {
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import * as i0 from "@angular/core";
2+
function MyComponent_p_1_Template(rf, ctx) { if (rf & 1) {
3+
i0.ɵɵelementStart(0, "p");
4+
i0.ɵɵanimateEnter("slide");
5+
i0.ɵɵtext(1, "Sliding Content");
6+
i0.ɵɵelementEnd();
7+
} }
8+
9+
MyComponent.ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: MyComponent, selectors: [["my-component"]], decls: 2, vars: 0, consts: [[4, "any-structural-directive"]], template: function MyComponent_Template(rf, ctx) { if (rf & 1) {
10+
i0.ɵɵelementStart(0, "div");
11+
i0.ɵɵtemplate(1, MyComponent_p_1_Template, 2, 0, "p", 0);
12+
i0.ɵɵelementEnd();
13+
} }, dependencies: [AnyStructuralDirective], encapsulation: 2 });

packages/compiler/src/render3/r3_template_transform.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -961,6 +961,14 @@ class HtmlAstToIvyAst implements html.Visitor {
961961
return directives;
962962
}
963963

964+
private filterAnimationAttributes(attributes: t.TextAttribute[]): t.TextAttribute[] {
965+
return attributes.filter((a) => !a.name.startsWith('animate.'));
966+
}
967+
968+
private filterAnimationInputs(attributes: t.BoundAttribute[]): t.BoundAttribute[] {
969+
return attributes.filter((a) => a.type !== BindingType.Animation);
970+
}
971+
964972
private wrapInTemplate(
965973
node: t.Element | t.Component | t.Content | t.Template,
966974
templateProperties: ParsedProperty[],
@@ -986,8 +994,8 @@ class HtmlAstToIvyAst implements html.Visitor {
986994
};
987995

988996
if (node instanceof t.Element || node instanceof t.Component) {
989-
hoistedAttrs.attributes.push(...node.attributes);
990-
hoistedAttrs.inputs.push(...node.inputs);
997+
hoistedAttrs.attributes.push(...this.filterAnimationAttributes(node.attributes));
998+
hoistedAttrs.inputs.push(...this.filterAnimationInputs(node.inputs));
991999
hoistedAttrs.outputs.push(...node.outputs);
9921000
}
9931001

packages/core/src/render3/instructions/animation.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,9 @@ export function ɵɵanimateEnter(value: string | Function): typeof ɵɵanimateEn
166166

167167
const tNode = getCurrentTNode()!;
168168
const nativeElement = getNativeByTNode(tNode, lView) as HTMLElement;
169+
170+
ngDevMode && assertElementNodes(nativeElement, 'animate.enter');
171+
169172
const renderer = lView[RENDERER];
170173
const ngZone = lView[INJECTOR]!.get(NgZone);
171174

@@ -270,6 +273,8 @@ export function ɵɵanimateEnterListener(value: AnimationFunction): typeof ɵɵa
270273
const tNode = getCurrentTNode()!;
271274
const nativeElement = getNativeByTNode(tNode, lView) as HTMLElement;
272275

276+
ngDevMode && assertElementNodes(nativeElement, 'animate.enter');
277+
273278
cancelLeavingNodes(tNode, lView);
274279

275280
value.call(lView[CONTEXT], {target: nativeElement, animationComplete: noOpAnimationComplete});
@@ -306,6 +311,8 @@ export function ɵɵanimateLeave(value: string | Function): typeof ɵɵanimateLe
306311
const tNode = getCurrentTNode()!;
307312
const nativeElement = getNativeByTNode(tNode, lView) as Element;
308313

314+
ngDevMode && assertElementNodes(nativeElement, 'animate.leave');
315+
309316
// This instruction is called in the update pass.
310317
const renderer = lView[RENDERER];
311318
const elementRegistry = getAnimationElementRemovalRegistry();
@@ -374,9 +381,7 @@ export function ɵɵanimateLeaveListener(value: AnimationFunction): typeof ɵɵa
374381
const tView = getTView();
375382
const nativeElement = getNativeByTNode(tNode, lView) as Element;
376383

377-
if ((nativeElement as Node).nodeType !== Node.ELEMENT_NODE) {
378-
return ɵɵanimateLeaveListener;
379-
}
384+
ngDevMode && assertElementNodes(nativeElement, 'animate.leave');
380385

381386
const elementRegistry = getAnimationElementRemovalRegistry();
382387
ngDevMode &&
@@ -524,6 +529,15 @@ function assertAnimationTypes(value: string | Function, instruction: string) {
524529
}
525530
}
526531

532+
function assertElementNodes(nativeElement: Element, instruction: string) {
533+
if ((nativeElement as Node).nodeType !== Node.ELEMENT_NODE) {
534+
throw new RuntimeError(
535+
RuntimeErrorCode.ANIMATE_INVALID_VALUE,
536+
`'${instruction}' can only be used on an element node, got ${stringify((nativeElement as Node).nodeType)}`,
537+
);
538+
}
539+
}
540+
527541
/**
528542
* This function actually adds the classes that animate element that's leaving the DOM.
529543
* Once it finishes, it calls the remove function that was provided by the DOM renderer.

0 commit comments

Comments
 (0)