fix(animations): normalize final styles in buildStyles (#42763)

the final styles created in buildStyles lack normalization, meaning that pixel values remain as numbers (without "px") and so such properties fail to be correctly set/applied

Example: "width: 300" is applies as "width": "300" (and thus ignored) instead of the correct "width": "300px"

PR Close #42763
This commit is contained in:
dario-piotrowicz 2021-07-04 09:57:55 +01:00 committed by Alex Rickabaugh
parent 5d7359e3f0
commit f12c53342c
7 changed files with 66 additions and 16 deletions

View File

@ -16,6 +16,7 @@ import {buildAnimationTimelines} from './animation_timeline_builder';
import {TransitionMatcherFn} from './animation_transition_expr'; import {TransitionMatcherFn} from './animation_transition_expr';
import {AnimationTransitionInstruction, createTransitionInstruction} from './animation_transition_instruction'; import {AnimationTransitionInstruction, createTransitionInstruction} from './animation_transition_instruction';
import {ElementInstructionMap} from './element_instruction_map'; import {ElementInstructionMap} from './element_instruction_map';
import {AnimationStyleNormalizer} from './style_normalization/animation_style_normalizer';
const EMPTY_OBJECT = {}; const EMPTY_OBJECT = {};
@ -99,7 +100,9 @@ function oneOrMoreTransitionsMatch(
} }
export class AnimationStateStyles { export class AnimationStateStyles {
constructor(private styles: StyleAst, private defaultParams: {[key: string]: any}) {} constructor(
private styles: StyleAst, private defaultParams: {[key: string]: any},
private normalizer: AnimationStyleNormalizer) {}
buildStyles(params: {[key: string]: any}, errors: string[]): ɵStyleData { buildStyles(params: {[key: string]: any}, errors: string[]): ɵStyleData {
const finalStyles: ɵStyleData = {}; const finalStyles: ɵStyleData = {};
@ -118,7 +121,9 @@ export class AnimationStateStyles {
if (val.length > 1) { if (val.length > 1) {
val = interpolateParams(val, combinedParams, errors); val = interpolateParams(val, combinedParams, errors);
} }
finalStyles[prop] = val; const normalizedProp = this.normalizer.normalizePropertyName(prop, errors);
val = this.normalizer.normalizeStyleValue(prop, normalizedProp, val, errors);
finalStyles[normalizedProp] = val;
}); });
} }
}); });

View File

@ -11,14 +11,16 @@ import {copyStyles, interpolateParams} from '../util';
import {SequenceAst, StyleAst, TransitionAst, TriggerAst} from './animation_ast'; import {SequenceAst, StyleAst, TransitionAst, TriggerAst} from './animation_ast';
import {AnimationStateStyles, AnimationTransitionFactory} from './animation_transition_factory'; import {AnimationStateStyles, AnimationTransitionFactory} from './animation_transition_factory';
import {AnimationStyleNormalizer} from './style_normalization/animation_style_normalizer';
/** /**
* @publicApi * @publicApi
*/ */
export function buildTrigger(name: string, ast: TriggerAst): AnimationTrigger { export function buildTrigger(
return new AnimationTrigger(name, ast); name: string, ast: TriggerAst, normalizer: AnimationStyleNormalizer): AnimationTrigger {
return new AnimationTrigger(name, ast, normalizer);
} }
/** /**
@ -29,10 +31,11 @@ export class AnimationTrigger {
public fallbackTransition: AnimationTransitionFactory; public fallbackTransition: AnimationTransitionFactory;
public states: {[stateName: string]: AnimationStateStyles} = {}; public states: {[stateName: string]: AnimationStateStyles} = {};
constructor(public name: string, public ast: TriggerAst) { constructor(
public name: string, public ast: TriggerAst, private _normalizer: AnimationStyleNormalizer) {
ast.states.forEach(ast => { ast.states.forEach(ast => {
const defaultParams = (ast.options && ast.options.params) || {}; const defaultParams = (ast.options && ast.options.params) || {};
this.states[ast.name] = new AnimationStateStyles(ast.style, defaultParams); this.states[ast.name] = new AnimationStateStyles(ast.style, defaultParams, _normalizer);
}); });
balanceProperties(this.states, 'true', '1'); balanceProperties(this.states, 'true', '1');
@ -42,7 +45,7 @@ export class AnimationTrigger {
this.transitionFactories.push(new AnimationTransitionFactory(name, ast, this.states)); this.transitionFactories.push(new AnimationTransitionFactory(name, ast, this.states));
}); });
this.fallbackTransition = createFallbackTransition(name, this.states); this.fallbackTransition = createFallbackTransition(name, this.states, this._normalizer);
} }
get containsQueries() { get containsQueries() {
@ -62,8 +65,8 @@ export class AnimationTrigger {
} }
function createFallbackTransition( function createFallbackTransition(
triggerName: string, triggerName: string, states: {[stateName: string]: AnimationStateStyles},
states: {[stateName: string]: AnimationStateStyles}): AnimationTransitionFactory { normalizer: AnimationStyleNormalizer): AnimationTransitionFactory {
const matchers = [(fromState: any, toState: any) => true]; const matchers = [(fromState: any, toState: any) => true];
const animation: SequenceAst = {type: AnimationMetadataType.Sequence, steps: [], options: null}; const animation: SequenceAst = {type: AnimationMetadataType.Sequence, steps: [], options: null};
const transition: TransitionAst = { const transition: TransitionAst = {

View File

@ -27,9 +27,9 @@ export class AnimationEngine {
constructor( constructor(
private bodyNode: any, private _driver: AnimationDriver, private bodyNode: any, private _driver: AnimationDriver,
normalizer: AnimationStyleNormalizer) { private _normalizer: AnimationStyleNormalizer) {
this._transitionEngine = new TransitionAnimationEngine(bodyNode, _driver, normalizer); this._transitionEngine = new TransitionAnimationEngine(bodyNode, _driver, _normalizer);
this._timelineEngine = new TimelineAnimationEngine(bodyNode, _driver, normalizer); this._timelineEngine = new TimelineAnimationEngine(bodyNode, _driver, _normalizer);
this._transitionEngine.onRemovalComplete = (element: any, context: any) => this._transitionEngine.onRemovalComplete = (element: any, context: any) =>
this.onRemovalComplete(element, context); this.onRemovalComplete(element, context);
@ -48,7 +48,7 @@ export class AnimationEngine {
throw new Error(`The animation trigger "${ throw new Error(`The animation trigger "${
name}" has failed to build due to the following errors:\n - ${errors.join('\n - ')}`); name}" has failed to build due to the following errors:\n - ${errors.join('\n - ')}`);
} }
trigger = buildTrigger(name, ast); trigger = buildTrigger(name, ast, this._normalizer);
this._triggerCache[cacheKey] = trigger; this._triggerCache[cacheKey] = trigger;
} }
this._transitionEngine.registerTrigger(namespaceId, name, trigger); this._transitionEngine.registerTrigger(namespaceId, name, trigger);

View File

@ -72,7 +72,6 @@ import {makeTrigger} from '../shared';
transition('off => on', animate(1000)) transition('off => on', animate(1000))
]); ]);
expect(result.states['on'].buildStyles({}, [])).toEqual({width: 50}); expect(result.states['on'].buildStyles({}, [])).toEqual({width: 50});
expect(result.states['off'].buildStyles({}, [])).toEqual({width: 50}); expect(result.states['off'].buildStyles({}, [])).toEqual({width: 50});
}); });

View File

@ -736,7 +736,7 @@ function registerTrigger(
const ast = buildAnimationAst(driver, metadata as AnimationMetadata, errors) as TriggerAst; const ast = buildAnimationAst(driver, metadata as AnimationMetadata, errors) as TriggerAst;
if (errors.length) { if (errors.length) {
} }
const trigger = buildTrigger(name, ast); const trigger = buildTrigger(name, ast, new NoopAnimationStyleNormalizer());
engine.register(id, element); engine.register(id, element);
engine.registerTrigger(id, name, trigger); engine.registerTrigger(id, name, trigger);
} }

View File

@ -11,6 +11,7 @@ import {trigger} from '@angular/animations';
import {TriggerAst} from '../src/dsl/animation_ast'; import {TriggerAst} from '../src/dsl/animation_ast';
import {buildAnimationAst} from '../src/dsl/animation_ast_builder'; import {buildAnimationAst} from '../src/dsl/animation_ast_builder';
import {AnimationTrigger, buildTrigger} from '../src/dsl/animation_trigger'; import {AnimationTrigger, buildTrigger} from '../src/dsl/animation_trigger';
import {NoopAnimationStyleNormalizer} from '../src/dsl/style_normalization/animation_style_normalizer';
import {MockAnimationDriver} from '../testing/src/mock_animation_driver'; import {MockAnimationDriver} from '../testing/src/mock_animation_driver';
export function makeTrigger( export function makeTrigger(
@ -24,5 +25,5 @@ export function makeTrigger(
throw new Error(`Animation parsing for the ${name} trigger have failed:${LINE_START}${ throw new Error(`Animation parsing for the ${name} trigger have failed:${LINE_START}${
errors.join(LINE_START)}`); errors.join(LINE_START)}`);
} }
return buildTrigger(name, triggerAst); return buildTrigger(name, triggerAst, new NoopAnimationStyleNormalizer());
} }

View File

@ -512,6 +512,48 @@ describe('animation integration tests using web animations', function() {
expect(elm.style.getPropertyValue('display')).toEqual('inline-block'); expect(elm.style.getPropertyValue('display')).toEqual('inline-block');
expect(elm.style.getPropertyValue('position')).toEqual('fixed'); expect(elm.style.getPropertyValue('position')).toEqual('fixed');
}); });
it('should set normalized style property values on animation end', () => {
@Component({
selector: 'ani-cmp',
template: `
<div #elm [@myAnimation]="myAnimationExp" style="width: 100%; font-size: 2rem"></div>
`,
animations: [
trigger(
'myAnimation',
[
state('go', style({width: 300, 'font-size': 14})),
transition('* => go', [animate('1s')])
]),
]
})
class Cmp {
@ViewChild('elm', {static: true}) public element: any;
public myAnimationExp = '';
}
TestBed.configureTestingModule({declarations: [Cmp]});
const engine = TestBed.inject(ɵAnimationEngine);
const fixture = TestBed.createComponent(Cmp);
const cmp = fixture.componentInstance;
const elm = cmp.element.nativeElement;
expect(elm.style.getPropertyValue('width')).toEqual('100%');
expect(elm.style.getPropertyValue('font-size')).toEqual('2rem');
cmp.myAnimationExp = 'go';
fixture.detectChanges();
const player = engine.players.pop()!;
player.finish();
player.destroy();
expect(elm.style.getPropertyValue('width')).toEqual('300px');
expect(elm.style.getPropertyValue('font-size')).toEqual('14px');
});
}); });
})(); })();