fix(animations): properly collect :enter nodes in a partially updated collection

This PR fixes an issue where `query(':enter')` will only collect elements up until it an element that is found that isn't apart of the `:enter` query.

Closes #17440
This commit is contained in:
Matias Niemelä 2017-06-12 11:35:22 -07:00 committed by Alex Rickabaugh
parent 185075d870
commit 6ca46929fa
2 changed files with 106 additions and 15 deletions

View File

@ -1249,6 +1249,17 @@ function cloakElement(element: any, value?: string) {
return oldValue;
}
/*
1. start from the root, find the first matching child
a) if not found then check to see if a previously stopped node was set in the stack
-> if so then use that as the nextCursor
b) if no queried item and no parent then stop completely
c) if no queried item and yes parent then jump to the parent and restart loop
2. visit the next node, check if matches
a) if doesn't exist then set that as the cursor and repeat
-> add to the previous cursor stack when the inner queries return nothing
b) if matches then add it and continue
*/
function filterNodeClasses(
driver: AnimationDriver, rootElement: any | null, selector: string): any[] {
const rootElements: any[] = [];
@ -1256,23 +1267,53 @@ function filterNodeClasses(
let cursor: any = rootElement;
let nextCursor: any = {};
let potentialCursorStack: any[] = [];
do {
nextCursor = driver.query(cursor, selector, false)[0];
// 1. query from root
nextCursor = cursor ? driver.query(cursor, selector, false)[0] : null;
// this is used to avoid the extra matchesElement call when we
// know that the element does match based it on being queried
let justQueried = !!nextCursor;
if (!nextCursor) {
cursor = cursor.parentElement;
if (!cursor) break;
nextCursor = cursor = cursor.nextElementSibling;
} else {
while (nextCursor && driver.matchesElement(nextCursor, selector)) {
rootElements.push(nextCursor);
nextCursor = nextCursor.nextElementSibling;
if (nextCursor) {
cursor = nextCursor;
} else {
cursor = cursor.parentElement;
if (!cursor) break;
nextCursor = cursor = cursor.nextElementSibling;
}
const nextPotentialCursor = potentialCursorStack.pop();
if (nextPotentialCursor) {
// 1a)
nextCursor = nextPotentialCursor;
} else {
cursor = cursor.parentElement;
// 1b)
if (!cursor) break;
// 1c)
nextCursor = cursor = cursor.nextElementSibling;
continue;
}
}
// 2. visit the next node
while (nextCursor) {
const matches = justQueried || driver.matchesElement(nextCursor, selector);
justQueried = false;
const nextPotentialCursor = nextCursor.nextElementSibling;
// 2a)
if (!matches) {
potentialCursorStack.push(nextPotentialCursor);
cursor = nextCursor;
break;
}
// 2b)
rootElements.push(nextCursor);
nextCursor = nextPotentialCursor;
if (nextCursor) {
cursor = nextCursor;
} else {
cursor = cursor.parentElement;
if (!cursor) break;
nextCursor = cursor = cursor.nextElementSibling;
}
}
} while (nextCursor && nextCursor !== rootElement);

View File

@ -735,6 +735,56 @@ export function main() {
});
});
it('should find :enter nodes that have been inserted around non enter nodes', () => {
@Component({
selector: 'ani-cmp',
template: `
<div [@myAnimation]="exp" class="parent">
<div *ngFor="let item of items" class="child">
{{ item }}
</div>
</div>
`,
animations: [trigger(
'myAnimation',
[
transition(
'* => go',
[query(':enter', [style({opacity: 0}), animate(1000, style({opacity: 1}))])]),
])]
})
class Cmp {
public exp: any;
public items: any[] = [];
}
TestBed.configureTestingModule({declarations: [Cmp]});
const engine = TestBed.get(ɵAnimationEngine);
const fixture = TestBed.createComponent(Cmp);
const cmp = fixture.componentInstance;
cmp.exp = 'no';
cmp.items = [2];
fixture.detectChanges();
engine.flush();
resetLog();
cmp.exp = 'go';
cmp.items = [0, 1, 2, 3, 4];
fixture.detectChanges();
engine.flush();
const players = getLog();
expect(players.length).toEqual(4);
const [p1, p2, p3, p4] = players;
expect(p1.element.innerText.trim()).toEqual('0');
expect(p2.element.innerText.trim()).toEqual('1');
expect(p3.element.innerText.trim()).toEqual('3');
expect(p4.element.innerText.trim()).toEqual('4');
});
it('should properly cancel items that were queried into a former animation', () => {
@Component({
selector: 'ani-cmp',