I made an accordion component (angular18) with lazy loading of it's body content and smooth animation of opening and closing. So it works pretty well when i toggle it from inside (by clicking on header), but when i toggle it from outside - it doesn't open smoothly (but closes smoothly).
Here's the stackblitz: https://stackblitz.com/edit/stackblitz-starters-rjsmkv
What's the point of this behavior? i'm quite new to Angular animations..
tried to replace model with input - same thing. Css animations with maxheight are not good enough because of different content sizes.
upd. added squares to ensure than animations works there - they do.
I think the problem is due to mixing of signal
and observable
updates. Where the signal is updated first followed by the observable being updated later.
The scenario is there is no content inside the accordion and the signal is updated, then the animation has already started. At a later point of time the observable delayedIsOpen
is updated by that time the animation is midway or even over. This causes the jerk.
I have noticed that using both the sources as observables, solves the issue.
So we can create another observable isOpenDelayed = toObservable(this.isOpen);
just to fix the timing issue, then use this to trigger the animation.
<div
class="main-content"
[@content]="
(isOpenDelayed | async)
? { value: 'visible', params: { transitionParams } }
: { value: 'hidden', params: { transitionParams } }
"
>
@if (delayedIsOpen | async) {
<ng-container *ngTemplateOutlet="accordionBodyRef" />
}
</div>
<div
class="test"
[@sqare]="
isOpen()
? { value: 'red', params: { transitionParams } }
: { value: 'blue', params: { transitionParams } }
"
>
isOpen
</div>
<p></p>
<div
class="test"
[@sqare]="
(delayedIsOpen | async)
? { value: 'red', params: { transitionParams } }
: { value: 'blue', params: { transitionParams } }
"
>
delayedIsOpen
</div>
<p></p>
<div
class="accordion"
[ngStyle]="{ '--transiton-time': transitionTime + 'ms' }"
>
<div class="accordion-head">
<div class="top-side top-side_left" (click)="toggle()">
<ng-content select="[topLeft]" />
</div>
</div>
<div
class="main-content"
[@content]="
(isOpenDelayed | async)
? { value: 'visible', params: { transitionParams } }
: { value: 'hidden', params: { transitionParams } }
"
>
@if (delayedIsOpen | async) {
<ng-container *ngTemplateOutlet="accordionBodyRef" />
}
</div>
</div>
import {
animate,
state,
style,
transition,
trigger,
} from '@angular/animations';
import {
ChangeDetectionStrategy,
Component,
input,
model,
ContentChild,
TemplateRef,
} from '@angular/core';
import { toObservable } from '@angular/core/rxjs-interop';
import { delay, of, switchMap } from 'rxjs';
@Component({
selector: 'accordion',
templateUrl: './accordion.component.html',
styleUrl: './accordion.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
animations: [
trigger('content', [
state(
'hidden',
style({
height: '0',
visibility: 'hidden',
})
),
state(
'visible',
style({
height: '*',
visibility: 'visible',
})
),
transition('visible <=> hidden', [animate('{{transitionParams}}')]),
transition('void => *', animate(0)),
]),
trigger('sqare', [
state(
'red',
style({
background: 'red',
})
),
state(
'blue',
style({
background: 'blue',
})
),
transition('red <=> blue', [animate('{{transitionParams}}')]),
transition('void => *', animate(0)),
]),
],
})
export class AccordionComponent {
isOpen = model<boolean>(false);
transitionTime = 500;
transitionParams = `${this.transitionTime}ms linear`;
delayedIsOpen = toObservable(this.isOpen).pipe(
switchMap((open) =>
open ? of(open) : of(open).pipe(delay(this.transitionTime))
)
);
isOpenDelayed = toObservable(this.isOpen);
@ContentChild('accordionBody', { read: TemplateRef })
accordionBodyRef: TemplateRef<unknown> | null = null;
toggle(): void {
this.isOpen.update((value) => !value);
}
}