diff --git a/packages/animations/browser/src/dsl/animation.ts b/packages/animations/browser/src/dsl/animation.ts index f861efe91203..ba51897ac6da 100644 --- a/packages/animations/browser/src/dsl/animation.ts +++ b/packages/animations/browser/src/dsl/animation.ts @@ -10,6 +10,7 @@ import {AnimationMetadata, AnimationMetadataType, AnimationOptions, ɵStyleData} import {buildingFailed, validationFailed} from '../error_helpers'; import {AnimationDriver} from '../render/animation_driver'; import {ENTER_CLASSNAME, LEAVE_CLASSNAME, normalizeStyles} from '../util'; +import {warnValidation} from '../warning_helpers'; import {Ast} from './animation_ast'; import {buildAnimationAst} from './animation_ast_builder'; @@ -21,10 +22,14 @@ export class Animation { private _animationAst: Ast; constructor(private _driver: AnimationDriver, input: AnimationMetadata|AnimationMetadata[]) { const errors: Error[] = []; - const ast = buildAnimationAst(_driver, input, errors); + const warnings: string[] = []; + const ast = buildAnimationAst(_driver, input, errors, warnings); if (errors.length) { throw validationFailed(errors); } + if (warnings.length) { + warnValidation(warnings); + } this._animationAst = ast; } diff --git a/packages/animations/browser/src/dsl/animation_ast_builder.ts b/packages/animations/browser/src/dsl/animation_ast_builder.ts index fb49c29ea325..7b708533fbe2 100644 --- a/packages/animations/browser/src/dsl/animation_ast_builder.ts +++ b/packages/animations/browser/src/dsl/animation_ast_builder.ts @@ -11,6 +11,7 @@ import {invalidDefinition, invalidKeyframes, invalidOffset, invalidParallelAnima import {AnimationDriver} from '../render/animation_driver'; import {getOrSetAsInMap} from '../render/shared'; import {copyObj, extractStyleParams, iteratorToArray, NG_ANIMATING_SELECTOR, NG_TRIGGER_SELECTOR, normalizeAnimationEntry, resolveTiming, SUBSTITUTION_EXPR_START, validateStyleParams, visitDslNode} from '../util'; +import {pushUnrecognizedPropertiesWarning} from '../warning_helpers'; import {AnimateAst, AnimateChildAst, AnimateRefAst, Ast, DynamicTimingAst, GroupAst, KeyframesAst, QueryAst, ReferenceAst, SequenceAst, StaggerAst, StateAst, StyleAst, TimingAst, TransitionAst, TriggerAst} from './animation_ast'; import {AnimationDslVisitor} from './animation_dsl_visitor'; @@ -56,9 +57,9 @@ const SELF_TOKEN_REGEX = new RegExp(`\s*${SELF_TOKEN}\s*,?`, 'g'); * Otherwise an error will be thrown. */ export function buildAnimationAst( - driver: AnimationDriver, metadata: AnimationMetadata|AnimationMetadata[], - errors: Error[]): Ast { - return new AnimationAstBuilderVisitor(driver).build(metadata, errors); + driver: AnimationDriver, metadata: AnimationMetadata|AnimationMetadata[], errors: Error[], + warnings: string[]): Ast { + return new AnimationAstBuilderVisitor(driver).build(metadata, errors, warnings); } const ROOT_SELECTOR = ''; @@ -66,12 +67,20 @@ const ROOT_SELECTOR = ''; export class AnimationAstBuilderVisitor implements AnimationDslVisitor { constructor(private _driver: AnimationDriver) {} - build(metadata: AnimationMetadata|AnimationMetadata[], errors: Error[]): + build(metadata: AnimationMetadata|AnimationMetadata[], errors: Error[], warnings: string[]): Ast { const context = new AnimationAstBuilderContext(errors); this._resetContextStyleTimingState(context); - return >visitDslNode( - this, normalizeAnimationEntry(metadata), context); + const ast = + >visitDslNode(this, normalizeAnimationEntry(metadata), context); + + if (context.unsupportedCSSPropertiesFound.size) { + pushUnrecognizedPropertiesWarning( + warnings, + [...context.unsupportedCSSPropertiesFound.keys()], + ); + } + return ast; } private _resetContextStyleTimingState(context: AnimationAstBuilderContext) { @@ -303,7 +312,8 @@ export class AnimationAstBuilderVisitor implements AnimationDslVisitor { Object.keys(tuple).forEach(prop => { if (!this._driver.validateStyleProperty(prop)) { - context.errors.push(invalidProperty(prop)); + delete tuple[prop]; + context.unsupportedCSSPropertiesFound.add(prop); return; } @@ -507,6 +517,7 @@ export class AnimationAstBuilderContext { public currentTime: number = 0; public collectedStyles: {[selectorName: string]: {[propName: string]: StyleTimeTuple}} = {}; public options: AnimationOptions|null = null; + public unsupportedCSSPropertiesFound: Set = new Set(); constructor(public errors: Error[]) {} } diff --git a/packages/animations/browser/src/render/animation_engine_next.ts b/packages/animations/browser/src/render/animation_engine_next.ts index 2202743e87fa..f34f17273d4a 100644 --- a/packages/animations/browser/src/render/animation_engine_next.ts +++ b/packages/animations/browser/src/render/animation_engine_next.ts @@ -12,6 +12,7 @@ import {buildAnimationAst} from '../dsl/animation_ast_builder'; import {AnimationTrigger, buildTrigger} from '../dsl/animation_trigger'; import {AnimationStyleNormalizer} from '../dsl/style_normalization/animation_style_normalizer'; import {triggerBuildFailed} from '../error_helpers'; +import {warnTriggerBuild} from '../warning_helpers'; import {AnimationDriver} from './animation_driver'; import {parseTimelineCommand} from './shared'; @@ -44,11 +45,15 @@ export class AnimationEngine { let trigger = this._triggerCache[cacheKey]; if (!trigger) { const errors: Error[] = []; - const ast = - buildAnimationAst(this._driver, metadata as AnimationMetadata, errors) as TriggerAst; + const warnings: string[] = []; + const ast = buildAnimationAst( + this._driver, metadata as AnimationMetadata, errors, warnings) as TriggerAst; if (errors.length) { throw triggerBuildFailed(name, errors); } + if (warnings.length) { + warnTriggerBuild(name, warnings); + } trigger = buildTrigger(name, ast, this._normalizer); this._triggerCache[cacheKey] = trigger; } diff --git a/packages/animations/browser/src/render/timeline_animation_engine.ts b/packages/animations/browser/src/render/timeline_animation_engine.ts index 77469cfebdb0..2aa999c68d7a 100644 --- a/packages/animations/browser/src/render/timeline_animation_engine.ts +++ b/packages/animations/browser/src/render/timeline_animation_engine.ts @@ -15,6 +15,7 @@ import {ElementInstructionMap} from '../dsl/element_instruction_map'; import {AnimationStyleNormalizer} from '../dsl/style_normalization/animation_style_normalizer'; import {createAnimationFailed, missingOrDestroyedAnimation, missingPlayer, registerFailed} from '../error_helpers'; import {ENTER_CLASSNAME, LEAVE_CLASSNAME} from '../util'; +import {warnRegister} from '../warning_helpers'; import {AnimationDriver} from './animation_driver'; import {getOrSetAsInMap, listenOnPlayer, makeAnimationEvent, normalizeKeyframes, optimizeGroupPlayer} from './shared'; @@ -32,10 +33,14 @@ export class TimelineAnimationEngine { register(id: string, metadata: AnimationMetadata|AnimationMetadata[]) { const errors: Error[] = []; - const ast = buildAnimationAst(this._driver, metadata, errors); + const warnings: string[] = []; + const ast = buildAnimationAst(this._driver, metadata, errors, warnings); if (errors.length) { throw registerFailed(errors); } else { + if (warnings.length) { + warnRegister(warnings); + } this._animations[id] = ast; } } diff --git a/packages/animations/browser/src/warning_helpers.ts b/packages/animations/browser/src/warning_helpers.ts new file mode 100644 index 000000000000..d93ca15dd7ef --- /dev/null +++ b/packages/animations/browser/src/warning_helpers.ts @@ -0,0 +1,43 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +const NG_DEV_MODE = typeof ngDevMode === 'undefined' || !!ngDevMode; + +function createListOfWarnings(warnings: string[]): string { + const LINE_START = '\n - '; + return `${LINE_START}${warnings.filter(Boolean).map(warning => warning).join(LINE_START)}`; +} + +export function warnValidation(warnings: string[]): void { + NG_DEV_MODE && console.warn(`animation validation warnings:${createListOfWarnings(warnings)}`); +} + +export function warnTriggerBuild(name: string, warnings: string[]): void { + NG_DEV_MODE && + console.warn(`The animation trigger "${name}" has built with the following warnings:${ + createListOfWarnings(warnings)}`); +} + +export function warnRegister(warnings: string[]): void { + NG_DEV_MODE && + console.warn(`Animation built with the following warnings:${createListOfWarnings(warnings)}`); +} + +export function triggerParsingWarnings(name: string, warnings: string[]): void { + NG_DEV_MODE && + console.warn(`Animation parsing for the ${name} trigger presents the following warnings:${ + createListOfWarnings(warnings)}`); +} + +export function pushUnrecognizedPropertiesWarning(warnings: string[], props: string[]): void { + if (ngDevMode && props.length) { + warnings.push( + `The provided CSS properties are not recognized properties supported for animations: ${ + props.join(', ')}`); + } +} diff --git a/packages/animations/browser/test/dsl/animation_spec.ts b/packages/animations/browser/test/dsl/animation_spec.ts index 330d03ecbcba..c76b4afdaa65 100644 --- a/packages/animations/browser/test/dsl/animation_spec.ts +++ b/packages/animations/browser/test/dsl/animation_spec.ts @@ -218,16 +218,28 @@ function createDiv() { /state\("panfinal", ...\) must define default values for all the following style substitutions: greyColor/); }); - it('should throw an error if an invalid CSS property is used in the animation', () => { + it('should provide a warning if an invalid CSS property is used in the animation', () => { const steps = [animate(1000, style({abc: '500px'}))]; - expect(() => { - validateAndThrowAnimationSequence(steps); - }) - .toThrowError( - /The provided animation property "abc" is not a supported CSS property for animations/); + expect(getValidationWarningsForAnimationSequence(steps)).toEqual([ + 'The provided CSS properties are not recognized properties supported for animations: abc' + ]); }); + it('should provide a warning if multiple invalid CSS properties are used in the animation', + () => { + const steps = [ + state('state', style({ + '123': '100px', + })), + style({abc: '200px'}), animate(1000, style({xyz: '300px'})) + ]; + + expect(getValidationWarningsForAnimationSequence(steps)).toEqual([ + 'The provided CSS properties are not recognized properties supported for animations: 123, abc, xyz' + ]); + }); + it('should allow a vendor-prefixed property to be used in an animation sequence without throwing an error', () => { const steps = [ @@ -1070,12 +1082,20 @@ function invokeAnimationSequence( function validateAndThrowAnimationSequence(steps: AnimationMetadata|AnimationMetadata[]) { const driver = new MockAnimationDriver(); const errors: Error[] = []; - const ast = buildAnimationAst(driver, steps, errors); + const ast = buildAnimationAst(driver, steps, errors, []); if (errors.length) { throw new Error(errors.join('\n')); } } +function getValidationWarningsForAnimationSequence(steps: AnimationMetadata| + AnimationMetadata[]): string[] { + const driver = new MockAnimationDriver(); + const warnings: string[] = []; + buildAnimationAst(driver, steps, [], warnings); + return warnings; +} + function buildParams(params: {[name: string]: any}): AnimationOptions { return {params}; } diff --git a/packages/animations/browser/test/render/transition_animation_engine_spec.ts b/packages/animations/browser/test/render/transition_animation_engine_spec.ts index 1a041e1fd3d4..5134bd39eacb 100644 --- a/packages/animations/browser/test/render/transition_animation_engine_spec.ts +++ b/packages/animations/browser/test/render/transition_animation_engine_spec.ts @@ -732,9 +732,11 @@ function registerTrigger( element: any, engine: TransitionAnimationEngine, metadata: AnimationTriggerMetadata, id: string = DEFAULT_NAMESPACE_ID) { const errors: Error[] = []; + const warnings: string[] = []; const driver = new MockAnimationDriver(); const name = metadata.name; - const ast = buildAnimationAst(driver, metadata as AnimationMetadata, errors) as TriggerAst; + const ast = + buildAnimationAst(driver, metadata as AnimationMetadata, errors, warnings) as TriggerAst; if (errors.length) { } const trigger = buildTrigger(name, ast, new NoopAnimationStyleNormalizer()); diff --git a/packages/animations/browser/test/shared.ts b/packages/animations/browser/test/shared.ts index bcbd55a66486..e699c243dbcc 100644 --- a/packages/animations/browser/test/shared.ts +++ b/packages/animations/browser/test/shared.ts @@ -13,16 +13,21 @@ import {buildAnimationAst} from '../src/dsl/animation_ast_builder'; import {AnimationTrigger, buildTrigger} from '../src/dsl/animation_trigger'; import {NoopAnimationStyleNormalizer} from '../src/dsl/style_normalization/animation_style_normalizer'; import {triggerParsingFailed} from '../src/error_helpers'; +import {triggerParsingWarnings} from '../src/warning_helpers'; import {MockAnimationDriver} from '../testing/src/mock_animation_driver'; export function makeTrigger( name: string, steps: any, skipErrors: boolean = false): AnimationTrigger { const driver = new MockAnimationDriver(); const errors: Error[] = []; + const warnings: string[] = []; const triggerData = trigger(name, steps); - const triggerAst = buildAnimationAst(driver, triggerData, errors) as TriggerAst; + const triggerAst = buildAnimationAst(driver, triggerData, errors, warnings) as TriggerAst; if (!skipErrors && errors.length) { throw triggerParsingFailed(name, errors); } + if (warnings.length) { + triggerParsingWarnings(name, warnings); + } return buildTrigger(name, triggerAst, new NoopAnimationStyleNormalizer()); }