Skip to content

Commit 409688f

Browse files
matskomhevery
authored andcommitted
feat(animations): report errors when invalid CSS properties are detected (angular#18718)
Closes angular#18701 PR Close angular#18718
1 parent ec56760 commit 409688f

File tree

16 files changed

+88
-52
lines changed

16 files changed

+88
-52
lines changed

packages/animations/browser/src/dsl/animation.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export class Animation {
2020
private _animationAst: Ast;
2121
constructor(private _driver: AnimationDriver, input: AnimationMetadata|AnimationMetadata[]) {
2222
const errors: any[] = [];
23-
const ast = buildAnimationAst(input, errors);
23+
const ast = buildAnimationAst(_driver, input, errors);
2424
if (errors.length) {
2525
const errorMessage = `animation validation failed:\n${errors.join("\n")}`;
2626
throw new Error(errorMessage);

packages/animations/browser/src/dsl/animation_ast_builder.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88
import {AUTO_STYLE, AnimateTimings, AnimationAnimateChildMetadata, AnimationAnimateMetadata, AnimationAnimateRefMetadata, AnimationGroupMetadata, AnimationKeyframesSequenceMetadata, AnimationMetadata, AnimationMetadataType, AnimationOptions, AnimationQueryMetadata, AnimationQueryOptions, AnimationReferenceMetadata, AnimationSequenceMetadata, AnimationStaggerMetadata, AnimationStateMetadata, AnimationStyleMetadata, AnimationTransitionMetadata, AnimationTriggerMetadata, style, ɵStyleData} from '@angular/animations';
99

10+
import {AnimationDriver} from '../render/animation_driver';
1011
import {getOrSetAsInMap} from '../render/shared';
1112
import {ENTER_SELECTOR, LEAVE_SELECTOR, NG_ANIMATING_SELECTOR, NG_TRIGGER_SELECTOR, SUBSTITUTION_EXPR_START, copyObj, extractStyleParams, iteratorToArray, normalizeAnimationEntry, resolveTiming, validateStyleParams} from '../util';
1213

@@ -54,8 +55,9 @@ const SELF_TOKEN_REGEX = new RegExp(`\s*${SELF_TOKEN}\s*,?`, 'g');
5455
* Otherwise an error will be thrown.
5556
*/
5657
export function buildAnimationAst(
57-
metadata: AnimationMetadata | AnimationMetadata[], errors: any[]): Ast {
58-
return new AnimationAstBuilderVisitor().build(metadata, errors);
58+
driver: AnimationDriver, metadata: AnimationMetadata | AnimationMetadata[],
59+
errors: any[]): Ast {
60+
return new AnimationAstBuilderVisitor(driver).build(metadata, errors);
5961
}
6062

6163
const LEAVE_TOKEN = ':leave';
@@ -65,6 +67,8 @@ const ENTER_TOKEN_REGEX = new RegExp(ENTER_TOKEN, 'g');
6567
const ROOT_SELECTOR = '';
6668

6769
export class AnimationAstBuilderVisitor implements AnimationDslVisitor {
70+
constructor(private _driver: AnimationDriver) {}
71+
6872
build(metadata: AnimationMetadata|AnimationMetadata[], errors: any[]): Ast {
6973
const context = new AnimationAstBuilderContext(errors);
7074
this._resetContextStyleTimingState(context);
@@ -273,6 +277,12 @@ export class AnimationAstBuilderVisitor implements AnimationDslVisitor {
273277
if (typeof tuple == 'string') return;
274278

275279
Object.keys(tuple).forEach(prop => {
280+
if (!this._driver.validateStyleProperty(prop)) {
281+
context.errors.push(
282+
`The provided animation property "${prop}" is not a supported CSS property for animations`);
283+
return;
284+
}
285+
276286
const collectedStyles = context.collectedStyles[context.currentQuerySelector !];
277287
const collectedEntry = collectedStyles[prop];
278288
let updateCollectedStyle = true;

packages/animations/browser/src/dsl/animation_timeline_builder.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -447,7 +447,7 @@ export class AnimationTimelineContext {
447447
private _driver: AnimationDriver, public element: any,
448448
public subInstructions: ElementInstructionMap, public errors: any[],
449449
public timelines: TimelineBuilder[], initialTimeline?: TimelineBuilder) {
450-
this.currentTimeline = initialTimeline || new TimelineBuilder(element, 0);
450+
this.currentTimeline = initialTimeline || new TimelineBuilder(this._driver, element, 0);
451451
timelines.push(this.currentTimeline);
452452
}
453453

@@ -530,7 +530,7 @@ export class AnimationTimelineContext {
530530
easing: ''
531531
};
532532
const builder = new SubTimelineBuilder(
533-
instruction.element, instruction.keyframes, instruction.preStyleProps,
533+
this._driver, instruction.element, instruction.keyframes, instruction.preStyleProps,
534534
instruction.postStyleProps, updatedTimings, instruction.stretchStartingKeyframe);
535535
this.timelines.push(builder);
536536
return updatedTimings;
@@ -582,7 +582,7 @@ export class TimelineBuilder {
582582
private _currentEmptyStepKeyframe: ɵStyleData|null = null;
583583

584584
constructor(
585-
public element: any, public startTime: number,
585+
private _driver: AnimationDriver, public element: any, public startTime: number,
586586
private _elementTimelineStylesLookup?: Map<any, ɵStyleData>) {
587587
if (!this._elementTimelineStylesLookup) {
588588
this._elementTimelineStylesLookup = new Map<any, ɵStyleData>();
@@ -632,7 +632,7 @@ export class TimelineBuilder {
632632
fork(element: any, currentTime?: number): TimelineBuilder {
633633
this.applyStylesToKeyframe();
634634
return new TimelineBuilder(
635-
element, currentTime || this.currentTime, this._elementTimelineStylesLookup);
635+
this._driver, element, currentTime || this.currentTime, this._elementTimelineStylesLookup);
636636
}
637637

638638
private _loadKeyframe() {
@@ -796,10 +796,10 @@ class SubTimelineBuilder extends TimelineBuilder {
796796
public timings: AnimateTimings;
797797

798798
constructor(
799-
public element: any, public keyframes: ɵStyleData[], public preStyleProps: string[],
800-
public postStyleProps: string[], timings: AnimateTimings,
799+
driver: AnimationDriver, public element: any, public keyframes: ɵStyleData[],
800+
public preStyleProps: string[], public postStyleProps: string[], timings: AnimateTimings,
801801
private _stretchStartingKeyframe: boolean = false) {
802-
super(element, timings.delay);
802+
super(driver, element, timings.delay);
803803
this.timings = {duration: timings.duration, delay: timings.delay, easing: timings.easing};
804804
}
805805

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@
77
*/
88
import {AnimationPlayer, NoopAnimationPlayer} from '@angular/animations';
99

10-
import {containsElement, invokeQuery, matchesElement} from './shared';
11-
10+
import {containsElement, invokeQuery, matchesElement, validateStyleProperty} from './shared';
1211

1312
/**
1413
* @experimental
1514
*/
1615
export class NoopAnimationDriver implements AnimationDriver {
16+
validateStyleProperty(prop: string): boolean { return validateStyleProperty(prop); }
17+
1718
matchesElement(element: any, selector: string): boolean {
1819
return matchesElement(element, selector);
1920
}
@@ -41,6 +42,8 @@ export class NoopAnimationDriver implements AnimationDriver {
4142
export abstract class AnimationDriver {
4243
static NOOP: AnimationDriver = new NoopAnimationDriver();
4344

45+
abstract validateStyleProperty(prop: string): boolean;
46+
4447
abstract matchesElement(element: any, selector: string): boolean;
4548

4649
abstract containsElement(elm1: any, elm2: any): boolean;

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ export class AnimationEngine {
2525
// this method is designed to be overridden by the code that uses this engine
2626
public onRemovalComplete = (element: any, context: any) => {};
2727

28-
constructor(driver: AnimationDriver, normalizer: AnimationStyleNormalizer) {
29-
this._transitionEngine = new TransitionAnimationEngine(driver, normalizer);
30-
this._timelineEngine = new TimelineAnimationEngine(driver, normalizer);
28+
constructor(private _driver: AnimationDriver, normalizer: AnimationStyleNormalizer) {
29+
this._transitionEngine = new TransitionAnimationEngine(_driver, normalizer);
30+
this._timelineEngine = new TimelineAnimationEngine(_driver, normalizer);
3131

3232
this._transitionEngine.onRemovalComplete = (element: any, context: any) =>
3333
this.onRemovalComplete(element, context);
@@ -40,7 +40,8 @@ export class AnimationEngine {
4040
let trigger = this._triggerCache[cacheKey];
4141
if (!trigger) {
4242
const errors: any[] = [];
43-
const ast = buildAnimationAst(metadata as AnimationMetadata, errors) as TriggerAst;
43+
const ast =
44+
buildAnimationAst(this._driver, metadata as AnimationMetadata, errors) as TriggerAst;
4445
if (errors.length) {
4546
throw new Error(
4647
`The animation trigger "${name}" has failed to build due to the following errors:\n - ${errors.join("\n - ")}`);

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,21 @@ if (typeof Element != 'undefined') {
166166
};
167167
}
168168

169+
let _CACHED_BODY: {style: any}|null = null;
170+
export function validateStyleProperty(prop: string): boolean {
171+
if (!_CACHED_BODY) {
172+
_CACHED_BODY = getBodyNode() || {};
173+
}
174+
return _CACHED_BODY !.style ? prop in _CACHED_BODY !.style : true;
175+
}
176+
177+
export function getBodyNode(): any|null {
178+
if (typeof document != 'undefined') {
179+
return document.body;
180+
}
181+
return null;
182+
}
183+
169184
export const matchesElement = _matches;
170185
export const containsElement = _contains;
171186
export const invokeQuery = _query;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export class TimelineAnimationEngine {
2828

2929
register(id: string, metadata: AnimationMetadata|AnimationMetadata[]) {
3030
const errors: any[] = [];
31-
const ast = buildAnimationAst(metadata, errors);
31+
const ast = buildAnimationAst(this._driver, metadata, errors);
3232
if (errors.length) {
3333
throw new Error(
3434
`Unable to build the animation due to the following errors: ${errors.join("\n")}`);

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

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {AnimationStyleNormalizer} from '../dsl/style_normalization/animation_sty
1616
import {ENTER_CLASSNAME, LEAVE_CLASSNAME, NG_ANIMATING_CLASSNAME, NG_ANIMATING_SELECTOR, NG_TRIGGER_CLASSNAME, NG_TRIGGER_SELECTOR, copyObj, eraseStyles, setStyles} from '../util';
1717

1818
import {AnimationDriver} from './animation_driver';
19-
import {getOrSetAsInMap, listenOnPlayer, makeAnimationEvent, normalizeKeyframes, optimizeGroupPlayer} from './shared';
19+
import {getBodyNode, getOrSetAsInMap, listenOnPlayer, makeAnimationEvent, normalizeKeyframes, optimizeGroupPlayer} from './shared';
2020

2121
const QUEUED_CLASSNAME = 'ng-animate-queued';
2222
const QUEUED_SELECTOR = '.ng-animate-queued';
@@ -1525,13 +1525,6 @@ function removeClass(element: any, className: string) {
15251525
}
15261526
}
15271527

1528-
function getBodyNode(): any|null {
1529-
if (typeof document != 'undefined') {
1530-
return document.body;
1531-
}
1532-
return null;
1533-
}
1534-
15351528
function removeNodesAfterAnimationDone(
15361529
engine: TransitionAnimationEngine, element: any, players: AnimationPlayer[]) {
15371530
optimizeGroupPlayer(players).onDone(() => engine.processLeaveNode(element));

packages/animations/browser/src/render/web_animations/web_animations_driver.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@
88
import {AnimationPlayer, ɵStyleData} from '@angular/animations';
99

1010
import {AnimationDriver} from '../animation_driver';
11-
import {containsElement, invokeQuery, matchesElement} from '../shared';
11+
import {containsElement, invokeQuery, matchesElement, validateStyleProperty} from '../shared';
1212

1313
import {WebAnimationsPlayer} from './web_animations_player';
1414

1515
export class WebAnimationsDriver implements AnimationDriver {
16+
validateStyleProperty(prop: string): boolean { return validateStyleProperty(prop); }
17+
1618
matchesElement(element: any, selector: string): boolean {
1719
return matchesElement(element, selector);
1820
}

packages/animations/browser/test/dsl/animation_spec.ts

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -177,10 +177,12 @@ export function main() {
177177

178178
it('should throw if dynamic style substitutions are used without defaults within state() definitions',
179179
() => {
180-
const steps = [state('final', style({
181-
'width': '{{ one }}px',
182-
'borderRadius': '{{ two }}px {{ three }}px',
183-
}))];
180+
const steps = [
181+
state('final', style({
182+
'width': '{{ one }}px',
183+
'borderRadius': '{{ two }}px {{ three }}px',
184+
})),
185+
];
184186

185187
expect(() => { validateAndThrowAnimationSequence(steps); })
186188
.toThrowError(
@@ -198,6 +200,14 @@ export function main() {
198200
.toThrowError(
199201
/state\("panfinal", ...\) must define default values for all the following style substitutions: greyColor/);
200202
});
203+
204+
it('should throw an error if an invalid CSS property is used in the animation', () => {
205+
const steps = [animate(1000, style({abc: '500px'}))];
206+
207+
expect(() => { validateAndThrowAnimationSequence(steps); })
208+
.toThrowError(
209+
/The provided animation property "abc" is not a supported CSS property for animations/);
210+
});
201211
});
202212

203213
describe('keyframe building', () => {
@@ -388,14 +398,13 @@ export function main() {
388398

389399
it('should allow multiple substitutions to occur within the same style value', () => {
390400
const steps = [
391-
style({transform: ''}),
392-
animate(1000, style({transform: 'translateX({{ x }}) translateY({{ y }})'}))
401+
style({borderRadius: '100px 100px'}),
402+
animate(1000, style({borderRadius: '{{ one }}px {{ two }}'})),
393403
];
394404
const players =
395-
invokeAnimationSequence(rootElement, steps, buildParams({x: '200px', y: '400px'}));
405+
invokeAnimationSequence(rootElement, steps, buildParams({one: '200', two: '400px'}));
396406
expect(players[0].keyframes).toEqual([
397-
{offset: 0, transform: ''},
398-
{offset: 1, transform: 'translateX(200px) translateY(400px)'}
407+
{offset: 0, borderRadius: '100px 100px'}, {offset: 1, borderRadius: '200px 400px'}
399408
]);
400409
});
401410

@@ -571,18 +580,12 @@ export function main() {
571580
() => {
572581
const steps = [
573582
animate(1000, style({height: '50px'})),
574-
animate(
575-
2000, keyframes([
576-
style({left: '0', transform: 'rotate(0deg)', offset: 0}),
577-
style({
578-
left: '40%',
579-
transform: 'rotate(250deg) translateY(-200px)',
580-
offset: .33
581-
}),
582-
style(
583-
{left: '60%', transform: 'rotate(180deg) translateY(200px)', offset: .66}),
584-
style({left: 'calc(100% - 100px)', transform: 'rotate(0deg)', offset: 1}),
585-
])),
583+
animate(2000, keyframes([
584+
style({left: '0', top: '0', offset: 0}),
585+
style({left: '40%', top: '50%', offset: .33}),
586+
style({left: '60%', top: '80%', offset: .66}),
587+
style({left: 'calc(100% - 100px)', top: '100%', offset: 1}),
588+
])),
586589
group([animate('2s', style({width: '200px'}))]),
587590
animate('2s', style({height: '300px'})),
588591
group([animate('2s', style({height: '500px', width: '500px'}))])
@@ -987,8 +990,9 @@ function invokeAnimationSequence(
987990
}
988991

989992
function validateAndThrowAnimationSequence(steps: AnimationMetadata | AnimationMetadata[]) {
993+
const driver = new MockAnimationDriver();
990994
const errors: any[] = [];
991-
const ast = buildAnimationAst(steps, errors);
995+
const ast = buildAnimationAst(driver, steps, errors);
992996
if (errors.length) {
993997
throw new Error(errors.join('\n'));
994998
}

0 commit comments

Comments
 (0)