Revert "fix(animations): ensure multi-level enter animations work (#19455)"

This reverts commit dd6237ecd9.
This commit is contained in:
Miško Hevery 2017-11-28 15:08:44 -06:00
parent 6b4c24020d
commit add5953aa1
8 changed files with 76 additions and 165 deletions

View File

@ -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 {ENTER_CLASSNAME, normalizeStyles} from '../util'; import {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,8 +39,7 @@ 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, ENTER_CLASSNAME, start, dest, options, this._driver, element, this._animationAst, start, dest, options, subInstructions, errors);
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);

View File

@ -62,6 +62,8 @@ 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 {
@ -476,7 +478,8 @@ function normalizeSelector(selector: string): [string, boolean] {
selector = selector.replace(SELF_TOKEN_REGEX, ''); selector = selector.replace(SELF_TOKEN_REGEX, '');
} }
selector = selector.replace(LEAVE_TOKEN_REGEX, LEAVE_SELECTOR) selector = selector.replace(ENTER_TOKEN_REGEX, ENTER_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);

View File

@ -15,8 +15,6 @@ 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
@ -103,22 +101,20 @@ const ENTER_TOKEN_REGEX = new RegExp(ENTER_TOKEN, 'g');
* the `AnimationValidatorVisitor` code. * the `AnimationValidatorVisitor` code.
*/ */
export function buildAnimationTimelines( export function buildAnimationTimelines(
driver: AnimationDriver, rootElement: any, ast: Ast<AnimationMetadataType>, enterClassName: string, driver: AnimationDriver, rootElement: any, ast: Ast<AnimationMetadataType>,
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, enterClassName, startingStyles, finalStyles, options, driver, rootElement, ast, startingStyles, finalStyles, options, subInstructions, errors);
subInstructions, errors);
} }
export class AnimationTimelineBuilderVisitor implements AstVisitor { export class AnimationTimelineBuilderVisitor implements AstVisitor {
buildKeyframes( buildKeyframes(
driver: AnimationDriver, rootElement: any, ast: Ast<AnimationMetadataType>, enterClassName: string, driver: AnimationDriver, rootElement: any, ast: Ast<AnimationMetadataType>,
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( const context = new AnimationTimelineContext(driver, rootElement, subInstructions, errors, []);
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);
@ -449,9 +445,8 @@ export class AnimationTimelineContext {
constructor( constructor(
private _driver: AnimationDriver, public element: any, private _driver: AnimationDriver, public element: any,
public subInstructions: ElementInstructionMap, private _enterClassName: string, public subInstructions: ElementInstructionMap, public errors: any[],
public errors: any[], public timelines: TimelineBuilder[], public timelines: TimelineBuilder[], initialTimeline?: 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);
} }
@ -504,8 +499,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._enterClassName, this.errors, this._driver, target, this.subInstructions, this.errors, this.timelines,
this.timelines, this.currentTimeline.fork(target, newTime || 0)); this.currentTimeline.fork(target, newTime || 0));
context.previousNode = this.previousNode; context.previousNode = this.previousNode;
context.currentAnimateTimings = this.currentAnimateTimings; context.currentAnimateTimings = this.currentAnimateTimings;
@ -560,7 +555,6 @@ 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) {

View File

@ -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,
enterClassName: string, currentOptions?: AnimationOptions, nextOptions?: AnimationOptions, 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, enterClassName, currentStateStyles, nextStateStyles, driver, element, this.ast.animation, currentStateStyles, nextStateStyles, animationOptions,
animationOptions, subInstructions, errors); subInstructions, errors);
if (errors.length) { if (errors.length) {
return createTransitionInstruction( return createTransitionInstruction(

View File

@ -13,7 +13,6 @@ 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';
@ -56,8 +55,7 @@ export class TimelineAnimationEngine {
if (ast) { if (ast) {
instructions = buildAnimationTimelines( instructions = buildAnimationTimelines(
this._driver, element, ast, ENTER_CLASSNAME, {}, {}, options, EMPTY_INSTRUCTION_MAP, this._driver, element, ast, {}, {}, options, EMPTY_INSTRUCTION_MAP, errors);
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);

View File

@ -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, iteratorToArray, setStyles} from '../util'; import {ENTER_CLASSNAME, LEAVE_CLASSNAME, NG_ANIMATING_CLASSNAME, NG_ANIMATING_SELECTOR, NG_TRIGGER_CLASSNAME, NG_TRIGGER_SELECTOR, copyObj, eraseStyles, 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,10 +714,9 @@ export class TransitionAnimationEngine {
return () => {}; return () => {};
} }
private _buildInstruction( private _buildInstruction(entry: QueueInstruction, subTimelines: ElementInstructionMap) {
entry: QueueInstruction, subTimelines: ElementInstructionMap, enterClassName: string) {
return entry.transition.build( return entry.transition.build(
this.driver, entry.element, entry.fromState.value, entry.toState.value, enterClassName, this.driver, entry.element, entry.fromState.value, entry.toState.value,
entry.fromState.options, entry.toState.options, subTimelines); entry.fromState.options, entry.toState.options, subTimelines);
} }
@ -863,19 +862,16 @@ export class TransitionAnimationEngine {
}); });
const bodyNode = getBodyNode(); const bodyNode = getBodyNode();
const enterNodeMap = const allEnterNodes: any[] = this.collectedEnterElements.length ?
buildRootMap(Array.from(this.statesByElement.keys()), this.collectedEnterElements); 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 enterNodeMapIds = new Map<any, string>(); for (let i = 0; i < allEnterNodes.length; i++) {
let i = 0; addClass(allEnterNodes[i], ENTER_CLASSNAME);
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>();
@ -892,11 +888,7 @@ export class TransitionAnimationEngine {
} }
cleanupFns.push(() => { cleanupFns.push(() => {
enterNodeMap.forEach((nodes, root) => { allEnterNodes.forEach(element => removeClass(element, ENTER_CLASSNAME));
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);
@ -917,8 +909,7 @@ export class TransitionAnimationEngine {
return; return;
} }
const enterClassName = enterNodeMapIds.get(element) !; const instruction = this._buildInstruction(entry, subTimelines) !;
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;
@ -982,6 +973,18 @@ 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
@ -1019,9 +1022,8 @@ export class TransitionAnimationEngine {
}); });
// POST STAGE: fill the * styles // POST STAGE: fill the * styles
const postStylesMap = new Map<any, ɵStyleData>(); const [postStylesMap, allLeaveQueriedNodes] = cloakAndComputeStyles(
const allLeaveQueriedNodes = cloakAndComputeStyles( this.driver, leaveNodesWithoutAnimations, allPostStyleElements, AUTO_STYLE);
postStylesMap, this.driver, leaveNodesWithoutAnimations, allPostStyleElements, AUTO_STYLE);
allLeaveQueriedNodes.forEach(node => { allLeaveQueriedNodes.forEach(node => {
if (replacePostStylesAsPre(node, allPreStyleElements, allPostStyleElements)) { if (replacePostStylesAsPre(node, allPreStyleElements, allPostStyleElements)) {
@ -1030,11 +1032,10 @@ export class TransitionAnimationEngine {
}); });
// PRE STAGE: fill the ! styles // PRE STAGE: fill the ! styles
const preStylesMap = new Map<any, ɵStyleData>(); const [preStylesMap] = allPreStyleElements.size ?
enterNodeMap.forEach((nodes, root) => {
cloakAndComputeStyles( cloakAndComputeStyles(
preStylesMap, this.driver, new Set(nodes), allPreStyleElements, PRE_STYLE); this.driver, enterNodesWithoutAnimations, allPreStyleElements, PRE_STYLE) :
}); [new Map<any, ɵStyleData>()];
replaceNodes.forEach(node => { replaceNodes.forEach(node => {
const post = postStylesMap.get(node); const post = postStylesMap.get(node);
@ -1504,11 +1505,12 @@ function cloakElement(element: any, value?: string) {
} }
function cloakAndComputeStyles( function cloakAndComputeStyles(
valuesMap: Map<any, ɵStyleData>, driver: AnimationDriver, elements: Set<any>, driver: AnimationDriver, elements: Set<any>, elementPropsMap: Map<any, Set<string>>,
elementPropsMap: Map<any, Set<string>>, defaultStyle: string): any[] { defaultStyle: string): [Map<any, ɵStyleData>, 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) => {
@ -1530,57 +1532,39 @@ 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 for each animation @trigger. out which nodes are roots.
By placing each inserted node into a Set and traversing upwards, it By placing all nodes into a set and traversing upwards to the edge,
is possible to find the @trigger elements and well any direct *star the recursive code can figure out if a clean path from the DOM node
insertion nodes, if a @trigger root is found then the enter element to the edge container is clear. If no other node is detected in the
is placed into the Map[@trigger] spot. 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 buildRootMap(roots: any[], nodes: any[]): Map<any, any[]> { function createIsRootFilterFn(nodes: any): (node: any) => boolean {
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 localRootMap = new Map<any, any>(); const knownRootContainer = new Set();
let isRoot: (node: any) => boolean;
function getRoot(node: any): any { isRoot = node => {
if (!node) return NULL_NODE; if (!node) return true;
if (nodeSet.has(node.parentNode)) return false;
let root = localRootMap.get(node); if (knownRootContainer.has(node.parentNode)) return true;
if (root) return root; if (isRoot(node.parentNode)) {
knownRootContainer.add(node);
const parent = node.parentNode; return true;
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 root; return isRoot;
}
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';

View File

@ -10,7 +10,6 @@ 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';
@ -229,8 +228,7 @@ 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( return trans.build(driver, element, fromState, toState, fromOptions, toOptions) !;
driver, element, fromState, toState, ENTER_CLASSNAME, fromOptions, toOptions) !;
} }
return null; return null;
} }

View File

@ -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, group, query, sequence, stagger, state, style, transition, trigger, ɵAnimationGroupPlayer as AnimationGroupPlayer} from '@angular/animations'; import {AUTO_STYLE, AnimationPlayer, animate, animateChild, query, 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,71 +2968,6 @@ 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', () => {