How to animate Vuetify <v-badge /> on state change? - css

In my navbar, I have a "Cart" item with a <v-badge /> on it that displays how many items are in the cart. When a user adds or removes to the cart, the number correctly increments and decrements. On state change of that number however, I'd like to be able to "bounce" the badge to provide the user with feedback that the item has been added or removed from the cart. I've been looking at the Vue docs for animations and transitions, I'm just not quite understanding how I'd go about achieving this.
I've attempted wrapping the badge in a <transition /> element and applying some keyframes animations I found on CSS Tricks, however it's still not working.
html:
<v-tabs
class="hidden-sm-and-down"
optional>
<v-tab
v-for="(item, i) in items"
:key="i"
:exact="item.title === 'Home'"
:to="item.to"
:ripple="false"
active-class="text--primary"
class="font-weight-bold nav-link"
min-width="96"
nuxt
text>
<transition
name="ballmove"
enter-active-class="bouncein"
leave-active-class="rollout">
<v-badge
v-if="item.badge && hasCartItems"
color="red"
:content="cartItems"
:value="cartItems"
class="default-badge"
overlap>
{{ item.title }}
</v-badge>
<span v-else>{{ item.title }}</span>
</transition>
</v-tab>
</v-tabs>
scss:
#mixin ballb($yaxis: 0) {
transform: translate3d(0, $yaxis, 0);
}
#keyframes bouncein {
1% { #include ballb(-400px); }
20%, 40%, 60%, 80%, 95%, 99%, 100% { #include ballb() }
30% { #include ballb(-80px); }
50% { #include ballb(-40px); }
70% { #include ballb(-30px); }
90% { #include ballb(-15px); }
97% { #include ballb(-10px); }
}
#keyframes rollout {
0% { transform: translate3d(0, 300px, 0); }
100% { transform: translate3d(1000px, 300px, 0); }
}
#keyframes ballroll {
0% { transform: rotate(0); }
100% { transform: rotate(1000deg); }
}
.rollout {
width: 60px;
height: 60px;
animation: rollout 2s cubic-bezier(0.55, 0.085, 0.68, 0.53) both;
div {
animation: ballroll 2s cubic-bezier(0.55, 0.085, 0.68, 0.53) both;
}
}
.bouncein {
animation: bouncein 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
}
.ballmove-enter {
#include ballb(-400px);
}

So I ended up going more of an old-school way. I took out the <transition /> wrapper around the <v-badge />, then added a watch function as such:
watch: {
cartItems: function(newValue, oldValue) {
const badge = document.querySelector('.v-badge__badge');
if (newValue !== oldValue) {
badge.classList.add('bounce');
this.delay(500).then(() => {
badge.classList.remove('bounce');
});
}
}
},
and my scss looks like:
#mixin ballb($yaxis: 0) {
transform: translate3d(0, $yaxis, 0);
}
#keyframes bouncein {
0%, 50% { #include ballb(-3px); }
25%, 75%, 100% { #include ballb() }
}
.bounce {
animation: bouncein 500ms cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
}
This gives the little "shake" of the badge I was looking for, however if someone has a more native Vue way of handling it, I'd absolutely love to see it.

In Vue 2.6.11, here is how I got a bit of a bounce, with some help from this answer from #Bill Criswell.
From his answer I got that I needed the :key on the badge element to force a re-render. The rest is straight out of Vue.js example here.
<transition name="bounce">
<v-badge
:key="item.comments.length"
v-if="item.comments && item.comments.length > 0"
>
<span slot="badge">
{{ item.comments.length }}
</span>
<v-icon>mdi-comment </v-icon>
</v-badge>
</transition>
And the associated SCSS just copied from the Vue example above:
.bounce-enter-active {
animation: bounce-in 0.5s;
}
.bounce-leave-active {
animation: bounce-in 0.5s reverse;
}
#keyframes bounce-in {
0% {
transform: scale(0);
}
50% {
transform: scale(1.5);
}
100% {
transform: scale(1);
}
}

Related

How I can make animation for background?

I have a menu form. To add and remove items from this menu, I use React Transition Group
ReactJS:
<TransitionGroup>
{
menu.map(meal =>
<CSSTransition
key={meal.id}
timeout={500}
classNames="meMeals"
>
<Meal meal={meal} deleteFromMenu={deleteMealFromMenu}/>
</CSSTransition>
)
}
</TransitionGroup>
CSS:
.meMeals-enter {
opacity: 0;
transform: translateY(-30px);
}
.meMeals-enter-active {
opacity: 1;
transform: translateY(0);
transition: all 500ms ease-in;
}
.meMeals-exit {
opacity: 1;
transform: translateY(0);
}
.meMeals-exit-active {
opacity: 0;
transform: translateY(-30px);
transition: all 500ms ease-in;
}
and I am completely satisfied with the behavior of the menu items.
Now I want the background element (grey) as well as the add button to move smoothly as the menu item appears or disappears. How can i do this?
I solved the problem by writing a method that is not directly related to the TransitionGroup, but works in parallel. I also set my window:
transition: 0.5s;
whatever the animation
Now I call this method every time the list changes....
function replaceMenuSize(value) {
const menuSize = menuEditorRef.current.getBoundingClientRect().height
if (value > 0) {
menuEditorRef.current.setAttribute("style", "height: " + (menuSize + 41) + "px")
} else {
menuEditorRef.current.setAttribute("style", "height: " + (menuSize - 41) + "px")
}
}

React - Can I apply css effect on the first render only?

I have a simple React component that displays some text with a 'typewriter' kind of effect via CSS. The problem I have is that this typewriter effect is re-displayed every time the component re-renders but I only want it applied on the first render and never again. Is there a way to achieve this? Below is an example...
import React from 'react';
import './Typewriter.css'
export function Blah() {
return <div className='typewriter'>
My typewriter text here
</div>
}
.typewriter {
font-family: monospace;
color:#0000;
background:
linear-gradient(-90deg, #39ff14 5px,#0000 0) 10px 0,
linear-gradient(#39ff14 0 0) 0 0;
background-size:calc(100*1ch) 200%;
-webkit-background-clip:padding-box,text;
background-clip:padding-box,text;
background-repeat:no-repeat;
animation:
b .7s infinite steps(1),
t calc(100*.01s) steps(100) forwards;
}
#keyframes t{
from {background-size:0 200%}
}
#keyframes b{
50% {background-position:0 -100%,0 0}
}
Stop the animation after the first time by using useRef and useEffect as in the following example
const { useRef, useEffect } = React;
const Example = () => {
const firstRender = useRef(true);
useEffect(() => {
firstRender.current = false;
}, []);
return <div className={firstRender.current ? 'typewriter' : ''}>
My typewriter text here
</div>
}
ReactDOM.render(<Example />, document.getElementById("react"));
.typewriter {
font-family: monospace;
color:#0000;
background:
linear-gradient(-90deg, #39ff14 5px,#0000 0) 10px 0,
linear-gradient(#39ff14 0 0) 0 0;
background-size:calc(100*1ch) 200%;
-webkit-background-clip:padding-box,text;
background-clip:padding-box,text;
background-repeat:no-repeat;
animation:
b .7s infinite steps(1),
t calc(100*.01s) steps(100) forwards;
}
#keyframes t{
from {background-size:0 200%}
}
#keyframes b{
50% {background-position:0 -100%,0 0}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="react"></div>

CSS animation-direction not working as I would have expected

N.B. With the (more complex) setup I'm actually working with I can't use CSS Transitions. I recognise that CSS Transitions would be a
perfectly good solution in the example below.
I'm having a little trouble with
animation-direction: reverse
which I've never used before but doesn't seem to be running the way I might have expected it to.
The easiest solution to my problem would be to write two CSS #keyframes animations and use one or the other.
But for the sake of economy and elegance I would like to use a single animation and play it forwards or backwards.
This example below shows the effect I'm trying to achieve.
When the page loads, pressing either button will fire the intended animation.
However, after one button is pushed, the animation no longer runs and only the end-frame of the forwards or reverse animation is displayed.
What am I doing wrong here?
Working Example:
const square = document.querySelector('.square');
const buttonOutbound = document.querySelector('button.outboundButton');
const buttonReturn = document.querySelector('button.returnButton');
buttonOutbound.addEventListener('click', () => {
square.className = 'square';
square.classList.add('outbound');
}, false);
buttonReturn.addEventListener('click', () => {
square.className = 'square';
square.classList.add('return');
}, false);
.square {
width: 100px;
height: 100px;
margin: 12px;
background-color: red;
transform: translateX(0) scale(1);
}
.square.outbound {
animation: animateSquare 1s linear normal forwards;
}
.square.return {
animation: animateSquare 1s linear reverse forwards;
}
#keyframes animateSquare {
100% {
background-color: blue;
transform: translateX(200px) scale(0.5);
}
}
<div class="square"></div>
<button type="button" class="outboundButton">Outbound animation</button>
<button type="button" class="returnButton">Return animation</button>
The browser considers that animation as complete therefore it does not restart it in order to restart the animation you need to first remove the class and then re-add it however for the browser to recognize this change you need add a slight delay. Adding a setTimeout does the trick even if the timeout is 0 because js is single threaded.
const square = document.querySelector('.square');
const buttonOutbound = document.querySelector('button.outboundButton');
const buttonReturn = document.querySelector('button.returnButton');
buttonOutbound.addEventListener('click', () => {
square.classList.remove('return');
square.classList.remove('outbound');
setTimeout(() => {
square.classList.add('outbound');
}, 0)
}, false);
buttonReturn.addEventListener('click', () => {
square.classList.remove('return');
square.classList.remove('outbound');
setTimeout(() => {
square.classList.add('return');
}, 0)
}, false);
.square {
width: 100px;
height: 100px;
margin: 12px;
background-color: red;
transform: translateX(0) scale(1);
}
.square.outbound {
animation: animateSquare 1s linear normal forwards;
}
.square.return {
animation: animateSquare 1s linear reverse forwards;
}
#keyframes animateSquare {
100% {
background-color: blue;
transform: translateX(200px) scale(0.5);
}
}
<div class="square"></div>
<button type="button" class="outboundButton">Outbound animation</button>
<button type="button" class="returnButton">Return animation</button>
I had a think about this away from my laptop screen and realised that... what was missing from my original set up was one additional static class, describing the presentational state of .square after the .outbound animation has run:
.square.outbounded {
background-color: blue;
transform: translateX(200px) scale(0.5);
}
(N.B. There's no need for a corresponding static class, describing the state of .square after the .return animation has run, since the presentational state that class would describe is already described in the initial styles of .square)
Working Example (with .outbounded class added)
const square = document.querySelector('.square');
const buttonOutbound = document.querySelector('button.outboundButton');
const buttonReturn = document.querySelector('button.returnButton');
buttonOutbound.addEventListener('click', () => {
square.className = 'square outbound';
setTimeout(() => {
square.classList.add('outbounded');
square.classList.remove('outbound');
}, 1000);
}, false);
buttonReturn.addEventListener('click', () => {
square.className = 'square return';
setTimeout(() => {
square.classList.remove('return');
}, 1000);
}, false);
.square {
width: 100px;
height: 100px;
margin: 12px;
background-color: red;
transform: translateX(0) scale(1);
}
.square.outbounded {
background-color: blue;
transform: translateX(200px) scale(0.5);
}
.square.outbound {
animation: animateSquare 1s linear normal forwards;
}
.square.return {
animation: animateSquare 1s linear reverse forwards;
}
#keyframes animateSquare {
100% {
background-color: blue;
transform: translateX(200px) scale(0.5);
}
}
<div class="square"></div>
<button type="button" class="outboundButton">Outbound animation</button>
<button type="button" class="returnButton">Return animation</button>

use react-transition-group as child component receives new props

I have a child component where I would like to use slide-out animation as new props are getting passed to it and I try to use react-transition-group/switch-transition but is not really clear how to use it
The child component render method looks as it follows
return (
<SwitchTransition mode="out-in">
<CSSTransition
classNames="slide"
>
<div className={classnames("fields-group", containerClass)}>
{/* <pre>{JSON.stringify(this.props.fields, null, 2)}</pre>*/}
{fields}
</div>
</CSSTransition>
</SwitchTransition>
);
There are more things you need to do:
CSSTransition should has a prop key. When it changed, the transition will take affect.
You need to add the transition styles by yourself because, React Transition Group is not an animation library like React-Motion, it does not animate styles by itself. reference
So the child component will look something like that:
function Child({ propToAnimate }) {
return (
<>
<h4>Child Component</h4>
<div className="main">
<SwitchTransition mode="out-in">
<CSSTransition
key={propToAnimate}
addEndListener={(node, done) => {
node.addEventListener("transitionend", done, false);
}}
classNames="fade"
>
<div className="button-container">
<div className="animate">
<pre>state: {propToAnimate}</pre>
</div>
</div>
</CSSTransition>
</SwitchTransition>
</div>
</>
);
}
And the styles (for slide animation for example):
.fade-enter .animate {
opacity: 0;
transform: translateX(-100%);
}
.fade-enter-active .animate {
opacity: 1;
transform: translateX(0%);
}
.fade-exit .animate {
opacity: 1;
transform: translateX(0%);
}
.fade-exit-active .animate {
opacity: 0;
transform: translateX(100%);
}
.fade-enter-active .animate,
.fade-exit-active .animate {
transition: opacity 500ms, transform 500ms;
}
https://codesandbox.io/s/switchtransition-child-component-dk4jo

What is wrong with ReactCSSTransitionGroup?

I have this code inside render() function in my component:
..
<ReactCSSTransitionGroup
transitionName="testing"
transitionEnterTimeout={600}
transitionLeaveTimeout={600}>
<div>testing animations!</div>
</ReactCSSTransitionGroup>
..
And i have this code in my CSS file:
.testing-enter {
animation: slideIn 0.6s cubic-bezier(0.165, 0.84, 0.44, 1);
}
.testing-leave {
animation: slideOut 0.6s cubic-bezier(0.165, 0.84, 0.44, 1);
}
#keyframes slideIn {
0% {
opacity 0
transform translate3d(-50px, 0, 0)
}
100% {
opacity 1
transform translate3d(0, 0, 0)
}
}
#keyframes slideOut {
0% {
opacity: 1
transform: translate3d(0, 0, 0)
}
100% {
opacity: 0
transform: translate3d(50px, 0, 0)
}
}
I just want my div block to slideIn from the right, but nothing happens! Could not find the wrong piece of code, it looks like everything is ok.
You have static content inside the <ReactCSSTransitionGroup/> tag. You need to have dynamic content. Let's say you need to render <div>testing animations!</div> upon mouse click. You need to assign the children to a variable and modify the variable upon mouse click.
let myDiv = buttonClicked ? <div>testing animations!</div> : <div></div>
<ReactCSSTransitionGroup
transitionName="testing"
transitionEnterTimeout={600}
transitionLeaveTimeout={600}>
{myDiv}
</ReactCSSTransitionGroup>
If no actions are performed and you simply need to animate it on initial mount, use transition-appear instead.
ReactCSSTransitionGroup provides the optional prop transitionAppear,
to add an extra transition phase at the initial mount of the
component. There is generally no transition phase at the initial mount
as the default value of transitionAppear is false. The following is an
example which passes the prop transitionAppear with the value true.
<ReactCSSTransitionGroup transitionName="example" transitionAppear={true} transitionAppearTimeout={500}>
<div>testing animations!</div>
</ReactCSSTransitionGroup>
.example-appear {
opacity: 0.01;
}
.example-appear.example-appear-active {
opacity: 1;
transition: opacity .5s ease-in;
}

Resources