feat(ivy): enhance [style] and [class] bindings to be animation aware (#26096)
PR Close #26096
This commit is contained in:

committed by
Misko Hevery

parent
be337a2e52
commit
fa8e633be5
@ -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",
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -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"
|
||||
},
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -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;
|
||||
|
||||
|
@ -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,
|
||||
|
Reference in New Issue
Block a user