Skip to content

Commit

Permalink
fix(animations): fix stagger timing not handling params (angular#47208)
Browse files Browse the repository at this point in the history
prior to this change the stagger timing was being built during the
ast building instead of dynamically when visiting the stagger animation,
thus it could not handle params correctly, this change makes it so that
during ast building a timing ast is built instead and that ast is used
dynammically to build animations which can handle params correctly

(this PR reinstates the changes done in angular#47208 which have been reverted
because negative non-parametrized stagger values were not handled
correctly, alongside the original changes the current commit also
handles corretly negative non-parametrized values and improves unit
testing)

resolves angular#19786
  • Loading branch information
dario-piotrowicz committed Aug 23, 2022
1 parent 8340da9 commit 3683177
Show file tree
Hide file tree
Showing 4 changed files with 176 additions and 6 deletions.
7 changes: 4 additions & 3 deletions packages/animations/browser/src/dsl/animation_ast_builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -473,7 +473,7 @@ export class AnimationAstBuilderVisitor implements AnimationDslVisitor {
}
const timings = metadata.timings === 'full' ?
{duration: 0, delay: 0, easing: 'full'} :
resolveTiming(metadata.timings, context.errors, true);
constructTimingAst(metadata.timings, context.errors, true);

return {
type: AnimationMetadataType.Stagger,
Expand Down Expand Up @@ -545,13 +545,14 @@ function consumeOffset(styles: OffsetStyles|Array<OffsetStyles>): number|null {
return offset;
}

function constructTimingAst(value: string|number|AnimateTimings, errors: Error[]) {
function constructTimingAst(
value: string|number|AnimateTimings, errors: Error[], allowNegativeValue = false) {
if (value.hasOwnProperty('duration')) {
return value as AnimateTimings;
}

if (typeof value == 'number') {
const duration = resolveTiming(value, errors).duration;
const duration = resolveTiming(value, errors, allowNegativeValue).duration;
return makeTimingAst(duration, 0, '');
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -278,12 +278,14 @@ export class AnimationTimelineBuilderVisitor implements AstVisitor {
context.previousNode = ast;
}

private _visitTiming(ast: TimingAst, context: AnimationTimelineContext): AnimateTimings {
private _visitTiming(
ast: TimingAst, context: AnimationTimelineContext,
allowNegativeValues = false): AnimateTimings {
if ((ast as DynamicTimingAst).dynamic) {
const strValue = (ast as DynamicTimingAst).strValue;
const timingValue =
context.params ? interpolateParams(strValue, context.params, context.errors) : strValue;
return resolveTiming(timingValue, context.errors);
return resolveTiming(timingValue, context.errors, allowNegativeValues);
} else {
return {duration: ast.duration, delay: ast.delay, easing: ast.easing};
}
Expand Down Expand Up @@ -413,7 +415,8 @@ export class AnimationTimelineBuilderVisitor implements AstVisitor {
visitStagger(ast: StaggerAst, context: AnimationTimelineContext) {
const parentContext = context.parentContext!;
const tl = context.currentTimeline;
const timings = ast.timings;
const timings = this._visitTiming(ast.timings, context, true);

const duration = Math.abs(timings.duration);
const maxTime = duration * (context.currentQueryTotal - 1);
let delay = duration * context.currentQueryIndex;
Expand Down
163 changes: 163 additions & 0 deletions packages/core/test/animation/animation_query_integration_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -742,6 +742,169 @@ describe('animation query tests', function() {
expect(p5.delay).toEqual(6000);
});

it(`should handle params in the stagger's timing argument`, () => {
const animationDuration = 500;
const staggerDelay = 1000;

@Component({
selector: 'ani-cmp',
template: `
<div [@myAnimation]="exp">
<div *ngFor="let item of items" class="item">
{{ item }}
</div>
</div>
`,
animations: [
trigger('myAnimation', [
transition('* => go', [
query('.item', [
stagger('{{staggerDelay}}ms',[
style({ transform: 'scale(0)' }),
animate(animationDuration, style({transform: 'scale(1)'}))
])
])
], {params: { staggerDelay }})
])
]
})
class Cmp {
public exp: any;
public items: any[] = [0, 1, 2, 3, 4];
}

TestBed.configureTestingModule({declarations: [Cmp]});

const engine = TestBed.inject(ɵAnimationEngine);
const fixture = TestBed.createComponent(Cmp);
const cmp = fixture.componentInstance;

cmp.exp = 'go';
fixture.detectChanges();
engine.flush();

const players = getLog();
expect(players.length).toEqual(5);

players.forEach((player, i) => {
expect(player.delay).toEqual(i * staggerDelay);
expect(player.duration).toEqual(animationDuration);
expect(player.keyframes).toEqual([
new Map<string, string|number>([['transform', 'scale(0)'], ['offset', 0]]),
new Map<string, string|number>([['transform', 'scale(1)'], ['offset', 1]])
]);
});
});

it(`should handle params in the stagger's timing argument with a negative value`, () => {
const animationDuration = 700;
const absoluteStaggerDelay = 500;

@Component({
selector: 'ani-cmp',
template: `
<div [@myAnimation]="exp">
<div *ngFor="let item of items" class="item">
{{ item }}
</div>
</div>
`,
animations: [
trigger('myAnimation', [
transition('* => go', [
query('.item', [
stagger('{{staggerDelay}}ms',[
style({opacity: 0}),
animate(animationDuration, style({opacity: 1}))
])
])
], {params: { staggerDelay: -absoluteStaggerDelay }})
])
]
})
class Cmp {
public exp: any;
public items: any[] = [0, 1, 2, 3, 4];
}

TestBed.configureTestingModule({declarations: [Cmp]});

const engine = TestBed.inject(ɵAnimationEngine);
const fixture = TestBed.createComponent(Cmp);
const cmp = fixture.componentInstance;

cmp.exp = 'go';
fixture.detectChanges();
engine.flush();

const players = getLog();
expect(players.length).toEqual(5);

players.reverse().forEach((player, i) => {
expect(player.delay).toEqual(i * absoluteStaggerDelay);
expect(player.duration).toEqual(animationDuration);
expect(player.keyframes).toEqual([
new Map<string, string|number>([['opacity', '0'], ['offset', 0]]),
new Map<string, string|number>([['opacity', '1'], ['offset', 1]])
]);
});
});

it(`should handle a negative non-parametrized value provided for the stagger's timing argument`,
() => {
const animationDuration = 700;
const absoluteStaggerDelay = 500;

@Component({
selector: 'ani-cmp',
template: `
<div [@myAnimation]="exp">
<div *ngFor="let item of items" class="item">
{{ item }}
</div>
</div>
`,
animations: [
trigger('myAnimation', [
transition('* => go', [
query('.item', [
stagger(-absoluteStaggerDelay,[
style({backgroundColor: 'red'}),
animate(animationDuration, style({backgroundColor: 'blue'}))
])
])
])
])
]
})
class Cmp {
public exp: any;
public items: any[] = [0, 1, 2, 3, 4];
}

TestBed.configureTestingModule({declarations: [Cmp]});

const engine = TestBed.inject(ɵAnimationEngine);
const fixture = TestBed.createComponent(Cmp);
const cmp = fixture.componentInstance;

cmp.exp = 'go';
fixture.detectChanges();
engine.flush();

const players = getLog();
expect(players.length).toEqual(5);

players.reverse().forEach((player, i) => {
expect(player.delay).toEqual(i * absoluteStaggerDelay);
expect(player.duration).toEqual(animationDuration);
expect(player.keyframes).toEqual([
new Map<string, string|number>([['backgroundColor', 'red'], ['offset', 0]]),
new Map<string, string|number>([['backgroundColor', 'blue'], ['offset', 1]])
]);
});
});

it('should persist inner sub trigger styles once their animation is complete', () => {
@Component({
selector: 'ani-cmp',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,9 @@
{
"name": "connectableObservableDescriptor"
},
{
"name": "constructTimingAst"
},
{
"name": "containsElement"
},
Expand Down

0 comments on commit 3683177

Please sign in to comment.