feat(ivy): enhance [style] and [class] bindings to be animation aware (#26096)

PR Close #26096
This commit is contained in:
Matias Niemelä
2018-10-03 14:09:59 -07:00
committed by Misko Hevery
parent be337a2e52
commit fa8e633be5
19 changed files with 2045 additions and 439 deletions

View File

@ -8,6 +8,7 @@ ng_module(
name = "animation_world",
srcs = ["index.ts"],
tags = ["ivy-only"],
type_check = False, # see #26462
deps = [
"//packages/common",
"//packages/core",

View File

@ -6,7 +6,7 @@
"name": "AnimationWorldComponent"
},
{
"name": "AnimationWorldComponent_div_Template_4"
"name": "AnimationWorldComponent_div_Template_6"
},
{
"name": "BINDING_INDEX"
@ -14,6 +14,9 @@
{
"name": "BLOOM_MASK"
},
{
"name": "BoundPlayerFactory"
},
{
"name": "CIRCULAR$1"
},
@ -32,6 +35,9 @@
{
"name": "ChangeDetectionStrategy"
},
{
"name": "ClassAndStylePlayerBuilder"
},
{
"name": "CorePlayerHandler"
},
@ -212,6 +218,9 @@
{
"name": "ViewRef"
},
{
"name": "WebAnimationsPlayer"
},
{
"name": "_CLEAN_PROMISE"
},
@ -251,6 +260,9 @@
{
"name": "_c3"
},
{
"name": "_c4"
},
{
"name": "_currentInjector"
},
@ -278,6 +290,9 @@
{
"name": "addPlayer"
},
{
"name": "addPlayerInternal"
},
{
"name": "addRemoveViewFromContainer"
},
@ -290,6 +305,9 @@
{
"name": "allocStylingContext"
},
{
"name": "animateStyleFactory"
},
{
"name": "appendChild"
},
@ -302,6 +320,9 @@
{
"name": "bind"
},
{
"name": "bindPlayerFactory"
},
{
"name": "bindingRootIndex"
},
@ -644,6 +665,15 @@
{
"name": "getPipeDef"
},
{
"name": "getPlayerBuilder"
},
{
"name": "getPlayerBuilderIndex"
},
{
"name": "getPlayerContext"
},
{
"name": "getPointers"
},
@ -671,9 +701,15 @@
{
"name": "getRootContext"
},
{
"name": "getRootContext$2"
},
{
"name": "getRootView"
},
{
"name": "getRootView$1"
},
{
"name": "getStyleSanitizer"
},
@ -698,6 +734,9 @@
{
"name": "getValue"
},
{
"name": "hasPlayerBuilderChanged"
},
{
"name": "hasValueChanged"
},
@ -797,6 +836,9 @@
{
"name": "listener"
},
{
"name": "loadContext"
},
{
"name": "locateHostElement"
},
@ -809,6 +851,9 @@
{
"name": "makeParamDecorator"
},
{
"name": "markDirty"
},
{
"name": "markDirtyIfOnPush"
},
@ -900,7 +945,7 @@
"name": "renderEmbeddedTemplate"
},
{
"name": "renderStyling"
"name": "renderStyleAndClassBindings"
},
{
"name": "resetComponentState"
@ -932,6 +977,9 @@
{
"name": "setContextDirty"
},
{
"name": "setContextPlayersDirty"
},
{
"name": "setCurrentInjector"
},
@ -953,6 +1001,12 @@
{
"name": "setInputsFromAttrs"
},
{
"name": "setPlayerBuilder"
},
{
"name": "setPlayerBuilderIndex"
},
{
"name": "setProp"
},

View File

@ -9,16 +9,19 @@
import '@angular/core/test/bundling/util/src/reflect_metadata';
import {CommonModule} from '@angular/common';
import {Component, ElementRef, NgModule, ɵPlayState as PlayState, ɵPlayer as Player, ɵPlayerHandler as PlayerHandler, ɵaddPlayer as addPlayer, ɵrenderComponent as renderComponent} from '@angular/core';
import {Component, ElementRef, NgModule, ɵPlayState as PlayState, ɵPlayer as Player, ɵPlayerHandler as PlayerHandler, ɵaddPlayer as addPlayer, ɵbindPlayerFactory as bindPlayerFactory, ɵmarkDirty as markDirty, ɵrenderComponent as renderComponent} from '@angular/core';
@Component({
selector: 'animation-world',
template: `
<nav>
<button (click)="doAnimate()">Populate List</button>
<button (click)="animateWithCustomPlayer()">Animate List (custom player)</button>
<button (click)="animateWithStyles()">Populate List (style bindings)</button>
</nav>
<div class="list">
<div *ngFor="let item of items" class="record" [class]="makeClass(item)">
<div
*ngFor="let item of items" class="record" [class]="makeClass(item)" style="border-radius: 10px"
[style]="styles">
{{ item }}
</div>
</div>
@ -27,12 +30,18 @@ import {Component, ElementRef, NgModule, ɵPlayState as PlayState, ɵPlayer as P
class AnimationWorldComponent {
items: any[] = [1, 2, 3, 4, 5, 6, 7, 8, 9];
private _hostElement: HTMLElement;
public styles: {[key: string]: any}|null = null;
constructor(element: ElementRef) { this._hostElement = element.nativeElement; }
makeClass(index: number) { return `record-${index}`; }
doAnimate() {
animateWithStyles() {
this.styles = animateStyleFactory([{opacity: 0}, {opacity: 1}], 300, 'ease-out');
markDirty(this);
}
animateWithCustomPlayer() {
const elements = this._hostElement.querySelectorAll('div.record') as any as HTMLElement[];
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
@ -55,13 +64,13 @@ function buildAnimationPlayer(element: HTMLElement, animationName: string, time:
class SimpleKeyframePlayer implements Player {
state = PlayState.Pending;
parent: Player|null = null;
private _animationStyle: string;
private _animationStyle: string = '';
private _listeners: {[stateName: string]: (() => any)[]} = {};
constructor(private _element: HTMLElement, private _animationName: string, time: string) {
this._animationStyle = `${time} ${_animationName}`;
}
private _start() {
this._element.style.animation = this._animationStyle;
(this._element as any).style.animation = this._animationStyle;
const animationFn = (event: AnimationEvent) => {
if (event.animationName == this._animationName) {
this._element.removeEventListener('animationend', animationFn);
@ -134,3 +143,66 @@ class AnimationDebugger implements PlayerHandler {
const playerHandler = new AnimationDebugger();
renderComponent(AnimationWorldComponent, {playerHandler});
function animateStyleFactory(keyframes: any[], duration: number, easing: string) {
const limit = keyframes.length - 1;
const finalKeyframe = keyframes[limit];
return bindPlayerFactory((element: HTMLElement, type: number, values: {[key: string]: any}) => {
const kf = keyframes.slice(0, limit);
kf.push(values);
return new WebAnimationsPlayer(element, keyframes, duration, easing);
}, finalKeyframe);
}
class WebAnimationsPlayer implements Player {
state = PlayState.Pending;
parent: Player|null = null;
private _listeners: {[stateName: string]: (() => any)[]} = {};
constructor(
private _element: HTMLElement, private _keyframes: {[key: string]: any}[],
private _duration: number, private _easing: string) {}
private _start() {
const player = this._element.animate(
this._keyframes as any[], {duration: this._duration, easing: this._easing, fill: 'both'});
player.addEventListener('finish', e => { this.finish(); });
}
addEventListener(state: PlayState|string, cb: () => any): void {
const key = state.toString();
const arr = this._listeners[key] = (this._listeners[key] || []);
arr.push(cb);
}
play(): void {
if (this.state <= PlayState.Pending) {
this._start();
}
if (this.state != PlayState.Running) {
this.state = PlayState.Running;
this._emit(this.state);
}
}
pause(): void {
if (this.state != PlayState.Paused) {
this.state = PlayState.Paused;
this._emit(this.state);
}
}
finish(): void {
if (this.state < PlayState.Finished) {
this._element.style.animation = '';
this.state = PlayState.Finished;
this._emit(this.state);
}
}
destroy(): void {
if (this.state < PlayState.Destroyed) {
this.finish();
this.state = PlayState.Destroyed;
this._emit(this.state);
}
}
capture(): any {}
private _emit(state: PlayState) {
const arr = this._listeners[state.toString()] || [];
arr.forEach(cb => cb());
}
}

View File

@ -8,6 +8,9 @@
{
"name": "BLOOM_MASK"
},
{
"name": "BoundPlayerFactory"
},
{
"name": "CIRCULAR$1"
},
@ -26,6 +29,12 @@
{
"name": "ChangeDetectionStrategy"
},
{
"name": "ClassAndStylePlayerBuilder"
},
{
"name": "CorePlayerHandler"
},
{
"name": "DECLARATION_VIEW"
},
@ -344,12 +353,18 @@
{
"name": "addComponentLogic"
},
{
"name": "addPlayerInternal"
},
{
"name": "addRemoveViewFromContainer"
},
{
"name": "addToViewTree"
},
{
"name": "allocPlayerContext"
},
{
"name": "allocStylingContext"
},
@ -686,6 +701,15 @@
{
"name": "getPipeDef"
},
{
"name": "getPlayerBuilder"
},
{
"name": "getPlayerBuilderIndex"
},
{
"name": "getPlayerContext"
},
{
"name": "getPointers"
},
@ -710,6 +734,12 @@
{
"name": "getRendererFactory"
},
{
"name": "getRootContext"
},
{
"name": "getRootView"
},
{
"name": "getStyleSanitizer"
},
@ -734,6 +764,9 @@
{
"name": "getValue"
},
{
"name": "hasPlayerBuilderChanged"
},
{
"name": "hasValueChanged"
},
@ -930,7 +963,7 @@
"name": "renderEmbeddedTemplate"
},
{
"name": "renderStyling"
"name": "renderStyleAndClassBindings"
},
{
"name": "resetComponentState"
@ -962,6 +995,9 @@
{
"name": "setContextDirty"
},
{
"name": "setContextPlayersDirty"
},
{
"name": "setCurrentInjector"
},
@ -983,6 +1019,12 @@
{
"name": "setInputsFromAttrs"
},
{
"name": "setPlayerBuilder"
},
{
"name": "setPlayerBuilderIndex"
},
{
"name": "setProp"
},

View File

@ -59,6 +59,9 @@
{
"name": "BROWSER_SANITIZATION_PROVIDERS"
},
{
"name": "BoundPlayerFactory"
},
{
"name": "BrowserDomAdapter"
},
@ -128,6 +131,9 @@
{
"name": "ChangeDetectorRef"
},
{
"name": "ClassAndStylePlayerBuilder"
},
{
"name": "CommonModule"
},
@ -164,6 +170,9 @@
{
"name": "Console"
},
{
"name": "CorePlayerHandler"
},
{
"name": "CurrencyPipe"
},
@ -1169,6 +1178,9 @@
{
"name": "addDateMinutes"
},
{
"name": "addPlayerInternal"
},
{
"name": "addRemoveViewFromContainer"
},
@ -1178,6 +1190,9 @@
{
"name": "adjustBlueprintForNewNode"
},
{
"name": "allocPlayerContext"
},
{
"name": "allocStylingContext"
},
@ -1793,6 +1808,15 @@
{
"name": "getPlatform"
},
{
"name": "getPlayerBuilder"
},
{
"name": "getPlayerBuilderIndex"
},
{
"name": "getPlayerContext"
},
{
"name": "getPluralCategory"
},
@ -1823,6 +1847,12 @@
{
"name": "getRendererFactory"
},
{
"name": "getRootContext"
},
{
"name": "getRootView"
},
{
"name": "getStyleSanitizer"
},
@ -1868,6 +1898,9 @@
{
"name": "hasOnDestroy"
},
{
"name": "hasPlayerBuilderChanged"
},
{
"name": "hasValueChanged"
},
@ -2262,7 +2295,7 @@
"name": "renderEmbeddedTemplate"
},
{
"name": "renderStyling"
"name": "renderStyleAndClassBindings"
},
{
"name": "resetComponentState"
@ -2315,6 +2348,9 @@
{
"name": "setContextDirty"
},
{
"name": "setContextPlayersDirty"
},
{
"name": "setCurrentInjector"
},
@ -2336,6 +2372,12 @@
{
"name": "setInputsFromAttrs"
},
{
"name": "setPlayerBuilder"
},
{
"name": "setPlayerBuilderIndex"
},
{
"name": "setProp"
},

View File

@ -14,6 +14,8 @@ export class MockPlayer implements Player {
state: PlayState = PlayState.Pending;
private _listeners: {[state: string]: (() => any)[]} = {};
constructor(public value?: any) {}
play(): void {
if (this.state === PlayState.Running) return;

View File

@ -9,9 +9,9 @@ import {RenderFlags} from '@angular/core/src/render3';
import {defineComponent, getHostElement} from '../../../src/render3/index';
import {element, elementEnd, elementStart, elementStyling, elementStylingApply, load, markDirty} from '../../../src/render3/instructions';
import {PlayState, Player, PlayerContext, PlayerHandler} from '../../../src/render3/interfaces/player';
import {PlayState, Player, PlayerHandler} from '../../../src/render3/interfaces/player';
import {RElement} from '../../../src/render3/interfaces/renderer';
import {addPlayer, getPlayers} from '../../../src/render3/player';
import {addPlayer, getPlayers} from '../../../src/render3/players';
import {QueryList, query, queryRefresh} from '../../../src/render3/query';
import {getOrCreatePlayerContext} from '../../../src/render3/styling/util';
import {ComponentFixture} from '../render_util';
@ -56,14 +56,14 @@ describe('animation player access', () => {
it('should add a player to the element animation context and remove it once it completes', () => {
const element = buildElement();
const context = getOrCreatePlayerContext(element);
expect(context).toEqual([]);
expect(getPlayers(element)).toEqual([]);
const player = new MockPlayer();
addPlayer(element, player);
expect(readPlayers(context)).toEqual([player]);
expect(getPlayers(element)).toEqual([player]);
player.destroy();
expect(readPlayers(context)).toEqual([]);
expect(getPlayers(element)).toEqual([]);
});
it('should flush all pending animation players after change detection', () => {
@ -226,10 +226,6 @@ function buildElementWithStyling() {
return fixture.hostElement.querySelector('div') as RElement;
}
function readPlayers(context: PlayerContext): Player[] {
return context;
}
class Comp {
static ngComponentDef = defineComponent({
type: Comp,