fix(animations): always normalize style properties and values during compilation (#12755)

Closes #11582
Closes #12481
Closes #12755
This commit is contained in:
Matias Niemelä 2016-11-08 15:45:30 -08:00 committed by vikerman
parent 3dc61779f0
commit a0e9fde653
16 changed files with 448 additions and 271 deletions

View File

@ -138,7 +138,8 @@ export class CodeGenerator {
new compiler.DirectiveWrapperCompiler( new compiler.DirectiveWrapperCompiler(
config, expressionParser, elementSchemaRegistry, console), config, expressionParser, elementSchemaRegistry, console),
new compiler.NgModuleCompiler(), new compiler.TypeScriptEmitter(reflectorHost), new compiler.NgModuleCompiler(), new compiler.TypeScriptEmitter(reflectorHost),
cliOptions.locale, cliOptions.i18nFormat); cliOptions.locale, cliOptions.i18nFormat,
new compiler.AnimationParser(elementSchemaRegistry));
return new CodeGenerator( return new CodeGenerator(
options, program, compilerHost, staticReflector, offlineCompiler, reflectorHost); options, program, compilerHost, staticReflector, offlineCompiler, reflectorHost);

View File

@ -52,5 +52,6 @@ export * from './src/selector';
export * from './src/style_compiler'; export * from './src/style_compiler';
export * from './src/template_parser/template_parser'; export * from './src/template_parser/template_parser';
export {ViewCompiler} from './src/view_compiler/view_compiler'; export {ViewCompiler} from './src/view_compiler/view_compiler';
export {AnimationParser} from './src/animation/animation_parser';
// This file only reexports content of the `src` folder. Keep it that way. // This file only reexports content of the `src` folder. Keep it that way.

View File

