fix(animations): support persisting dynamic styles within animation states (#18468)
Closes #18423 Closes #17505
This commit is contained in:

committed by
Victor Berchet

parent
c0c03dc4ba
commit
05472cb21b
@ -82,6 +82,7 @@ export class AnimateAst extends Ast {
|
||||
|
||||
export class StyleAst extends Ast {
|
||||
public isEmptyStep = false;
|
||||
public containsDynamicStyles = false;
|
||||
|
||||
constructor(
|
||||
public styles: (ɵStyleData|string)[], public easing: string|null,
|
||||
|
@ -8,7 +8,7 @@
|
||||
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';
|
||||
|
||||
import {getOrSetAsInMap} from '../render/shared';
|
||||
import {ENTER_SELECTOR, LEAVE_SELECTOR, NG_ANIMATING_SELECTOR, NG_TRIGGER_SELECTOR, copyObj, normalizeAnimationEntry, resolveTiming, validateStyleParams} from '../util';
|
||||
import {ENTER_SELECTOR, LEAVE_SELECTOR, NG_ANIMATING_SELECTOR, NG_TRIGGER_SELECTOR, SUBSTITUTION_EXPR_START, copyObj, extractStyleParams, iteratorToArray, normalizeAnimationEntry, resolveTiming, validateStyleParams} from '../util';
|
||||
|
||||
import {AnimateAst, AnimateChildAst, AnimateRefAst, Ast, DynamicTimingAst, GroupAst, KeyframesAst, QueryAst, ReferenceAst, SequenceAst, StaggerAst, StateAst, StyleAst, TimingAst, TransitionAst, TriggerAst} from './animation_ast';
|
||||
import {AnimationDslVisitor, visitAnimationNode} from './animation_dsl_visitor';
|
||||
@ -112,7 +112,35 @@ export class AnimationAstBuilderVisitor implements AnimationDslVisitor {
|
||||
}
|
||||
|
||||
visitState(metadata: AnimationStateMetadata, context: AnimationAstBuilderContext): StateAst {
|
||||
return new StateAst(metadata.name, this.visitStyle(metadata.styles, context));
|
||||
const styleAst = this.visitStyle(metadata.styles, context);
|
||||
const astParams = (metadata.options && metadata.options.params) || null;
|
||||
if (styleAst.containsDynamicStyles) {
|
||||
const missingSubs = new Set<string>();
|
||||
const params = astParams || {};
|
||||
styleAst.styles.forEach(value => {
|
||||
if (isObject(value)) {
|
||||
const stylesObj = value as any;
|
||||
Object.keys(stylesObj).forEach(prop => {
|
||||
extractStyleParams(stylesObj[prop]).forEach(sub => {
|
||||
if (!params.hasOwnProperty(sub)) {
|
||||
missingSubs.add(sub);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
if (missingSubs.size) {
|
||||
const missingSubsArr = iteratorToArray(missingSubs.values());
|
||||
context.errors.push(
|
||||
`state("${metadata.name}", ...) must define default values for all the following style substitutions: ${missingSubsArr.join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
const stateAst = new StateAst(metadata.name, styleAst);
|
||||
if (astParams) {
|
||||
stateAst.options = {params: astParams};
|
||||
}
|
||||
return stateAst;
|
||||
}
|
||||
|
||||
visitTransition(metadata: AnimationTransitionMetadata, context: AnimationAstBuilderContext):
|
||||
@ -206,6 +234,7 @@ export class AnimationAstBuilderVisitor implements AnimationDslVisitor {
|
||||
styles.push(metadata.styles);
|
||||
}
|
||||
|
||||
let containsDynamicStyles = false;
|
||||
let collectedEasing: string|null = null;
|
||||
styles.forEach(styleData => {
|
||||
if (isObject(styleData)) {
|
||||
@ -215,9 +244,21 @@ export class AnimationAstBuilderVisitor implements AnimationDslVisitor {
|
||||
collectedEasing = easing as string;
|
||||
delete styleMap['easing'];
|
||||
}
|
||||
if (!containsDynamicStyles) {
|
||||
for (let prop in styleMap) {
|
||||
const value = styleMap[prop];
|
||||
if (value.toString().indexOf(SUBSTITUTION_EXPR_START) >= 0) {
|
||||
containsDynamicStyles = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return new StyleAst(styles, collectedEasing, metadata.offset);
|
||||
|
||||
const ast = new StyleAst(styles, collectedEasing, metadata.offset);
|
||||
ast.containsDynamicStyles = containsDynamicStyles;
|
||||
return ast;
|
||||
}
|
||||
|
||||
private _validateStyleAst(ast: StyleAst, context: AnimationAstBuilderContext): void {
|
||||
|
@ -9,38 +9,51 @@ import {AnimationOptions, ɵStyleData} from '@angular/animations';
|
||||
|
||||
import {AnimationDriver} from '../render/animation_driver';
|
||||
import {getOrSetAsInMap} from '../render/shared';
|
||||
import {iteratorToArray, mergeAnimationOptions} from '../util';
|
||||
import {copyObj, interpolateParams, iteratorToArray, mergeAnimationOptions} from '../util';
|
||||
|
||||
import {TransitionAst} from './animation_ast';
|
||||
import {StyleAst, TransitionAst} from './animation_ast';
|
||||
import {buildAnimationTimelines} from './animation_timeline_builder';
|
||||
import {TransitionMatcherFn} from './animation_transition_expr';
|
||||
import {AnimationTransitionInstruction, createTransitionInstruction} from './animation_transition_instruction';
|
||||
import {ElementInstructionMap} from './element_instruction_map';
|
||||
|
||||
const EMPTY_OBJECT = {};
|
||||
|
||||
export class AnimationTransitionFactory {
|
||||
constructor(
|
||||
private _triggerName: string, public ast: TransitionAst,
|
||||
private _stateStyles: {[stateName: string]: ɵStyleData}) {}
|
||||
private _stateStyles: {[stateName: string]: AnimationStateStyles}) {}
|
||||
|
||||
match(currentState: any, nextState: any): boolean {
|
||||
return oneOrMoreTransitionsMatch(this.ast.matchers, currentState, nextState);
|
||||
}
|
||||
|
||||
buildStyles(stateName: string, params: {[key: string]: any}, errors: any[]) {
|
||||
const backupStateStyler = this._stateStyles['*'];
|
||||
const stateStyler = this._stateStyles[stateName];
|
||||
const backupStyles = backupStateStyler ? backupStateStyler.buildStyles(params, errors) : {};
|
||||
return stateStyler ? stateStyler.buildStyles(params, errors) : backupStyles;
|
||||
}
|
||||
|
||||
build(
|
||||
driver: AnimationDriver, element: any, currentState: any, nextState: any,
|
||||
options?: AnimationOptions,
|
||||
currentOptions?: AnimationOptions, nextOptions?: AnimationOptions,
|
||||
subInstructions?: ElementInstructionMap): AnimationTransitionInstruction {
|
||||
const animationOptions = mergeAnimationOptions(this.ast.options || {}, options || {});
|
||||
const errors: any[] = [];
|
||||
|
||||
const transitionAnimationParams = this.ast.options && this.ast.options.params || EMPTY_OBJECT;
|
||||
const currentAnimationParams = currentOptions && currentOptions.params || EMPTY_OBJECT;
|
||||
const currentStateStyles = this.buildStyles(currentState, currentAnimationParams, errors);
|
||||
const nextAnimationParams = nextOptions && nextOptions.params || EMPTY_OBJECT;
|
||||
const nextStateStyles = this.buildStyles(nextState, nextAnimationParams, errors);
|
||||
|
||||
const backupStateStyles = this._stateStyles['*'] || {};
|
||||
const currentStateStyles = this._stateStyles[currentState] || backupStateStyles;
|
||||
const nextStateStyles = this._stateStyles[nextState] || backupStateStyles;
|
||||
const queriedElements = new Set<any>();
|
||||
const preStyleMap = new Map<any, {[prop: string]: boolean}>();
|
||||
const postStyleMap = new Map<any, {[prop: string]: boolean}>();
|
||||
const isRemoval = nextState === 'void';
|
||||
|
||||
const errors: any[] = [];
|
||||
const animationOptions = {params: {...transitionAnimationParams, ...nextAnimationParams}};
|
||||
|
||||
const timelines = buildAnimationTimelines(
|
||||
driver, element, this.ast.animation, currentStateStyles, nextStateStyles, animationOptions,
|
||||
subInstructions, errors);
|
||||
@ -75,3 +88,31 @@ function oneOrMoreTransitionsMatch(
|
||||
matchFns: TransitionMatcherFn[], currentState: any, nextState: any): boolean {
|
||||
return matchFns.some(fn => fn(currentState, nextState));
|
||||
}
|
||||
|
||||
export class AnimationStateStyles {
|
||||
constructor(private styles: StyleAst, private defaultParams: {[key: string]: any}) {}
|
||||
|
||||
buildStyles(params: {[key: string]: any}, errors: string[]): ɵStyleData {
|
||||
const finalStyles: ɵStyleData = {};
|
||||
const combinedParams = copyObj(this.defaultParams);
|
||||
Object.keys(params).forEach(key => {
|
||||
const value = params[key];
|
||||
if (value != null) {
|
||||
combinedParams[key] = value;
|
||||
}
|
||||
});
|
||||
this.styles.styles.forEach(value => {
|
||||
if (typeof value !== 'string') {
|
||||
const styleObj = value as any;
|
||||
Object.keys(styleObj).forEach(prop => {
|
||||
let val = styleObj[prop];
|
||||
if (val.length > 1) {
|
||||
val = interpolateParams(val, combinedParams, errors);
|
||||
}
|
||||
finalStyles[prop] = val;
|
||||
});
|
||||
}
|
||||
});
|
||||
return finalStyles;
|
||||
}
|
||||
}
|
||||
|
@ -7,10 +7,11 @@
|
||||
*/
|
||||
import {ɵStyleData} from '@angular/animations';
|
||||
|
||||
import {copyStyles} from '../util';
|
||||
import {copyStyles, interpolateParams} from '../util';
|
||||
|
||||
import {SequenceAst, StyleAst, TransitionAst, TriggerAst} from './animation_ast';
|
||||
import {AnimationStateStyles, AnimationTransitionFactory} from './animation_transition_factory';
|
||||
|
||||
import {SequenceAst, TransitionAst, TriggerAst} from './animation_ast';
|
||||
import {AnimationTransitionFactory} from './animation_transition_factory';
|
||||
|
||||
/**
|
||||
* @experimental Animation support is experimental.
|
||||
@ -25,16 +26,12 @@ export function buildTrigger(name: string, ast: TriggerAst): AnimationTrigger {
|
||||
export class AnimationTrigger {
|
||||
public transitionFactories: AnimationTransitionFactory[] = [];
|
||||
public fallbackTransition: AnimationTransitionFactory;
|
||||
public states: {[stateName: string]: ɵStyleData} = {};
|
||||
public states: {[stateName: string]: AnimationStateStyles} = {};
|
||||
|
||||
constructor(public name: string, public ast: TriggerAst) {
|
||||
ast.states.forEach(ast => {
|
||||
const obj = this.states[ast.name] = {};
|
||||
ast.style.styles.forEach(styleTuple => {
|
||||
if (typeof styleTuple == 'object') {
|
||||
copyStyles(styleTuple as ɵStyleData, false, obj);
|
||||
}
|
||||
});
|
||||
const defaultParams = (ast.options && ast.options.params) || {};
|
||||
this.states[ast.name] = new AnimationStateStyles(ast.style, defaultParams);
|
||||
});
|
||||
|
||||
balanceProperties(this.states, 'true', '1');
|
||||
@ -53,10 +50,15 @@ export class AnimationTrigger {
|
||||
const entry = this.transitionFactories.find(f => f.match(currentState, nextState));
|
||||
return entry || null;
|
||||
}
|
||||
|
||||
matchStyles(currentState: any, params: {[key: string]: any}, errors: any[]): ɵStyleData {
|
||||
return this.fallbackTransition.buildStyles(currentState, params, errors);
|
||||
}
|
||||
}
|
||||
|
||||
function createFallbackTransition(
|
||||
triggerName: string, states: {[stateName: string]: ɵStyleData}): AnimationTransitionFactory {
|
||||
triggerName: string,
|
||||
states: {[stateName: string]: AnimationStateStyles}): AnimationTransitionFactory {
|
||||
const matchers = [(fromState: any, toState: any) => true];
|
||||
const animation = new SequenceAst([]);
|
||||
const transition = new TransitionAst(matchers, animation);
|
||||
|
Reference in New Issue
Block a user