Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular 5 Animation: How to work with :leave transition while the component is removed from DOM

We have been using @ng-bootstrap/ng-bootstrap library in our project for some behaviours/components like Modal. Recently i wanted to eliminate that dependency and implemented the bootstrap modal behaviour with Angular. It was actually pretty easy. Let me shortly tell you how it works:

I have a modal-service, and a modal-component. Service creates the modal component dynamically via ComponentFactoryResolver (details can be seen in this SO post) and add in DOM. By closing the modal, modal just calls the callback function which was defined from service as well, and this just destroys the component, removes from DOM.

So: i have 2 animation states for this modal component, enter and leave. Enter works well. As soon as the component appears in dom, the predefined :enter state is triggered and my animation works. But :leave does not.

This is exactly how closing modal works: Modal is open, you click on the close button or anywhere else on the modal-backdrop. This just calls close function, which is defined as an input, and given from service during the creation.

@Input() closeCallback: Function;

And service just removes the component from DOM.

Since the component is removed as soon as the close button is clicked, the animation does not have the time it needs i think. So :leave does not work.

I thought to put a timeout (delay) on close, and trigger the animation manually, but since i want to use the predefined behaviours :enter and :leave, i could not figure it out how it possible. So how can i make my leaving animation work? (with or without :leave)

Service-Code:

@Injectable()
export class ModalService implements OnDestroy {

  private renderer: Renderer2;
  private componentRef: ComponentRef<ModalComponent>;

  constructor(private rendererFactory: RendererFactory2,
              private componentFactoryResolver: ComponentFactoryResolver,
              private appRef: ApplicationRef,
              private injector: Injector) {
    this.renderer = rendererFactory.createRenderer(null, null);
  }

  ngOnDestroy() {
    this.componentRef.destroy();
  }

  open(content: string, titel: string, primaryButtonLabel: string, secondaryButtonLabel?: string, primaryButtonCallback?: Function, secondaryButtonCallback?: Function) {
    // 1. Create a component reference from the component
    this.componentRef = this.componentFactoryResolver
      .resolveComponentFactory(ModalComponent)
      .create(this.injector);

    this.componentRef.instance.content = content;
    this.componentRef.instance.titel = titel;
    this.componentRef.instance.primaryButtonLabel = primaryButtonLabel;
    this.componentRef.instance.secondaryButtonLabel = secondaryButtonLabel;
    this.componentRef.instance.primaryButtonCallback = primaryButtonCallback;
    this.componentRef.instance.secondaryButtonCallback = secondaryButtonCallback;
    this.componentRef.instance.closeCallback = (() => {
      this.close();
    });

    // 2. Attach component to the appRef so that it's inside the ng component tree
    this.appRef.attachView(this.componentRef.hostView);

    // 3. Get DOM element from component
    const domElem = (this.componentRef.hostView as EmbeddedViewRef<any>)
      .rootNodes[0] as HTMLElement;

    // 4. Append DOM element to the body
    this.renderer.appendChild(document.body, domElem);
    this.renderer.addClass(document.body, 'modal-open');
  }

  close() {
    this.renderer.removeClass(document.body, 'modal-open');
    this.appRef.detachView(this.componentRef.hostView);
    this.componentRef.destroy();
  }
}

Modal-Component.ts:

@Component({
  selector: '[modal]',
  templateUrl: './modal.component.html',
  styleUrls: ['./modal.component.scss'],
  animations: [
    trigger('modalSlideInOut', [
      transition(':enter', [
        style({opacity: 0, transform: 'translateY(-100%)'}),
        animate('0.3s ease-in', style({'opacity': '1', transform: 'translateY(0%)'}))
      ]) ,
      transition(':leave', [
        style({opacity: 1, transform: 'translateY(0%)'}),
        animate('0.3s ease-out', style({'opacity': '0', transform: 'translateY(-100%)'}))
      ])
    ])
  ]
})
export class ModalComponent implements AfterViewInit {

  ....
  @Input() closeCallback: Function;

  constructor() { }

  close() {
    this.closeCallback();
  }
}

Modal-HTML is not very relevant but you can imagine sth like this:

<div [@modalSlideInOut] role="document" class="modal-dialog">
  <div ....
      <button (click)="close()">
        CLOSE
      </button>

       ...
  </div>
</div>
like image 935
akcasoy Avatar asked Jul 30 '18 08:07

akcasoy


2 Answers

I ran into a similar issue today, and the solution I found was to simply bind the animation to Host component itself via:

@HostBinding('@modalSlideInOut')

This way, you don't have to do any trickery at all with animation timings. When you call destroy, Angular knows the component is going away, so it handles it for you, the same way it would as if you were calling ngIf on the component.

like image 63
Cin Avatar answered Oct 22 '22 07:10

Cin


So i have already found a workaround. But i let the question open, if there are better ways of doing this.

From my understanding, :leave animation is a shortcut for (* => void). * is "any state", void is "tag is not visible". So when a component is removed from DOM, it is not visible but the animation does still not work, since the element does not exist anymore (my assumption).

so i have given a ngIf flag for the modal parent element:

<div *ngIf="showModal" [@modalSlideInOut] role="document" class="modal-dialog">

showModal is true per default, since we want that the modal is shown as soon as it is in DOM. The close function first sets the flag auf false, makes the modal unvisible. And after some timeout, calls the callback function, which removes the component from DOM. This is the close function:

  close() {
    this.showModal = false;
    setTimeout(() => {
      this.closeCallback();
    }, 300);
  }

300 is the wait until the component is removed, since my animations need 0.3s.

like image 1
akcasoy Avatar answered Oct 22 '22 06:10

akcasoy