Revert "fix(animations): ensure animateChild() works with all inner leave animations (#19532)"
This reverts commit 1c77cdadaf
.
This commit is contained in:
parent
8e5b582b61
commit
f4480d46b4
|
@ -68,7 +68,7 @@ export class StateValue {
|
||||||
|
|
||||||
get params(): {[key: string]: any} { return this.options.params as{[key: string]: any}; }
|
get params(): {[key: string]: any} { return this.options.params as{[key: string]: any}; }
|
||||||
|
|
||||||
constructor(input: any, public namespaceId: string = '') {
|
constructor(input: any) {
|
||||||
const isObj = input && input.hasOwnProperty('value');
|
const isObj = input && input.hasOwnProperty('value');
|
||||||
const value = isObj ? input['value'] : input;
|
const value = isObj ? input['value'] : input;
|
||||||
this.value = normalizeTriggerValue(value);
|
this.value = normalizeTriggerValue(value);
|
||||||
|
@ -192,7 +192,7 @@ export class AnimationTransitionNamespace {
|
||||||
}
|
}
|
||||||
|
|
||||||
let fromState = triggersWithStates[triggerName];
|
let fromState = triggersWithStates[triggerName];
|
||||||
const toState = new StateValue(value, this.id);
|
const toState = new StateValue(value);
|
||||||
|
|
||||||
const isObj = value && value.hasOwnProperty('value');
|
const isObj = value && value.hasOwnProperty('value');
|
||||||
if (!isObj && fromState) {
|
if (!isObj && fromState) {
|
||||||
|
@ -305,31 +305,38 @@ export class AnimationTransitionNamespace {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _signalRemovalForInnerTriggers(rootElement: any, context: any, animate: boolean = false) {
|
private _destroyInnerNodes(rootElement: any, context: any, animate: boolean = false) {
|
||||||
// emulate a leave animation for all inner nodes within this node.
|
|
||||||
// If there are no animations found for any of the nodes then clear the cache
|
|
||||||
// for the element.
|
|
||||||
this._engine.driver.query(rootElement, NG_TRIGGER_SELECTOR, true).forEach(elm => {
|
this._engine.driver.query(rootElement, NG_TRIGGER_SELECTOR, true).forEach(elm => {
|
||||||
const namespaces = this._engine.fetchNamespacesByElement(elm);
|
if (animate && containsClass(elm, this._hostClassName)) {
|
||||||
if (namespaces.size) {
|
const innerNs = this._engine.namespacesByHostElement.get(elm);
|
||||||
namespaces.forEach(ns => { ns.triggerLeaveAnimation(elm, context, false, true); });
|
|
||||||
|
// special case for a host element with animations on the same element
|
||||||
|
if (innerNs) {
|
||||||
|
innerNs.removeNode(elm, context, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.removeNode(elm, context, true);
|
||||||
} else {
|
} else {
|
||||||
this.clearElementCache(elm);
|
this.clearElementCache(elm);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
triggerLeaveAnimation(
|
removeNode(element: any, context: any, doNotRecurse?: boolean): void {
|
||||||
element: any, context: any, destroyAfterComplete?: boolean,
|
const engine = this._engine;
|
||||||
defaultToFallback?: boolean): boolean {
|
|
||||||
const triggerStates = this._engine.statesByElement.get(element);
|
if (!doNotRecurse && element.childElementCount) {
|
||||||
|
this._destroyInnerNodes(element, context, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const triggerStates = engine.statesByElement.get(element);
|
||||||
if (triggerStates) {
|
if (triggerStates) {
|
||||||
const players: TransitionAnimationPlayer[] = [];
|
const players: TransitionAnimationPlayer[] = [];
|
||||||
Object.keys(triggerStates).forEach(triggerName => {
|
Object.keys(triggerStates).forEach(triggerName => {
|
||||||
// this check is here in the event that an element is removed
|
// this check is here in the event that an element is removed
|
||||||
// twice (both on the host level and the component level)
|
// twice (both on the host level and the component level)
|
||||||
if (this._triggers[triggerName]) {
|
if (this._triggers[triggerName]) {
|
||||||
const player = this.trigger(element, triggerName, VOID_VALUE, defaultToFallback);
|
const player = this.trigger(element, triggerName, VOID_VALUE, false);
|
||||||
if (player) {
|
if (player) {
|
||||||
players.push(player);
|
players.push(player);
|
||||||
}
|
}
|
||||||
|
@ -337,55 +344,11 @@ export class AnimationTransitionNamespace {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (players.length) {
|
if (players.length) {
|
||||||
this._engine.markElementAsRemoved(this.id, element, true, context);
|
engine.markElementAsRemoved(this.id, element, true, context);
|
||||||
if (destroyAfterComplete) {
|
optimizeGroupPlayer(players).onDone(() => engine.processLeaveNode(element));
|
||||||
optimizeGroupPlayer(players).onDone(() => this._engine.processLeaveNode(element));
|
return;
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
prepareLeaveAnimationListeners(element: any) {
|
|
||||||
const listeners = this._elementListeners.get(element);
|
|
||||||
if (listeners) {
|
|
||||||
const visitedTriggers = new Set<string>();
|
|
||||||
listeners.forEach(listener => {
|
|
||||||
const triggerName = listener.name;
|
|
||||||
if (visitedTriggers.has(triggerName)) return;
|
|
||||||
visitedTriggers.add(triggerName);
|
|
||||||
|
|
||||||
const trigger = this._triggers[triggerName];
|
|
||||||
const transition = trigger.fallbackTransition;
|
|
||||||
const elementStates = this._engine.statesByElement.get(element) !;
|
|
||||||
const fromState = elementStates[triggerName] || DEFAULT_STATE_VALUE;
|
|
||||||
const toState = new StateValue(VOID_VALUE);
|
|
||||||
const player = new TransitionAnimationPlayer(this.id, triggerName, element);
|
|
||||||
|
|
||||||
this._engine.totalQueuedPlayers++;
|
|
||||||
this._queue.push({
|
|
||||||
element,
|
|
||||||
triggerName,
|
|
||||||
transition,
|
|
||||||
fromState,
|
|
||||||
toState,
|
|
||||||
player,
|
|
||||||
isFallbackTransition: true
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
removeNode(element: any, context: any): void {
|
|
||||||
const engine = this._engine;
|
|
||||||
|
|
||||||
if (element.childElementCount) {
|
|
||||||
this._signalRemovalForInnerTriggers(element, context, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// this means that a * => VOID animation was detected and kicked off
|
|
||||||
if (this.triggerLeaveAnimation(element, context, true)) return;
|
|
||||||
|
|
||||||
// find the player that is animating and make sure that the
|
// find the player that is animating and make sure that the
|
||||||
// removal is delayed until that player has completed
|
// removal is delayed until that player has completed
|
||||||
|
@ -416,7 +379,33 @@ export class AnimationTransitionNamespace {
|
||||||
// during flush or will be picked up by a parent query. Either way
|
// during flush or will be picked up by a parent query. Either way
|
||||||
// we need to fire the listeners for this element when it DOES get
|
// we need to fire the listeners for this element when it DOES get
|
||||||
// removed (once the query parent animation is done or after flush)
|
// removed (once the query parent animation is done or after flush)
|
||||||
this.prepareLeaveAnimationListeners(element);
|
const listeners = this._elementListeners.get(element);
|
||||||
|
if (listeners) {
|
||||||
|
const visitedTriggers = new Set<string>();
|
||||||
|
listeners.forEach(listener => {
|
||||||
|
const triggerName = listener.name;
|
||||||
|
if (visitedTriggers.has(triggerName)) return;
|
||||||
|
visitedTriggers.add(triggerName);
|
||||||
|
|
||||||
|
const trigger = this._triggers[triggerName];
|
||||||
|
const transition = trigger.fallbackTransition;
|
||||||
|
const elementStates = engine.statesByElement.get(element) !;
|
||||||
|
const fromState = elementStates[triggerName] || DEFAULT_STATE_VALUE;
|
||||||
|
const toState = new StateValue(VOID_VALUE);
|
||||||
|
const player = new TransitionAnimationPlayer(this.id, triggerName, element);
|
||||||
|
|
||||||
|
this._engine.totalQueuedPlayers++;
|
||||||
|
this._queue.push({
|
||||||
|
element,
|
||||||
|
triggerName,
|
||||||
|
transition,
|
||||||
|
fromState,
|
||||||
|
toState,
|
||||||
|
player,
|
||||||
|
isFallbackTransition: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// whether or not a parent has an animation we need to delay the deferral of the leave
|
// whether or not a parent has an animation we need to delay the deferral of the leave
|
||||||
// operation until we have more information (which we do after flush() has been called)
|
// operation until we have more information (which we do after flush() has been called)
|
||||||
|
@ -479,7 +468,7 @@ export class AnimationTransitionNamespace {
|
||||||
|
|
||||||
destroy(context: any) {
|
destroy(context: any) {
|
||||||
this.players.forEach(p => p.destroy());
|
this.players.forEach(p => p.destroy());
|
||||||
this._signalRemovalForInnerTriggers(this.hostElement, context);
|
this._destroyInnerNodes(this.hostElement, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
elementContainsData(element: any): boolean {
|
elementContainsData(element: any): boolean {
|
||||||
|
@ -614,29 +603,6 @@ export class TransitionAnimationEngine {
|
||||||
|
|
||||||
private _fetchNamespace(id: string) { return this._namespaceLookup[id]; }
|
private _fetchNamespace(id: string) { return this._namespaceLookup[id]; }
|
||||||
|
|
||||||
fetchNamespacesByElement(element: any): Set<AnimationTransitionNamespace> {
|
|
||||||
// normally there should only be one namespace per element, however
|
|
||||||
// if @triggers are placed on both the component element and then
|
|
||||||
// its host element (within the component code) then there will be
|
|
||||||
// two namespaces returned. We use a set here to simply the dedupe
|
|
||||||
// of namespaces incase there are multiple triggers both the elm and host
|
|
||||||
const namespaces = new Set<AnimationTransitionNamespace>();
|
|
||||||
const elementStates = this.statesByElement.get(element);
|
|
||||||
if (elementStates) {
|
|
||||||
const keys = Object.keys(elementStates);
|
|
||||||
for (let i = 0; i < keys.length; i++) {
|
|
||||||
const nsId = elementStates[keys[i]].namespaceId;
|
|
||||||
if (nsId) {
|
|
||||||
const ns = this._fetchNamespace(nsId);
|
|
||||||
if (ns) {
|
|
||||||
namespaces.add(ns);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return namespaces;
|
|
||||||
}
|
|
||||||
|
|
||||||
trigger(namespaceId: string, element: any, name: string, value: any): boolean {
|
trigger(namespaceId: string, element: any, name: string, value: any): boolean {
|
||||||
if (isElementNode(element)) {
|
if (isElementNode(element)) {
|
||||||
this._fetchNamespace(namespaceId).trigger(element, name, value);
|
this._fetchNamespace(namespaceId).trigger(element, name, value);
|
||||||
|
@ -682,7 +648,7 @@ export class TransitionAnimationEngine {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
removeNode(namespaceId: string, element: any, context: any): void {
|
removeNode(namespaceId: string, element: any, context: any, doNotRecurse?: boolean): void {
|
||||||
if (!isElementNode(element)) {
|
if (!isElementNode(element)) {
|
||||||
this._onRemovalComplete(element, context);
|
this._onRemovalComplete(element, context);
|
||||||
return;
|
return;
|
||||||
|
@ -690,7 +656,7 @@ export class TransitionAnimationEngine {
|
||||||
|
|
||||||
const ns = namespaceId ? this._fetchNamespace(namespaceId) : null;
|
const ns = namespaceId ? this._fetchNamespace(namespaceId) : null;
|
||||||
if (ns) {
|
if (ns) {
|
||||||
ns.removeNode(element, context);
|
ns.removeNode(element, context, doNotRecurse);
|
||||||
} else {
|
} else {
|
||||||
this.markElementAsRemoved(namespaceId, element, false, context);
|
this.markElementAsRemoved(namespaceId, element, false, context);
|
||||||
}
|
}
|
||||||
|
@ -722,39 +688,37 @@ export class TransitionAnimationEngine {
|
||||||
|
|
||||||
destroyInnerAnimations(containerElement: any) {
|
destroyInnerAnimations(containerElement: any) {
|
||||||
let elements = this.driver.query(containerElement, NG_TRIGGER_SELECTOR, true);
|
let elements = this.driver.query(containerElement, NG_TRIGGER_SELECTOR, true);
|
||||||
elements.forEach(element => this.destroyActiveAnimationsForElement(element));
|
elements.forEach(element => {
|
||||||
|
const players = this.playersByElement.get(element);
|
||||||
|
if (players) {
|
||||||
|
players.forEach(player => {
|
||||||
|
// special case for when an element is set for destruction, but hasn't started.
|
||||||
|
// in this situation we want to delay the destruction until the flush occurs
|
||||||
|
// so that any event listeners attached to the player are triggered.
|
||||||
|
if (player.queued) {
|
||||||
|
player.markedForDestroy = true;
|
||||||
|
} else {
|
||||||
|
player.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const stateMap = this.statesByElement.get(element);
|
||||||
|
if (stateMap) {
|
||||||
|
Object.keys(stateMap).forEach(triggerName => stateMap[triggerName] = DELETED_STATE_VALUE);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (this.playersByQueriedElement.size == 0) return;
|
if (this.playersByQueriedElement.size == 0) return;
|
||||||
|
|
||||||
elements = this.driver.query(containerElement, NG_ANIMATING_SELECTOR, true);
|
elements = this.driver.query(containerElement, NG_ANIMATING_SELECTOR, true);
|
||||||
elements.forEach(element => this.finishActiveQueriedAnimationOnElement(element));
|
if (elements.length) {
|
||||||
}
|
elements.forEach(element => {
|
||||||
|
const players = this.playersByQueriedElement.get(element);
|
||||||
destroyActiveAnimationsForElement(element: any) {
|
if (players) {
|
||||||
const players = this.playersByElement.get(element);
|
players.forEach(player => player.finish());
|
||||||
if (players) {
|
|
||||||
players.forEach(player => {
|
|
||||||
// special case for when an element is set for destruction, but hasn't started.
|
|
||||||
// in this situation we want to delay the destruction until the flush occurs
|
|
||||||
// so that any event listeners attached to the player are triggered.
|
|
||||||
if (player.queued) {
|
|
||||||
player.markedForDestroy = true;
|
|
||||||
} else {
|
|
||||||
player.destroy();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const stateMap = this.statesByElement.get(element);
|
|
||||||
if (stateMap) {
|
|
||||||
Object.keys(stateMap).forEach(triggerName => stateMap[triggerName] = DELETED_STATE_VALUE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
finishActiveQueriedAnimationOnElement(element: any) {
|
|
||||||
const players = this.playersByQueriedElement.get(element);
|
|
||||||
if (players) {
|
|
||||||
players.forEach(player => player.finish());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
whenRenderingDone(): Promise<any> {
|
whenRenderingDone(): Promise<any> {
|
||||||
|
|
|
@ -2309,133 +2309,6 @@ export function main() {
|
||||||
expect(childCmp.childEvent.totalTime).toEqual(1000);
|
expect(childCmp.childEvent.totalTime).toEqual(1000);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should emulate a leave animation on a sub component\'s inner elements when a parent leave animation occurs with animateChild',
|
|
||||||
() => {
|
|
||||||
@Component({
|
|
||||||
selector: 'ani-cmp',
|
|
||||||
template: `
|
|
||||||
<div @myAnimation *ngIf="exp" class="parent">
|
|
||||||
<child-cmp></child-cmp>
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
animations: [
|
|
||||||
trigger(
|
|
||||||
'myAnimation',
|
|
||||||
[
|
|
||||||
transition(
|
|
||||||
':leave',
|
|
||||||
[
|
|
||||||
query('@*', animateChild()),
|
|
||||||
]),
|
|
||||||
]),
|
|
||||||
]
|
|
||||||
})
|
|
||||||
class ParentCmp {
|
|
||||||
public exp: boolean = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'child-cmp',
|
|
||||||
template: `
|
|
||||||
<section>
|
|
||||||
<div class="inner-div" @myChildAnimation></div>
|
|
||||||
</section>
|
|
||||||
`,
|
|
||||||
animations: [
|
|
||||||
trigger(
|
|
||||||
'myChildAnimation',
|
|
||||||
[
|
|
||||||
transition(
|
|
||||||
':leave',
|
|
||||||
[
|
|
||||||
style({opacity: 0}),
|
|
||||||
animate('1s', style({opacity: 1})),
|
|
||||||
]),
|
|
||||||
]),
|
|
||||||
]
|
|
||||||
})
|
|
||||||
class ChildCmp {
|
|
||||||
}
|
|
||||||
|
|
||||||
TestBed.configureTestingModule({declarations: [ParentCmp, ChildCmp]});
|
|
||||||
|
|
||||||
const engine = TestBed.get(ɵAnimationEngine);
|
|
||||||
const fixture = TestBed.createComponent(ParentCmp);
|
|
||||||
const cmp = fixture.componentInstance;
|
|
||||||
|
|
||||||
cmp.exp = true;
|
|
||||||
fixture.detectChanges();
|
|
||||||
|
|
||||||
cmp.exp = false;
|
|
||||||
fixture.detectChanges();
|
|
||||||
|
|
||||||
let players = getLog();
|
|
||||||
expect(players.length).toEqual(1);
|
|
||||||
const [player] = players;
|
|
||||||
|
|
||||||
expect(player.element.classList.contains('inner-div')).toBeTruthy();
|
|
||||||
expect(player.keyframes).toEqual([
|
|
||||||
{opacity: '0', offset: 0},
|
|
||||||
{opacity: '1', offset: 1},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not cause a removal of inner @trigger DOM nodes when a parent animation occurs',
|
|
||||||
fakeAsync(() => {
|
|
||||||
@Component({
|
|
||||||
selector: 'ani-cmp',
|
|
||||||
template: `
|
|
||||||
<div @parent *ngIf="exp" class="parent">
|
|
||||||
this <div @child>child</div>
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
animations: [
|
|
||||||
trigger(
|
|
||||||
'parent',
|
|
||||||
[
|
|
||||||
transition(
|
|
||||||
':leave',
|
|
||||||
[
|
|
||||||
style({opacity: 0}),
|
|
||||||
animate('1s', style({opacity: 1})),
|
|
||||||
]),
|
|
||||||
]),
|
|
||||||
trigger(
|
|
||||||
'child',
|
|
||||||
[
|
|
||||||
transition(
|
|
||||||
'* => something',
|
|
||||||
[
|
|
||||||
style({opacity: 0}),
|
|
||||||
animate('1s', style({opacity: 1})),
|
|
||||||
]),
|
|
||||||
]),
|
|
||||||
]
|
|
||||||
})
|
|
||||||
class Cmp {
|
|
||||||
public exp: boolean = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
TestBed.configureTestingModule({declarations: [Cmp]});
|
|
||||||
|
|
||||||
const fixture = TestBed.createComponent(Cmp);
|
|
||||||
const cmp = fixture.componentInstance;
|
|
||||||
|
|
||||||
cmp.exp = true;
|
|
||||||
fixture.detectChanges();
|
|
||||||
flushMicrotasks();
|
|
||||||
|
|
||||||
cmp.exp = false;
|
|
||||||
fixture.detectChanges();
|
|
||||||
flushMicrotasks();
|
|
||||||
|
|
||||||
const players = getLog();
|
|
||||||
expect(players.length).toEqual(1);
|
|
||||||
|
|
||||||
const element = players[0] !.element;
|
|
||||||
expect(element.innerText.trim()).toMatch(/this\s+child/mg);
|
|
||||||
}));
|
|
||||||
|
|
||||||
it('should only mark outermost *directive nodes :enter and :leave when inserts and removals occur',
|
it('should only mark outermost *directive nodes :enter and :leave when inserts and removals occur',
|
||||||
() => {
|
() => {
|
||||||
@Component({
|
@Component({
|
||||||
|
@ -2675,8 +2548,8 @@ export function main() {
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
flushMicrotasks();
|
flushMicrotasks();
|
||||||
expect(cmp.log).toEqual([
|
expect(cmp.log).toEqual([
|
||||||
'c1-start', 'c1-done', 'c2-start', 'c2-done', 'p-start', 'c3-start', 'c3-done',
|
'c1-start', 'c1-done', 'c2-start', 'c2-done', 'p-start', 'p-done', 'c3-start',
|
||||||
'p-done'
|
'c3-done'
|
||||||
]);
|
]);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue