import { AnimationPlayer, AnimationBuilder, animate, keyframes, style } from '@angular/animations'; import { Directive, ElementRef, HostBinding, Input, OnChanges, OnInit, SimpleChanges, } from '@angular/core'; // Credit to s_bighead on stackoverflow. // https://stackoverflow.com/questions/40142922/ng-bootstrap-collapse-how-to-apply-animations @Directive({ selector: '[ogCollapseAnimated]', }) export class CollapseAnimatedDirective implements OnChanges, OnInit { private static readonly SHOW_STYLE = 'show'; private static readonly COLLAPSING = 'collapsing'; @Input('ogCollapseAnimated') collapsed = true; @Input() skipClosingAnimation = false; @Input() collapseDirection: 'horizontal' | 'vertical' | 'both' = 'vertical'; @Input() duration = '300ms'; @HostBinding('class.collapse') public readonly addCollapseClass = true; private currentEffect: AnimationPlayer; private _closeEffect: AnimationPlayer; private _openEffect: AnimationPlayer; private _closedState = { height: '0', width: '0', }; private _openState = { height: '*', width: '*', }; constructor(private el: ElementRef, private builder: AnimationBuilder) {} ngOnInit(): void { if (!this.collapsed) { this.getClassList().add(CollapseAnimatedDirective.SHOW_STYLE); } switch (this.collapseDirection) { case 'horizontal': this._closedState.height = '*'; break; case 'vertical': this._closedState.width = '*'; break; } } private get openEffect(): AnimationPlayer { if (!this._openEffect) { this._openEffect = this.builder .build( animate( this.duration, keyframes([style(this._closedState), style(this._openState)]), ), ) .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( this.duration, keyframes([style(this._openState), style(this._closedState)]), ), ) .create(this.el.nativeElement); } this._closeEffect.onDone(() => this.effectDone()); return this._closeEffect; } private effectDone(): void { 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(): DOMTokenList { 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): void { if (!this.currentEffect) { this.getClassList().add(CollapseAnimatedDirective.COLLAPSING); this.currentEffect = effect; this.currentEffect.play(); } } }