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 thedismiss
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>