Skip to content

Commit b2f366b

Browse files
matskovicb
authored andcommitted
fix(animations): only use the WA-polyfill alongside AnimationBuilder (#22143)
This patch removes the need to include the Web Animations API Polyfill (web-animations-js) as a dependency. Angular will now fallback to using CSS Keyframes in the event that `element.animate` is no longer supported by the browser. In the event that an application does use `AnimationBuilder` then the web-animations-js polyfill is required to enable programmatic, position-based access to an animation. Closes #17496 PR Close #22143
1 parent 9eecb0b commit b2f366b

23 files changed

+1680
-81
lines changed

aio/content/guide/animations.md

+4-2
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@ animation logic with the rest of your application code, for ease of control.
1616
Angular animations are built on top of the standard [Web Animations API](https://w3c.github.io/web-animations/)
1717
and run natively on [browsers that support it](http://caniuse.com/#feat=web-animation).
1818

19-
For other browsers, a polyfill is required. Uncomment the `web-animations-js` polyfill from the `polyfills.ts` file.
20-
19+
As of Angular 6, If the Web Animations API is not supported natively by the browser, then Angular will use CSS
20+
keyframes as a fallback instead (automatically). This means that the polyfill is no longer required unless any
21+
code uses [AnimationBuilder](/api/animations/AnimationBuilder). If your code does use AnimationBuilder, then
22+
uncomment the `web-animations-js` polyfill from the `polyfills.ts` file generated by Angular CLI.
2123
</div>
2224

2325
<div class="l-sub-section">

aio/content/guide/browser-support.md

+8-2
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,8 @@ But if you need an optional polyfill, you'll have to install its npm package.
133133
For example, [if you need the web animations polyfill](http://caniuse.com/#feat=web-animation), you could install it with `npm`, using the following command (or the `yarn` equivalent):
134134

135135
<code-example language="sh" class="code-shell">
136+
# note that the web-animations-js polyfill is only here as an example
137+
# it isn't a strict requirement of Angular anymore (more below)
136138
npm install --save web-animations-js
137139
</code-example>
138140

@@ -226,7 +228,8 @@ These are the polyfills required to run an Angular application on each supported
226228

227229
Some features of Angular may require additional polyfills.
228230

229-
For example, the animations library relies on the standard web animation API, which is only available in Chrome and Firefox today. You'll need a polyfill to use animations in other browsers.
231+
For example, the animations library relies on the standard web animation API, which is only available in Chrome and Firefox today.
232+
(note that the dependency of web-animations-js in Angular is only necessary if `AnimationBuilder` is used.)
230233

231234
Here are the features which may require additional polyfills:
232235

@@ -276,6 +279,8 @@ Here are the features which may require additional polyfills:
276279
<td>
277280

278281
[Animations](guide/animations)
282+
<br>Only if `Animation Builder` is used within the application--standard
283+
animation support in Angular doesn't require any polyfills (as of NG6).
279284

280285
</td>
281286

@@ -286,7 +291,8 @@ Here are the features which may require additional polyfills:
286291
</td>
287292

288293
<td>
289-
All but Chrome and Firefox<br>Not supported in IE9
294+
<p>If AnimationBuilder is used then the polyfill will enable scrubbing
295+
support for IE/Edge and Safari (Chrome and Firefox support this natively).</p>
290296
</td>
291297

292298
</tr>

packages/animations/browser/src/private_export.ts

+2
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,7 @@ export {AnimationStyleNormalizer as ɵAnimationStyleNormalizer, NoopAnimationSty
1010
export {WebAnimationsStyleNormalizer as ɵWebAnimationsStyleNormalizer} from './dsl/style_normalization/web_animations_style_normalizer';
1111
export {NoopAnimationDriver as ɵNoopAnimationDriver} from './render/animation_driver';
1212
export {AnimationEngine as ɵAnimationEngine} from './render/animation_engine_next';
13+
export {CssKeyframesDriver as ɵCssKeyframesDriver} from './render/css_keyframes/css_keyframes_driver';
14+
export {CssKeyframesPlayer as ɵCssKeyframesPlayer} from './render/css_keyframes/css_keyframes_player';
1315
export {WebAnimationsDriver as ɵWebAnimationsDriver, supportsWebAnimations as ɵsupportsWebAnimations} from './render/web_animations/web_animations_driver';
1416
export {WebAnimationsPlayer as ɵWebAnimationsPlayer} from './render/web_animations/web_animations_player';

packages/animations/browser/src/render/animation_driver.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ export class NoopAnimationDriver implements AnimationDriver {
3333

3434
animate(
3535
element: any, keyframes: {[key: string]: string | number}[], duration: number, delay: number,
36-
easing: string, previousPlayers: any[] = []): AnimationPlayer {
36+
easing: string, previousPlayers: any[] = [],
37+
scrubberAccessRequested?: boolean): AnimationPlayer {
3738
return new NoopAnimationPlayer(duration, delay);
3839
}
3940
}
@@ -56,5 +57,5 @@ export abstract class AnimationDriver {
5657

5758
abstract animate(
5859
element: any, keyframes: {[key: string]: string | number}[], duration: number, delay: number,
59-
easing?: string|null, previousPlayers?: any[]): any;
60+
easing?: string|null, previousPlayers?: any[], scrubberAccessRequested?: boolean): any;
6061
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
import {AnimationPlayer, ɵStyleData} from '@angular/animations';
9+
10+
import {allowPreviousPlayerStylesMerge, balancePreviousStylesIntoKeyframes, computeStyle} from '../../util';
11+
import {AnimationDriver} from '../animation_driver';
12+
import {containsElement, invokeQuery, matchesElement, validateStyleProperty} from '../shared';
13+
14+
import {CssKeyframesPlayer} from './css_keyframes_player';
15+
import {DirectStylePlayer} from './direct_style_player';
16+
17+
const KEYFRAMES_NAME_PREFIX = 'gen_css_kf_';
18+
const TAB_SPACE = ' ';
19+
20+
export class CssKeyframesDriver implements AnimationDriver {
21+
private _count = 0;
22+
private readonly _head: any = document.querySelector('head');
23+
private _warningIssued = false;
24+
25+
validateStyleProperty(prop: string): boolean { return validateStyleProperty(prop); }
26+
27+
matchesElement(element: any, selector: string): boolean {
28+
return matchesElement(element, selector);
29+
}
30+
31+
containsElement(elm1: any, elm2: any): boolean { return containsElement(elm1, elm2); }
32+
33+
query(element: any, selector: string, multi: boolean): any[] {
34+
return invokeQuery(element, selector, multi);
35+
}
36+
37+
computeStyle(element: any, prop: string, defaultValue?: string): string {
38+
return (window.getComputedStyle(element) as any)[prop] as string;
39+
}
40+
41+
buildKeyframeElement(element: any, name: string, keyframes: {[key: string]: any}[]): any {
42+
keyframes = keyframes.map(kf => hypenatePropsObject(kf));
43+
let keyframeStr = `@keyframes ${name} {\n`;
44+
let tab = '';
45+
keyframes.forEach(kf => {
46+
tab = TAB_SPACE;
47+
const offset = parseFloat(kf.offset);
48+
keyframeStr += `${tab}${offset * 100}% {\n`;
49+
tab += TAB_SPACE;
50+
Object.keys(kf).forEach(prop => {
51+
const value = kf[prop];
52+
switch (prop) {
53+
case 'offset':
54+
return;
55+
case 'easing':
56+
if (value) {
57+
keyframeStr += `${tab}animation-timing-function: ${value};\n`;
58+
}
59+
return;
60+
default:
61+
keyframeStr += `${tab}${prop}: ${value};\n`;
62+
return;
63+
}
64+
});
65+
keyframeStr += `${tab}}\n`;
66+
});
67+
keyframeStr += `}\n`;
68+
69+
const kfElm = document.createElement('style');
70+
kfElm.innerHTML = keyframeStr;
71+
return kfElm;
72+
}
73+
74+
animate(
75+
element: any, keyframes: ɵStyleData[], duration: number, delay: number, easing: string,
76+
previousPlayers: AnimationPlayer[] = [], scrubberAccessRequested?: boolean): AnimationPlayer {
77+
if (scrubberAccessRequested) {
78+
this._notifyFaultyScrubber();
79+
}
80+
81+
const previousCssKeyframePlayers = <CssKeyframesPlayer[]>previousPlayers.filter(
82+
player => player instanceof CssKeyframesPlayer);
83+
84+
const previousStyles: {[key: string]: any} = {};
85+
86+
if (allowPreviousPlayerStylesMerge(duration, delay)) {
87+
previousCssKeyframePlayers.forEach(player => {
88+
let styles = player.currentSnapshot;
89+
Object.keys(styles).forEach(prop => previousStyles[prop] = styles[prop]);
90+
});
91+
}
92+
93+
keyframes = balancePreviousStylesIntoKeyframes(element, keyframes, previousStyles);
94+
const finalStyles = flattenKeyframesIntoStyles(keyframes);
95+
96+
// if there is no animation then there is no point in applying
97+
// styles and waiting for an event to get fired. This causes lag.
98+
// It's better to just directly apply the styles to the element
99+
// via the direct styling animation player.
100+
if (duration == 0) {
101+
return new DirectStylePlayer(element, finalStyles);
102+
}
103+
104+
const animationName = `${KEYFRAMES_NAME_PREFIX}${this._count++}`;
105+
const kfElm = this.buildKeyframeElement(element, animationName, keyframes);
106+
document.querySelector('head') !.appendChild(kfElm);
107+
108+
const player = new CssKeyframesPlayer(
109+
element, keyframes, animationName, duration, delay, easing, finalStyles);
110+
111+
player.onDestroy(() => removeElement(kfElm));
112+
return player;
113+
}
114+
115+
private _notifyFaultyScrubber() {
116+
if (!this._warningIssued) {
117+
console.warn(
118+
'@angular/animations: please load the web-animations.js polyfill to allow programmatic access...\n',
119+
' visit http://bit.ly/IWukam to learn more about using the web-animation-js polyfill.');
120+
this._warningIssued = true;
121+
}
122+
}
123+
}
124+
125+
function flattenKeyframesIntoStyles(
126+
keyframes: null | {[key: string]: any} | {[key: string]: any}[]): {[key: string]: any} {
127+
let flatKeyframes: {[key: string]: any} = {};
128+
if (keyframes) {
129+
const kfs = Array.isArray(keyframes) ? keyframes : [keyframes];
130+
kfs.forEach(kf => {
131+
Object.keys(kf).forEach(prop => {
132+
if (prop == 'offset' || prop == 'easing') return;
133+
flatKeyframes[prop] = kf[prop];
134+
});
135+
});
136+
}
137+
return flatKeyframes;
138+
}
139+
140+
function hypenatePropsObject(object: {[key: string]: any}): {[key: string]: any} {
141+
const newObj: {[key: string]: any} = {};
142+
Object.keys(object).forEach(prop => {
143+
const newProp = prop.replace(/([a-z])([A-Z])/g, '$1-$2');
144+
newObj[newProp] = object[prop];
145+
});
146+
return newObj;
147+
}
148+
149+
function removeElement(node: any) {
150+
node.parentNode.removeChild(node);
151+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
import {AnimationPlayer} from '@angular/animations';
9+
10+
import {computeStyle} from '../../util';
11+
12+
import {ElementAnimationStyleHandler} from './element_animation_style_handler';
13+
14+
const DEFAULT_FILL_MODE = 'forwards';
15+
const DEFAULT_EASING = 'linear';
16+
const ANIMATION_END_EVENT = 'animationend';
17+
18+
export enum AnimatorControlState {
19+
INITIALIZED = 1,
20+
STARTED = 2,
21+
FINISHED = 3,
22+
DESTROYED = 4
23+
}
24+
25+
export class CssKeyframesPlayer implements AnimationPlayer {
26+
private _onDoneFns: Function[] = [];
27+
private _onStartFns: Function[] = [];
28+
private _onDestroyFns: Function[] = [];
29+
30+
private _started = false;
31+
private _styler: ElementAnimationStyleHandler;
32+
33+
public parentPlayer: AnimationPlayer;
34+
public readonly totalTime: number;
35+
public readonly easing: string;
36+
public currentSnapshot: {[key: string]: string} = {};
37+
public state = 0;
38+
39+
constructor(
40+
public readonly element: any, public readonly keyframes: {[key: string]: string | number}[],
41+
public readonly animationName: string, private readonly _duration: number,
42+
private readonly _delay: number, easing: string,
43+
private readonly _finalStyles: {[key: string]: any}) {
44+
this.easing = easing || DEFAULT_EASING;
45+
this.totalTime = _duration + _delay;
46+
this._buildStyler();
47+
}
48+
49+
onStart(fn: () => void): void { this._onStartFns.push(fn); }
50+
51+
onDone(fn: () => void): void { this._onDoneFns.push(fn); }
52+
53+
onDestroy(fn: () => void): void { this._onDestroyFns.push(fn); }
54+
55+
destroy() {
56+
this.init();
57+
if (this.state >= AnimatorControlState.DESTROYED) return;
58+
this.state = AnimatorControlState.DESTROYED;
59+
this._styler.destroy();
60+
this._flushStartFns();
61+
this._flushDoneFns();
62+
this._onDestroyFns.forEach(fn => fn());
63+
this._onDestroyFns = [];
64+
}
65+
66+
private _flushDoneFns() {
67+
this._onDoneFns.forEach(fn => fn());
68+
this._onDoneFns = [];
69+
}
70+
71+
private _flushStartFns() {
72+
this._onStartFns.forEach(fn => fn());
73+
this._onStartFns = [];
74+
}
75+
76+
finish() {
77+
this.init();
78+
if (this.state >= AnimatorControlState.FINISHED) return;
79+
this.state = AnimatorControlState.FINISHED;
80+
this._styler.finish();
81+
this._flushStartFns();
82+
this._flushDoneFns();
83+
}
84+
85+
setPosition(value: number) { this._styler.setPosition(value); }
86+
87+
getPosition(): number { return this._styler.getPosition(); }
88+
89+
hasStarted(): boolean { return this.state >= AnimatorControlState.STARTED; }
90+
init(): void {
91+
if (this.state >= AnimatorControlState.INITIALIZED) return;
92+
this.state = AnimatorControlState.INITIALIZED;
93+
const elm = this.element;
94+
this._styler.apply();
95+
if (this._delay) {
96+
this._styler.pause();
97+
}
98+
}
99+
100+
play(): void {
101+
this.init();
102+
if (!this.hasStarted()) {
103+
this._flushStartFns();
104+
this.state = AnimatorControlState.STARTED;
105+
}
106+
this._styler.resume();
107+
}
108+
109+
pause(): void {
110+
this.init();
111+
this._styler.pause();
112+
}
113+
restart(): void {
114+
this.reset();
115+
this.play();
116+
}
117+
reset(): void {
118+
this._styler.destroy();
119+
this._buildStyler();
120+
this._styler.apply();
121+
}
122+
123+
private _buildStyler() {
124+
this._styler = new ElementAnimationStyleHandler(
125+
this.element, this.animationName, this._duration, this._delay, this.easing,
126+
DEFAULT_FILL_MODE, () => this.finish());
127+
}
128+
129+
/* @internal */
130+
triggerCallback(phaseName: string): void {
131+
const methods = phaseName == 'start' ? this._onStartFns : this._onDoneFns;
132+
methods.forEach(fn => fn());
133+
methods.length = 0;
134+
}
135+
136+
beforeDestroy() {
137+
this.init();
138+
const styles: {[key: string]: string} = {};
139+
if (this.hasStarted()) {
140+
const finished = this.state >= AnimatorControlState.FINISHED;
141+
Object.keys(this._finalStyles).forEach(prop => {
142+
if (prop != 'offset') {
143+
styles[prop] = finished ? this._finalStyles[prop] : computeStyle(this.element, prop);
144+
}
145+
});
146+
}
147+
this.currentSnapshot = styles;
148+
}
149+
}

0 commit comments

Comments
 (0)