admin管理员组

文章数量:1026989

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:

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 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/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.

Share Improve this question edited Nov 16, 2024 at 17:45 argentum4k asked Nov 16, 2024 at 14:35 argentum4kargentum4k 331 silver badge5 bronze badges
Add a comment  | 

1 Answer 1

Reset to default 1

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>

Full Code:

HTML:

<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>

TS:

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: './accordionponent.html',
  styleUrl: './accordionponent.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);
  }
}

Stackblitz Demo

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:

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 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/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.

Share Improve this question edited Nov 16, 2024 at 17:45 argentum4k asked Nov 16, 2024 at 14:35 argentum4kargentum4k 331 silver badge5 bronze badges
Add a comment  | 

1 Answer 1

Reset to default 1

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>

Full Code:

HTML:

<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>

TS:

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: './accordionponent.html',
  styleUrl: './accordionponent.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);
  }
}

Stackblitz Demo

本文标签: