Skip to content

Commit 05472cb

Browse files
matskovicb
authored andcommitted
fix(animations): support persisting dynamic styles within animation states (#18468)
Closes #18423 Closes #17505
1 parent c0c03dc commit 05472cb

File tree

11 files changed

+408
-46
lines changed

11 files changed

+408
-46
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ export class AnimateAst extends Ast {
8282

8383
export class StyleAst extends Ast {
8484
public isEmptyStep = false;
85+
public containsDynamicStyles = false;
8586

8687
constructor(
8788
public styles: (ɵStyleData|string)[], public easing: string|null,

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

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
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

1010
import {getOrSetAsInMap} from '../render/shared';
11-
import {ENTER_SELECTOR, LEAVE_SELECTOR, NG_ANIMATING_SELECTOR, NG_TRIGGER_SELECTOR, copyObj, normalizeAnimationEntry, resolveTiming, validateStyleParams} from '../util';
11+
import {ENTER_SELECTOR, LEAVE_SELECTOR, NG_ANIMATING_SELECTOR, NG_TRIGGER_SELECTOR, SUBSTITUTION_EXPR_START, copyObj, extractStyleParams, iteratorToArray, normalizeAnimationEntry, resolveTiming, validateStyleParams} from '../util';
1212

1313
import {AnimateAst, AnimateChildAst, AnimateRefAst, Ast, DynamicTimingAst, GroupAst, KeyframesAst, QueryAst, ReferenceAst, SequenceAst, StaggerAst, StateAst, StyleAst, TimingAst, TransitionAst, TriggerAst} from './animation_ast';
1414
import {AnimationDslVisitor, visitAnimationNode} from './animation_dsl_visitor';
@@ -112,7 +112,35 @@ export class AnimationAstBuilderVisitor implements AnimationDslVisitor {
112112
}
113113

114114
visitState(metadata: AnimationStateMetadata, context: AnimationAstBuilderContext): StateAst {
115-
return new StateAst(metadata.name, this.visitStyle(metadata.styles, context));
115+
const styleAst = this.visitStyle(metadata.styles, context);
116+
const astParams = (metadata.options && metadata.options.params) || null;
117+
if (styleAst.containsDynamicStyles) {
118+
const missingSubs = new Set<string>();
119+
const params = astParams || {};
120+
styleAst.styles.forEach(value => {
121+
if (isObject(value)) {
122+
const stylesObj = value as any;
123+
Object.keys(stylesObj).forEach(prop => {
124+
extractStyleParams(stylesObj[prop]).forEach(sub => {
125+
if (!params.hasOwnProperty(sub)) {
126+
missingSubs.add(sub);
127+
}
128+
});
129+
});
130+
}
131+
});
132+
if (missingSubs.size) {
133+
const missingSubsArr = iteratorToArray(missingSubs.values());
134+
context.errors.push(
135+
`state("${metadata.name}", ...) must define default values for all the following style substitutions: ${missingSubsArr.join(', ')}`);
136+
}
137+
}
138+
139+
const stateAst = new StateAst(metadata.name, styleAst);
140+
if (astParams) {
141+
stateAst.options = {params: astParams};
142+
}
143+
return stateAst;
116144
}
117145

118146
visitTransition(metadata: AnimationTransitionMetadata, context: AnimationAstBuilderContext):
@@ -206,6 +234,7 @@ export class AnimationAstBuilderVisitor implements AnimationDslVisitor {
206234
styles.push(metadata.styles);
207235
}
208236

237+
let containsDynamicStyles = false;
209238
let collectedEasing: string|null = null;
210239
styles.forEach(styleData => {
211240
if (isObject(styleData)) {
@@ -215,9 +244,21 @@ export class AnimationAstBuilderVisitor implements AnimationDslVisitor {
215244
collectedEasing = easing as string;
216245
delete styleMap['easing'];
217246
}
247+
if (!containsDynamicStyles) {
248+
for (let prop in styleMap) {
249+
const value = styleMap[prop];
250+
if (value.toString().indexOf(SUBSTITUTION_EXPR_START) >= 0) {
251+
containsDynamicStyles = true;
252+
break;
253+
}
254+
}
255+
}
218256
}
219257
});
220-
return new StyleAst(styles, collectedEasing, metadata.offset);
258+
259+
const ast = new StyleAst(styles, collectedEasing, metadata.offset);
260+
ast.containsDynamicStyles = containsDynamicStyles;
261+
return ast;
221262
}
222263

223264
private _validateStyleAst(ast: StyleAst, context: AnimationAstBuilderContext): void {

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

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,38 +9,51 @@ import {AnimationOptions, ɵStyleData} from '@angular/animations';
99

1010
import {AnimationDriver} from '../render/animation_driver';
1111
import {getOrSetAsInMap} from '../render/shared';
12-
import {iteratorToArray, mergeAnimationOptions} from '../util';
12+
import {copyObj, interpolateParams, iteratorToArray, mergeAnimationOptions} from '../util';
1313

14-
import {TransitionAst} from './animation_ast';
14+
import {StyleAst, TransitionAst} from './animation_ast';
1515
import {buildAnimationTimelines} from './animation_timeline_builder';
1616
import {TransitionMatcherFn} from './animation_transition_expr';
1717
import {AnimationTransitionInstruction, createTransitionInstruction} from './animation_transition_instruction';
1818
import {ElementInstructionMap} from './element_instruction_map';
1919

20+
const EMPTY_OBJECT = {};
21+
2022
export class AnimationTransitionFactory {
2123
constructor(
2224
private _triggerName: string, public ast: TransitionAst,
23-
private _stateStyles: {[stateName: string]: ɵStyleData}) {}
25+
private _stateStyles: {[stateName: string]: AnimationStateStyles}) {}
2426

2527
match(currentState: any, nextState: any): boolean {
2628
return oneOrMoreTransitionsMatch(this.ast.matchers, currentState, nextState);
2729
}
2830

31+
buildStyles(stateName: string, params: {[key: string]: any}, errors: any[]) {
32+
const backupStateStyler = this._stateStyles['*'];
33+
const stateStyler = this._stateStyles[stateName];
34+
const backupStyles = backupStateStyler ? backupStateStyler.buildStyles(params, errors) : {};
35+
return stateStyler ? stateStyler.buildStyles(params, errors) : backupStyles;
36+
}
37+
2938
build(
3039
driver: AnimationDriver, element: any, currentState: any, nextState: any,
31-
options?: AnimationOptions,
40+
currentOptions?: AnimationOptions, nextOptions?: AnimationOptions,
3241
subInstructions?: ElementInstructionMap): AnimationTransitionInstruction {
33-
const animationOptions = mergeAnimationOptions(this.ast.options || {}, options || {});
42+
const errors: any[] = [];
43+
44+
const transitionAnimationParams = this.ast.options && this.ast.options.params || EMPTY_OBJECT;
45+
const currentAnimationParams = currentOptions && currentOptions.params || EMPTY_OBJECT;
46+
const currentStateStyles = this.buildStyles(currentState, currentAnimationParams, errors);
47+
const nextAnimationParams = nextOptions && nextOptions.params || EMPTY_OBJECT;
48+
const nextStateStyles = this.buildStyles(nextState, nextAnimationParams, errors);
3449

35-
const backupStateStyles = this._stateStyles['*'] || {};
36-
const currentStateStyles = this._stateStyles[currentState] || backupStateStyles;
37-
const nextStateStyles = this._stateStyles[nextState] || backupStateStyles;
3850
const queriedElements = new Set<any>();
3951
const preStyleMap = new Map<any, {[prop: string]: boolean}>();
4052
const postStyleMap = new Map<any, {[prop: string]: boolean}>();
4153
const isRemoval = nextState === 'void';
4254

43-
const errors: any[] = [];
55+
const animationOptions = {params: {...transitionAnimationParams, ...nextAnimationParams}};
56+
4457
const timelines = buildAnimationTimelines(
4558
driver, element, this.ast.animation, currentStateStyles, nextStateStyles, animationOptions,
4659
subInstructions, errors);
@@ -75,3 +88,31 @@ function oneOrMoreTransitionsMatch(
7588
matchFns: TransitionMatcherFn[], currentState: any, nextState: any): boolean {
7689
return matchFns.some(fn => fn(currentState, nextState));
7790
}
91+
92+
export class AnimationStateStyles {
93+
constructor(private styles: StyleAst, private defaultParams: {[key: string]: any}) {}
94+
95+
buildStyles(params: {[key: string]: any}, errors: string[]): ɵStyleData {
96+
const finalStyles: ɵStyleData = {};
97+
const combinedParams = copyObj(this.defaultParams);
98+
Object.keys(params).forEach(key => {
99+
const value = params[key];
100+
if (value != null) {
101+
combinedParams[key] = value;
102+
}
103+
});
104+
this.styles.styles.forEach(value => {
105+
if (typeof value !== 'string') {
106+
const styleObj = value as any;
107+
Object.keys(styleObj).forEach(prop => {
108+
let val = styleObj[prop];
109+
if (val.length > 1) {
110+
val = interpolateParams(val, combinedParams, errors);
111+
}
112+
finalStyles[prop] = val;
113+
});
114+
}
115+
});
116+
return finalStyles;
117+
}
118+
}

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

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@
77
*/
88
import {ɵStyleData} from '@angular/animations';
99

10-
import {copyStyles} from '../util';
10+
import {copyStyles, interpolateParams} from '../util';
11+
12+
import {SequenceAst, StyleAst, TransitionAst, TriggerAst} from './animation_ast';
13+
import {AnimationStateStyles, AnimationTransitionFactory} from './animation_transition_factory';
1114

12-
import {SequenceAst, TransitionAst, TriggerAst} from './animation_ast';
13-
import {AnimationTransitionFactory} from './animation_transition_factory';
1415

1516
/**
1617
* @experimental Animation support is experimental.
@@ -25,16 +26,12 @@ export function buildTrigger(name: string, ast: TriggerAst): AnimationTrigger {
2526
export class AnimationTrigger {
2627
public transitionFactories: AnimationTransitionFactory[] = [];
2728
public fallbackTransition: AnimationTransitionFactory;
28-
public states: {[stateName: string]: ɵStyleData} = {};
29+
public states: {[stateName: string]: AnimationStateStyles} = {};
2930

3031
constructor(public name: string, public ast: TriggerAst) {
3132
ast.states.forEach(ast => {
32-
const obj = this.states[ast.name] = {};
33-
ast.style.styles.forEach(styleTuple => {
34-
if (typeof styleTuple == 'object') {
35-
copyStyles(styleTuple as ɵStyleData, false, obj);
36-
}
37-
});
33+
const defaultParams = (ast.options && ast.options.params) || {};
34+
this.states[ast.name] = new AnimationStateStyles(ast.style, defaultParams);
3835
});
3936

4037
balanceProperties(this.states, 'true', '1');
@@ -53,10 +50,15 @@ export class AnimationTrigger {
5350
const entry = this.transitionFactories.find(f => f.match(currentState, nextState));
5451
return entry || null;
5552
}
53+
54+
matchStyles(currentState: any, params: {[key: string]: any}, errors: any[]): ɵStyleData {
55+
return this.fallbackTransition.buildStyles(currentState, params, errors);
56+
}
5657
}
5758

5859
function createFallbackTransition(
59-
triggerName: string, states: {[stateName: string]: ɵStyleData}): AnimationTransitionFactory {
60+
triggerName: string,
61+
states: {[stateName: string]: AnimationStateStyles}): AnimationTransitionFactory {
6062
const matchers = [(fromState: any, toState: any) => true];
6163
const animation = new SequenceAst([]);
6264
const transition = new TransitionAst(matchers, animation);

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

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ export class StateValue {
6666
public value: string;
6767
public options: AnimationOptions;
6868

69+
get params(): {[key: string]: any} { return this.options.params as{[key: string]: any}; }
70+
6971
constructor(input: any) {
7072
const isObj = input && input.hasOwnProperty('value');
7173
const value = isObj ? input['value'] : input;
@@ -213,7 +215,24 @@ export class AnimationTransitionNamespace {
213215
// The removal arc here is special cased because the same element is triggered
214216
// twice in the event that it contains animations on the outer/inner portions
215217
// of the host container
216-
if (!isRemoval && fromState.value === toState.value) return;
218+
if (!isRemoval && fromState.value === toState.value) {
219+
// this means that despite the value not changing, some inner params
220+
// have changed which means that the animation final styles need to be applied
221+
if (!objEquals(fromState.params, toState.params)) {
222+
const errors: any[] = [];
223+
const fromStyles = trigger.matchStyles(fromState.value, fromState.params, errors);
224+
const toStyles = trigger.matchStyles(toState.value, toState.params, errors);
225+
if (errors.length) {
226+
this._engine.reportError(errors);
227+
} else {
228+
this._engine.afterFlush(() => {
229+
eraseStyles(element, fromStyles);
230+
setStyles(element, toStyles);
231+
});
232+
}
233+
}
234+
return;
235+
}
217236

218237
const playersOnElement: TransitionAnimationPlayer[] =
219238
getOrSetAsInMap(this._engine.playersByElement, element, []);
@@ -664,7 +683,7 @@ export class TransitionAnimationEngine {
664683
private _buildInstruction(entry: QueueInstruction, subTimelines: ElementInstructionMap) {
665684
return entry.transition.build(
666685
this.driver, entry.element, entry.fromState.value, entry.toState.value,
667-
entry.toState.options, subTimelines);
686+
entry.fromState.options, entry.toState.options, subTimelines);
668687
}
669688

670689
destroyInnerAnimations(containerElement: any) {
@@ -781,6 +800,11 @@ export class TransitionAnimationEngine {
781800
}
782801
}
783802

803+
reportError(errors: string[]) {
804+
throw new Error(
805+
`Unable to process animations due to the following failed trigger transitions\n ${errors.join("\n")}`);
806+
}
807+
784808
private _flushAnimations(cleanupFns: Function[], microtaskId: number):
785809
TransitionAnimationPlayer[] {
786810
const subTimelines = new ElementInstructionMap();
@@ -901,14 +925,14 @@ export class TransitionAnimationEngine {
901925
}
902926

903927
if (erroneousTransitions.length) {
904-
let msg = `Unable to process animations due to the following failed trigger transitions\n`;
928+
const errors: string[] = [];
905929
erroneousTransitions.forEach(instruction => {
906-
msg += `@${instruction.triggerName} has failed due to:\n`;
907-
instruction.errors !.forEach(error => { msg += `- ${error}\n`; });
930+
errors.push(`@${instruction.triggerName} has failed due to:\n`);
931+
instruction.errors !.forEach(error => errors.push(`- ${error}\n`));
908932
});
909933

910934
allPlayers.forEach(player => player.destroy());
911-
throw new Error(msg);
935+
this.reportError(errors);
912936
}
913937

914938
// these can only be detected here since we have a map of all the elements
@@ -1491,3 +1515,14 @@ function _flattenGroupPlayersRecur(players: AnimationPlayer[], finalPlayers: Ani
14911515
}
14921516
}
14931517
}
1518+
1519+
function objEquals(a: {[key: string]: any}, b: {[key: string]: any}): boolean {
1520+
const k1 = Object.keys(a);
1521+
const k2 = Object.keys(b);
1522+
if (k1.length != k2.length) return false;
1523+
for (let i = 0; i < k1.length; i++) {
1524+
const prop = k1[i];
1525+
if (!b.hasOwnProperty(prop) || a[prop] !== b[prop]) return false;
1526+
}
1527+
return true;
1528+
}

packages/animations/browser/src/util.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import {AnimateTimings, AnimationMetadata, AnimationOptions, sequence, ɵStyleDa
99

1010
export const ONE_SECOND = 1000;
1111

12+
export const SUBSTITUTION_EXPR_START = '{{';
13+
export const SUBSTITUTION_EXPR_END = '}}';
1214
export const ENTER_CLASSNAME = 'ng-enter';
1315
export const LEAVE_CLASSNAME = 'ng-leave';
1416
export const ENTER_SELECTOR = '.ng-enter';
@@ -151,10 +153,8 @@ export function normalizeAnimationEntry(steps: AnimationMetadata | AnimationMeta
151153
export function validateStyleParams(
152154
value: string | number, options: AnimationOptions, errors: any[]) {
153155
const params = options.params || {};
154-
if (typeof value !== 'string') return;
155-
156-
const matches = value.toString().match(PARAM_REGEX);
157-
if (matches) {
156+
const matches = extractStyleParams(value);
157+
if (matches.length) {
158158
matches.forEach(varName => {
159159
if (!params.hasOwnProperty(varName)) {
160160
errors.push(
@@ -164,7 +164,22 @@ export function validateStyleParams(
164164
}
165165
}
166166

167-
const PARAM_REGEX = /\{\{\s*(.+?)\s*\}\}/g;
167+
const PARAM_REGEX =
168+
new RegExp(`${SUBSTITUTION_EXPR_START}\\s*(.+?)\\s*${SUBSTITUTION_EXPR_END}`, 'g');
169+
export function extractStyleParams(value: string | number): string[] {
170+
let params: string[] = [];
171+
if (typeof value === 'string') {
172+
const val = value.toString();
173+
174+
let match: any;
175+
while (match = PARAM_REGEX.exec(val)) {
176+
params.push(match[1] as string);
177+
}
178+
PARAM_REGEX.lastIndex = 0;
179+
}
180+
return params;
181+
}
182+
168183
export function interpolateParams(
169184
value: string | number, params: {[name: string]: any}, errors: any[]): string|number {
170185
const original = value.toString();

0 commit comments

Comments
 (0)