diff --git a/packages/animations/browser/src/render/transition_animation_engine.ts b/packages/animations/browser/src/render/transition_animation_engine.ts index bbe6ca3e48..7cafcd3722 100644 --- a/packages/animations/browser/src/render/transition_animation_engine.ts +++ b/packages/animations/browser/src/render/transition_animation_engine.ts @@ -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); diff --git a/packages/core/test/animation/animation_query_integration_spec.ts b/packages/core/test/animation/animation_query_integration_spec.ts index 03e8393f65..04d6c73d80 100644 --- a/packages/core/test/animation/animation_query_integration_spec.ts +++ b/packages/core/test/animation/animation_query_integration_spec.ts @@ -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: ` +
+
+ {{ item }} +
+
+ `, + 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',