fix(animations): ensure multi-level enter animations work (#19455)
PR Close #19455
This commit is contained in:
parent
8bb42df47e
commit
b2a586cee1
|
@ -8,7 +8,7 @@
|
||||||
import {AnimationMetadata, AnimationMetadataType, AnimationOptions, ɵStyleData} from '@angular/animations';
|
import {AnimationMetadata, AnimationMetadataType, AnimationOptions, ɵStyleData} from '@angular/animations';
|
||||||
|
|
||||||
import {AnimationDriver} from '../render/animation_driver';
|
import {AnimationDriver} from '../render/animation_driver';
|
||||||
import {normalizeStyles} from '../util';
|
import {ENTER_CLASSNAME, normalizeStyles} from '../util';
|
||||||
|
|
||||||
import {Ast} from './animation_ast';
|
import {Ast} from './animation_ast';
|
||||||
import {buildAnimationAst} from './animation_ast_builder';
|
import {buildAnimationAst} from './animation_ast_builder';
|
||||||
|
@ -39,7 +39,8 @@ export class Animation {
|
||||||
const errors: any = [];
|
const errors: any = [];
|
||||||
subInstructions = subInstructions || new ElementInstructionMap();
|
subInstructions = subInstructions || new ElementInstructionMap();
|
||||||
const result = buildAnimationTimelines(
|
const result = buildAnimationTimelines(
|
||||||
this._driver, element, this._animationAst, start, dest, options, subInstructions, errors);
|
this._driver, element, this._animationAst, ENTER_CLASSNAME, start, dest, options,
|
||||||
|
subInstructions, errors);
|
||||||
if (errors.length) {
|
if (errors.length) {
|
||||||
const errorMessage = `animation building failed:\n${errors.join("\n")}`;
|
const errorMessage = `animation building failed:\n${errors.join("\n")}`;
|
||||||
throw new Error(errorMessage);
|
throw new Error(errorMessage);
|
||||||
|
|
|
@ -62,8 +62,6 @@ export function buildAnimationAst(
|
||||||
|
|
||||||
const LEAVE_TOKEN = ':leave';
|
const LEAVE_TOKEN = ':leave';
|
||||||
const LEAVE_TOKEN_REGEX = new RegExp(LEAVE_TOKEN, 'g');
|
const LEAVE_TOKEN_REGEX = new RegExp(LEAVE_TOKEN, 'g');
|
||||||
const ENTER_TOKEN = ':enter';
|
|
||||||
const ENTER_TOKEN_REGEX = new RegExp(ENTER_TOKEN, 'g');
|
|
||||||
const ROOT_SELECTOR = '';
|
const ROOT_SELECTOR = '';
|
||||||
|
|
||||||
export class AnimationAstBuilderVisitor implements AnimationDslVisitor {
|
export class AnimationAstBuilderVisitor implements AnimationDslVisitor {
|
||||||
|
@ -478,8 +476,7 @@ function normalizeSelector(selector: string): [string, boolean] {
|
||||||
selector = selector.replace(SELF_TOKEN_REGEX, '');
|
selector = selector.replace(SELF_TOKEN_REGEX, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
selector = selector.replace(ENTER_TOKEN_REGEX, ENTER_SELECTOR)
|
selector = selector.replace(LEAVE_TOKEN_REGEX, LEAVE_SELECTOR)
|
||||||
.replace(LEAVE_TOKEN_REGEX, LEAVE_SELECTOR)
|
|
||||||
.replace(/@\*/g, NG_TRIGGER_SELECTOR)
|
.replace(/@\*/g, NG_TRIGGER_SELECTOR)
|
||||||
.replace(/@\w+/g, match => NG_TRIGGER_SELECTOR + '-' + match.substr(1))
|
.replace(/@\w+/g, match => NG_TRIGGER_SELECTOR + '-' + match.substr(1))
|
||||||
.replace(/:animating/g, NG_ANIMATING_SELECTOR);
|
.replace(/:animating/g, NG_ANIMATING_SELECTOR);
|
||||||
|
|
|
@ -15,6 +15,8 @@ import {AnimationTimelineInstruction, createTimelineInstruction} from './animati
|
||||||
import {ElementInstructionMap} from './element_instruction_map';
|
import {ElementInstructionMap} from './element_instruction_map';
|
||||||
|
|
||||||
const ONE_FRAME_IN_MILLISECONDS = 1;
|
const ONE_FRAME_IN_MILLISECONDS = 1;
|
||||||
|
const ENTER_TOKEN = ':enter';
|
||||||
|
const ENTER_TOKEN_REGEX = new RegExp(ENTER_TOKEN, 'g');
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* The code within this file aims to generate web-animations-compatible keyframes from Angular's
|
* The code within this file aims to generate web-animations-compatible keyframes from Angular's
|
||||||
|
@ -101,20 +103,22 @@ const ONE_FRAME_IN_MILLISECONDS = 1;
|
||||||
* the `AnimationValidatorVisitor` code.
|
* the `AnimationValidatorVisitor` code.
|
||||||
*/
|
*/
|
||||||
export function buildAnimationTimelines(
|
export function buildAnimationTimelines(
|
||||||
driver: AnimationDriver, rootElement: any, ast: Ast<AnimationMetadataType>,
|
driver: AnimationDriver, rootElement: any, ast: Ast<AnimationMetadataType>, enterClassName: string,
|
||||||
startingStyles: ɵStyleData = {}, finalStyles: ɵStyleData = {}, options: AnimationOptions,
|
startingStyles: ɵStyleData = {}, finalStyles: ɵStyleData = {}, options: AnimationOptions,
|
||||||
subInstructions?: ElementInstructionMap, errors: any[] = []): AnimationTimelineInstruction[] {
|
subInstructions?: ElementInstructionMap, errors: any[] = []): AnimationTimelineInstruction[] {
|
||||||
return new AnimationTimelineBuilderVisitor().buildKeyframes(
|
return new AnimationTimelineBuilderVisitor().buildKeyframes(
|
||||||
driver, rootElement, ast, startingStyles, finalStyles, options, subInstructions, errors);
|
driver, rootElement, ast, enterClassName, startingStyles, finalStyles, options,
|
||||||
|
subInstructions, errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AnimationTimelineBuilderVisitor implements AstVisitor {
|
export class AnimationTimelineBuilderVisitor implements AstVisitor {
|
||||||
buildKeyframes(
|
buildKeyframes(
|
||||||
driver: AnimationDriver, rootElement: any, ast: Ast<AnimationMetadataType>,
|
driver: AnimationDriver, rootElement: any, ast: Ast<AnimationMetadataType>, enterClassName: string,
|
||||||
startingStyles: ɵStyleData, finalStyles: ɵStyleData, options: AnimationOptions,
|
startingStyles: ɵStyleData, finalStyles: ɵStyleData, options: AnimationOptions,
|
||||||
subInstructions?: ElementInstructionMap, errors: any[] = []): AnimationTimelineInstruction[] {
|
subInstructions?: ElementInstructionMap, errors: any[] = []): AnimationTimelineInstruction[] {
|
||||||
subInstructions = subInstructions || new ElementInstructionMap();
|
subInstructions = subInstructions || new ElementInstructionMap();
|
||||||
const context = new AnimationTimelineContext(driver, rootElement, subInstructions, errors, []);
|
const context = new AnimationTimelineContext(
|
||||||
|
driver, rootElement, subInstructions, enterClassName, errors, []);
|
||||||
context.options = options;
|
context.options = options;
|
||||||
context.currentTimeline.setStyles([startingStyles], null, context.errors, options);
|
context.currentTimeline.setStyles([startingStyles], null, context.errors, options);
|
||||||
|
|
||||||
|
@ -445,8 +449,9 @@ export class AnimationTimelineContext {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private _driver: AnimationDriver, public element: any,
|
private _driver: AnimationDriver, public element: any,
|
||||||
public subInstructions: ElementInstructionMap, public errors: any[],
|
public subInstructions: ElementInstructionMap, private _enterClassName: string,
|
||||||
public timelines: TimelineBuilder[], initialTimeline?: TimelineBuilder) {
|
public errors: any[], public timelines: TimelineBuilder[],
|
||||||
|
initialTimeline?: TimelineBuilder) {
|
||||||
this.currentTimeline = initialTimeline || new TimelineBuilder(this._driver, element, 0);
|
this.currentTimeline = initialTimeline || new TimelineBuilder(this._driver, element, 0);
|
||||||
timelines.push(this.currentTimeline);
|
timelines.push(this.currentTimeline);
|
||||||
}
|
}
|
||||||
|
@ -499,8 +504,8 @@ export class AnimationTimelineContext {
|
||||||
AnimationTimelineContext {
|
AnimationTimelineContext {
|
||||||
const target = element || this.element;
|
const target = element || this.element;
|
||||||
const context = new AnimationTimelineContext(
|
const context = new AnimationTimelineContext(
|
||||||
this._driver, target, this.subInstructions, this.errors, this.timelines,
|
this._driver, target, this.subInstructions, this._enterClassName, this.errors,
|
||||||
this.currentTimeline.fork(target, newTime || 0));
|
this.timelines, this.currentTimeline.fork(target, newTime || 0));
|
||||||
context.previousNode = this.previousNode;
|
context.previousNode = this.previousNode;
|
||||||
context.currentAnimateTimings = this.currentAnimateTimings;
|
context.currentAnimateTimings = this.currentAnimateTimings;
|
||||||
|
|
||||||
|
@ -555,6 +560,7 @@ export class AnimationTimelineContext {
|
||||||
results.push(this.element);
|
results.push(this.element);
|
||||||
}
|
}
|
||||||
if (selector.length > 0) { // if :self is only used then the selector is empty
|
if (selector.length > 0) { // if :self is only used then the selector is empty
|
||||||
|
selector = selector.replace(ENTER_TOKEN_REGEX, '.' + this._enterClassName);
|
||||||
const multi = limit != 1;
|
const multi = limit != 1;
|
||||||
let elements = this._driver.query(this.element, selector, multi);
|
let elements = this._driver.query(this.element, selector, multi);
|
||||||
if (limit !== 0) {
|
if (limit !== 0) {
|
||||||
|
|
|
@ -37,7 +37,7 @@ export class AnimationTransitionFactory {
|
||||||
|
|
||||||
build(
|
build(
|
||||||
driver: AnimationDriver, element: any, currentState: any, nextState: any,
|
driver: AnimationDriver, element: any, currentState: any, nextState: any,
|
||||||
currentOptions?: AnimationOptions, nextOptions?: AnimationOptions,
|
enterClassName: string, currentOptions?: AnimationOptions, nextOptions?: AnimationOptions,
|
||||||
subInstructions?: ElementInstructionMap): AnimationTransitionInstruction {
|
subInstructions?: ElementInstructionMap): AnimationTransitionInstruction {
|
||||||
const errors: any[] = [];
|
const errors: any[] = [];
|
||||||
|
|
||||||
|
@ -55,8 +55,8 @@ export class AnimationTransitionFactory {
|
||||||
const animationOptions = {params: {...transitionAnimationParams, ...nextAnimationParams}};
|
const animationOptions = {params: {...transitionAnimationParams, ...nextAnimationParams}};
|
||||||
|
|
||||||
const timelines = buildAnimationTimelines(
|
const timelines = buildAnimationTimelines(
|
||||||
driver, element, this.ast.animation, currentStateStyles, nextStateStyles, animationOptions,
|
driver, element, this.ast.animation, enterClassName, currentStateStyles, nextStateStyles,
|
||||||
subInstructions, errors);
|
animationOptions, subInstructions, errors);
|
||||||
|
|
||||||
if (errors.length) {
|
if (errors.length) {
|
||||||
return createTransitionInstruction(
|
return createTransitionInstruction(
|
||||||
|
|
|
@ -13,6 +13,7 @@ import {buildAnimationTimelines} from '../dsl/animation_timeline_builder';
|
||||||
import {AnimationTimelineInstruction} from '../dsl/animation_timeline_instruction';
|
import {AnimationTimelineInstruction} from '../dsl/animation_timeline_instruction';
|
||||||
import {ElementInstructionMap} from '../dsl/element_instruction_map';
|
import {ElementInstructionMap} from '../dsl/element_instruction_map';
|
||||||
import {AnimationStyleNormalizer} from '../dsl/style_normalization/animation_style_normalizer';
|
import {AnimationStyleNormalizer} from '../dsl/style_normalization/animation_style_normalizer';
|
||||||
|
import {ENTER_CLASSNAME} from '../util';
|
||||||
|
|
||||||
import {AnimationDriver} from './animation_driver';
|
import {AnimationDriver} from './animation_driver';
|
||||||
import {getOrSetAsInMap, listenOnPlayer, makeAnimationEvent, normalizeKeyframes, optimizeGroupPlayer} from './shared';
|
import {getOrSetAsInMap, listenOnPlayer, makeAnimationEvent, normalizeKeyframes, optimizeGroupPlayer} from './shared';
|
||||||
|
@ -55,7 +56,8 @@ export class TimelineAnimationEngine {
|
||||||
|
|
||||||
if (ast) {
|
if (ast) {
|
||||||
instructions = buildAnimationTimelines(
|
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 => {
|
instructions.forEach(inst => {
|
||||||
const styles = getOrSetAsInMap(autoStylesMap, inst.element, {});
|
const styles = getOrSetAsInMap(autoStylesMap, inst.element, {});
|
||||||
inst.postStyleProps.forEach(prop => styles[prop] = null);
|
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 {AnimationTrigger} from '../dsl/animation_trigger';
|
||||||
import {ElementInstructionMap} from '../dsl/element_instruction_map';
|
import {ElementInstructionMap} from '../dsl/element_instruction_map';
|
||||||
import {AnimationStyleNormalizer} from '../dsl/style_normalization/animation_style_normalizer';
|
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 {AnimationDriver} from './animation_driver';
|
||||||
import {getBodyNode, getOrSetAsInMap, listenOnPlayer, makeAnimationEvent, normalizeKeyframes, optimizeGroupPlayer} from './shared';
|
import {getBodyNode, getOrSetAsInMap, listenOnPlayer, makeAnimationEvent, normalizeKeyframes, optimizeGroupPlayer} from './shared';
|
||||||
|
@ -714,9 +714,10 @@ export class TransitionAnimationEngine {
|
||||||
return () => {};
|
return () => {};
|
||||||
}
|
}
|
||||||
|
|
||||||
private _buildInstruction(entry: QueueInstruction, subTimelines: ElementInstructionMap) {
|
private _buildInstruction(
|
||||||
|
entry: QueueInstruction, subTimelines: ElementInstructionMap, enterClassName: string) {
|
||||||
return entry.transition.build(
|
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);
|
entry.fromState.options, entry.toState.options, subTimelines);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -862,16 +863,19 @@ export class TransitionAnimationEngine {
|
||||||
});
|
});
|
||||||
|
|
||||||
const bodyNode = getBodyNode();
|
const bodyNode = getBodyNode();
|
||||||
const allEnterNodes: any[] = this.collectedEnterElements.length ?
|
const enterNodeMap =
|
||||||
this.collectedEnterElements.filter(createIsRootFilterFn(this.collectedEnterElements)) :
|
buildRootMap(Array.from(this.statesByElement.keys()), 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).
|
||||||
for (let i = 0; i < allEnterNodes.length; i++) {
|
const enterNodeMapIds = new Map<any, string>();
|
||||||
addClass(allEnterNodes[i], ENTER_CLASSNAME);
|
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 allLeaveNodes: any[] = [];
|
||||||
const leaveNodesWithoutAnimations = new Set<any>();
|
const leaveNodesWithoutAnimations = new Set<any>();
|
||||||
|
@ -888,7 +892,11 @@ export class TransitionAnimationEngine {
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanupFns.push(() => {
|
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 => {
|
allLeaveNodes.forEach(element => {
|
||||||
removeClass(element, LEAVE_CLASSNAME);
|
removeClass(element, LEAVE_CLASSNAME);
|
||||||
this.processLeaveNode(element);
|
this.processLeaveNode(element);
|
||||||
|
@ -909,7 +917,8 @@ export class TransitionAnimationEngine {
|
||||||
return;
|
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) {
|
if (instruction.errors && instruction.errors.length) {
|
||||||
erroneousTransitions.push(instruction);
|
erroneousTransitions.push(instruction);
|
||||||
return;
|
return;
|
||||||
|
@ -973,18 +982,6 @@ export class TransitionAnimationEngine {
|
||||||
this.reportError(errors);
|
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[]>();
|
const allPreviousPlayersMap = new Map<any, TransitionAnimationPlayer[]>();
|
||||||
// this map works to tell which element in the DOM tree is contained by
|
// 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
|
// which animation. Further down below this map will get populated once
|
||||||
|
@ -1022,8 +1019,9 @@ export class TransitionAnimationEngine {
|
||||||
});
|
});
|
||||||
|
|
||||||
// POST STAGE: fill the * styles
|
// POST STAGE: fill the * styles
|
||||||
const [postStylesMap, allLeaveQueriedNodes] = cloakAndComputeStyles(
|
const postStylesMap = new Map<any, ɵStyleData>();
|
||||||
this.driver, leaveNodesWithoutAnimations, allPostStyleElements, AUTO_STYLE);
|
const allLeaveQueriedNodes = cloakAndComputeStyles(
|
||||||
|
postStylesMap, this.driver, leaveNodesWithoutAnimations, allPostStyleElements, AUTO_STYLE);
|
||||||
|
|
||||||
allLeaveQueriedNodes.forEach(node => {
|
allLeaveQueriedNodes.forEach(node => {
|
||||||
if (replacePostStylesAsPre(node, allPreStyleElements, allPostStyleElements)) {
|
if (replacePostStylesAsPre(node, allPreStyleElements, allPostStyleElements)) {
|
||||||
|
@ -1032,10 +1030,11 @@ export class TransitionAnimationEngine {
|
||||||
});
|
});
|
||||||
|
|
||||||
// PRE STAGE: fill the ! styles
|
// PRE STAGE: fill the ! styles
|
||||||
const [preStylesMap] = allPreStyleElements.size ?
|
const preStylesMap = new Map<any, ɵStyleData>();
|
||||||
cloakAndComputeStyles(
|
enterNodeMap.forEach((nodes, root) => {
|
||||||
this.driver, enterNodesWithoutAnimations, allPreStyleElements, PRE_STYLE) :
|
cloakAndComputeStyles(
|
||||||
[new Map<any, ɵStyleData>()];
|
preStylesMap, this.driver, new Set(nodes), allPreStyleElements, PRE_STYLE);
|
||||||
|
});
|
||||||
|
|
||||||
replaceNodes.forEach(node => {
|
replaceNodes.forEach(node => {
|
||||||
const post = postStylesMap.get(node);
|
const post = postStylesMap.get(node);
|
||||||
|
@ -1505,12 +1504,11 @@ function cloakElement(element: any, value?: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function cloakAndComputeStyles(
|
function cloakAndComputeStyles(
|
||||||
driver: AnimationDriver, elements: Set<any>, elementPropsMap: Map<any, Set<string>>,
|
valuesMap: Map<any, ɵStyleData>, driver: AnimationDriver, elements: Set<any>,
|
||||||
defaultStyle: string): [Map<any, ɵStyleData>, any[]] {
|
elementPropsMap: Map<any, Set<string>>, defaultStyle: string): any[] {
|
||||||
const cloakVals: string[] = [];
|
const cloakVals: string[] = [];
|
||||||
elements.forEach(element => cloakVals.push(cloakElement(element)));
|
elements.forEach(element => cloakVals.push(cloakElement(element)));
|
||||||
|
|
||||||
const valuesMap = new Map<any, ɵStyleData>();
|
|
||||||
const failedElements: any[] = [];
|
const failedElements: any[] = [];
|
||||||
|
|
||||||
elementPropsMap.forEach((props: Set<string>, element: 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)
|
// an index value for the closure (but instead just the value)
|
||||||
let i = 0;
|
let i = 0;
|
||||||
elements.forEach(element => cloakElement(element, cloakVals[i++]));
|
elements.forEach(element => cloakElement(element, cloakVals[i++]));
|
||||||
return [valuesMap, failedElements];
|
|
||||||
|
return failedElements;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Since the Angular renderer code will return a collection of inserted
|
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
|
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,
|
By placing each inserted node into a Set and traversing upwards, it
|
||||||
the recursive code can figure out if a clean path from the DOM node
|
is possible to find the @trigger elements and well any direct *star
|
||||||
to the edge container is clear. If no other node is detected in the
|
insertion nodes, if a @trigger root is found then the enter element
|
||||||
set then it is a root element.
|
is placed into the Map[@trigger] spot.
|
||||||
|
|
||||||
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 {
|
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 nodeSet = new Set(nodes);
|
||||||
const knownRootContainer = new Set();
|
const localRootMap = new Map<any, any>();
|
||||||
let isRoot: (node: any) => boolean;
|
|
||||||
isRoot = node => {
|
function getRoot(node: any): any {
|
||||||
if (!node) return true;
|
if (!node) return NULL_NODE;
|
||||||
if (nodeSet.has(node.parentNode)) return false;
|
|
||||||
if (knownRootContainer.has(node.parentNode)) return true;
|
let root = localRootMap.get(node);
|
||||||
if (isRoot(node.parentNode)) {
|
if (root) return root;
|
||||||
knownRootContainer.add(node);
|
|
||||||
return true;
|
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;
|
|
||||||
};
|
localRootMap.set(node, root);
|
||||||
return isRoot;
|
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';
|
const CLASSES_CACHE_KEY = '$$classes';
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {AnimationOptions, animate, state, style, transition} from '@angular/anim
|
||||||
import {AnimationTransitionInstruction} from '@angular/animations/browser/src/dsl/animation_transition_instruction';
|
import {AnimationTransitionInstruction} from '@angular/animations/browser/src/dsl/animation_transition_instruction';
|
||||||
import {AnimationTrigger} from '@angular/animations/browser/src/dsl/animation_trigger';
|
import {AnimationTrigger} from '@angular/animations/browser/src/dsl/animation_trigger';
|
||||||
|
|
||||||
|
import {ENTER_CLASSNAME} from '../../src/util';
|
||||||
import {MockAnimationDriver} from '../../testing';
|
import {MockAnimationDriver} from '../../testing';
|
||||||
import {makeTrigger} from '../shared';
|
import {makeTrigger} from '../shared';
|
||||||
|
|
||||||
|
@ -228,7 +229,8 @@ function buildTransition(
|
||||||
const trans = trigger.matchTransition(fromState, toState) !;
|
const trans = trigger.matchTransition(fromState, toState) !;
|
||||||
if (trans) {
|
if (trans) {
|
||||||
const driver = new MockAnimationDriver();
|
const driver = new MockAnimationDriver();
|
||||||
return trans.build(driver, element, fromState, toState, fromOptions, toOptions) !;
|
return trans.build(
|
||||||
|
driver, element, fromState, toState, ENTER_CLASSNAME, fromOptions, toOptions) !;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
* Use of this source code is governed by an MIT-style license that can be
|
* Use of this source code is governed by an MIT-style license that can be
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
import {AUTO_STYLE, AnimationPlayer, animate, animateChild, query, stagger, state, style, transition, trigger, ɵAnimationGroupPlayer as AnimationGroupPlayer} from '@angular/animations';
|
import {AUTO_STYLE, AnimationPlayer, animate, animateChild, group, query, sequence, stagger, state, style, transition, trigger, ɵAnimationGroupPlayer as AnimationGroupPlayer} from '@angular/animations';
|
||||||
import {AnimationDriver, ɵAnimationEngine} from '@angular/animations/browser';
|
import {AnimationDriver, ɵAnimationEngine} from '@angular/animations/browser';
|
||||||
import {matchesElement} from '@angular/animations/browser/src/render/shared';
|
import {matchesElement} from '@angular/animations/browser/src/render/shared';
|
||||||
import {ENTER_CLASSNAME, LEAVE_CLASSNAME} from '@angular/animations/browser/src/util';
|
import {ENTER_CLASSNAME, LEAVE_CLASSNAME} from '@angular/animations/browser/src/util';
|
||||||
|
@ -2968,6 +2968,71 @@ export function main() {
|
||||||
{offset: 0, width: '0px'}, {offset: .67, width: '0px'}, {offset: 1, width: '200px'}
|
{offset: 0, width: '0px'}, {offset: .67, width: '0px'}, {offset: 1, width: '200px'}
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should scope :enter queries between sub animations', () => {
|
||||||
|
@Component({
|
||||||
|
selector: 'cmp',
|
||||||
|
animations: [
|
||||||
|
trigger(
|
||||||
|
'parent',
|
||||||
|
[
|
||||||
|
transition(':enter', group([
|
||||||
|
sequence([
|
||||||
|
style({opacity: 0}),
|
||||||
|
animate(1000, style({opacity: 1})),
|
||||||
|
]),
|
||||||
|
query(':enter @child', animateChild()),
|
||||||
|
])),
|
||||||
|
]),
|
||||||
|
trigger(
|
||||||
|
'child',
|
||||||
|
[
|
||||||
|
transition(
|
||||||
|
':enter',
|
||||||
|
[
|
||||||
|
query(
|
||||||
|
':enter .item',
|
||||||
|
[style({opacity: 0}), animate(1000, style({opacity: 1}))]),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
template: `
|
||||||
|
<div @parent *ngIf="exp1" class="container">
|
||||||
|
<div *ngIf="exp2">
|
||||||
|
<div @child>
|
||||||
|
<div *ngIf="exp3">
|
||||||
|
<div class="item"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
class Cmp {
|
||||||
|
public exp1: any;
|
||||||
|
public exp2: any;
|
||||||
|
public exp3: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({declarations: [Cmp]});
|
||||||
|
|
||||||
|
const fixture = TestBed.createComponent(Cmp);
|
||||||
|
fixture.detectChanges();
|
||||||
|
resetLog();
|
||||||
|
|
||||||
|
const cmp = fixture.componentInstance;
|
||||||
|
cmp.exp1 = true;
|
||||||
|
cmp.exp2 = true;
|
||||||
|
cmp.exp3 = true;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const players = getLog();
|
||||||
|
expect(players.length).toEqual(2);
|
||||||
|
|
||||||
|
const [p1, p2] = players;
|
||||||
|
expect(p1.element.classList.contains('container')).toBeTruthy();
|
||||||
|
expect(p2.element.classList.contains('item')).toBeTruthy();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('animation control flags', () => {
|
describe('animation control flags', () => {
|
||||||
|
|
Loading…
Reference in New Issue