flash messages with stimulus.js and animate.css

1.1k Views Asked by At

I have a Rails 7 web app that is using Tailwind CSS, Stimulus.js, and animate.css. I have flash messages setup in Rails and I'm trying to add a fadeIn and FadeOut animation. The message will appear for 5 seconds and then will disappear, it also has a button where the user can dismiss the message immediately.

The fadeIn is working after I added class="animate__animated animate__fadeIn" however, I don't know how to get the fadeOut to work correctly when the button is pressed or when the message disappears after 5 seconds.

<% flash.each do |message_type, message| %>
  <div data-controller="flash" class="animate__animated animate__fadeIn">
    <div class="mt-5 mb-15 sm:mx-auto sm:w-full sm:max-w-md px-2">
      <div class="rounded-md bg-red-50 p-4">
        <div class="flex">
          <div class="flex-shrink-0">
            <!-- Heroicon name: solid/x-circle -->
            <svg class="h-5 w-5 text-red-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
              <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
            </svg>
          </div>
          <div class="ml-3">
            <h3 class="text-sm font-medium text-red-800"><%= message %></h3>

          </div>
          <div class="ml-auto pl-3">
            <div class="-mx-1.5 -my-1.5">
              <button type="button" data-action="flash#dismiss" class="inline-flex bg-red-50 rounded-md p-1.5 text-red-500 hover:bg-red-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-red-50 focus:ring-red-600">
                <span class="sr-only">Dismiss</span>
                <!-- Heroicon name: solid/x -->
                <svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
                  <path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
                </svg>
              </button>
            </div>
          </div>    
        </div>
      </div>
    </div>
  </div>
<% end %>

flash_controller.js

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {

  connect() {
    setTimeout(() => {      
      this.dismiss();
    }, 5000);
  }

  dismiss() {    
    // document.getElementById("flash").className = document.getElementById("flash").className.replace(/(?:^|\s)animate__fadeIn(?!\S)/g, 'animate__fadeOut'); 

    // const div = this.element.querySelector('animate__fadeIn');
    // div.classList.replace('animate__fadeIn','animate__fadeOut');

    // this.element.classList.remove(animate__fadeIn); 
    // this.element.classList.add(animate__fadeOut);
 

    this.element.remove();
  }
}
2

There are 2 best solutions below

4
LB Ben Johnston On

Animate.css documentation provides a very solid solution to this kind of problem in its javascript section - https://animate.style/#javascript

The approach is to create a reusable function that allows for a callback (in the form of a Promise) to run after an animation has completed.

Below is an example of your code with this function, but all credit codes to the Animate.css documentation for the underlying code.

Example

  • First, update your controller attribute to give control over animation classes to the controller for fade in.
  • Add a hidden attribute (note: this could be a tailwind class also) to hide by default.
  • Caveat is that this element will not be visible until JS runs, this delay may be minimal but it is a change in behaviour.
<div data-controller="flash" hidden>
  • Then update your controller to use the animation util on connect to trigger the fadeIn animation, and fadeOut in the dismiss method.
import { Controller } from "@hotwired/stimulus";

// Note: You may want to move this code to an external util file to use in other files

const animateCSS = (element, animation, prefix = 'animate__') =>
  // We create a Promise and return it
  new Promise((resolve, reject) => {
    const animationName = `${prefix}${animation}`;
    // This line has changed - allowing us to pass in an actual node
    const node = typeof element === 'string' ? document.querySelector(element) : element;

    node.classList.add(`${prefix}animated`, animationName);

    // When the animation ends, we clean the classes and resolve the Promise
    function handleAnimationEnd(event) {
      event.stopPropagation();
      node.classList.remove(`${prefix}animated`, animationName);
      resolve('Animation ended');
    }

    node.addEventListener('animationend', handleAnimationEnd, {once: true});
  });

// the Stimulus controller using the util above

export default class extends Controller {

  connect() {
    const element = this.element;
    element.removeAttribute('hidden');
    animateCSS(element, 'fadeIn').then(() => {
      setTimeout(() => {      
        this.dismiss();
      }, 5000);
    });
  }

  dismiss() {    
    const element = this.element;
    animateCSS(element, 'fadeOut').then(() => element.remove());
  }
}
0
random_user_0891 On

Here's what I ended up with in the Stimulus controller, please let me know if it can be improved.

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  // displays a flash message for a certain period of time
  connect() {
    const element = document.querySelector('.flash-msg');
    element.removeAttribute('hidden');
    element.classList.add('animate__animated', 'animate__fadeIn');
    
    setTimeout(() => {      
      this.dismiss();
    }, 5000);
  }

  // the cancel button was pressed or the timer has run down so the message will be removed
  dismiss() {
    this.element.classList.add('animate__animated', 'animate__fadeOut');    
    
    // wait for the animation fadeOut to end then remove the element
    this.element.addEventListener('animationend', () => {
      this.element.remove();
    });
  }
}

for the html

<div data-controller="flash" class="flash-msg" hidden>
 ...
</div>