14

I'm using Collapse: https://ng-bootstrap.github.io/#/components/collapse

However, it does not animate; even not on the demo site. How should I implement this??

Boland
  • 1,531
  • 1
  • 14
  • 42

4 Answers4

16

Here's a good way to do it, I think, and it works both for revealing and collapsing: (though it doesn't really need ng-bootstrap anymore)

template.html:

<button (click)="isCollapsed = !isCollapsed">Toggle</button>
<p [@smoothCollapse]="isCollapsed?'initial':'final'">
    your data here
</p>

.

component.ts:

import { trigger, state, style, animate, transition } from '@angular/animations';

@Component({
  selector: 'app-my-component',
  templateUrl: './template.html',
  styleUrls: ['./style.scss'],
  animations: [
    trigger('smoothCollapse', [
      state('initial', style({
        height:'0',
        overflow:'hidden',
        opacity:'0'
      })),
      state('final', style({
        overflow:'hidden',
        opacity:'1'
      })),
      transition('initial=>final', animate('750ms')),
      transition('final=>initial', animate('750ms'))
    ]),
  ]
})
export class MyComponent ...

Details:

  • initial height:0 allows to start from nothing
  • not specifying a final height let the element stop growing when it's all displayed
  • overflow:hidden everywhere so your element doesn't run on other elements
  • opacity from 0 to 1 makes it nicer (in my opinion)
  • An important thing which took me some time to realize is to NOT put [ngbCollapse]="isCollapsed" in the <p> otherwise it breaks all the animation. And we don't need it since we set the height to 0

Hope it helps, I spent a day on it :P

Loki Faër
  • 169
  • 1
  • 4
  • 1
    It worked but partially. Animation from initial state to final state is visible but from final state to initial state no animation happens. – Arun Kumar Oct 02 '20 at 14:34
8

Because they use "display: none" to hide and "display: block" to show element, you can't apply "transition" CSS property.

So, force display block, manage height & opacity to toggle hide/show :

.collapse, .collapse.in {
  display: block !important;
  transition: all .25s ease-in-out;
}

.collapse {
 opacity: 0;
 height: 0;
}

.collapse.in {
  opacity: 1;
  height: 100%;
}

With basic transition and opacity/height, it seem to be more smooth.

You can make your own animation with keyframe and apply to .collapse.in to get better toggle hide/show experience.

And then, if you use Angular 2 in your project, you can switch to ng2-bootstrap : http://valor-software.com/ng2-bootstrap/

Robin Meillet
  • 293
  • 1
  • 3
  • 9
3

inside your component you can add something like this:

 animations: [
    trigger('expandCollapse', [
                state('open', style({height: '100%', opacity: 1})),
                state('closed', style({height: 0, opacity: 0})),
                transition('* => *', [animate('100ms')])
            ]),
 ]


 <div [ngbCollapse]="isCollapsed" [@expandCollapse]="isCollapsed ? 'closed' : 'open'"> ... </div>

See more details here https://angular.io/guide/animations#animating-a-simple-transition

Omiod
  • 11,285
  • 11
  • 53
  • 59
Rammgarot
  • 1,467
  • 14
  • 9
  • 1
    This sort of works, but it animates my element growing in size only once. After that, the unfolding happens instantly and only the opacity effect seems to work. Any clue why? – DaVince Oct 26 '18 at 08:30
  • try changing transition('* => *', [animate('100ms')]) to transition('* <=> *', [animate('100ms')]) – Loveen Dyall Jan 16 '19 at 23:23
  • 1
    I killed about 4 hours to make simple dropdown animation, and this is closest I could get. thanks a lot. – Alexander Kozachenko Feb 20 '19 at 05:41
0

I've created a directive using only bootstrap classes that performs the same original bootstrap effect using only angular-animation, check it out!

Live example

Directive code

import {Directive, ElementRef, HostBinding, Input, OnChanges, OnInit, SimpleChanges} from '@angular/core';
import {animate, AnimationBuilder, AnimationPlayer, keyframes, style} from '@angular/animations';

@Directive({
  selector: '[appCollapseAnimated]'
})
export class CollapseAnimatedDirective implements OnChanges, OnInit {
  private static readonly SHOW_STYLE = 'show';
  private static readonly COLLAPSING = 'collapsing';

  @Input('appCollapseAnimated')
  collapsed = true;

  @Input()
  skipClosingAnimation = false;

  @HostBinding('class.collapse')
  private readonly addCollapseClass = true;

  private currentEffect: AnimationPlayer;
  private _closeEffect: AnimationPlayer;
  private _openEffect: AnimationPlayer;

  constructor(private el: ElementRef,
              private builder: AnimationBuilder) {

  }

  ngOnInit(): void {
    if (!this.collapsed) {
      this.getClassList().add(CollapseAnimatedDirective.SHOW_STYLE);
    }
  }

  private get openEffect(): AnimationPlayer {
    if (!this._openEffect) {
      this._openEffect = this.builder.build(animate('500ms', keyframes([
        style({height: '0'}),
        style({height: '*'}),
      ]))).create(this.el.nativeElement);
    }
    this._openEffect.onDone(() => this.effectDone());
    return this._openEffect;
  }


  private get closeEffect(): AnimationPlayer {
    if (!this._closeEffect) {
      this._closeEffect = this.builder.build(animate('500ms', keyframes([
        style({height: '*'}),
        style({height: '0'}),
      ]))).create(this.el.nativeElement);
    }
    this._closeEffect.onDone(() => this.effectDone());
    return this._closeEffect;
  }

  private effectDone() {
    if (this.collapsed) {
      this.getClassList().remove(CollapseAnimatedDirective.SHOW_STYLE);
    }
    this.getClassList().remove(CollapseAnimatedDirective.COLLAPSING);
    if (this.currentEffect) {
      this.currentEffect.reset();
      this.currentEffect = null;
    }
  }

  ngOnChanges(changes: SimpleChanges): void {
    if (changes.collapsed && !changes.collapsed.firstChange) {
      if (changes.collapsed.previousValue === true && changes.collapsed.currentValue === false) {
        this.startOpening();
      }
      if (changes.collapsed.previousValue === false && changes.collapsed.currentValue === true) {
        this.startClosing();
      }
    }
  }

  private startOpening(): void {
    this.getClassList().add(CollapseAnimatedDirective.SHOW_STYLE);
    const effect = this.openEffect;
    this.playEffect(effect);
  }

  private getClassList() {
    const nativeElement = this.el.nativeElement as HTMLElement;
    return nativeElement.classList;
  }

  private startClosing(): void {
    const effect = this.closeEffect;
    if (this.skipClosingAnimation) {
      this.effectDone();
    } else {
      this.playEffect(effect);
    }
  }

  private playEffect(effect: AnimationPlayer) {
    if (!this.currentEffect) {
      this.getClassList().add(CollapseAnimatedDirective.COLLAPSING);
      this.currentEffect = effect;
      this.currentEffect.play();
    }
  }
}

Use it in your template:

<button class="btn btn-primary" (click)="collapsed = !collapsed">
  Toggle
</button>
<div [appCollapseAnimated]="collapsed" class="beautiful-div border border-secondary mt-5">
  this should collapse with the default bootstrap behaviour
</div>
s_bighead
  • 1,014
  • 13
  • 24