@ -6,11 +6,14 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {Injectable} from '@angular/core';
import {CompileAnimationAnimateMetadata, CompileAnimationEntryMetadata, CompileAnimationGroupMetadata, CompileAnimationKeyframesSequenceMetadata, CompileAnimationMetadata, CompileAnimationSequenceMetadata, CompileAnimationStateDeclarationMetadata, CompileAnimationStateTransitionMetadata, CompileAnimationStyleMetadata, CompileAnimationWithStepsMetadata, CompileDirectiveMetadata} from '../compile_metadata'; import {CompileAnimationAnimateMetadata, CompileAnimationEntryMetadata, CompileAnimationGroupMetadata, CompileAnimationKeyframesSequenceMetadata, CompileAnimationMetadata, CompileAnimationSequenceMetadata, CompileAnimationStateDeclarationMetadata, CompileAnimationStateTransitionMetadata, CompileAnimationStyleMetadata, CompileAnimationWithStepsMetadata, CompileDirectiveMetadata} from '../compile_metadata';
import {StringMapWrapper} from '../facade/collection'; import {StringMapWrapper} from '../facade/collection';
import {isBlank, isPresent} from '../facade/lang'; import {isBlank, isPresent} from '../facade/lang';
import {ParseError} from '../parse_util'; import {ParseError} from '../parse_util';
import {ANY_STATE, FILL_STYLE_FLAG} from '../private_import_core'; import {ANY_STATE, FILL_STYLE_FLAG} from '../private_import_core';
import {ElementSchemaRegistry} from '../schema/element_schema_registry';
import {AnimationAst, AnimationEntryAst, AnimationGroupAst, AnimationKeyframeAst, AnimationSequenceAst, AnimationStateDeclarationAst, AnimationStateTransitionAst, AnimationStateTransitionExpression, AnimationStepAst, AnimationStylesAst, AnimationWithStepsAst} from './animation_ast'; import {AnimationAst, AnimationEntryAst, AnimationGroupAst, AnimationKeyframeAst, AnimationSequenceAst, AnimationStateDeclarationAst, AnimationStateTransitionAst, AnimationStateTransitionExpression, AnimationStepAst, AnimationStylesAst, AnimationWithStepsAst} from './animation_ast';
import {StylesCollection} from './styles_collection'; import {StylesCollection} from './styles_collection';
@ -32,7 +35,10 @@ export class AnimationEntryParseResult {
constructor(public ast: AnimationEntryAst, public errors: AnimationParseError[]) {} constructor(public ast: AnimationEntryAst, public errors: AnimationParseError[]) {}
} }
@Injectable()
export class AnimationParser { export class AnimationParser {
constructor(private _schema: ElementSchemaRegistry) {}
parseComponent(component: CompileDirectiveMetadata): AnimationEntryAst[] { parseComponent(component: CompileDirectiveMetadata): AnimationEntryAst[] {
const errors: string[] = []; const errors: string[] = [];
const componentName = component.type.name; const componentName = component.type.name;
@ -73,7 +79,7 @@ export class AnimationParser {
var stateDeclarationAsts: AnimationStateDeclarationAst[] = []; var stateDeclarationAsts: AnimationStateDeclarationAst[] = [];
entry.definitions.forEach(def => { entry.definitions.forEach(def => {
if (def instanceof CompileAnimationStateDeclarationMetadata) { if (def instanceof CompileAnimationStateDeclarationMetadata) {
_parseAnimationDeclarationStates(def, errors).forEach(ast => { _parseAnimationDeclarationStates(def, this._schema, errors).forEach(ast => {
stateDeclarationAsts.push(ast); stateDeclarationAsts.push(ast);
stateStyles[ast.stateName] = ast.styles; stateStyles[ast.stateName] = ast.styles;
}); });
@ -82,8 +88,8 @@ export class AnimationParser {
} }
}); });
var stateTransitionAsts = var stateTransitionAsts = transitions.map(
transitions.map(transDef => _parseAnimationStateTransition(transDef, stateStyles, errors)); transDef => _parseAnimationStateTransition(transDef, stateStyles, this._schema, errors));
var ast = new AnimationEntryAst(entry.name, stateDeclarationAsts, stateTransitionAsts); var ast = new AnimationEntryAst(entry.name, stateDeclarationAsts, stateTransitionAsts);
return new AnimationEntryParseResult(ast, errors); return new AnimationEntryParseResult(ast, errors);
@ -91,27 +97,17 @@ export class AnimationParser {
} }
function _parseAnimationDeclarationStates( function _parseAnimationDeclarationStates(
stateMetadata: CompileAnimationStateDeclarationMetadata, stateMetadata: CompileAnimationStateDeclarationMetadata, schema: ElementSchemaRegistry,
errors: AnimationParseError[]): AnimationStateDeclarationAst[] { errors: AnimationParseError[]): AnimationStateDeclarationAst[] {
var styleValues: Styles[] = []; var normalizedStyles = _normalizeStyleMetadata(stateMetadata.styles, {}, schema, errors, false);
stateMetadata.styles.styles.forEach(stylesEntry => { var defStyles = new AnimationStylesAst(normalizedStyles);
// TODO (matsko): change this when we get CSS class integration support
if (typeof stylesEntry === 'object' && stylesEntry !== null) {
styleValues.push(stylesEntry as Styles);
} else {
errors.push(new AnimationParseError(
`State based animations cannot contain references to other states`));
}
});
var defStyles = new AnimationStylesAst(styleValues);
var states = stateMetadata.stateNameExpr.split(/\s*,\s*/); var states = stateMetadata.stateNameExpr.split(/\s*,\s*/);
return states.map(state => new AnimationStateDeclarationAst(state, defStyles)); return states.map(state => new AnimationStateDeclarationAst(state, defStyles));
} }
function _parseAnimationStateTransition( function _parseAnimationStateTransition(
transitionStateMetadata: CompileAnimationStateTransitionMetadata, transitionStateMetadata: CompileAnimationStateTransitionMetadata,
stateStyles: {[key: string]: AnimationStylesAst}, stateStyles: {[key: string]: AnimationStylesAst}, schema: ElementSchemaRegistry,
errors: AnimationParseError[]): AnimationStateTransitionAst { errors: AnimationParseError[]): AnimationStateTransitionAst {
var styles = new StylesCollection(); var styles = new StylesCollection();
var transitionExprs: AnimationStateTransitionExpression[] = []; var transitionExprs: AnimationStateTransitionExpression[] = [];
@ -119,7 +115,7 @@ function _parseAnimationStateTransition(
transitionStates.forEach( transitionStates.forEach(
expr => { transitionExprs.push(..._parseAnimationTransitionExpr(expr, errors)); }); expr => { transitionExprs.push(..._parseAnimationTransitionExpr(expr, errors)); });
var entry = _normalizeAnimationEntry(transitionStateMetadata.steps); var entry = _normalizeAnimationEntry(transitionStateMetadata.steps);
var animation = _normalizeStyleSteps(entry, stateStyles, errors); var animation = _normalizeStyleSteps(entry, stateStyles, schema, errors);
var animationAst = _parseTransitionAnimation(animation, 0, styles, stateStyles, errors); var animationAst = _parseTransitionAnimation(animation, 0, styles, stateStyles, errors);
if (errors.length == 0) { if (errors.length == 0) {
_fillAnimationAstStartingKeyframes(animationAst, styles, errors); _fillAnimationAstStartingKeyframes(animationAst, styles, errors);
@ -176,13 +172,31 @@ function _normalizeAnimationEntry(entry: CompileAnimationMetadata | CompileAnima
function _normalizeStyleMetadata( function _normalizeStyleMetadata(
entry: CompileAnimationStyleMetadata, stateStyles: {[key: string]: AnimationStylesAst}, entry: CompileAnimationStyleMetadata, stateStyles: {[key: string]: AnimationStylesAst},
errors: AnimationParseError[]): {[key: string]: string | number}[] { schema: ElementSchemaRegistry, errors: AnimationParseError[],
permitStateReferences: boolean): {[key: string]: string | number}[] {
var normalizedStyles: {[key: string]: string | number}[] = []; var normalizedStyles: {[key: string]: string | number}[] = [];
entry.styles.forEach(styleEntry => { entry.styles.forEach(styleEntry => {
if (typeof styleEntry === 'string') { if (typeof styleEntry === 'string') {
if (permitStateReferences) {
normalizedStyles.push(..._resolveStylesFromState(<string>styleEntry, stateStyles, errors)); normalizedStyles.push(..._resolveStylesFromState(<string>styleEntry, stateStyles, errors));
} else { } else {
normalizedStyles.push(<{[key: string]: string | number}>styleEntry); errors.push(new AnimationParseError(
`State based animations cannot contain references to other states`));
}
} else {
var stylesObj = <Styles>styleEntry;
var normalizedStylesObj: Styles = {};
Object.keys(stylesObj).forEach(propName => {
var normalizedProp = schema.normalizeAnimationStyleProperty(propName);
var normalizedOutput =
schema.normalizeAnimationStyleValue(normalizedProp, propName, stylesObj[propName]);
var normalizationError = normalizedOutput['error'];
if (normalizationError) {
errors.push(new AnimationParseError(normalizationError));
}
normalizedStylesObj[normalizedProp] = normalizedOutput['value'];
});
normalizedStyles.push(normalizedStylesObj);
} }
}); });
return normalizedStyles; return normalizedStyles;
@ -190,8 +204,8 @@ function _normalizeStyleMetadata(
function _normalizeStyleSteps( function _normalizeStyleSteps(
entry: CompileAnimationMetadata, stateStyles: {[key: string]: AnimationStylesAst}, entry: CompileAnimationMetadata, stateStyles: {[key: string]: AnimationStylesAst},
errors: AnimationParseError[]): CompileAnimationMetadata { schema: ElementSchemaRegistry, errors: AnimationParseError[]): CompileAnimationMetadata {
var steps = _normalizeStyleStepEntry(entry, stateStyles, errors); var steps = _normalizeStyleStepEntry(entry, stateStyles, schema, errors);
return (entry instanceof CompileAnimationGroupMetadata) ? return (entry instanceof CompileAnimationGroupMetadata) ?
new CompileAnimationGroupMetadata(steps) : new CompileAnimationGroupMetadata(steps) :
new CompileAnimationSequenceMetadata(steps); new CompileAnimationSequenceMetadata(steps);
@ -213,7 +227,7 @@ function _mergeAnimationStyles(
function _normalizeStyleStepEntry( function _normalizeStyleStepEntry(
entry: CompileAnimationMetadata, stateStyles: {[key: string]: AnimationStylesAst}, entry: CompileAnimationMetadata, stateStyles: {[key: string]: AnimationStylesAst},
errors: AnimationParseError[]): CompileAnimationMetadata[] { schema: ElementSchemaRegistry, errors: AnimationParseError[]): CompileAnimationMetadata[] {
var steps: CompileAnimationMetadata[]; var steps: CompileAnimationMetadata[];
if (entry instanceof CompileAnimationWithStepsMetadata) { if (entry instanceof CompileAnimationWithStepsMetadata) {
steps = entry.steps; steps = entry.steps;
@ -232,7 +246,8 @@ function _normalizeStyleStepEntry(
if (!isPresent(combinedStyles)) { if (!isPresent(combinedStyles)) {
combinedStyles = []; combinedStyles = [];
} }
_normalizeStyleMetadata(<CompileAnimationStyleMetadata>step, stateStyles, errors) _normalizeStyleMetadata(
<CompileAnimationStyleMetadata>step, stateStyles, schema, errors, true)
.forEach(entry => { _mergeAnimationStyles(combinedStyles, entry); }); .forEach(entry => { _mergeAnimationStyles(combinedStyles, entry); });
} else { } else {
// it is important that we create a metadata entry of the combined styles // it is important that we create a metadata entry of the combined styles
@ -250,13 +265,14 @@ function _normalizeStyleStepEntry(
var animateStyleValue = (<CompileAnimationAnimateMetadata>step).styles; var animateStyleValue = (<CompileAnimationAnimateMetadata>step).styles;
if (animateStyleValue instanceof CompileAnimationStyleMetadata) { if (animateStyleValue instanceof CompileAnimationStyleMetadata) {
animateStyleValue.styles = animateStyleValue.styles =
_normalizeStyleMetadata(animateStyleValue, stateStyles, errors); _normalizeStyleMetadata(animateStyleValue, stateStyles, schema, errors, true);
} else if (animateStyleValue instanceof CompileAnimationKeyframesSequenceMetadata) { } else if (animateStyleValue instanceof CompileAnimationKeyframesSequenceMetadata) {
animateStyleValue.steps.forEach( animateStyleValue.steps.forEach(step => {
step => { step.styles = _normalizeStyleMetadata(step, stateStyles, errors); }); step.styles = _normalizeStyleMetadata(step, stateStyles, schema, errors, true);
});
} }
} else if (step instanceof CompileAnimationWithStepsMetadata) { } else if (step instanceof CompileAnimationWithStepsMetadata) {
let innerSteps = _normalizeStyleStepEntry(step, stateStyles, errors); let innerSteps = _normalizeStyleStepEntry(step, stateStyles, schema, errors);
step = step instanceof CompileAnimationGroupMetadata ? step = step instanceof CompileAnimationGroupMetadata ?
new CompileAnimationGroupMetadata(innerSteps) : new CompileAnimationGroupMetadata(innerSteps) :
new CompileAnimationSequenceMetadata(innerSteps); new CompileAnimationSequenceMetadata(innerSteps);

View File

@ -8,6 +8,7 @@
import {COMPILER_OPTIONS, Compiler, CompilerFactory, CompilerOptions, Inject, Injectable, Optional, PLATFORM_INITIALIZER, PlatformRef, Provider, ReflectiveInjector, TRANSLATIONS, TRANSLATIONS_FORMAT, Type, ViewEncapsulation, createPlatformFactory, isDevMode, platformCore} from '@angular/core'; import {COMPILER_OPTIONS, Compiler, CompilerFactory, CompilerOptions, Inject, Injectable, Optional, PLATFORM_INITIALIZER, PlatformRef, Provider, ReflectiveInjector, TRANSLATIONS, TRANSLATIONS_FORMAT, Type, ViewEncapsulation, createPlatformFactory, isDevMode, platformCore} from '@angular/core';
import {AnimationParser} from './animation/animation_parser';
import {CompilerConfig} from './config'; import {CompilerConfig} from './config';
import {DirectiveNormalizer} from './directive_normalizer'; import {DirectiveNormalizer} from './directive_normalizer';
import {DirectiveResolver} from './directive_resolver'; import {DirectiveResolver} from './directive_resolver';
@ -74,7 +75,8 @@ export const COMPILER_PROVIDERS: Array<any|Type<any>|{[k: string]: any}|any[]> =
UrlResolver, UrlResolver,
DirectiveResolver, DirectiveResolver,
PipeResolver, PipeResolver,
NgModuleResolver NgModuleResolver,
AnimationParser
]; ];

View File

@ -109,7 +109,6 @@ export function analyzeNgModules(
} }
export class OfflineCompiler { export class OfflineCompiler {
private _animationParser = new AnimationParser();
private _animationCompiler = new AnimationCompiler(); private _animationCompiler = new AnimationCompiler();
constructor( constructor(
@ -118,7 +117,8 @@ export class OfflineCompiler {
private _styleCompiler: StyleCompiler, private _viewCompiler: ViewCompiler, private _styleCompiler: StyleCompiler, private _viewCompiler: ViewCompiler,
private _dirWrapperCompiler: DirectiveWrapperCompiler, private _dirWrapperCompiler: DirectiveWrapperCompiler,
private _ngModuleCompiler: NgModuleCompiler, private _outputEmitter: OutputEmitter, private _ngModuleCompiler: NgModuleCompiler, private _outputEmitter: OutputEmitter,
private _localeId: string, private _translationFormat: string) {} private _localeId: string, private _translationFormat: string,
private _animationParser: AnimationParser) {}
clearCache() { clearCache() {
this._directiveNormalizer.clearCache(); this._directiveNormalizer.clearCache();

View File

@ -43,7 +43,6 @@ export class RuntimeCompiler implements Compiler {
private _compiledHostTemplateCache = new Map<Type<any>, CompiledTemplate>(); private _compiledHostTemplateCache = new Map<Type<any>, CompiledTemplate>();
private _compiledDirectiveWrapperCache = new Map<Type<any>, Type<any>>(); private _compiledDirectiveWrapperCache = new Map<Type<any>, Type<any>>();
private _compiledNgModuleCache = new Map<Type<any>, NgModuleFactory<any>>(); private _compiledNgModuleCache = new Map<Type<any>, NgModuleFactory<any>>();
private _animationParser = new AnimationParser();
private _animationCompiler = new AnimationCompiler(); private _animationCompiler = new AnimationCompiler();
constructor( constructor(
@ -52,7 +51,7 @@ export class RuntimeCompiler implements Compiler {
private _styleCompiler: StyleCompiler, private _viewCompiler: ViewCompiler, private _styleCompiler: StyleCompiler, private _viewCompiler: ViewCompiler,
private _ngModuleCompiler: NgModuleCompiler, private _ngModuleCompiler: NgModuleCompiler,
private _directiveWrapperCompiler: DirectiveWrapperCompiler, private _directiveWrapperCompiler: DirectiveWrapperCompiler,
private _compilerConfig: CompilerConfig) {} private _compilerConfig: CompilerConfig, private _animationParser: AnimationParser) {}
get injector(): Injector { return this._injector; } get injector(): Injector { return this._injector; }

View File

@ -6,7 +6,9 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {CUSTOM_ELEMENTS_SCHEMA, Injectable, NO_ERRORS_SCHEMA, SchemaMetadata, SecurityContext} from '@angular/core'; import {AUTO_STYLE, CUSTOM_ELEMENTS_SCHEMA, Injectable, NO_ERRORS_SCHEMA, SchemaMetadata, SecurityContext} from '@angular/core';
import {dashCaseToCamelCase} from '../util';
import {SECURITY_SCHEMA} from './dom_security_schema'; import {SECURITY_SCHEMA} from './dom_security_schema';
import {ElementSchemaRegistry} from './element_schema_registry'; import {ElementSchemaRegistry} from './element_schema_registry';
@ -373,4 +375,64 @@ export class DomElementSchemaRegistry extends ElementSchemaRegistry {
} }
allKnownElementNames(): string[] { return Object.keys(this._schema); } allKnownElementNames(): string[] { return Object.keys(this._schema); }
normalizeAnimationStyleProperty(propName: string): string {
return dashCaseToCamelCase(propName);
}
normalizeAnimationStyleValue(camelCaseProp: string, userProvidedProp: string, val: string|number):
{error: string, value: string} {
var unit: string = '';
var strVal = val.toString().trim();
var errorMsg: string = null;
if (_isPixelDimensionStyle(camelCaseProp) && val !== 0 && val !== '0') {
if (typeof val === 'number') {
unit = 'px';
} else {
let valAndSuffixMatch = val.match(/^[+-]?[\d\.]+([a-z]*)$/);
if (valAndSuffixMatch && valAndSuffixMatch[1].length == 0) {
errorMsg = `Please provide a CSS unit value for ${userProvidedProp}:${val}`;
}
}
}
return {error: errorMsg, value: strVal + unit};
}
}
function _isPixelDimensionStyle(prop: string): boolean {
switch (prop) {
case 'width':
case 'height':
case 'minWidth':
case 'minHeight':
case 'maxWidth':
case 'maxHeight':
case 'left':
case 'top':
case 'bottom':
case 'right':
case 'fontSize':
case 'outlineWidth':
case 'outlineOffset':
case 'paddingTop':
case 'paddingLeft':
case 'paddingBottom':
case 'paddingRight':
case 'marginTop':
case 'marginLeft':
case 'marginBottom':
case 'marginRight':
case 'borderRadius':
case 'borderWidth':
case 'borderTopWidth':
case 'borderLeftWidth':
case 'borderRightWidth':
case 'borderBottomWidth':
case 'textIndent':
return true;
default:
return false;
}
} }

View File

@ -18,4 +18,8 @@ export abstract class ElementSchemaRegistry {
abstract getDefaultComponentElementName(): string; abstract getDefaultComponentElementName(): string;
abstract validateProperty(name: string): {error: boolean, msg?: string}; abstract validateProperty(name: string): {error: boolean, msg?: string};
abstract validateAttribute(name: string): {error: boolean, msg?: string}; abstract validateAttribute(name: string): {error: boolean, msg?: string};
abstract normalizeAnimationStyleProperty(propName: string): string;
abstract normalizeAnimationStyleValue(
camelCaseProp: string, userProvidedProp: string,
val: string|number): {error: string, value: string};
} }

View File

@ -11,11 +11,16 @@ import {isBlank, isPrimitive, isStrictStringMap} from './facade/lang';
export const MODULE_SUFFIX = ''; export const MODULE_SUFFIX = '';
const CAMEL_CASE_REGEXP = /([A-Z])/g; const CAMEL_CASE_REGEXP = /([A-Z])/g;
const DASH_CASE_REGEXP = /-+([a-z0-9])/g;
export function camelCaseToDashCase(input: string): string { export function camelCaseToDashCase(input: string): string {
return input.replace(CAMEL_CASE_REGEXP, (...m: any[]) => '-' + m[1].toLowerCase()); return input.replace(CAMEL_CASE_REGEXP, (...m: any[]) => '-' + m[1].toLowerCase());
} }
export function dashCaseToCamelCase(input: string): string {
return input.replace(DASH_CASE_REGEXP, (...m: any[]) => m[1].toUpperCase());
}
export function splitAtColon(input: string, defaultValues: string[]): string[] { export function splitAtColon(input: string, defaultValues: string[]): string[] {
return _splitAt(input, ':', defaultValues); return _splitAt(input, ':', defaultValues);
} }

View File

@ -12,14 +12,19 @@ import {AnimationCompiler, AnimationEntryCompileResult} from '../../src/animatio
import {AnimationParser} from '../../src/animation/animation_parser'; import {AnimationParser} from '../../src/animation/animation_parser';
import {CompileAnimationEntryMetadata, CompileDirectiveMetadata, CompileTemplateMetadata, CompileTypeMetadata} from '../../src/compile_metadata'; import {CompileAnimationEntryMetadata, CompileDirectiveMetadata, CompileTemplateMetadata, CompileTypeMetadata} from '../../src/compile_metadata';
import {CompileMetadataResolver} from '../../src/metadata_resolver'; import {CompileMetadataResolver} from '../../src/metadata_resolver';
import {ElementSchemaRegistry} from '../../src/schema/element_schema_registry';
export function main() { export function main() {
describe('RuntimeAnimationCompiler', () => { describe('RuntimeAnimationCompiler', () => {
var resolver: CompileMetadataResolver; var resolver: CompileMetadataResolver;
beforeEach( var parser: AnimationParser;
inject([CompileMetadataResolver], (res: CompileMetadataResolver) => { resolver = res; })); beforeEach(inject(
[CompileMetadataResolver, ElementSchemaRegistry],
(res: CompileMetadataResolver, schema: ElementSchemaRegistry) => {
resolver = res;
parser = new AnimationParser(schema);
}));
const parser = new AnimationParser();
const compiler = new AnimationCompiler(); const compiler = new AnimationCompiler();
var compileAnimations = var compileAnimations =

View File

@ -13,6 +13,7 @@ import {expect} from '@angular/platform-browser/testing/matchers';
import {AnimationEntryAst, AnimationGroupAst, AnimationKeyframeAst, AnimationSequenceAst, AnimationStepAst, AnimationStylesAst} from '../../src/animation/animation_ast'; import {AnimationEntryAst, AnimationGroupAst, AnimationKeyframeAst, AnimationSequenceAst, AnimationStepAst, AnimationStylesAst} from '../../src/animation/animation_ast';
import {AnimationParser} from '../../src/animation/animation_parser'; import {AnimationParser} from '../../src/animation/animation_parser';
import {CompileMetadataResolver} from '../../src/metadata_resolver'; import {CompileMetadataResolver} from '../../src/metadata_resolver';
import {ElementSchemaRegistry} from '../../src/schema/element_schema_registry';
import {FILL_STYLE_FLAG, flattenStyles} from '../private_import_core'; import {FILL_STYLE_FLAG, flattenStyles} from '../private_import_core';
export function main() { export function main() {
@ -39,13 +40,18 @@ export function main() {
}; };
var resolver: CompileMetadataResolver; var resolver: CompileMetadataResolver;
beforeEach( var schema: ElementSchemaRegistry;
inject([CompileMetadataResolver], (res: CompileMetadataResolver) => { resolver = res; })); beforeEach(inject(
[CompileMetadataResolver, ElementSchemaRegistry],
(res: CompileMetadataResolver, sch: ElementSchemaRegistry) => {
resolver = res;
schema = sch;
}));
var parseAnimation = (data: AnimationMetadata[]) => { var parseAnimation = (data: AnimationMetadata[]) => {
const entry = trigger('myAnimation', [transition('state1 => state2', sequence(data))]); const entry = trigger('myAnimation', [transition('state1 => state2', sequence(data))]);
const compiledAnimationEntry = resolver.getAnimationEntryMetadata(entry); const compiledAnimationEntry = resolver.getAnimationEntryMetadata(entry);
const parser = new AnimationParser(); const parser = new AnimationParser(schema);
return parser.parseEntry(compiledAnimationEntry); return parser.parseEntry(compiledAnimationEntry);
}; };
@ -59,21 +65,21 @@ export function main() {
it('should merge repeated style steps into a single style ast step entry', () => { it('should merge repeated style steps into a single style ast step entry', () => {
var ast = parseAnimationAst([ var ast = parseAnimationAst([
style({'color': 'black'}), style({'background': 'red'}), style({'opacity': 0}), style({'color': 'black'}), style({'background': 'red'}), style({'opacity': '0'}),
animate(1000, style({'color': 'white', 'background': 'black', 'opacity': 1})) animate(1000, style({'color': 'white', 'background': 'black', 'opacity': '1'}))
]); ]);
expect(ast.steps.length).toEqual(1); expect(ast.steps.length).toEqual(1);
var step = <AnimationStepAst>ast.steps[0]; var step = <AnimationStepAst>ast.steps[0];
expect(step.startingStyles.styles[0]) expect(step.startingStyles.styles[0])
.toEqual({'color': 'black', 'background': 'red', 'opacity': 0}); .toEqual({'color': 'black', 'background': 'red', 'opacity': '0'});
expect(step.keyframes[0].styles.styles[0]) expect(step.keyframes[0].styles.styles[0])
.toEqual({'color': 'black', 'background': 'red', 'opacity': 0}); .toEqual({'color': 'black', 'background': 'red', 'opacity': '0'});
expect(step.keyframes[1].styles.styles[0]) expect(step.keyframes[1].styles.styles[0])
.toEqual({'color': 'white', 'background': 'black', 'opacity': 1}); .toEqual({'color': 'white', 'background': 'black', 'opacity': '1'});
}); });
it('should animate only the styles requested within an animation step', () => { it('should animate only the styles requested within an animation step', () => {
@ -93,7 +99,7 @@ export function main() {
it('should populate the starting and duration times propertly', () => { it('should populate the starting and duration times propertly', () => {
var ast = parseAnimationAst([ var ast = parseAnimationAst([
style({'color': 'black', 'opacity': 1}), style({'color': 'black', 'opacity': '1'}),
animate(1000, style({'color': 'red'})), animate(1000, style({'color': 'red'})),
animate(4000, style({'color': 'yellow'})), animate(4000, style({'color': 'yellow'})),
sequence( sequence(
@ -144,13 +150,13 @@ export function main() {
it('should apply the correct animate() styles when parallel animations are active and use the same properties', it('should apply the correct animate() styles when parallel animations are active and use the same properties',
() => { () => {
var details = parseAnimation([ var details = parseAnimation([
style({'opacity': 0, 'color': 'red'}), group([ style({'opacity': '0', 'color': 'red'}), group([
sequence([ sequence([
animate(2000, style({'color': 'black'})), animate(2000, style({'color': 'black'})),
animate(2000, style({'opacity': 0.5})), animate(2000, style({'opacity': '0.5'})),
]), ]),
sequence([ sequence([
animate(2000, style({'opacity': 0.8})), animate(2000, style({'opacity': '0.8'})),
animate(2000, style({'color': 'blue'})) animate(2000, style({'color': 'blue'}))
]) ])
]) ])
@ -169,10 +175,10 @@ export function main() {
expect(collectStepStyles(sq1a1)).toEqual([{'color': 'red'}, {'color': 'black'}]); expect(collectStepStyles(sq1a1)).toEqual([{'color': 'red'}, {'color': 'black'}]);
var sq1a2 = <AnimationStepAst>sq1.steps[1]; var sq1a2 = <AnimationStepAst>sq1.steps[1];
expect(collectStepStyles(sq1a2)).toEqual([{'opacity': 0.8}, {'opacity': 0.5}]); expect(collectStepStyles(sq1a2)).toEqual([{'opacity': '0.8'}, {'opacity': '0.5'}]);
var sq2a1 = <AnimationStepAst>sq2.steps[0]; var sq2a1 = <AnimationStepAst>sq2.steps[0];
expect(collectStepStyles(sq2a1)).toEqual([{'opacity': 0}, {'opacity': 0.8}]); expect(collectStepStyles(sq2a1)).toEqual([{'opacity': '0'}, {'opacity': '0.8'}]);
var sq2a2 = <AnimationStepAst>sq2.steps[1]; var sq2a2 = <AnimationStepAst>sq2.steps[1];
expect(collectStepStyles(sq2a2)).toEqual([{'color': 'black'}, {'color': 'blue'}]); expect(collectStepStyles(sq2a2)).toEqual([{'color': 'black'}, {'color': 'blue'}]);
@ -180,8 +186,8 @@ export function main() {
it('should throw errors when animations animate a CSS property at the same time', () => { it('should throw errors when animations animate a CSS property at the same time', () => {
var animation1 = parseAnimation([ var animation1 = parseAnimation([
style({'opacity': 0}), style({'opacity': '0'}),
group([animate(1000, style({'opacity': 1})), animate(2000, style({'opacity': 0.5}))]) group([animate(1000, style({'opacity': '1'})), animate(2000, style({'opacity': '0.5'}))])
]); ]);
var errors1 = animation1.errors; var errors1 = animation1.errors;
@ -205,22 +211,23 @@ export function main() {
it('should return an error when an animation style contains an invalid timing value', () => { it('should return an error when an animation style contains an invalid timing value', () => {
var errors = parseAnimationAndGetErrors( var errors = parseAnimationAndGetErrors(
[style({'opacity': 0}), animate('one second', style({'opacity': 1}))]); [style({'opacity': '0'}), animate('one second', style({'opacity': '1'}))]);
expect(errors[0].msg).toContainError(`The provided timing value "one second" is invalid.`); expect(errors[0].msg).toContainError(`The provided timing value "one second" is invalid.`);
}); });
it('should collect and return any errors collected when parsing the metadata', () => { it('should collect and return any errors collected when parsing the metadata', () => {
var errors = parseAnimationAndGetErrors([ var errors = parseAnimationAndGetErrors([
style({'opacity': 0}), animate('one second', style({'opacity': 1})), style({'opacity': 0}), style({'opacity': '0'}), animate('one second', style({'opacity': '1'})),
animate('one second', null), style({'background': 'red'}) style({'opacity': '0'}), animate('one second', null), style({'background': 'red'})
]); ]);
expect(errors.length).toBeGreaterThan(1); expect(errors.length).toBeGreaterThan(1);
}); });
it('should normalize a series of keyframe styles into a list of offset steps', () => { it('should normalize a series of keyframe styles into a list of offset steps', () => {
var ast = parseAnimationAst([animate(1000, keyframes([ var ast =
style({'width': 0}), style({'width': 25}), parseAnimationAst([animate(1000, keyframes([
style({'width': 50}), style({'width': 75}) style({'width': '0'}), style({'width': '25px'}),
style({'width': '50px'}), style({'width': '75px'})
]))]); ]))]);
var step = <AnimationStepAst>ast.steps[0]; var step = <AnimationStepAst>ast.steps[0];
@ -233,10 +240,10 @@ export function main() {
}); });
it('should use an existing collection of offset steps if provided', () => { it('should use an existing collection of offset steps if provided', () => {
var ast = parseAnimationAst( var ast = parseAnimationAst([animate(
[animate(1000, keyframes([ 1000, keyframes([
style({'height': 0, 'offset': 0}), style({'height': 25, 'offset': 0.6}), style({'height': '0', 'offset': 0}), style({'height': '25px', 'offset': 0.6}),
style({'height': 50, 'offset': 0.7}), style({'height': 75, 'offset': 1}) style({'height': '50px', 'offset': 0.7}), style({'height': '75px', 'offset': 1})
]))]); ]))]);
var step = <AnimationStepAst>ast.steps[0]; var step = <AnimationStepAst>ast.steps[0];
@ -251,24 +258,25 @@ export function main() {
it('should sort the provided collection of steps that contain offsets', () => { it('should sort the provided collection of steps that contain offsets', () => {
var ast = parseAnimationAst([animate( var ast = parseAnimationAst([animate(
1000, keyframes([ 1000, keyframes([
style({'opacity': 0, 'offset': 0.9}), style({'opacity': .25, 'offset': 0}), style({'opacity': '0', 'offset': 0.9}), style({'opacity': '0.25', 'offset': 0}),
style({'opacity': .50, 'offset': 1}), style({'opacity': .75, 'offset': 0.91}) style({'opacity': '0.50', 'offset': 1}),
style({'opacity': '0.75', 'offset': 0.91})
]))]); ]))]);
var step = <AnimationStepAst>ast.steps[0]; var step = <AnimationStepAst>ast.steps[0];
expect(step.keyframes.length).toEqual(4); expect(step.keyframes.length).toEqual(4);
expect(step.keyframes[0].offset).toEqual(0); expect(step.keyframes[0].offset).toEqual(0);
expect(step.keyframes[0].styles.styles[0]['opacity']).toEqual(.25); expect(step.keyframes[0].styles.styles[0]['opacity']).toEqual('0.25');
expect(step.keyframes[1].offset).toEqual(0.9); expect(step.keyframes[1].offset).toEqual(0.9);
expect(step.keyframes[1].styles.styles[0]['opacity']).toEqual(0); expect(step.keyframes[1].styles.styles[0]['opacity']).toEqual('0');
expect(step.keyframes[2].offset).toEqual(0.91); expect(step.keyframes[2].offset).toEqual(0.91);
expect(step.keyframes[2].styles.styles[0]['opacity']).toEqual(.75); expect(step.keyframes[2].styles.styles[0]['opacity']).toEqual('0.75');
expect(step.keyframes[3].offset).toEqual(1); expect(step.keyframes[3].offset).toEqual(1);
expect(step.keyframes[3].styles.styles[0]['opacity']).toEqual(.50); expect(step.keyframes[3].styles.styles[0]['opacity']).toEqual('0.50');
}); });
it('should throw an error if a partial amount of keyframes contain an offset', () => { it('should throw an error if a partial amount of keyframes contain an offset', () => {
@ -302,7 +310,7 @@ export function main() {
it('should copy over any missing styles to the final keyframe if not already defined', () => { it('should copy over any missing styles to the final keyframe if not already defined', () => {
var ast = parseAnimationAst([animate( var ast = parseAnimationAst([animate(
1000, keyframes([ 1000, keyframes([
style({'color': 'white', 'border-color': 'white'}), style({'color': 'white', 'borderColor': 'white'}),
style({'color': 'red', 'background': 'blue'}), style({'background': 'blue'}) style({'color': 'red', 'background': 'blue'}), style({'background': 'blue'})
]))]); ]))]);
@ -312,19 +320,16 @@ export function main() {
var kf3 = keyframesStep.keyframes[2]; var kf3 = keyframesStep.keyframes[2];
expect(flattenStyles(kf3.styles.styles)) expect(flattenStyles(kf3.styles.styles))
.toEqual({'background': 'blue', 'color': 'red', 'border-color': 'white'}); .toEqual({'background': 'blue', 'color': 'red', 'borderColor': 'white'});
}); });
it('should create an initial keyframe if not detected and place all keyframes styles there', it('should create an initial keyframe if not detected and place all keyframes styles there',
() => { () => {
var ast = parseAnimationAst( var ast = parseAnimationAst([animate(
[animate(1000, keyframes([ 1000, keyframes([
style({'color': 'white', 'background': 'black', 'offset': 0.5}), style({ style({'color': 'white', 'background': 'black', 'offset': 0.5}),
'color': 'orange', style(
'background': 'red', {'color': 'orange', 'background': 'red', 'fontSize': '100px', 'offset': 1})
'font-size': '100px',
'offset': 1
})
]))]); ]))]);
var keyframesStep = <AnimationStepAst>ast.steps[0]; var keyframesStep = <AnimationStepAst>ast.steps[0];
@ -335,7 +340,7 @@ export function main() {
expect(kf1.offset).toEqual(0); expect(kf1.offset).toEqual(0);
expect(flattenStyles(kf1.styles.styles)).toEqual({ expect(flattenStyles(kf1.styles.styles)).toEqual({
'font-size': FILL_STYLE_FLAG, 'fontSize': FILL_STYLE_FLAG,
'background': FILL_STYLE_FLAG, 'background': FILL_STYLE_FLAG,
'color': FILL_STYLE_FLAG 'color': FILL_STYLE_FLAG
}); });
@ -353,7 +358,7 @@ export function main() {
style({ style({
'color': 'orange', 'color': 'orange',
'background': 'red', 'background': 'red',
'font-size': '100px', 'fontSize': '100px',
'offset': 0.5 'offset': 0.5
}) })
]))]); ]))]);
@ -369,13 +374,13 @@ export function main() {
'color': 'orange', 'color': 'orange',
'background': 'red', 'background': 'red',
'transform': 'rotate(360deg)', 'transform': 'rotate(360deg)',
'font-size': '100px' 'fontSize': '100px'
}); });
}); });
describe('easing / duration / delay', () => { describe('easing / duration / delay', () => {
it('should parse simple string-based values', () => { it('should parse simple string-based values', () => {
var ast = parseAnimationAst([animate('1s .5s ease-out', style({'opacity': 1}))]); var ast = parseAnimationAst([animate('1s .5s ease-out', style({'opacity': '1'}))]);
var step = <AnimationStepAst>ast.steps[0]; var step = <AnimationStepAst>ast.steps[0];
expect(step.duration).toEqual(1000); expect(step.duration).toEqual(1000);
@ -384,7 +389,7 @@ export function main() {
}); });
it('should parse a numeric duration value', () => { it('should parse a numeric duration value', () => {
var ast = parseAnimationAst([animate(666, style({'opacity': 1}))]); var ast = parseAnimationAst([animate(666, style({'opacity': '1'}))]);
var step = <AnimationStepAst>ast.steps[0]; var step = <AnimationStepAst>ast.steps[0];
expect(step.duration).toEqual(666); expect(step.duration).toEqual(666);
@ -393,7 +398,7 @@ export function main() {
}); });
it('should parse an easing value without a delay', () => { it('should parse an easing value without a delay', () => {
var ast = parseAnimationAst([animate('5s linear', style({'opacity': 1}))]); var ast = parseAnimationAst([animate('5s linear', style({'opacity': '1'}))]);
var step = <AnimationStepAst>ast.steps[0]; var step = <AnimationStepAst>ast.steps[0];
expect(step.duration).toEqual(5000); expect(step.duration).toEqual(5000);
@ -403,7 +408,7 @@ export function main() {
it('should parse a complex easing value', () => { it('should parse a complex easing value', () => {
var ast = var ast =
parseAnimationAst([animate('30ms cubic-bezier(0, 0,0, .69)', style({'opacity': 1}))]); parseAnimationAst([animate('30ms cubic-bezier(0, 0,0, .69)', style({'opacity': '1'}))]);
var step = <AnimationStepAst>ast.steps[0]; var step = <AnimationStepAst>ast.steps[0];
expect(step.duration).toEqual(30); expect(step.duration).toEqual(30);

View File

@ -192,5 +192,44 @@ If 'onAnything' is a directive input, make sure the directive is imported by the
}); });
} }
describe('normalizeAnimationStyleProperty', () => {
it('should normalize the given CSS property to camelCase', () => {
expect(registry.normalizeAnimationStyleProperty('border-radius')).toBe('borderRadius');
expect(registry.normalizeAnimationStyleProperty('zIndex')).toBe('zIndex');
expect(registry.normalizeAnimationStyleProperty('-webkit-animation'))
.toBe('WebkitAnimation');
});
});
describe('normalizeAnimationStyleValue', () => {
it('should normalize the given dimensional CSS style value to contain a PX value when numeric',
() => {
expect(
registry.normalizeAnimationStyleValue('borderRadius', 'border-radius', 10)['value'])
.toBe('10px');
});
it('should not normalize any values that are of zero', () => {
expect(registry.normalizeAnimationStyleValue('opacity', 'opacity', 0)['value']).toBe('0');
expect(registry.normalizeAnimationStyleValue('width', 'width', 0)['value']).toBe('0');
});
it('should retain the given dimensional CSS style value\'s unit if it already exists', () => {
expect(
registry.normalizeAnimationStyleValue('borderRadius', 'border-radius', '10em')['value'])
.toBe('10em');
});
it('should trim the provided CSS style value', () => {
expect(registry.normalizeAnimationStyleValue('color', 'color', ' red ')['value'])
.toBe('red');
});
it('should stringify all non dimensional numeric style values', () => {
expect(registry.normalizeAnimationStyleValue('zIndex', 'zIndex', 10)['value']).toBe('10');
expect(registry.normalizeAnimationStyleValue('opacity', 'opacity', 0.5)['value'])
.toBe('0.5');
});
});
}); });
} }

View File

@ -54,4 +54,10 @@ export class MockSchemaRegistry implements ElementSchemaRegistry {
return {error: false}; return {error: false};
} }
} }
normalizeAnimationStyleProperty(propName: string): string { return propName; }
normalizeAnimationStyleValue(camelCaseProp: string, userProvidedProp: string, val: string|number):
{error: string, value: string} {
return {error: null, value: val.toString()};
}
} }

View File

@ -7,6 +7,7 @@
*/ */
import {CommonModule} from '@angular/common'; import {CommonModule} from '@angular/common';
import {DomElementSchemaRegistry, ElementSchemaRegistry} from '@angular/compiler';
import {AnimationDriver} from '@angular/platform-browser/src/dom/animation_driver'; import {AnimationDriver} from '@angular/platform-browser/src/dom/animation_driver';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {MockAnimationDriver} from '@angular/platform-browser/testing/mock_animation_driver'; import {MockAnimationDriver} from '@angular/platform-browser/testing/mock_animation_driver';
@ -52,7 +53,7 @@ function declareTests({useJit}: {useJit: boolean}) {
'myAnimation', 'myAnimation',
[transition( [transition(
'void => *', 'void => *',
[style({'opacity': 0}), animate(500, style({'opacity': 1}))])])] [style({'opacity': '0'}), animate(500, style({'opacity': '1'}))])])]
} }
}); });
@ -68,8 +69,8 @@ function declareTests({useJit}: {useJit: boolean}) {
var keyframes2 = driver.log[0]['keyframeLookup']; var keyframes2 = driver.log[0]['keyframeLookup'];
expect(keyframes2.length).toEqual(2); expect(keyframes2.length).toEqual(2);
expect(keyframes2[0]).toEqual([0, {'opacity': 0}]); expect(keyframes2[0]).toEqual([0, {'opacity': '0'}]);
expect(keyframes2[1]).toEqual([1, {'opacity': 1}]); expect(keyframes2[1]).toEqual([1, {'opacity': '1'}]);
})); }));
it('should trigger a state change animation from state => void', fakeAsync(() => { it('should trigger a state change animation from state => void', fakeAsync(() => {
@ -82,7 +83,7 @@ function declareTests({useJit}: {useJit: boolean}) {
'myAnimation', 'myAnimation',
[transition( [transition(
'* => void', '* => void',
[style({'opacity': 1}), animate(500, style({'opacity': 0}))])])] [style({'opacity': '1'}), animate(500, style({'opacity': '0'}))])])]
} }
}); });
@ -102,11 +103,13 @@ function declareTests({useJit}: {useJit: boolean}) {
var keyframes2 = driver.log[0]['keyframeLookup']; var keyframes2 = driver.log[0]['keyframeLookup'];
expect(keyframes2.length).toEqual(2); expect(keyframes2.length).toEqual(2);
expect(keyframes2[0]).toEqual([0, {'opacity': 1}]); expect(keyframes2[0]).toEqual([0, {'opacity': '1'}]);
expect(keyframes2[1]).toEqual([1, {'opacity': 0}]); expect(keyframes2[1]).toEqual([1, {'opacity': '0'}]);
})); }));
it('should animate the element when the expression changes between states', fakeAsync(() => { it('should animate the element when the expression changes between states',
fakeAsync(
() => {
TestBed.overrideComponent(DummyIfCmp, { TestBed.overrideComponent(DummyIfCmp, {
set: { set: {
template: ` template: `
@ -115,8 +118,8 @@ function declareTests({useJit}: {useJit: boolean}) {
animations: [ animations: [
trigger('myAnimation', [ trigger('myAnimation', [
transition('* => state1', [ transition('* => state1', [
style({'background': 'red'}), style({'backgroundColor': 'red'}),
animate('0.5s 1s ease-out', style({'background': 'blue'})) animate('0.5s 1s ease-out', style({'backgroundColor': 'blue'}))
]) ])
]) ])
] ]
@ -139,11 +142,11 @@ function declareTests({useJit}: {useJit: boolean}) {
expect(animation1['easing']).toEqual('ease-out'); expect(animation1['easing']).toEqual('ease-out');
var startingStyles = animation1['startingStyles']; var startingStyles = animation1['startingStyles'];
expect(startingStyles).toEqual({'background': 'red'}); expect(startingStyles).toEqual({'backgroundColor': 'red'});
var kf = animation1['keyframeLookup']; var kf = animation1['keyframeLookup'];
expect(kf[0]).toEqual([0, {'background': 'red'}]); expect(kf[0]).toEqual([0, {'backgroundColor': 'red'}]);
expect(kf[1]).toEqual([1, {'background': 'blue'}]); expect(kf[1]).toEqual([1, {'backgroundColor': 'blue'}]);
})); }));
describe('animation aliases', () => { describe('animation aliases', () => {
@ -154,10 +157,12 @@ function declareTests({useJit}: {useJit: boolean}) {
<div *ngIf="exp" [@myAnimation]="exp"></div> <div *ngIf="exp" [@myAnimation]="exp"></div>
`, `,
animations: [trigger( animations: [trigger(
'myAnimation', 'myAnimation', [transition(
[transition(
':enter', ':enter',
[style({'opacity': 0}), animate('500ms', style({opacity: 1}))])])] [
style({'opacity': '0'}),
animate('500ms', style({'opacity': '1'}))
])])]
} }
}); });
@ -181,7 +186,7 @@ function declareTests({useJit}: {useJit: boolean}) {
`, `,
animations: [trigger( animations: [trigger(
'myAnimation', 'myAnimation',
[transition(':leave', [animate('999ms', style({opacity: 0}))])])] [transition(':leave', [animate('999ms', style({'opacity': '0'}))])])]
} }
}); });
@ -211,7 +216,8 @@ function declareTests({useJit}: {useJit: boolean}) {
`, `,
animations: [trigger( animations: [trigger(
'myAnimation', 'myAnimation',
[transition(':dont_leave_me', [animate('444ms', style({opacity: 0}))])])] [transition(
':dont_leave_me', [animate('444ms', style({'opacity': '0'}))])])]
} }
}); });
@ -265,6 +271,135 @@ function declareTests({useJit}: {useJit: boolean}) {
expect(result['keyframeLookup']).toEqual([[0, {'opacity': '1'}], [1, {'opacity': '0'}]]); expect(result['keyframeLookup']).toEqual([[0, {'opacity': '1'}], [1, {'opacity': '0'}]]);
})); }));
describe('schema normalization', () => {
beforeEach(() => {
TestBed.overrideComponent(DummyIfCmp, {
set: {
template: `<div [@myAnimation] *ngIf="exp"></div>`,
animations: [trigger(
'myAnimation',
[
state('*', style({'border-width': '10px', 'height': 111})),
state('void', style({'z-index': '20'})),
transition('* => *', [
style({ height: '200px ', '-webkit-border-radius': '10px' }),
animate('500ms')
])
])]
}
});
});
describe('via DomElementSchemaRegistry', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [{provide: ElementSchemaRegistry, useClass: DomElementSchemaRegistry}]
});
});
it('should normalize all CSS style properties to camelCase during compile time if a DOM schema is used',
fakeAsync(() => {
const driver = TestBed.get(AnimationDriver) as MockAnimationDriver;
let fixture = TestBed.createComponent(DummyIfCmp);
var cmp = fixture.componentInstance;
cmp.exp = true;
fixture.detectChanges();
flushMicrotasks();
var result = driver.log.pop();
var styleProps1 = Object.keys(result['keyframeLookup'][0][1]).sort();
var styleProps2 = Object.keys(result['keyframeLookup'][1][1]).sort();
var expectedProps = ['WebkitBorderRadius', 'borderWidth', 'height', 'zIndex'];
expect(styleProps1)
.toEqual(expectedProps); // the start/end styles are always balanced
expect(styleProps2).toEqual(expectedProps);
}));
it('should normalize all dimensional CSS style values to `px` values if the value is a number',
fakeAsync(() => {
const driver = TestBed.get(AnimationDriver) as MockAnimationDriver;
let fixture = TestBed.createComponent(DummyIfCmp);
var cmp = fixture.componentInstance;
cmp.exp = true;
fixture.detectChanges();
flushMicrotasks();
var result = driver.log.pop();
var styleVals1 = result['keyframeLookup'][0][1];
expect(styleVals1['zIndex']).toEqual('20');
expect(styleVals1['height']).toEqual('200px');
var styleVals2 = result['keyframeLookup'][1][1];
expect(styleVals2['borderWidth']).toEqual('10px');
expect(styleVals2['height']).toEqual('111px');
}));
it('should throw an error when a string-based dimensional style value is used that does not contain a unit value is detected',
fakeAsync(() => {
TestBed.overrideComponent(DummyIfCmp, {
set: {
template: `<div [@myAnimation] *ngIf="exp"></div>`,
animations: [trigger(
'myAnimation',
[state('*', style({width: '123'})), transition('* => *', animate(500))])]
}
});
expect(() => {
TestBed.createComponent(DummyIfCmp);
}).toThrowError(/Please provide a CSS unit value for width:123/);
}));
});
describe('not using DomElementSchemaRegistry', () => {
beforeEach(() => {
TestBed.configureTestingModule(
{providers: [{provide: ElementSchemaRegistry, useClass: _NaiveElementSchema}]});
it('should not normalize any CSS style properties to camelCase during compile time if a DOM schema is used',
fakeAsync(() => {
const driver = TestBed.get(AnimationDriver) as MockAnimationDriver;
let fixture = TestBed.createComponent(DummyIfCmp);
var cmp = fixture.componentInstance;
cmp.exp = true;
fixture.detectChanges();
flushMicrotasks();
var result = driver.log.pop();
var styleProps1 = Object.keys(result['keyframeLookup'][0][1]).sort();
var styleProps2 = Object.keys(result['keyframeLookup'][1][1]).sort();
var expectedProps = ['-webkit-border-radius', 'border-width', 'height', 'z-index'];
expect(styleProps1)
.toEqual(expectedProps); // the start/end styles are always balanced
expect(styleProps2).toEqual(expectedProps);
}));
it('should not normalize nay dimensional CSS style values to `px` values if the value is a number',
fakeAsync(() => {
const driver = TestBed.get(AnimationDriver) as MockAnimationDriver;
let fixture = TestBed.createComponent(DummyIfCmp);
var cmp = fixture.componentInstance;
cmp.exp = true;
fixture.detectChanges();
flushMicrotasks();
var result = driver.log.pop();
var styleVals1 = result['keyframeLookup'][0][1];
expect(styleVals1['z-index']).toEqual('20');
expect(styleVals1['height']).toEqual('200px');
var styleVals2 = result['keyframeLookup'][1][1];
expect(styleVals2['border-width']).toEqual('10px');
expect(styleVals2['height']).toEqual(111);
}));
});
});
});
it('should combine repeated style steps into a single step', fakeAsync(() => { it('should combine repeated style steps into a single step', fakeAsync(() => {
TestBed.overrideComponent(DummyIfCmp, { TestBed.overrideComponent(DummyIfCmp, {
set: { set: {
@ -277,11 +412,11 @@ function declareTests({useJit}: {useJit: boolean}) {
style({'background': 'red'}), style({'background': 'red'}),
style({'width': '100px'}), style({'width': '100px'}),
style({'background': 'gold'}), style({'background': 'gold'}),
style({'height': 111}), style({'height': '111px'}),
animate('999ms', style({'width': '200px', 'background': 'blue'})), animate('999ms', style({'width': '200px', 'background': 'blue'})),
style({'opacity': '1'}), style({'opacity': '1'}),
style({'border-width': '100px'}), style({'borderWidth': '100px'}),
animate('999ms', style({'opacity': '0', 'height': '200px', 'border-width': '10px'})) animate('999ms', style({'opacity': '0', 'height': '200px', 'borderWidth': '10px'}))
]) ])
]) ])
] ]
@ -303,7 +438,7 @@ function declareTests({useJit}: {useJit: boolean}) {
expect(animation1['delay']).toEqual(0); expect(animation1['delay']).toEqual(0);
expect(animation1['easing']).toEqual(null); expect(animation1['easing']).toEqual(null);
expect(animation1['startingStyles']) expect(animation1['startingStyles'])
.toEqual({'background': 'gold', 'width': '100px', 'height': 111}); .toEqual({'background': 'gold', 'width': '100px', 'height': '111px'});
var keyframes1 = animation1['keyframeLookup']; var keyframes1 = animation1['keyframeLookup'];
expect(keyframes1[0]).toEqual([0, {'background': 'gold', 'width': '100px'}]); expect(keyframes1[0]).toEqual([0, {'background': 'gold', 'width': '100px'}]);
@ -313,14 +448,14 @@ function declareTests({useJit}: {useJit: boolean}) {
expect(animation2['duration']).toEqual(999); expect(animation2['duration']).toEqual(999);
expect(animation2['delay']).toEqual(0); expect(animation2['delay']).toEqual(0);
expect(animation2['easing']).toEqual(null); expect(animation2['easing']).toEqual(null);
expect(animation2['startingStyles']).toEqual({'opacity': '1', 'border-width': '100px'}); expect(animation2['startingStyles']).toEqual({'opacity': '1', 'borderWidth': '100px'});
var keyframes2 = animation2['keyframeLookup']; var keyframes2 = animation2['keyframeLookup'];
expect(keyframes2[0]).toEqual([ expect(keyframes2[0]).toEqual([
0, {'opacity': '1', 'height': 111, 'border-width': '100px'} 0, {'opacity': '1', 'height': '111px', 'borderWidth': '100px'}
]); ]);
expect(keyframes2[1]).toEqual([ expect(keyframes2[1]).toEqual([
1, {'opacity': '0', 'height': '200px', 'border-width': '10px'} 1, {'opacity': '0', 'height': '200px', 'borderWidth': '10px'}
]); ]);
})); }));
@ -515,10 +650,10 @@ function declareTests({useJit}: {useJit: boolean}) {
var kf = driver.log[0]['keyframeLookup']; var kf = driver.log[0]['keyframeLookup'];
expect(kf.length).toEqual(4); expect(kf.length).toEqual(4);
expect(kf[0]).toEqual([0, {'width': 0}]); expect(kf[0]).toEqual([0, {'width': '0'}]);
expect(kf[1]).toEqual([0.25, {'width': 100}]); expect(kf[1]).toEqual([0.25, {'width': '100px'}]);
expect(kf[2]).toEqual([0.75, {'width': 200}]); expect(kf[2]).toEqual([0.75, {'width': '200px'}]);
expect(kf[3]).toEqual([1, {'width': 300}]); expect(kf[3]).toEqual([1, {'width': '300px'}]);
})); }));
it('should fetch any keyframe styles that are not defined in the first keyframe from the previous entries or getCompuedStyle', it('should fetch any keyframe styles that are not defined in the first keyframe from the previous entries or getCompuedStyle',
@ -535,7 +670,7 @@ function declareTests({useJit}: {useJit: boolean}) {
animate(1000, style({'color': 'silver'})), animate(1000, style({'color': 'silver'})),
animate(1000, keyframes([ animate(1000, keyframes([
style([{'color': 'gold', offset: 0.25}]), style([{'color': 'gold', offset: 0.25}]),
style([{'color': 'bronze', 'background-color': 'teal', offset: 0.50}]), style([{'color': 'bronze', 'backgroundColor': 'teal', offset: 0.50}]),
style([{'color': 'platinum', offset: 0.75}]), style([{'color': 'platinum', offset: 0.75}]),
style([{'color': 'diamond', offset: 1}]) style([{'color': 'diamond', offset: 1}])
])) ]))
@ -554,11 +689,11 @@ function declareTests({useJit}: {useJit: boolean}) {
var kf = driver.log[1]['keyframeLookup']; var kf = driver.log[1]['keyframeLookup'];
expect(kf.length).toEqual(5); expect(kf.length).toEqual(5);
expect(kf[0]).toEqual([0, {'color': 'silver', 'background-color': AUTO_STYLE}]); expect(kf[0]).toEqual([0, {'color': 'silver', 'backgroundColor': AUTO_STYLE}]);
expect(kf[1]).toEqual([0.25, {'color': 'gold'}]); expect(kf[1]).toEqual([0.25, {'color': 'gold'}]);
expect(kf[2]).toEqual([0.50, {'color': 'bronze', 'background-color': 'teal'}]); expect(kf[2]).toEqual([0.50, {'color': 'bronze', 'backgroundColor': 'teal'}]);
expect(kf[3]).toEqual([0.75, {'color': 'platinum'}]); expect(kf[3]).toEqual([0.75, {'color': 'platinum'}]);
expect(kf[4]).toEqual([1, {'color': 'diamond', 'background-color': 'teal'}]); expect(kf[4]).toEqual([1, {'color': 'diamond', 'backgroundColor': 'teal'}]);
})); }));
}); });
@ -573,7 +708,7 @@ function declareTests({useJit}: {useJit: boolean}) {
'myAnimation', 'myAnimation',
[transition( [transition(
'* => *', '* => *',
[style({'opacity': 0}), animate(500, style({'opacity': 1}))])])] [style({'opacity': '0'}), animate(500, style({'opacity': '1'}))])])]
} }
}); });
@ -607,7 +742,7 @@ function declareTests({useJit}: {useJit: boolean}) {
animations: [ animations: [
trigger('myAnimation', [ trigger('myAnimation', [
transition('void => *', [ transition('void => *', [
style({'background': 'red', 'opacity': 0.5}), style({'background': 'red', 'opacity': '0.5'}),
animate(500, style({'background': 'black'})), animate(500, style({'background': 'black'})),
group([ group([
animate(500, style({'background': 'black'})), animate(500, style({'background': 'black'})),
@ -714,7 +849,7 @@ function declareTests({useJit}: {useJit: boolean}) {
`, `,
animations: [trigger( animations: [trigger(
'myAnimation', 'myAnimation',
[transition('* => void', [animate(1000, style({'opacity': 0}))])])] [transition('* => void', [animate(1000, style({'opacity': '0'}))])])]
} }
}); });
@ -750,8 +885,8 @@ function declareTests({useJit}: {useJit: boolean}) {
[trigger('myAnimation', [transition( [trigger('myAnimation', [transition(
'* => *', '* => *',
[ [
animate(1000, style({'opacity': 0})), animate(1000, style({'opacity': '0'})),
animate(1000, style({'opacity': 1})) animate(1000, style({'opacity': '1'}))
])])] ])])]
} }
}); });
@ -766,12 +901,12 @@ function declareTests({useJit}: {useJit: boolean}) {
var animation1 = driver.log[0]; var animation1 = driver.log[0];
var keyframes1 = animation1['keyframeLookup']; var keyframes1 = animation1['keyframeLookup'];
expect(keyframes1[0]).toEqual([0, {'opacity': AUTO_STYLE}]); expect(keyframes1[0]).toEqual([0, {'opacity': AUTO_STYLE}]);
expect(keyframes1[1]).toEqual([1, {'opacity': 0}]); expect(keyframes1[1]).toEqual([1, {'opacity': '0'}]);
var animation2 = driver.log[1]; var animation2 = driver.log[1];
var keyframes2 = animation2['keyframeLookup']; var keyframes2 = animation2['keyframeLookup'];
expect(keyframes2[0]).toEqual([0, {'opacity': 0}]); expect(keyframes2[0]).toEqual([0, {'opacity': '0'}]);
expect(keyframes2[1]).toEqual([1, {'opacity': 1}]); expect(keyframes2[1]).toEqual([1, {'opacity': '1'}]);
})); }));
it('should perform two transitions in parallel if defined in different state triggers', it('should perform two transitions in parallel if defined in different state triggers',
@ -783,9 +918,10 @@ function declareTests({useJit}: {useJit: boolean}) {
`, `,
animations: [ animations: [
trigger( trigger(
'one', [transition( 'one',
[transition(
'state1 => state2', 'state1 => state2',
[style({'opacity': 0}), animate(1000, style({'opacity': 1}))])]), [style({'opacity': '0'}), animate(1000, style({'opacity': '1'}))])]),
trigger( trigger(
'two', 'two',
[transition( [transition(
@ -1661,8 +1797,7 @@ function declareTests({useJit}: {useJit: boolean}) {
animations: [trigger( animations: [trigger(
'status', 'status',
[ [
state('final', style({'top': '100px'})), state('final', style({'top': 100})), transition('* => final', [animate(1000)])
transition('* => final', [animate(1000)])
])] ])]
} }
}); });
@ -1778,8 +1913,8 @@ function declareTests({useJit}: {useJit: boolean}) {
animations: [trigger( animations: [trigger(
'status', 'status',
[ [
state('void', style({'width': '0px'})), state('void', style({'width': 0})),
state('final', style({'width': '100px'})), state('final', style({'width': 100})),
])] ])]
} }
}); });
@ -1909,7 +2044,7 @@ function declareTests({useJit}: {useJit: boolean}) {
animations: [trigger( animations: [trigger(
'status', 'status',
[ [
state('void', style({'height': '100px', 'opacity': 0})), state('void', style({'height': '100px', 'opacity': '0'})),
state('final', style({'height': '333px', 'width': '200px'})), state('final', style({'height': '333px', 'width': '200px'})),
transition('void => final', [animate(1000)]) transition('void => final', [animate(1000)])
])] ])]
@ -1927,7 +2062,7 @@ function declareTests({useJit}: {useJit: boolean}) {
var animation = driver.log.pop(); var animation = driver.log.pop();
var kf = animation['keyframeLookup']; var kf = animation['keyframeLookup'];
expect(kf[0]).toEqual([0, {'height': '100px', 'opacity': 0, 'width': AUTO_STYLE}]); expect(kf[0]).toEqual([0, {'height': '100px', 'opacity': '0', 'width': AUTO_STYLE}]);
expect(kf[1]).toEqual([1, {'height': '333px', 'opacity': AUTO_STYLE, 'width': '200px'}]); expect(kf[1]).toEqual([1, {'height': '333px', 'opacity': AUTO_STYLE, 'width': '200px'}]);
}); });
@ -2006,3 +2141,12 @@ class BrokenDummyLoadingCmp {
exp = false; exp = false;
callback = () => {}; callback = () => {};
} }
class _NaiveElementSchema extends DomElementSchemaRegistry {
normalizeAnimationStyleProperty(propName: string): string { return propName; }
normalizeAnimationStyleValue(camelCaseProp: string, userProvidedProp: string, val: string|number):
{error: string, value: string} {
return {error: null, value: <string>val};
}
}

View File

@ -6,13 +6,10 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {AUTO_STYLE} from '@angular/core';
import {isPresent} from '../facade/lang'; import {isPresent} from '../facade/lang';
import {AnimationKeyframe, AnimationStyles} from '../private_import_core'; import {AnimationKeyframe, AnimationStyles} from '../private_import_core';
import {AnimationDriver} from './animation_driver'; import {AnimationDriver} from './animation_driver';
import {dashCaseToCamelCase} from './util';
import {WebAnimationsPlayer} from './web_animations_player'; import {WebAnimationsPlayer} from './web_animations_player';
export class WebAnimationsDriver implements AnimationDriver { export class WebAnimationsDriver implements AnimationDriver {
@ -63,14 +60,8 @@ function _populateStyles(
element: any, styles: AnimationStyles, element: any, styles: AnimationStyles,
defaultStyles: {[key: string]: string | number}): {[key: string]: string | number} { defaultStyles: {[key: string]: string | number}): {[key: string]: string | number} {
var data: {[key: string]: string | number} = {}; var data: {[key: string]: string | number} = {};
styles.styles.forEach((entry) => { styles.styles.forEach(
Object.keys(entry).forEach(prop => { (entry) => { Object.keys(entry).forEach(prop => { data[prop] = entry[prop]; }); });
const val = entry[prop];
var formattedProp = dashCaseToCamelCase(prop);
data[formattedProp] =
val == AUTO_STYLE ? val : val.toString() + _resolveStyleUnit(val, prop, formattedProp);
});
});
Object.keys(defaultStyles).forEach(prop => { Object.keys(defaultStyles).forEach(prop => {
if (!isPresent(data[prop])) { if (!isPresent(data[prop])) {
data[prop] = defaultStyles[prop]; data[prop] = defaultStyles[prop];
@ -78,66 +69,3 @@ function _populateStyles(
}); });
return data; return data;
} }
function _resolveStyleUnit(
val: string | number, userProvidedProp: string, formattedProp: string): string {
var unit = '';
if (_isPixelDimensionStyle(formattedProp) && val != 0 && val != '0') {
if (typeof val === 'number') {
unit = 'px';
} else if (_findDimensionalSuffix(val.toString()).length == 0) {
throw new Error('Please provide a CSS unit value for ' + userProvidedProp + ':' + val);
}
}
return unit;
}
const _$0 = 48;
const _$9 = 57;
const _$PERIOD = 46;
function _findDimensionalSuffix(value: string): string {
for (var i = 0; i < value.length; i++) {
var c = value.charCodeAt(i);
if ((c >= _$0 && c <= _$9) || c == _$PERIOD) continue;
return value.substring(i, value.length);
}
return '';
}
function _isPixelDimensionStyle(prop: string): boolean {
switch (prop) {
case 'width':
case 'height':
case 'minWidth':
case 'minHeight':
case 'maxWidth':
case 'maxHeight':
case 'left':
case 'top':
case 'bottom':
case 'right':
case 'fontSize':
case 'outlineWidth':
case 'outlineOffset':
case 'paddingTop':
case 'paddingLeft':
case 'paddingBottom':
case 'paddingRight':
case 'marginTop':
case 'marginLeft':
case 'marginBottom':
case 'marginRight':
case 'borderRadius':
case 'borderWidth':
case 'borderTopWidth':
case 'borderLeftWidth':
case 'borderRightWidth':
case 'borderBottomWidth':
case 'textIndent':
return true;
default:
return false;
}
}

View File

@ -45,46 +45,6 @@ export function main() {
elm = el('<div></div>'); elm = el('<div></div>');
}); });
it('should convert all styles to camelcase', () => {
var startingStyles = _makeStyles({'border-top-right': '40px'});
var styles = [
_makeKeyframe(0, {'max-width': '100px', 'height': '200px'}),
_makeKeyframe(1, {'font-size': '555px'})
];
var player = driver.animate(elm, startingStyles, styles, 0, 0, 'linear');
var details = _formatOptions(player);
var startKeyframe = details['keyframes'][0];
var firstKeyframe = details['keyframes'][1];
var lastKeyframe = details['keyframes'][2];
expect(startKeyframe['borderTopRight']).toEqual('40px');
expect(firstKeyframe['maxWidth']).toEqual('100px');
expect(firstKeyframe['max-width']).toBeFalsy();
expect(firstKeyframe['height']).toEqual('200px');
expect(lastKeyframe['fontSize']).toEqual('555px');
expect(lastKeyframe['font-size']).toBeFalsy();
});
it('should auto prefix numeric properties with a `px` value', () => {
var startingStyles = _makeStyles({'borderTopWidth': 40});
var styles = [_makeKeyframe(0, {'font-size': 100}), _makeKeyframe(1, {'height': '555em'})];
var player = driver.animate(elm, startingStyles, styles, 0, 0, 'linear');
var details = _formatOptions(player);
var startKeyframe = details['keyframes'][0];
var firstKeyframe = details['keyframes'][1];
var lastKeyframe = details['keyframes'][2];
expect(startKeyframe['borderTopWidth']).toEqual('40px');
expect(firstKeyframe['fontSize']).toEqual('100px');
expect(lastKeyframe['height']).toEqual('555em');
});
it('should use a fill mode of `both`', () => { it('should use a fill mode of `both`', () => {
var startingStyles = _makeStyles({}); var startingStyles = _makeStyles({});
var styles = [_makeKeyframe(0, {'color': 'green'}), _makeKeyframe(1, {'color': 'red'})]; var styles = [_makeKeyframe(0, {'color': 'green'}), _makeKeyframe(1, {'color': 'red'})];