Home > Mobile >  flash messages with stimulus.js and animate.css
flash messages with stimulus.js and animate.css

Time:07-29

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 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" >
    <div >
      <div >
        <div >
          <div >
            <!-- Heroicon name: solid/x-circle -->
            <svg  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 >
            <h3 ><%= message %></h3>

          </div>
          <div >
            <div >
              <button type="button" data-action="flash#dismiss" >
                <span >Dismiss</span>
                <!-- Heroicon name: solid/x -->
                <svg  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();
  }
}

CodePudding user response:

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('fadeIn', element).then(() => {
      setTimeout(() => {      
        this.dismiss();
      }, 5000);
    });
  }

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

CodePudding user response:

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"  hidden>
 ...
</div>
  • Related