fix(animations): ensure multi-level enter animations work (#19455)
PR Close #19455
This commit is contained in:

committed by
Miško Hevery

parent
8bb42df47e
commit
b2a586cee1
@ -13,6 +13,7 @@ import {buildAnimationTimelines} from '../dsl/animation_timeline_builder';
|
||||
import {AnimationTimelineInstruction} from '../dsl/animation_timeline_instruction';
|
||||
import {ElementInstructionMap} from '../dsl/element_instruction_map';
|
||||
import {AnimationStyleNormalizer} from '../dsl/style_normalization/animation_style_normalizer';
|
||||
import {ENTER_CLASSNAME} from '../util';
|
||||
|
||||
import {AnimationDriver} from './animation_driver';
|
||||
import {getOrSetAsInMap, listenOnPlayer, makeAnimationEvent, normalizeKeyframes, optimizeGroupPlayer} from './shared';
|
||||
@ -55,7 +56,8 @@ export class TimelineAnimationEngine {
|
||||
|
||||
if (ast) {
|
||||
instructions = buildAnimationTimelines(
|
||||
this._driver, element, ast, {}, {}, options, EMPTY_INSTRUCTION_MAP, errors);
|
||||
this._driver, element, ast, ENTER_CLASSNAME, {}, {}, options, EMPTY_INSTRUCTION_MAP,
|
||||
errors);
|
||||
instructions.forEach(inst => {
|
||||
const styles = getOrSetAsInMap(autoStylesMap, inst.element, {});
|
||||
inst.postStyleProps.forEach(prop => styles[prop] = null);
|
||||
|
@ -13,7 +13,7 @@ import {AnimationTransitionInstruction} from '../dsl/animation_transition_instru
|
||||
import {AnimationTrigger} from '../dsl/animation_trigger';
|
||||
import {ElementInstructionMap} from '../dsl/element_instruction_map';
|
||||
import {AnimationStyleNormalizer} from '../dsl/style_normalization/animation_style_normalizer';
|
||||
import {ENTER_CLASSNAME, LEAVE_CLASSNAME, NG_ANIMATING_CLASSNAME, NG_ANIMATING_SELECTOR, NG_TRIGGER_CLASSNAME, NG_TRIGGER_SELECTOR, copyObj, eraseStyles, setStyles} from '../util';
|
||||
import {ENTER_CLASSNAME, LEAVE_CLASSNAME, NG_ANIMATING_CLASSNAME, NG_ANIMATING_SELECTOR, NG_TRIGGER_CLASSNAME, NG_TRIGGER_SELECTOR, copyObj, eraseStyles, iteratorToArray, setStyles} from '../util';
|
||||
|
||||
import {AnimationDriver} from './animation_driver';
|
||||
import {getBodyNode, getOrSetAsInMap, listenOnPlayer, makeAnimationEvent, normalizeKeyframes, optimizeGroupPlayer} from './shared';
|
||||
@ -714,9 +714,10 @@ export class TransitionAnimationEngine {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
private _buildInstruction(entry: QueueInstruction, subTimelines: ElementInstructionMap) {
|
||||
private _buildInstruction(
|
||||
entry: QueueInstruction, subTimelines: ElementInstructionMap, enterClassName: string) {
|
||||
return entry.transition.build(
|
||||
this.driver, entry.element, entry.fromState.value, entry.toState.value,
|
||||
this.driver, entry.element, entry.fromState.value, entry.toState.value, enterClassName,
|
||||
entry.fromState.options, entry.toState.options, subTimelines);
|
||||
}
|
||||
|
||||
@ -862,16 +863,19 @@ export class TransitionAnimationEngine {
|
||||
});
|
||||
|
||||
const bodyNode = getBodyNode();
|
||||
const allEnterNodes: any[] = this.collectedEnterElements.length ?
|
||||
this.collectedEnterElements.filter(createIsRootFilterFn(this.collectedEnterElements)) :
|
||||
[];
|
||||
const enterNodeMap =
|
||||
buildRootMap(Array.from(this.statesByElement.keys()), this.collectedEnterElements);
|
||||
|
||||
// this must occur before the instructions are built below such that
|
||||
// the :enter queries match the elements (since the timeline queries
|
||||
// are fired during instruction building).
|
||||
for (let i = 0; i < allEnterNodes.length; i++) {
|
||||
addClass(allEnterNodes[i], ENTER_CLASSNAME);
|
||||
}
|
||||
const enterNodeMapIds = new Map<any, string>();
|
||||
let i = 0;
|
||||
enterNodeMap.forEach((nodes, root) => {
|
||||
const className = ENTER_CLASSNAME + i++;
|
||||
enterNodeMapIds.set(root, className);
|
||||
nodes.forEach(node => addClass(node, className));
|
||||
});
|
||||
|
||||
const allLeaveNodes: any[] = [];
|
||||
const leaveNodesWithoutAnimations = new Set<any>();
|
||||
@ -888,7 +892,11 @@ export class TransitionAnimationEngine {
|
||||
}
|
||||
|
||||
cleanupFns.push(() => {
|
||||
allEnterNodes.forEach(element => removeClass(element, ENTER_CLASSNAME));
|
||||
enterNodeMap.forEach((nodes, root) => {
|
||||
const className = enterNodeMapIds.get(root) !;
|
||||
nodes.forEach(node => removeClass(node, className));
|
||||
});
|
||||
|
||||
allLeaveNodes.forEach(element => {
|
||||
removeClass(element, LEAVE_CLASSNAME);
|
||||
this.processLeaveNode(element);
|
||||
@ -909,7 +917,8 @@ export class TransitionAnimationEngine {
|
||||
return;
|
||||
}
|
||||
|
||||
const instruction = this._buildInstruction(entry, subTimelines) !;
|
||||
const enterClassName = enterNodeMapIds.get(element) !;
|
||||
const instruction = this._buildInstruction(entry, subTimelines, enterClassName) !;
|
||||
if (instruction.errors && instruction.errors.length) {
|
||||
erroneousTransitions.push(instruction);
|
||||
return;
|
||||
@ -973,18 +982,6 @@ export class TransitionAnimationEngine {
|
||||
this.reportError(errors);
|
||||
}
|
||||
|
||||
// these can only be detected here since we have a map of all the elements
|
||||
// that have animations attached to them... We use a set here in the event
|
||||
// multiple enter captures on the same element were caught in different
|
||||
// renderer namespaces (e.g. when a @trigger was on a host binding that had *ngIf)
|
||||
const enterNodesWithoutAnimations = new Set<any>();
|
||||
for (let i = 0; i < allEnterNodes.length; i++) {
|
||||
const element = allEnterNodes[i];
|
||||
if (!subTimelines.has(element)) {
|
||||
enterNodesWithoutAnimations.add(element);
|
||||
}
|
||||
}
|
||||
|
||||
const allPreviousPlayersMap = new Map<any, TransitionAnimationPlayer[]>();
|
||||
// this map works to tell which element in the DOM tree is contained by
|
||||
// which animation. Further down below this map will get populated once
|
||||
@ -1022,8 +1019,9 @@ export class TransitionAnimationEngine {
|
||||
});
|
||||
|
||||
// POST STAGE: fill the * styles
|
||||
const [postStylesMap, allLeaveQueriedNodes] = cloakAndComputeStyles(
|
||||
this.driver, leaveNodesWithoutAnimations, allPostStyleElements, AUTO_STYLE);
|
||||
const postStylesMap = new Map<any, ɵStyleData>();
|
||||
const allLeaveQueriedNodes = cloakAndComputeStyles(
|
||||
postStylesMap, this.driver, leaveNodesWithoutAnimations, allPostStyleElements, AUTO_STYLE);
|
||||
|
||||
allLeaveQueriedNodes.forEach(node => {
|
||||
if (replacePostStylesAsPre(node, allPreStyleElements, allPostStyleElements)) {
|
||||
@ -1032,10 +1030,11 @@ export class TransitionAnimationEngine {
|
||||
});
|
||||
|
||||
// PRE STAGE: fill the ! styles
|
||||
const [preStylesMap] = allPreStyleElements.size ?
|
||||
cloakAndComputeStyles(
|
||||
this.driver, enterNodesWithoutAnimations, allPreStyleElements, PRE_STYLE) :
|
||||
[new Map<any, ɵStyleData>()];
|
||||
const preStylesMap = new Map<any, ɵStyleData>();
|
||||
enterNodeMap.forEach((nodes, root) => {
|
||||
cloakAndComputeStyles(
|
||||
preStylesMap, this.driver, new Set(nodes), allPreStyleElements, PRE_STYLE);
|
||||
});
|
||||
|
||||
replaceNodes.forEach(node => {
|
||||
const post = postStylesMap.get(node);
|
||||
@ -1505,12 +1504,11 @@ function cloakElement(element: any, value?: string) {
|
||||
}
|
||||
|
||||
function cloakAndComputeStyles(
|
||||
driver: AnimationDriver, elements: Set<any>, elementPropsMap: Map<any, Set<string>>,
|
||||
defaultStyle: string): [Map<any, ɵStyleData>, any[]] {
|
||||
valuesMap: Map<any, ɵStyleData>, driver: AnimationDriver, elements: Set<any>,
|
||||
elementPropsMap: Map<any, Set<string>>, defaultStyle: string): any[] {
|
||||
const cloakVals: string[] = [];
|
||||
elements.forEach(element => cloakVals.push(cloakElement(element)));
|
||||
|
||||
const valuesMap = new Map<any, ɵStyleData>();
|
||||
const failedElements: any[] = [];
|
||||
|
||||
elementPropsMap.forEach((props: Set<string>, element: any) => {
|
||||
@ -1532,39 +1530,57 @@ function cloakAndComputeStyles(
|
||||
// an index value for the closure (but instead just the value)
|
||||
let i = 0;
|
||||
elements.forEach(element => cloakElement(element, cloakVals[i++]));
|
||||
return [valuesMap, failedElements];
|
||||
|
||||
return failedElements;
|
||||
}
|
||||
|
||||
/*
|
||||
Since the Angular renderer code will return a collection of inserted
|
||||
nodes in all areas of a DOM tree, it's up to this algorithm to figure
|
||||
out which nodes are roots.
|
||||
out which nodes are roots for each animation @trigger.
|
||||
|
||||
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.
|
||||
By placing each inserted node into a Set and traversing upwards, it
|
||||
is possible to find the @trigger elements and well any direct *star
|
||||
insertion nodes, if a @trigger root is found then the enter element
|
||||
is placed into the Map[@trigger] spot.
|
||||
*/
|
||||
function createIsRootFilterFn(nodes: any): (node: any) => boolean {
|
||||
function buildRootMap(roots: any[], nodes: any[]): Map<any, any[]> {
|
||||
const rootMap = new Map<any, any[]>();
|
||||
roots.forEach(root => rootMap.set(root, []));
|
||||
|
||||
if (nodes.length == 0) return rootMap;
|
||||
|
||||
const NULL_NODE = 1;
|
||||
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;
|
||||
const localRootMap = new Map<any, any>();
|
||||
|
||||
function getRoot(node: any): any {
|
||||
if (!node) return NULL_NODE;
|
||||
|
||||
let root = localRootMap.get(node);
|
||||
if (root) return root;
|
||||
|
||||
const parent = node.parentNode;
|
||||
if (rootMap.has(parent)) { // ngIf inside @trigger
|
||||
root = parent;
|
||||
} else if (nodeSet.has(parent)) { // ngIf inside ngIf
|
||||
root = NULL_NODE;
|
||||
} else { // recurse upwards
|
||||
root = getRoot(parent);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
return isRoot;
|
||||
|
||||
localRootMap.set(node, root);
|
||||
return root;
|
||||
}
|
||||
|
||||
nodes.forEach(node => {
|
||||
const root = getRoot(node);
|
||||
if (root !== NULL_NODE) {
|
||||
rootMap.get(root) !.push(node);
|
||||
}
|
||||
});
|
||||
|
||||
return rootMap;
|
||||
}
|
||||
|
||||
const CLASSES_CACHE_KEY = '$$classes';
|
||||
|
Reference in New Issue
Block a user