fix(animations): properly collect :enter nodes that exist within multi-level DOM trees
Closes #17632
This commit is contained in:
parent
6c1a8daafc
commit
40f77cb563
@ -94,9 +94,6 @@ export const VOID_VALUE = 'void';
|
|||||||
export const DEFAULT_STATE_VALUE = new StateValue(VOID_VALUE);
|
export const DEFAULT_STATE_VALUE = new StateValue(VOID_VALUE);
|
||||||
export const DELETED_STATE_VALUE = new StateValue('DELETED');
|
export const DELETED_STATE_VALUE = new StateValue('DELETED');
|
||||||
|
|
||||||
const POTENTIAL_ENTER_CLASSNAME = ENTER_CLASSNAME + '-temp';
|
|
||||||
const POTENTIAL_ENTER_SELECTOR = '.' + POTENTIAL_ENTER_CLASSNAME;
|
|
||||||
|
|
||||||
export class AnimationTransitionNamespace {
|
export class AnimationTransitionNamespace {
|
||||||
public players: TransitionAnimationPlayer[] = [];
|
public players: TransitionAnimationPlayer[] = [];
|
||||||
|
|
||||||
@ -749,13 +746,17 @@ export class TransitionAnimationEngine {
|
|||||||
const allPreStyleElements = new Map<any, Set<string>>();
|
const allPreStyleElements = new Map<any, Set<string>>();
|
||||||
const allPostStyleElements = new Map<any, Set<string>>();
|
const allPostStyleElements = new Map<any, Set<string>>();
|
||||||
|
|
||||||
|
const bodyNode = getBodyNode();
|
||||||
|
const allEnterNodes: any[] = this.collectedEnterElements.length ?
|
||||||
|
this.collectedEnterElements.filter(createIsRootFilterFn(this.collectedEnterElements)) :
|
||||||
|
[];
|
||||||
|
|
||||||
// this must occur before the instructions are built below such that
|
// this must occur before the instructions are built below such that
|
||||||
// the :enter queries match the elements (since the timeline queries
|
// the :enter queries match the elements (since the timeline queries
|
||||||
// are fired during instruction building).
|
// are fired during instruction building).
|
||||||
const bodyNode = getBodyNode();
|
for (let i = 0; i < allEnterNodes.length; i++) {
|
||||||
const allEnterNodes: any[] = this.collectedEnterElements.length ?
|
addClass(allEnterNodes[i], ENTER_CLASSNAME);
|
||||||
collectEnterElements(this.driver, this.collectedEnterElements) :
|
}
|
||||||
[];
|
|
||||||
|
|
||||||
const allLeaveNodes: any[] = [];
|
const allLeaveNodes: any[] = [];
|
||||||
const leaveNodesWithoutAnimations: any[] = [];
|
const leaveNodesWithoutAnimations: any[] = [];
|
||||||
@ -1283,78 +1284,6 @@ function cloakElement(element: any, value?: string) {
|
|||||||
return oldValue;
|
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[] = [];
|
|
||||||
if (!rootElement) return rootElements;
|
|
||||||
|
|
||||||
let cursor: any = rootElement;
|
|
||||||
let nextCursor: any = {};
|
|
||||||
let potentialCursorStack: any[] = [];
|
|
||||||
do {
|
|
||||||
// 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) {
|
|
||||||
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);
|
|
||||||
|
|
||||||
return rootElements;
|
|
||||||
}
|
|
||||||
|
|
||||||
function cloakAndComputeStyles(
|
function cloakAndComputeStyles(
|
||||||
driver: AnimationDriver, elements: any[], elementPropsMap: Map<any, Set<string>>,
|
driver: AnimationDriver, elements: any[], elementPropsMap: Map<any, Set<string>>,
|
||||||
defaultStyle: string): Map<any, ɵStyleData> {
|
defaultStyle: string): Map<any, ɵStyleData> {
|
||||||
@ -1379,12 +1308,36 @@ function cloakAndComputeStyles(
|
|||||||
return valuesMap;
|
return valuesMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
function collectEnterElements(driver: AnimationDriver, allEnterNodes: any[]) {
|
/*
|
||||||
allEnterNodes.forEach(element => addClass(element, POTENTIAL_ENTER_CLASSNAME));
|
Since the Angular renderer code will return a collection of inserted
|
||||||
const enterNodes = filterNodeClasses(driver, getBodyNode(), POTENTIAL_ENTER_SELECTOR);
|
nodes in all areas of a DOM tree, it's up to this algorithm to figure
|
||||||
enterNodes.forEach(element => addClass(element, ENTER_CLASSNAME));
|
out which nodes are roots.
|
||||||
allEnterNodes.forEach(element => removeClass(element, POTENTIAL_ENTER_CLASSNAME));
|
|
||||||
return enterNodes;
|
By placing all nodes into a set and traversing upwards to the edge,
|
||||||
|
the recursive code can figure out if a clean path from the DOM node
|
||||||
|
to the edge container is clear. If no other node is detected in the
|
||||||
|
set then it is a root element.
|
||||||
|
|
||||||
|
This algorithm also keeps track of all nodes along the path so that
|
||||||
|
if other sibling nodes are also tracked then the lookup process can
|
||||||
|
skip a lot of steps in between and avoid traversing the entire tree
|
||||||
|
multiple times to the edge.
|
||||||
|
*/
|
||||||
|
function createIsRootFilterFn(nodes: any): (node: any) => boolean {
|
||||||
|
const nodeSet = new Set(nodes);
|
||||||
|
const knownRootContainer = new Set();
|
||||||
|
let isRoot: (node: any) => boolean;
|
||||||
|
isRoot = node => {
|
||||||
|
if (!node) return true;
|
||||||
|
if (nodeSet.has(node.parentNode)) return false;
|
||||||
|
if (knownRootContainer.has(node.parentNode)) return true;
|
||||||
|
if (isRoot(node.parentNode)) {
|
||||||
|
knownRootContainer.add(node);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
return isRoot;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CLASSES_CACHE_KEY = '$$classes';
|
const CLASSES_CACHE_KEY = '$$classes';
|
||||||
|
@ -851,6 +851,91 @@ export function main() {
|
|||||||
expect(p4.element.innerText.trim()).toEqual('4');
|
expect(p4.element.innerText.trim()).toEqual('4');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should find :enter/:leave nodes that are nested inside of ng-container elements', () => {
|
||||||
|
@Component({
|
||||||
|
selector: 'ani-cmp',
|
||||||
|
template: `
|
||||||
|
<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>
|
||||||
|
</section>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
animations: [trigger(
|
||||||
|
'myAnimation',
|
||||||
|
[
|
||||||
|
transition('0 => 5', [
|
||||||
|
query(':enter', [
|
||||||
|
style({ opacity: '0' }),
|
||||||
|
animate(1000, style({ opacity: '1' }))
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
transition('5 => 0', [
|
||||||
|
query(':leave', [
|
||||||
|
style({ opacity: '1' }),
|
||||||
|
animate(1000, style({ opacity: '0' }))
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
])]
|
||||||
|
})
|
||||||
|
class Cmp {
|
||||||
|
public items: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({declarations: [Cmp]});
|
||||||
|
|
||||||
|
const engine = TestBed.get(ɵAnimationEngine);
|
||||||
|
const fixture = TestBed.createComponent(Cmp);
|
||||||
|
const cmp = fixture.componentInstance;
|
||||||
|
|
||||||
|
cmp.items = [];
|
||||||
|
fixture.detectChanges();
|
||||||
|
engine.flush();
|
||||||
|
resetLog();
|
||||||
|
|
||||||
|
cmp.items = [0, 1, 2, 3, 4];
|
||||||
|
fixture.detectChanges();
|
||||||
|
engine.flush();
|
||||||
|
|
||||||
|
let players = getLog();
|
||||||
|
expect(players.length).toEqual(5);
|
||||||
|
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
let player = players[i] !;
|
||||||
|
expect(player.keyframes).toEqual([
|
||||||
|
{opacity: '0', offset: 0},
|
||||||
|
{opacity: '1', offset: 1},
|
||||||
|
]);
|
||||||
|
|
||||||
|
let elm = player.element;
|
||||||
|
let text = i % 2 == 0 ? `even ${i}` : `odd ${i}`;
|
||||||
|
expect(elm.innerText.trim()).toEqual(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
resetLog();
|
||||||
|
cmp.items = [];
|
||||||
|
fixture.detectChanges();
|
||||||
|
engine.flush();
|
||||||
|
|
||||||
|
players = getLog();
|
||||||
|
expect(players.length).toEqual(5);
|
||||||
|
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
let player = players[i] !;
|
||||||
|
expect(player.keyframes).toEqual([
|
||||||
|
{opacity: '1', offset: 0},
|
||||||
|
{opacity: '0', offset: 1},
|
||||||
|
]);
|
||||||
|
|
||||||
|
let elm = player.element;
|
||||||
|
let text = i % 2 == 0 ? `even ${i}` : `odd ${i}`;
|
||||||
|
expect(elm.innerText.trim()).toEqual(text);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('should properly cancel items that were queried into a former animation', () => {
|
it('should properly cancel items that were queried into a former animation', () => {
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ani-cmp',
|
selector: 'ani-cmp',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user