-
Notifications
You must be signed in to change notification settings - Fork 27.2k
fix(animations): allow enter animation of every inserted element #44243
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2519,7 +2519,9 @@ describe('animation tests', function() { | |
| }); | ||
| }); | ||
|
|
||
| it('should not animate i18n insertBefore', () => { | ||
| // TO DISCUSS: every insertion is going to be threated as a move one, this will so also | ||
| // apply to i18n elements, I think it is ok, is it? can this test be removed? | ||
| xit('should not animate i18n insertBefore', () => { | ||
|
Comment on lines
2522
to
2524
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The comment should be self explanatory, but yeah, it seems like i18n insertions were done via insertBefore so they needed not to be treated as entering, since now everything is treated as entering the i18n insertions are no difference, I guess this should be ok 🤔
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Did you intend to say "threated"? Or did you mean threaded?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Or "treated"?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "treated" 😅 honestly I just wanted to remove this test and not x-it, I just put the comment in the code to start a discussion |
||
| // I18n uses `insertBefore` API to insert nodes in correct order. Animation assumes that | ||
| // any `insertBefore` is a move and tries to animate it. | ||
| // NOTE: This test was extracted from `g3` | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1017,8 +1017,8 @@ describe('animation query tests', function() { | |
| <div [@myAnimation]="items.length" class="parent"> | ||
| <ng-container *ngFor="let item of items"> | ||
| <section> | ||
| <div *ngIf="item % 2 == 0">even {{ item }}</div> | ||
| <div *ngIf="item % 2 == 1">odd {{ item }}</div> | ||
| <div class="item" *ngIf="item % 2 == 0">even {{ item }}</div> | ||
| <div class="item" *ngIf="item % 2 == 1">odd {{ item }}</div> | ||
| </section> | ||
| </ng-container> | ||
| </div> | ||
|
|
@@ -1027,12 +1027,13 @@ describe('animation query tests', function() { | |
| 'myAnimation', | ||
| [ | ||
| transition('0 => 5', [ | ||
| query(':enter', [ | ||
| query('.item:enter', [ | ||
| style({ opacity: '0' }), | ||
| animate(1000, style({ opacity: '1' })) | ||
| ]) | ||
| ]), | ||
| transition('5 => 0', [ | ||
| // TODO: :enter and :leave of inner elements behave differently they should probably be aligned | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. my changes are only fixing the |
||
| query(':leave', [ | ||
| style({ opacity: '1' }), | ||
| animate(1000, style({ opacity: '0' })) | ||
|
|
@@ -2515,79 +2516,118 @@ describe('animation query tests', function() { | |
| expect(element.innerText.trim()).toMatch(/this\s+child/mg); | ||
| })); | ||
|
|
||
| it('should only mark outermost *directive nodes :enter and :leave when inserts and removals occur', | ||
| () => { | ||
| @Component({ | ||
| selector: 'ani-cmp', | ||
| animations: [ | ||
| trigger( | ||
| 'anim', | ||
| [ | ||
| transition( | ||
| '* => enter', | ||
| [ | ||
| query(':enter', [animate(1000, style({color: 'red'}))]), | ||
| ]), | ||
| transition( | ||
| '* => leave', | ||
| [ | ||
| query(':leave', [animate(1000, style({color: 'blue'}))]), | ||
| ]), | ||
| ]), | ||
| ], | ||
| template: ` | ||
| it('should mark all nodes :enter when their parent\'s insertion occurs', () => { | ||
| @Component({ | ||
| selector: 'ani-cmp', | ||
| animations: [ | ||
| trigger( | ||
| 'anim', | ||
| [ | ||
| transition( | ||
| '* => enter', | ||
| [ | ||
| query(':enter', [animate(1000, style({color: 'red'}))]), | ||
| ]), | ||
| ]), | ||
| ], | ||
| template: ` | ||
| <section class="container" [@anim]="exp ? 'enter' : 'leave'"> | ||
| <div class="a" *ngIf="exp"> | ||
| <div class="b" *ngIf="exp"> | ||
| <div class="c" *ngIf="exp"> | ||
| <div class="b"> | ||
| <div class="c"> | ||
| text | ||
| </div> | ||
| </div> | ||
| </div> | ||
| <div> | ||
| <div class="d" *ngIf="exp"> | ||
| <div class="d"> | ||
| <div class="e"> | ||
| text2 | ||
| </div> | ||
| </div> | ||
| </section> | ||
| ` | ||
| }) | ||
| class Cmp { | ||
| // TODO(issue/24571): remove '!'. | ||
| public exp!: boolean; | ||
| } | ||
| }) | ||
| class Cmp { | ||
| public exp = false; | ||
| } | ||
|
|
||
| TestBed.configureTestingModule({declarations: [Cmp]}); | ||
| TestBed.configureTestingModule({declarations: [Cmp]}); | ||
|
|
||
| const engine = TestBed.inject(ɵAnimationEngine); | ||
| const fixture = TestBed.createComponent(Cmp); | ||
| const cmp = fixture.componentInstance; | ||
| const container = fixture.elementRef.nativeElement; | ||
| const engine = TestBed.inject(ɵAnimationEngine); | ||
| const fixture = TestBed.createComponent(Cmp); | ||
| const cmp = fixture.componentInstance; | ||
|
|
||
| cmp.exp = true; | ||
| fixture.detectChanges(); | ||
| engine.flush(); | ||
| cmp.exp = true; | ||
| fixture.detectChanges(); | ||
| engine.flush(); | ||
|
|
||
| let players = getLog(); | ||
| resetLog(); | ||
| expect(players.length).toEqual(2); | ||
| const [p1, p2] = players; | ||
| let players = getLog(); | ||
| resetLog(); | ||
| expect(players.length).toEqual(5); | ||
| const [p1, p2, p3, p4, p5] = players; | ||
|
|
||
| expect(p1.element.classList.contains('a')).toBeTrue(); | ||
| expect(p2.element.classList.contains('d')).toBeTrue(); | ||
| expect(p1.element.classList.contains('a')).toBeTrue(); | ||
| expect(p2.element.classList.contains('b')).toBeTrue(); | ||
| expect(p3.element.classList.contains('c')).toBeTrue(); | ||
| expect(p4.element.classList.contains('d')).toBeTrue(); | ||
| expect(p5.element.classList.contains('e')).toBeTrue(); | ||
| }); | ||
|
|
||
| cmp.exp = false; | ||
| fixture.detectChanges(); | ||
| engine.flush(); | ||
| // TODO: this should most likely not happen anymore and all children of a removed parent | ||
| // should be marked as :leave (so that they can be queried accordingly) | ||
|
Comment on lines
2576
to
2577
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. my changes are only fixing the |
||
| it('should only mark outermost *directive nodes :leave when removals occur', () => { | ||
| @Component({ | ||
| selector: 'ani-cmp', | ||
| animations: [ | ||
| trigger( | ||
| 'anim', | ||
| [ | ||
| transition( | ||
| '* => leave', | ||
| [ | ||
| query(':leave', [animate(1000, style({color: 'blue'}))]), | ||
| ]), | ||
| ]), | ||
| ], | ||
| template: ` | ||
| <section class="container" [@anim]="exp ? 'enter' : 'leave'"> | ||
| <div class="a" *ngIf="exp"> | ||
| <div class="b" *ngIf="exp"> | ||
| <div class="c" *ngIf="exp"> | ||
| text | ||
| </div> | ||
| </div> | ||
| </div> | ||
| <div> | ||
| <div class="d" *ngIf="exp"> | ||
| text2 | ||
| </div> | ||
| </div> | ||
| </section> | ||
| ` | ||
| }) | ||
| class Cmp { | ||
| public exp = true; | ||
| } | ||
|
|
||
| players = getLog(); | ||
| resetLog(); | ||
| expect(players.length).toEqual(2); | ||
| const [p3, p4] = players; | ||
| TestBed.configureTestingModule({declarations: [Cmp]}); | ||
|
|
||
| expect(p3.element.classList.contains('a')).toBeTrue(); | ||
| expect(p4.element.classList.contains('d')).toBeTrue(); | ||
| }); | ||
| const engine = TestBed.inject(ɵAnimationEngine); | ||
| const fixture = TestBed.createComponent(Cmp); | ||
| const cmp = fixture.componentInstance; | ||
| fixture.detectChanges(); | ||
| cmp.exp = false; | ||
| fixture.detectChanges(); | ||
| engine.flush(); | ||
|
|
||
| let players = getLog(); | ||
| resetLog(); | ||
| expect(players.length).toEqual(2); | ||
| const [p1, p2] = players; | ||
|
|
||
| expect(p1.element.classList.contains('a')).toBeTrue(); | ||
| expect(p2.element.classList.contains('d')).toBeTrue(); | ||
| }); | ||
|
|
||
| it('should collect multiple root levels of :enter and :leave nodes', () => { | ||
| @Component({ | ||
|
|
@@ -2599,22 +2639,22 @@ describe('animation query tests', function() { | |
| transition( | ||
| '* => *', | ||
| [ | ||
| query(':leave', [animate('1s', style({opacity: 0}))], {optional: true}), | ||
| query(':enter', [animate('1s', style({opacity: 1}))], {optional: true}) | ||
| query('.q:leave', [animate('1s', style({opacity: 0}))], {optional: true}), | ||
| query('.q:enter', [animate('1s', style({opacity: 1}))], {optional: true}) | ||
| ]) | ||
| ])], | ||
| template: ` | ||
| <div [@pageAnimation]="status"> | ||
| <header> | ||
| <div *ngIf="!loading" class="title">{{ title }}</div> | ||
| <div *ngIf="loading" class="loading">loading...</div> | ||
| <div *ngIf="!loading" class="title q">{{ title }}</div> | ||
| <div *ngIf="loading" class="loading q">loading...</div> | ||
| </header> | ||
| <section> | ||
| <div class="page"> | ||
| <div *ngIf="page1" class="page1"> | ||
| <div *ngIf="page1" class="page1 q"> | ||
| <div *ngIf="true">page 1</div> | ||
| </div> | ||
| <div *ngIf="page2" class="page2"> | ||
| <div *ngIf="page2" class="page2 q"> | ||
| <div *ngIf="true">page 2</div> | ||
| </div> | ||
| </div> | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am concerned about this approach for perf reasons; adding and removing classes is not free. Would it be possible to tweak query selectors from
.${class}to something like.${class}, .${class} *to select all descendants? I am not sure about the perf characteristics of such query, though.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes I understand what you mean....
I don't think that tweaking the selector should be too much of an issue
For context the code that does the querying is this one:
angular/packages/animations/browser/src/dsl/animation_timeline_builder.ts
Lines 563 to 588 in 155742e
I think that before
selector = selector.replace(ENTER_TOKEN_REGEX, '.' + this._enterClassName);we could have a check and if the element doing the querying is entering then we just substitute the assignment to:selector = selector.replace(ENTER_TOKEN_REGEX, '*');(ps: there is not info about the parent element entering so we would need to add that somehow too)
this seems like a pretty inexpensive change
Then what actually actually does the querying (called by the
driver._queryabove) is a simplequerySelector/querySelectorAll:angular/packages/animations/browser/src/render/shared.ts
Lines 192 to 214 in 8d0511d
this again doesn't seem like a too expensive change since we use the browser's native api... I think?! 🤷
what do you think? does this sound like a good thing to try out? shall I give it a shot?