fix(animations): support persisting dynamic styles within animation states (#18468)
Closes #18423 Closes #17505
This commit is contained in:
parent
c0c03dc4ba
commit
05472cb21b
|
@ -82,6 +82,7 @@ export class AnimateAst extends Ast {
|
||||||
|
|
||||||
export class StyleAst extends Ast {
|
export class StyleAst extends Ast {
|
||||||
public isEmptyStep = false;
|
public isEmptyStep = false;
|
||||||
|
public containsDynamicStyles = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public styles: (ɵStyleData|string)[], public easing: string|null,
|
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 {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 {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 {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';
|
import {AnimationDslVisitor, visitAnimationNode} from './animation_dsl_visitor';
|
||||||
|
@ -112,7 +112,35 @@ export class AnimationAstBuilderVisitor implements AnimationDslVisitor {
|
||||||
}
|
}
|
||||||
|
|
||||||
visitState(metadata: AnimationStateMetadata, context: AnimationAstBuilderContext): StateAst {
|
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):
|
visitTransition(metadata: AnimationTransitionMetadata, context: AnimationAstBuilderContext):
|
||||||
|
@ -206,6 +234,7 @@ export class AnimationAstBuilderVisitor implements AnimationDslVisitor {
|
||||||
styles.push(metadata.styles);
|
styles.push(metadata.styles);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let containsDynamicStyles = false;
|
||||||
let collectedEasing: string|null = null;
|
let collectedEasing: string|null = null;
|
||||||
styles.forEach(styleData => {
|
styles.forEach(styleData => {
|
||||||
if (isObject(styleData)) {
|
if (isObject(styleData)) {
|
||||||
|
@ -215,9 +244,21 @@ export class AnimationAstBuilderVisitor implements AnimationDslVisitor {
|
||||||
collectedEasing = easing as string;
|
collectedEasing = easing as string;
|
||||||
delete styleMap['easing'];
|
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 {
|
private _validateStyleAst(ast: StyleAst, context: AnimationAstBuilderContext): void {
|
||||||
|
|
|
@ -9,38 +9,51 @@ import {AnimationOptions, ɵStyleData} from '@angular/animations';
|
||||||
|
|
||||||
import {AnimationDriver} from '../render/animation_driver';
|
import {AnimationDriver} from '../render/animation_driver';
|
||||||
import {getOrSetAsInMap} from '../render/shared';
|
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 {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';
|
||||||
|
|
||||||
|
const EMPTY_OBJECT = {};
|
||||||
|
|
||||||
export class AnimationTransitionFactory {
|
export class AnimationTransitionFactory {
|
||||||
constructor(
|
constructor(
|
||||||
private _triggerName: string, public ast: TransitionAst,
|
private _triggerName: string, public ast: TransitionAst,
|
||||||
private _stateStyles: {[stateName: string]: ɵStyleData}) {}
|
private _stateStyles: {[stateName: string]: AnimationStateStyles}) {}
|
||||||
|
|
||||||
match(currentState: any, nextState: any): boolean {
|
match(currentState: any, nextState: any): boolean {
|
||||||
return oneOrMoreTransitionsMatch(this.ast.matchers, currentState, nextState);
|
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(
|
build(
|
||||||
driver: AnimationDriver, element: any, currentState: any, nextState: any,
|
driver: AnimationDriver, element: any, currentState: any, nextState: any,
|
||||||
options?: AnimationOptions,
|
currentOptions?: AnimationOptions, nextOptions?: AnimationOptions,
|
||||||
subInstructions?: ElementInstructionMap): AnimationTransitionInstruction {
|
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 queriedElements = new Set<any>();
|
||||||
const preStyleMap = new Map<any, {[prop: string]: boolean}>();
|
const preStyleMap = new Map<any, {[prop: string]: boolean}>();
|
||||||
const postStyleMap = new Map<any, {[prop: string]: boolean}>();
|
const postStyleMap = new Map<any, {[prop: string]: boolean}>();
|
||||||
const isRemoval = nextState === 'void';
|
const isRemoval = nextState === 'void';
|
||||||
|
|
||||||
const errors: any[] = [];
|
const animationOptions = {params: {...transitionAnimationParams, ...nextAnimationParams}};
|
||||||
|
|
||||||
const timelines = buildAnimationTimelines(
|
const timelines = buildAnimationTimelines(
|
||||||
driver, element, this.ast.animation, currentStateStyles, nextStateStyles, animationOptions,
|
driver, element, this.ast.animation, currentStateStyles, nextStateStyles, animationOptions,
|
||||||
subInstructions, errors);
|
subInstructions, errors);
|
||||||
|
@ -75,3 +88,31 @@ function oneOrMoreTransitionsMatch(
|
||||||
matchFns: TransitionMatcherFn[], currentState: any, nextState: any): boolean {
|
matchFns: TransitionMatcherFn[], currentState: any, nextState: any): boolean {
|
||||||
return matchFns.some(fn => fn(currentState, nextState));
|
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 {ɵ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.
|
* @experimental Animation support is experimental.
|
||||||
|
@ -25,16 +26,12 @@ export function buildTrigger(name: string, ast: TriggerAst): AnimationTrigger {
|
||||||
export class AnimationTrigger {
|
export class AnimationTrigger {
|
||||||
public transitionFactories: AnimationTransitionFactory[] = [];
|
public transitionFactories: AnimationTransitionFactory[] = [];
|
||||||
public fallbackTransition: AnimationTransitionFactory;
|
public fallbackTransition: AnimationTransitionFactory;
|
||||||
public states: {[stateName: string]: ɵStyleData} = {};
|
public states: {[stateName: string]: AnimationStateStyles} = {};
|
||||||
|
|
||||||
constructor(public name: string, public ast: TriggerAst) {
|
constructor(public name: string, public ast: TriggerAst) {
|
||||||
ast.states.forEach(ast => {
|
ast.states.forEach(ast => {
|
||||||
const obj = this.states[ast.name] = {};
|
const defaultParams = (ast.options && ast.options.params) || {};
|
||||||
ast.style.styles.forEach(styleTuple => {
|
this.states[ast.name] = new AnimationStateStyles(ast.style, defaultParams);
|
||||||
if (typeof styleTuple == 'object') {
|
|
||||||
copyStyles(styleTuple as ɵStyleData, false, obj);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
balanceProperties(this.states, 'true', '1');
|
balanceProperties(this.states, 'true', '1');
|
||||||
|
@ -53,10 +50,15 @@ export class AnimationTrigger {
|
||||||
const entry = this.transitionFactories.find(f => f.match(currentState, nextState));
|
const entry = this.transitionFactories.find(f => f.match(currentState, nextState));
|
||||||
return entry || null;
|
return entry || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
matchStyles(currentState: any, params: {[key: string]: any}, errors: any[]): ɵStyleData {
|
||||||
|
return this.fallbackTransition.buildStyles(currentState, params, errors);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createFallbackTransition(
|
function createFallbackTransition(
|
||||||
triggerName: string, states: {[stateName: string]: ɵStyleData}): AnimationTransitionFactory {
|
triggerName: string,
|
||||||
|
states: {[stateName: string]: AnimationStateStyles}): AnimationTransitionFactory {
|
||||||
const matchers = [(fromState: any, toState: any) => true];
|
const matchers = [(fromState: any, toState: any) => true];
|
||||||
const animation = new SequenceAst([]);
|
const animation = new SequenceAst([]);
|
||||||
const transition = new TransitionAst(matchers, animation);
|
const transition = new TransitionAst(matchers, animation);
|
||||||
|
|
|
@ -66,6 +66,8 @@ export class StateValue {
|
||||||
public value: string;
|
public value: string;
|
||||||
public options: AnimationOptions;
|
public options: AnimationOptions;
|
||||||
|
|
||||||
|
get params(): {[key: string]: any} { return this.options.params as{[key: string]: any}; }
|
||||||
|
|
||||||
constructor(input: any) {
|
constructor(input: any) {
|
||||||
const isObj = input && input.hasOwnProperty('value');
|
const isObj = input && input.hasOwnProperty('value');
|
||||||
const value = isObj ? input['value'] : input;
|
const value = isObj ? input['value'] : input;
|
||||||
|
@ -213,7 +215,24 @@ export class AnimationTransitionNamespace {
|
||||||
// The removal arc here is special cased because the same element is triggered
|
// The removal arc here is special cased because the same element is triggered
|
||||||
// twice in the event that it contains animations on the outer/inner portions
|
// twice in the event that it contains animations on the outer/inner portions
|
||||||
// of the host container
|
// of the host container
|
||||||
if (!isRemoval && fromState.value === toState.value) return;
|
if (!isRemoval && fromState.value === toState.value) {
|
||||||
|
// this means that despite the value not changing, some inner params
|
||||||
|
// have changed which means that the animation final styles need to be applied
|
||||||
|
if (!objEquals(fromState.params, toState.params)) {
|
||||||
|
const errors: any[] = [];
|
||||||
|
const fromStyles = trigger.matchStyles(fromState.value, fromState.params, errors);
|
||||||
|
const toStyles = trigger.matchStyles(toState.value, toState.params, errors);
|
||||||
|
if (errors.length) {
|
||||||
|
this._engine.reportError(errors);
|
||||||
|
} else {
|
||||||
|
this._engine.afterFlush(() => {
|
||||||
|
eraseStyles(element, fromStyles);
|
||||||
|
setStyles(element, toStyles);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const playersOnElement: TransitionAnimationPlayer[] =
|
const playersOnElement: TransitionAnimationPlayer[] =
|
||||||
getOrSetAsInMap(this._engine.playersByElement, element, []);
|
getOrSetAsInMap(this._engine.playersByElement, element, []);
|
||||||
|
@ -664,7 +683,7 @@ export class TransitionAnimationEngine {
|
||||||
private _buildInstruction(entry: QueueInstruction, subTimelines: ElementInstructionMap) {
|
private _buildInstruction(entry: QueueInstruction, subTimelines: ElementInstructionMap) {
|
||||||
return entry.transition.build(
|
return entry.transition.build(
|
||||||
this.driver, entry.element, entry.fromState.value, entry.toState.value,
|
this.driver, entry.element, entry.fromState.value, entry.toState.value,
|
||||||
entry.toState.options, subTimelines);
|
entry.fromState.options, entry.toState.options, subTimelines);
|
||||||
}
|
}
|
||||||
|
|
||||||
destroyInnerAnimations(containerElement: any) {
|
destroyInnerAnimations(containerElement: any) {
|
||||||
|
@ -781,6 +800,11 @@ export class TransitionAnimationEngine {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reportError(errors: string[]) {
|
||||||
|
throw new Error(
|
||||||
|
`Unable to process animations due to the following failed trigger transitions\n ${errors.join("\n")}`);
|
||||||
|
}
|
||||||
|
|
||||||
private _flushAnimations(cleanupFns: Function[], microtaskId: number):
|
private _flushAnimations(cleanupFns: Function[], microtaskId: number):
|
||||||
TransitionAnimationPlayer[] {
|
TransitionAnimationPlayer[] {
|
||||||
const subTimelines = new ElementInstructionMap();
|
const subTimelines = new ElementInstructionMap();
|
||||||
|
@ -901,14 +925,14 @@ export class TransitionAnimationEngine {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (erroneousTransitions.length) {
|
if (erroneousTransitions.length) {
|
||||||
let msg = `Unable to process animations due to the following failed trigger transitions\n`;
|
const errors: string[] = [];
|
||||||
erroneousTransitions.forEach(instruction => {
|
erroneousTransitions.forEach(instruction => {
|
||||||
msg += `@${instruction.triggerName} has failed due to:\n`;
|
errors.push(`@${instruction.triggerName} has failed due to:\n`);
|
||||||
instruction.errors !.forEach(error => { msg += `- ${error}\n`; });
|
instruction.errors !.forEach(error => errors.push(`- ${error}\n`));
|
||||||
});
|
});
|
||||||
|
|
||||||
allPlayers.forEach(player => player.destroy());
|
allPlayers.forEach(player => player.destroy());
|
||||||
throw new Error(msg);
|
this.reportError(errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
// these can only be detected here since we have a map of all the elements
|
// 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function objEquals(a: {[key: string]: any}, b: {[key: string]: any}): boolean {
|
||||||
|
const k1 = Object.keys(a);
|
||||||
|
const k2 = Object.keys(b);
|
||||||
|
if (k1.length != k2.length) return false;
|
||||||
|
for (let i = 0; i < k1.length; i++) {
|
||||||
|
const prop = k1[i];
|
||||||
|
if (!b.hasOwnProperty(prop) || a[prop] !== b[prop]) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
|
@ -9,6 +9,8 @@ import {AnimateTimings, AnimationMetadata, AnimationOptions, sequence, ɵStyleDa
|
||||||
|
|
||||||
export const ONE_SECOND = 1000;
|
export const ONE_SECOND = 1000;
|
||||||
|
|
||||||
|
export const SUBSTITUTION_EXPR_START = '{{';
|
||||||
|
export const SUBSTITUTION_EXPR_END = '}}';
|
||||||
export const ENTER_CLASSNAME = 'ng-enter';
|
export const ENTER_CLASSNAME = 'ng-enter';
|
||||||
export const LEAVE_CLASSNAME = 'ng-leave';
|
export const LEAVE_CLASSNAME = 'ng-leave';
|
||||||
export const ENTER_SELECTOR = '.ng-enter';
|
export const ENTER_SELECTOR = '.ng-enter';
|
||||||
|
@ -151,10 +153,8 @@ export function normalizeAnimationEntry(steps: AnimationMetadata | AnimationMeta
|
||||||
export function validateStyleParams(
|
export function validateStyleParams(
|
||||||
value: string | number, options: AnimationOptions, errors: any[]) {
|
value: string | number, options: AnimationOptions, errors: any[]) {
|
||||||
const params = options.params || {};
|
const params = options.params || {};
|
||||||
if (typeof value !== 'string') return;
|
const matches = extractStyleParams(value);
|
||||||
|
if (matches.length) {
|
||||||
const matches = value.toString().match(PARAM_REGEX);
|
|
||||||
if (matches) {
|
|
||||||
matches.forEach(varName => {
|
matches.forEach(varName => {
|
||||||
if (!params.hasOwnProperty(varName)) {
|
if (!params.hasOwnProperty(varName)) {
|
||||||
errors.push(
|
errors.push(
|
||||||
|
@ -164,7 +164,22 @@ export function validateStyleParams(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const PARAM_REGEX = /\{\{\s*(.+?)\s*\}\}/g;
|
const PARAM_REGEX =
|
||||||
|
new RegExp(`${SUBSTITUTION_EXPR_START}\\s*(.+?)\\s*${SUBSTITUTION_EXPR_END}`, 'g');
|
||||||
|
export function extractStyleParams(value: string | number): string[] {
|
||||||
|
let params: string[] = [];
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const val = value.toString();
|
||||||
|
|
||||||
|
let match: any;
|
||||||
|
while (match = PARAM_REGEX.exec(val)) {
|
||||||
|
params.push(match[1] as string);
|
||||||
|
}
|
||||||
|
PARAM_REGEX.lastIndex = 0;
|
||||||
|
}
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
export function interpolateParams(
|
export function interpolateParams(
|
||||||
value: string | number, params: {[name: string]: any}, errors: any[]): string|number {
|
value: string | number, params: {[name: string]: any}, errors: any[]): string|number {
|
||||||
const original = value.toString();
|
const original = value.toString();
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
* Use of this source code is governed by an MIT-style license that can be
|
* Use of this source code is governed by an MIT-style license that can be
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
import {AUTO_STYLE, AnimationMetadata, AnimationMetadataType, animate, animation, group, keyframes, query, sequence, style, transition, trigger, useAnimation, ɵStyleData} from '@angular/animations';
|
import {AUTO_STYLE, AnimationMetadata, AnimationMetadataType, animate, animation, group, keyframes, query, sequence, state, style, transition, trigger, useAnimation, ɵStyleData} from '@angular/animations';
|
||||||
import {AnimationOptions} from '@angular/core/src/animation/dsl';
|
import {AnimationOptions} from '@angular/core/src/animation/dsl';
|
||||||
|
|
||||||
import {Animation} from '../../src/dsl/animation';
|
import {Animation} from '../../src/dsl/animation';
|
||||||
|
@ -174,6 +174,30 @@ export function main() {
|
||||||
validateAndThrowAnimationSequence(steps2);
|
validateAndThrowAnimationSequence(steps2);
|
||||||
}).toThrowError(/keyframes\(\) must be placed inside of a call to animate\(\)/);
|
}).toThrowError(/keyframes\(\) must be placed inside of a call to animate\(\)/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should throw if dynamic style substitutions are used without defaults within state() definitions',
|
||||||
|
() => {
|
||||||
|
const steps = [state('final', style({
|
||||||
|
'width': '{{ one }}px',
|
||||||
|
'borderRadius': '{{ two }}px {{ three }}px',
|
||||||
|
}))];
|
||||||
|
|
||||||
|
expect(() => { validateAndThrowAnimationSequence(steps); })
|
||||||
|
.toThrowError(
|
||||||
|
/state\("final", ...\) must define default values for all the following style substitutions: one, two, three/);
|
||||||
|
|
||||||
|
const steps2 = [state(
|
||||||
|
'panfinal', style({
|
||||||
|
'color': '{{ greyColor }}',
|
||||||
|
'borderColor': '1px solid {{ greyColor }}',
|
||||||
|
'backgroundColor': '{{ redColor }}',
|
||||||
|
}),
|
||||||
|
{params: {redColor: 'maroon'}})];
|
||||||
|
|
||||||
|
expect(() => { validateAndThrowAnimationSequence(steps2); })
|
||||||
|
.toThrowError(
|
||||||
|
/state\("panfinal", ...\) must define default values for all the following style substitutions: greyColor/);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('keyframe building', () => {
|
describe('keyframe building', () => {
|
||||||
|
|
|
@ -51,12 +51,14 @@ export function main() {
|
||||||
describe('trigger usage', () => {
|
describe('trigger usage', () => {
|
||||||
it('should construct a trigger based on the states and transition data', () => {
|
it('should construct a trigger based on the states and transition data', () => {
|
||||||
const result = makeTrigger('name', [
|
const result = makeTrigger('name', [
|
||||||
state('on', style({width: 0})), state('off', style({width: 100})),
|
state('on', style({width: 0})),
|
||||||
transition('on => off', animate(1000)), transition('off => on', animate(1000))
|
state('off', style({width: 100})),
|
||||||
|
transition('on => off', animate(1000)),
|
||||||
|
transition('off => on', animate(1000)),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(result.states).toEqual({'on': {width: 0}, 'off': {width: 100}});
|
expect(result.states['on'].buildStyles({}, [])).toEqual({width: 0});
|
||||||
|
expect(result.states['off'].buildStyles({}, [])).toEqual({width: 100});
|
||||||
expect(result.transitionFactories.length).toEqual(2);
|
expect(result.transitionFactories.length).toEqual(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -66,7 +68,9 @@ export function main() {
|
||||||
transition('off => on', animate(1000))
|
transition('off => on', animate(1000))
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(result.states).toEqual({'on': {width: 50}, 'off': {width: 50}});
|
|
||||||
|
expect(result.states['on'].buildStyles({}, [])).toEqual({width: 50});
|
||||||
|
expect(result.states['off'].buildStyles({}, [])).toEqual({width: 50});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should find the first transition that matches', () => {
|
it('should find the first transition that matches', () => {
|
||||||
|
@ -145,7 +149,7 @@ export function main() {
|
||||||
'a => b', [style({height: '{{ a }}'}), animate(1000, style({height: '{{ b }}'}))],
|
'a => b', [style({height: '{{ a }}'}), animate(1000, style({height: '{{ b }}'}))],
|
||||||
buildParams({a: '100px', b: '200px'}))]);
|
buildParams({a: '100px', b: '200px'}))]);
|
||||||
|
|
||||||
const trans = buildTransition(result, element, 'a', 'b', buildParams({a: '300px'})) !;
|
const trans = buildTransition(result, element, 'a', 'b', {}, buildParams({a: '300px'})) !;
|
||||||
|
|
||||||
const keyframes = trans.timelines[0].keyframes;
|
const keyframes = trans.timelines[0].keyframes;
|
||||||
expect(keyframes).toEqual([{height: '300px', offset: 0}, {height: '200px', offset: 1}]);
|
expect(keyframes).toEqual([{height: '300px', offset: 0}, {height: '200px', offset: 1}]);
|
||||||
|
@ -219,11 +223,12 @@ export function main() {
|
||||||
|
|
||||||
function buildTransition(
|
function buildTransition(
|
||||||
trigger: AnimationTrigger, element: any, fromState: any, toState: any,
|
trigger: AnimationTrigger, element: any, fromState: any, toState: any,
|
||||||
params?: AnimationOptions): AnimationTransitionInstruction|null {
|
fromOptions?: AnimationOptions, toOptions?: AnimationOptions): AnimationTransitionInstruction|
|
||||||
|
null {
|
||||||
const trans = trigger.matchTransition(fromState, toState) !;
|
const trans = trigger.matchTransition(fromState, toState) !;
|
||||||
if (trans) {
|
if (trans) {
|
||||||
const driver = new MockAnimationDriver();
|
const driver = new MockAnimationDriver();
|
||||||
return trans.build(driver, element, fromState, toState, params) !;
|
return trans.build(driver, element, fromState, toState, fromOptions, toOptions) !;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -105,6 +105,7 @@ export interface AnimationTriggerMetadata extends AnimationMetadata {
|
||||||
export interface AnimationStateMetadata extends AnimationMetadata {
|
export interface AnimationStateMetadata extends AnimationMetadata {
|
||||||
name: string;
|
name: string;
|
||||||
styles: AnimationStyleMetadata;
|
styles: AnimationStyleMetadata;
|
||||||
|
options?: {params: {[name: string]: any}};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -567,8 +568,10 @@ export function style(
|
||||||
*
|
*
|
||||||
* @experimental Animation support is experimental.
|
* @experimental Animation support is experimental.
|
||||||
*/
|
*/
|
||||||
export function state(name: string, styles: AnimationStyleMetadata): AnimationStateMetadata {
|
export function state(
|
||||||
return {type: AnimationMetadataType.State, name, styles};
|
name: string, styles: AnimationStyleMetadata,
|
||||||
|
options?: {params: {[name: string]: any}}): AnimationStateMetadata {
|
||||||
|
return {type: AnimationMetadataType.State, name, styles, options};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1526,6 +1526,60 @@ export function main() {
|
||||||
expect(players.length).toEqual(0);
|
expect(players.length).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should update the final state styles when params update even if the expression hasn\'t changed',
|
||||||
|
fakeAsync(() => {
|
||||||
|
@Component({
|
||||||
|
selector: 'ani-cmp',
|
||||||
|
template: `
|
||||||
|
<div [@myAnimation]="{value:exp,params:{color:color}}"></div>
|
||||||
|
`,
|
||||||
|
animations: [
|
||||||
|
trigger(
|
||||||
|
'myAnimation',
|
||||||
|
[
|
||||||
|
state('*', style({color: '{{ color }}'}), {params: {color: 'black'}}),
|
||||||
|
transition('* => 1', animate(500))
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
class Cmp {
|
||||||
|
public exp: any;
|
||||||
|
public color: string|null;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({declarations: [Cmp]});
|
||||||
|
|
||||||
|
const engine = TestBed.get(ɵAnimationEngine);
|
||||||
|
const fixture = TestBed.createComponent(Cmp);
|
||||||
|
const cmp = fixture.componentInstance;
|
||||||
|
|
||||||
|
cmp.exp = '1';
|
||||||
|
cmp.color = 'red';
|
||||||
|
fixture.detectChanges();
|
||||||
|
const player = getLog()[0] !;
|
||||||
|
const element = player.element;
|
||||||
|
player.finish();
|
||||||
|
|
||||||
|
flushMicrotasks();
|
||||||
|
expect(getDOM().hasStyle(element, 'color', 'red')).toBeTruthy();
|
||||||
|
|
||||||
|
cmp.exp = '1';
|
||||||
|
cmp.color = 'blue';
|
||||||
|
fixture.detectChanges();
|
||||||
|
resetLog();
|
||||||
|
|
||||||
|
flushMicrotasks();
|
||||||
|
expect(getDOM().hasStyle(element, 'color', 'blue')).toBeTruthy();
|
||||||
|
|
||||||
|
cmp.exp = '1';
|
||||||
|
cmp.color = null;
|
||||||
|
fixture.detectChanges();
|
||||||
|
resetLog();
|
||||||
|
|
||||||
|
flushMicrotasks();
|
||||||
|
expect(getDOM().hasStyle(element, 'color', 'black')).toBeTruthy();
|
||||||
|
}));
|
||||||
|
|
||||||
it('should substitute in values if the provided state match is an object with values', () => {
|
it('should substitute in values if the provided state match is an object with values', () => {
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ani-cmp',
|
selector: 'ani-cmp',
|
||||||
|
@ -1563,6 +1617,138 @@ export function main() {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should retain substituted styles on the element once the animation is complete if referenced in the final state',
|
||||||
|
fakeAsync(() => {
|
||||||
|
@Component({
|
||||||
|
selector: 'ani-cmp',
|
||||||
|
template: `
|
||||||
|
<div [@myAnimation]="{value:exp, params: { color: color }}"></div>
|
||||||
|
`,
|
||||||
|
animations: [
|
||||||
|
trigger(
|
||||||
|
'myAnimation',
|
||||||
|
[
|
||||||
|
state(
|
||||||
|
'start', style({
|
||||||
|
color: '{{ color }}',
|
||||||
|
fontSize: '{{ fontSize }}px',
|
||||||
|
width: '{{ width }}'
|
||||||
|
}),
|
||||||
|
{params: {color: 'red', fontSize: '200', width: '10px'}}),
|
||||||
|
|
||||||
|
state(
|
||||||
|
'final',
|
||||||
|
style(
|
||||||
|
{color: '{{ color }}', fontSize: '{{ fontSize }}px', width: '888px'}),
|
||||||
|
{params: {color: 'green', fontSize: '50', width: '100px'}}),
|
||||||
|
|
||||||
|
transition('start => final', animate(500)),
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
class Cmp {
|
||||||
|
public exp: any;
|
||||||
|
public color: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({declarations: [Cmp]});
|
||||||
|
|
||||||
|
const engine = TestBed.get(ɵAnimationEngine);
|
||||||
|
const fixture = TestBed.createComponent(Cmp);
|
||||||
|
const cmp = fixture.componentInstance;
|
||||||
|
|
||||||
|
cmp.exp = 'start';
|
||||||
|
cmp.color = 'red';
|
||||||
|
fixture.detectChanges();
|
||||||
|
resetLog();
|
||||||
|
|
||||||
|
cmp.exp = 'final';
|
||||||
|
cmp.color = 'blue';
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const players = getLog();
|
||||||
|
expect(players.length).toEqual(1);
|
||||||
|
const [p1] = players;
|
||||||
|
|
||||||
|
expect(p1.keyframes).toEqual([
|
||||||
|
{color: 'red', fontSize: '200px', width: '10px', offset: 0},
|
||||||
|
{color: 'blue', fontSize: '50px', width: '888px', offset: 1}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const element = p1.element;
|
||||||
|
p1.finish();
|
||||||
|
flushMicrotasks();
|
||||||
|
|
||||||
|
expect(getDOM().hasStyle(element, 'color', 'blue')).toBeTruthy();
|
||||||
|
expect(getDOM().hasStyle(element, 'fontSize', '50px')).toBeTruthy();
|
||||||
|
expect(getDOM().hasStyle(element, 'width', '888px')).toBeTruthy();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should only evaluate final state param substitutions from the expression and state values and not from the transition options ',
|
||||||
|
fakeAsync(() => {
|
||||||
|
@Component({
|
||||||
|
selector: 'ani-cmp',
|
||||||
|
template: `
|
||||||
|
<div [@myAnimation]="exp"></div>
|
||||||
|
`,
|
||||||
|
animations: [
|
||||||
|
trigger(
|
||||||
|
'myAnimation',
|
||||||
|
[
|
||||||
|
state(
|
||||||
|
'start', style({
|
||||||
|
width: '{{ width }}',
|
||||||
|
height: '{{ height }}',
|
||||||
|
}),
|
||||||
|
{params: {width: '0px', height: '0px'}}),
|
||||||
|
|
||||||
|
state(
|
||||||
|
'final', style({
|
||||||
|
width: '{{ width }}',
|
||||||
|
height: '{{ height }}',
|
||||||
|
}),
|
||||||
|
{params: {width: '100px', height: '100px'}}),
|
||||||
|
|
||||||
|
transition(
|
||||||
|
'start => final', [animate(500)],
|
||||||
|
{params: {width: '333px', height: '666px'}}),
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
class Cmp {
|
||||||
|
public exp: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({declarations: [Cmp]});
|
||||||
|
|
||||||
|
const engine = TestBed.get(ɵAnimationEngine);
|
||||||
|
const fixture = TestBed.createComponent(Cmp);
|
||||||
|
const cmp = fixture.componentInstance;
|
||||||
|
|
||||||
|
cmp.exp = 'start';
|
||||||
|
fixture.detectChanges();
|
||||||
|
resetLog();
|
||||||
|
|
||||||
|
cmp.exp = 'final';
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const players = getLog();
|
||||||
|
expect(players.length).toEqual(1);
|
||||||
|
const [p1] = players;
|
||||||
|
|
||||||
|
expect(p1.keyframes).toEqual([
|
||||||
|
{width: '0px', height: '0px', offset: 0},
|
||||||
|
{width: '100px', height: '100px', offset: 1},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const element = p1.element;
|
||||||
|
p1.finish();
|
||||||
|
flushMicrotasks();
|
||||||
|
|
||||||
|
expect(getDOM().hasStyle(element, 'width', '100px')).toBeTruthy();
|
||||||
|
expect(getDOM().hasStyle(element, 'height', '100px')).toBeTruthy();
|
||||||
|
}));
|
||||||
|
|
||||||
it('should not flush animations twice when an inner component runs change detection', () => {
|
it('should not flush animations twice when an inner component runs change detection', () => {
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'outer-cmp',
|
selector: 'outer-cmp',
|
||||||
|
|
|
@ -151,6 +151,11 @@ export interface AnimationStaggerMetadata extends AnimationMetadata {
|
||||||
/** @experimental */
|
/** @experimental */
|
||||||
export interface AnimationStateMetadata extends AnimationMetadata {
|
export interface AnimationStateMetadata extends AnimationMetadata {
|
||||||
name: string;
|
name: string;
|
||||||
|
options?: {
|
||||||
|
params: {
|
||||||
|
[name: string]: any;
|
||||||
|
};
|
||||||
|
};
|
||||||
styles: AnimationStyleMetadata;
|
styles: AnimationStyleMetadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -221,7 +226,11 @@ export declare function sequence(steps: AnimationMetadata[], options?: Animation
|
||||||
export declare function stagger(timings: string | number, animation: AnimationMetadata | AnimationMetadata[]): AnimationStaggerMetadata;
|
export declare function stagger(timings: string | number, animation: AnimationMetadata | AnimationMetadata[]): AnimationStaggerMetadata;
|
||||||
|
|
||||||
/** @experimental */
|
/** @experimental */
|
||||||
export declare function state(name: string, styles: AnimationStyleMetadata): AnimationStateMetadata;
|
export declare function state(name: string, styles: AnimationStyleMetadata, options?: {
|
||||||
|
params: {
|
||||||
|
[name: string]: any;
|
||||||
|
};
|
||||||
|
}): AnimationStateMetadata;
|
||||||
|
|
||||||
/** @experimental */
|
/** @experimental */
|
||||||
export declare function style(tokens: '*' | {
|
export declare function style(tokens: '*' | {
|
||||||
|
|
Loading…
Reference in New Issue