diff --git a/packages/animations/browser/src/dsl/animation_transition_expr.ts b/packages/animations/browser/src/dsl/animation_transition_expr.ts index 0458e3fc71..23e3cdfc52 100644 --- a/packages/animations/browser/src/dsl/animation_transition_expr.ts +++ b/packages/animations/browser/src/dsl/animation_transition_expr.ts @@ -24,8 +24,14 @@ export function parseTransitionExpr( function parseInnerTransitionStr( eventStr: string, expressions: TransitionMatcherFn[], errors: string[]) { if (eventStr[0] == ':') { - eventStr = parseAnimationAlias(eventStr, errors); + const result = parseAnimationAlias(eventStr, errors); + if (typeof result == 'function') { + expressions.push(result); + return; + } + eventStr = result as string; } + const match = eventStr.match(/^(\*|[-\w]+)\s*()\s*(\*|[-\w]+)$/); if (match == null || match.length < 4) { errors.push(`The provided transition expression "${eventStr}" is not supported`); @@ -43,12 +49,16 @@ function parseInnerTransitionStr( } } -function parseAnimationAlias(alias: string, errors: string[]): string { +function parseAnimationAlias(alias: string, errors: string[]): string|TransitionMatcherFn { switch (alias) { case ':enter': return 'void => *'; case ':leave': return '* => void'; + case ':increment': + return (fromState: any, toState: any): boolean => parseFloat(toState) > parseFloat(fromState); + case ':decrement': + return (fromState: any, toState: any): boolean => parseFloat(toState) < parseFloat(fromState); default: errors.push(`The transition alias value "${alias}" is not supported`); return '* => *'; diff --git a/packages/animations/src/animation_metadata.ts b/packages/animations/src/animation_metadata.ts index 1cf6157eae..cabf6dd429 100755 --- a/packages/animations/src/animation_metadata.ts +++ b/packages/animations/src/animation_metadata.ts @@ -707,7 +707,7 @@ export function keyframes(steps: AnimationStyleMetadata[]): AnimationKeyframesSe * ]) * ``` * - * ### Transition Aliases (`:enter` and `:leave`) + * ### Using :enter and :leave * * Given that enter (insertion) and leave (removal) animations are so common, the `transition` * function accepts both `:enter` and `:leave` values which are aliases for the `void => *` and `* @@ -717,12 +717,88 @@ export function keyframes(steps: AnimationStyleMetadata[]): AnimationKeyframesSe * transition(":enter", [ * style({ opacity: 0 }), * animate(500, style({ opacity: 1 })) - * ]) + * ]), * transition(":leave", [ * animate(500, style({ opacity: 0 })) * ]) * ``` * + * ### Using :increment and :decrement + * In addition to the :enter and :leave transition aliases, the :increment and :decrement aliases + * can be used to kick off a transition when a numeric value has increased or decreased in value. + * + * ``` + * import {group, animate, query, transition, style, trigger} from '@angular/animations'; + * import {Component} from '@angular/core'; + * + * @Component({ + * selector: 'banner-carousel-component', + * styles: [` + * .banner-container { + * position:relative; + * height:500px; + * overflow:hidden; + * } + * .banner-container > .banner { + * position:absolute; + * left:0; + * top:0; + * font-size:200px; + * line-height:500px; + * font-weight:bold; + * text-align:center; + * width:100%; + * } + * `], + * template: ` + * + * + *
+ * + * ` + * animations: [ + * trigger('bannerAnimation', [ + * transition(":increment", group([ + * query(':enter', [ + * style({ left: '100%' }), + * animate('0.5s ease-out', style('*')) + * ]), + * query(':leave', [ + * animate('0.5s ease-out', style({ left: '-100%' })) + * ]) + * ])), + * transition(":decrement", group([ + * query(':enter', [ + * style({ left: '-100%' }), + * animate('0.5s ease-out', style('*')) + * ]), + * query(':leave', [ + * animate('0.5s ease-out', style({ left: '100%' })) + * ]) + * ])), + * ]) + * ] + * }) + * class BannerCarouselComponent { + * allBanners: string[] = ['1', '2', '3', '4']; + * selectedIndex: number = 0; + * + * get banners() { + * return [this.allBanners[this.selectedIndex]]; + * } + * + * previous() { + * this.selectedIndex = Math.max(this.selectedIndex - 1, 0); + * } + * + * next() { + * this.selectedIndex = Math.min(this.selectedIndex + 1, this.allBanners.length - 1); + * } + * } + * ``` + * * {@example core/animation/ts/dsl/animation_example.ts region='Component'} * * @experimental Animation support is experimental. diff --git a/packages/core/test/animation/animation_integration_spec.ts b/packages/core/test/animation/animation_integration_spec.ts index 775c73c2a4..ee5d8ec6cd 100644 --- a/packages/core/test/animation/animation_integration_spec.ts +++ b/packages/core/test/animation/animation_integration_spec.ts @@ -1541,6 +1541,98 @@ export function main() { const players = getLog(); expect(players.length).toEqual(2); }); + + describe('transition aliases', () => { + describe(':increment', () => { + it('should detect when a value has incremented', () => { + @Component({ + selector: 'if-cmp', + template: ` +
+ `, + animations: [ + trigger( + 'myAnimation', + [ + transition( + ':increment', + [ + animate(1234, style({background: 'red'})), + ]), + ]), + ] + }) + class Cmp { + exp: number = 0; + } + + TestBed.configureTestingModule({declarations: [Cmp]}); + const fixture = TestBed.createComponent(Cmp); + const cmp = fixture.componentInstance; + fixture.detectChanges(); + let players = getLog(); + expect(players.length).toEqual(0); + + cmp.exp++; + fixture.detectChanges(); + players = getLog(); + expect(players.length).toEqual(1); + expect(players[0].duration).toEqual(1234); + resetLog(); + + cmp.exp = 5; + fixture.detectChanges(); + players = getLog(); + expect(players.length).toEqual(1); + expect(players[0].duration).toEqual(1234); + }); + }); + + describe(':decrement', () => { + it('should detect when a value has decremented', () => { + @Component({ + selector: 'if-cmp', + template: ` +
+ `, + animations: [ + trigger( + 'myAnimation', + [ + transition( + ':decrement', + [ + animate(1234, style({background: 'red'})), + ]), + ]), + ] + }) + class Cmp { + exp: number = 5; + } + + TestBed.configureTestingModule({declarations: [Cmp]}); + const fixture = TestBed.createComponent(Cmp); + const cmp = fixture.componentInstance; + fixture.detectChanges(); + let players = getLog(); + expect(players.length).toEqual(0); + + cmp.exp--; + fixture.detectChanges(); + players = getLog(); + expect(players.length).toEqual(1); + expect(players[0].duration).toEqual(1234); + resetLog(); + + cmp.exp = 0; + fixture.detectChanges(); + players = getLog(); + expect(players.length).toEqual(1); + expect(players[0].duration).toEqual(1234); + }); + }); + }); }); describe('animation listeners', () => {